feat(profile): wire usernames through profile and resolver - #638
Merged
Conversation
Re-vendors the flipcash protobuf definitions and regenerates the Swift
bindings. The opencode protos were already up to date.
| File | Change |
|---|---|
| `common/v1/common.proto` | New `Username` message — lowercase `^[a-z0-9_]{2,15}$` |
| `profile/v1/model.proto` | `UserProfile` gains `user_id` (9, required) and `username` (8, optional) |
| `profile/v1/profile_service.proto` | `GetProfileRequest.user_id` becomes `oneof identifier { user_id, username }` |
| `resolver/v1/model.proto` | `Identifier` oneof gains `username` |
No RPCs were added, removed, or re-signatured, and no result enum gained a
case. `GetProfileRequest` losing `hasUserID`/`clearUserID` to the oneof is
source-compatible here: the generated `userID` setter writes through to the
oneof, and nothing read the presence accessors.
Matches code-payments/code-android-app#1316, which vendors the same four
files at the same revision.
Threads the new public username handle through the model and service layers. Mirrors code-payments/code-android-app#1316. `Username` is a validated value type rather than a bare `String`. Android validates at request time (`request.validate().orThrow()`); the Swift generator emits no validators — `Scripts/run` drops the unused `validate_validate.pb.swift` mirror — so the type boundary is where iOS enforces the contract instead. One asymmetry: it also validates inbound, so a handle violating the server's own `validate.rules` reads as nil here where Android would pass it through. `Profile` gains `username` and `userID`, both trailing and defaulted, so the existing call sites and previously persisted profile JSON keep working. Profiles persist as a JSON blob in a single-row table, so no `SQLiteVersion` bump is needed — there is a test pinning that older rows still decode. `ConversationMember` maps the handle too. It decodes the same `profile.v1.UserProfile` message off the embedded chat member rather than going through `Profile(_:)`, so it is a second mapping site that would otherwise drop the field on every conversation. `ProfileIdentifier` mirrors the `GetProfileRequest` oneof and is threaded through `ProfileService`. `FlipClient.fetchProfile(userID:)` is unchanged for callers; lookup by handle is the new `fetchProfile(username:)`. Since the response carries `user_id`, a caller holding only a handle learns the user's id from it. `resolveUsername` joins `resolvePhone`/`resolveUserID` as a third sibling building the new `Identifier` arm. Android modelled this as a `ResolveIdentifier` oneof; the iOS resolver already models the arms as sibling methods over a shared private `resolve`, so a third method fits the existing shape without refactoring working code. Usernames stay read-only on both platforms: `profile_service.proto` has no SetUsername RPC, so nothing can claim one until the backend adds it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings iOS to parity with code-payments/code-android-app#1316.
Proto sync
Re-vendored the core protos from
flipcash2-protobuf-apiand regenerated the Swift bindings. The whole delta is one feature — usernames:common.v1.Username— new message,string valueconstrained to^[a-z0-9_]{2,15}$profile.v1.UserProfile— gaineduser_id(9) andusername(8)profile.v1.GetProfileRequest—user_idmoved into anidentifieroneof alongsideusernameresolver.v1.Identifier.kind— gainedusernameNo RPCs were added or removed, and no result enums changed. The
GetProfileRequestreshape is source-compatible on our side: the generateduserIDsetter writes through the oneof, and nothing read the removedhasUserID/clearUserID.Payments (
ocp-protobuf-api) re-vendored with no diff, so it isn't in this PR.Model & service wiring
Username— a validated value type. swift-protobuf emits no validators (andScripts/rundrops the unusedvalidate_validate.pb.swift), so this is where the pattern is enforced.init?is strict rather than normalizing;Codableis a bare string so a persisted profile reads as"username": "ted".ProfileIdentifier— the Swift counterpart to theGetProfileRequestoneof, and to Android's sealed interface.Profile— gaineduserIDandusername, both optional and defaulted so existing call sites and persisted rows are unaffected.ConversationMember— the second place aprofile.v1.UserProfileis decoded. It readsproto.userProfile.*directly rather than going throughProfile(_:), so it needed the handle wired separately; usernames now arrive with the conversation and need no extra profile fetch.ProfileService.fetchProfiletakes aProfileIdentifier;FlipClientexposesfetchProfile(userID:owner:)andfetchProfile(username:owner:).ResolverService.resolveUsername/FlipClient.resolveUsernameresolve a handle to a payment destination.No schema bump: both profile tables store the model as a JSON blob, so the new optional fields change no SQLite schema and old rows decode to
nil. There's a regression test pinning that.Usernames are read-only on both platforms — there is no
SetUsernameRPC yet.Deliberate divergences from Android
request.validate().orThrow()on the outbound request; we validate at the type boundary instead. One asymmetry falls out of that: we also validate inbound, so a server value that violated its ownvalidate.ruleswould read asnilhere where Android passes it through.ResolveIdentifiersealed type. Ours already models each arm as a sibling method over a shared privateresolve, soresolveUsernamefollows that pattern rather than converting working code for symmetry.