feat(flipcash): wire usernames through profile and resolver - #1316
Merged
Conversation
Refresh the flipcash protos, which introduce a public `Username` handle:
- common/v1 gains a `Username` message (lowercase `^[a-z0-9_]{2,15}$`)
- `UserProfile` carries `user_id` and `username`
- `GetProfileRequest.user_id` becomes a `oneof identifier` of user ID or username
- the resolver `Identifier` oneof gains `username`
Wire all of it through the service layer:
- `UserProfile` gains `userId`/`username`, mapped in `UserProfileMapper` and in
the chat-member profile path, which decodes the same proto message. Both are
trailing and default to null so the positional constructor and previously
persisted profile JSON keep decoding.
- New `ProfileIdentifier` sealed interface mirrors the `GetProfileRequest` oneof
and is threaded through `ProfileApi` -> `ProfileService` -> `ProfileRepository`.
`ProfileController.getProfileForUser` is unchanged for callers; lookup by handle
is `getProfileForUsername`.
- `ResolveIdentifier.Username` plus `ResolverController.resolve(username)`.
`ProfileApi.getProfile` now validates its request like every other method in the
class, so a malformed handle fails locally instead of round-tripping: the oneof
carries `validate.required` and the username its character-set pattern.
The opencode protos were already up to date.
bmc08gt
added a commit
to code-payments/code-ios-app
that referenced
this pull request
Aug 24, 2026
* chore: sync core protos
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.
* feat(profile): wire usernames through profile and resolver
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.
bmc08gt
added a commit
that referenced
this pull request
Aug 24, 2026
…1320) Vendors the Profile service's new SetUsername RPC from flipcash2-protobuf-api and builds out the write path: Api, Service, Repository, and Controller, following the existing setDisplayName chain. On success the controller merges the username into the locally cached profile so observers see it without waiting for a refresh. The read path and UserProfile.username landed in #1316. SetUsernameError covers each non-OK result: InvalidUsername, Denied, AlreadyTaken, FailedModerated (carrying the flagged category), InsufficientBalance, ReservedWord, plus Unrecognized and Other. InternalProfileRepository skips ErrorUtils.handleError for the results that are the server answering a user's choice of handle rather than a fault. setDisplayName gets the same treatment for InvalidDisplayName and FailedModerated; its only caller, NameEntryViewModel, already renders its own alert for every failure, so this drops a duplicate report path rather than user-facing feedback. No UI yet — nothing calls ProfileController.setUsername.
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.
Refreshes the flipcash protobuf definitions and wires the new public username handle through the service layer. The opencode protos were already up to date.
Proto changes
common/v1/common.protoUsernamemessage — lowercase^[a-z0-9_]{2,15}$profile/v1/model.protoUserProfilegainsuser_id(9, required) andusername(8, optional)profile/v1/profile_service.protoGetProfileRequest.user_idbecomesoneof identifier { user_id, username }resolver/v1/model.protoIdentifieroneof gainsusernameNo RPCs were added, removed, or re-signatured.
Service layer
UserProfilegainsuserId/username, mapped inUserProfileMapperand in the chat-member profile path inProtobufToLocal, which decodes the sameprofile.v1.UserProfilemessage. Both properties are trailing and default tonull, so the positional constructor inDomainModelsTestand previously persisted profile JSON both keep working.ProfileIdentifier— new sealed interface mirroring theGetProfileRequestoneof, threaded throughProfileApi→ProfileService→ProfileRepository.ProfileController.getProfileForUser(userId)is unchanged for callers; lookup by handle is the newgetProfileForUsername(username). Since the response carriesuser_id, a caller holding only a handle learns the user's ID from it.ResolveIdentifier.Usernameplus aResolverController.resolve(username: String)overload alongside the existing phone and user-ID ones.String.asUsername()helper inLocalToProtobuf.Note on a behavior change
ProfileApi.getProfilenow callsrequest.validate().orThrow(). It was the only method in that class skipping validation, and the new oneof carriesvalidate.requiredwhile the username carries its character-set pattern — so a malformed handle now fails locally instead of round-tripping to the server.ProfileRepository.getProfilechanged signature (ID→ProfileIdentifier). Its only consumers wereProfileControllerandFakeProfileRepositoryin tests; both are updated.Follow-up
There is still no
SetUsername/claim RPC inprofile_service.proto, so usernames are read-only from the client. Nothing can set one until the backend adds that endpoint.