Skip to content

feat(flipcash): wire usernames through profile and resolver - #1316

Merged
bmc08gt merged 1 commit into
code/cashfrom
feat/username-profile-resolver
Aug 24, 2026
Merged

feat(flipcash): wire usernames through profile and resolver#1316
bmc08gt merged 1 commit into
code/cashfrom
feat/username-profile-resolver

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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

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.

Service layer

  • UserProfile gains userId/username, mapped in UserProfileMapper and in the chat-member profile path in ProtobufToLocal, which decodes the same profile.v1.UserProfile message. Both properties are trailing and default to null, so the positional constructor in DomainModelsTest and previously persisted profile JSON both keep working.
  • ProfileIdentifier — new sealed interface mirroring the GetProfileRequest oneof, threaded through ProfileApiProfileServiceProfileRepository. ProfileController.getProfileForUser(userId) is unchanged for callers; lookup by handle is the new getProfileForUsername(username). Since the response carries user_id, a caller holding only a handle learns the user's ID from it.
  • ResolveIdentifier.Username plus a ResolverController.resolve(username: String) overload alongside the existing phone and user-ID ones.
  • String.asUsername() helper in LocalToProtobuf.

Note on a behavior change

ProfileApi.getProfile now calls request.validate().orThrow(). It was the only method in that class skipping validation, and the new oneof carries validate.required while the username carries its character-set pattern — so a malformed handle now fails locally instead of round-tripping to the server.

ProfileRepository.getProfile changed signature (IDProfileIdentifier). Its only consumers were ProfileController and FakeProfileRepository in tests; both are updated.

Follow-up

There is still no SetUsername/claim RPC in profile_service.proto, so usernames are read-only from the client. Nothing can set one until the backend adds that endpoint.

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.
@github-actions github-actions Bot added type: feature New functionality area: network gRPC, connectivity, API, exchange rates and removed type: feature New functionality labels Aug 24, 2026
@bmc08gt
bmc08gt merged commit 4fa7cdc into code/cash Aug 24, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/username-profile-resolver branch August 24, 2026 17:47
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: network gRPC, connectivity, API, exchange rates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant