Skip to content

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

Merged
bmc08gt merged 2 commits into
mainfrom
feat/username-profile-resolver
Aug 24, 2026
Merged

feat(profile): wire usernames through profile and resolver#638
bmc08gt merged 2 commits into
mainfrom
feat/username-profile-resolver

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Brings iOS to parity with code-payments/code-android-app#1316.

Proto sync

Re-vendored the core protos from flipcash2-protobuf-api and regenerated the Swift bindings. The whole delta is one feature — usernames:

  • common.v1.Username — new message, string value constrained to ^[a-z0-9_]{2,15}$
  • profile.v1.UserProfile — gained user_id (9) and username (8)
  • profile.v1.GetProfileRequestuser_id moved into an identifier oneof alongside username
  • resolver.v1.Identifier.kind — gained username

No RPCs were added or removed, and no result enums changed. The GetProfileRequest reshape is source-compatible on our side: the generated userID setter writes through the oneof, and nothing read the removed hasUserID/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 (and Scripts/run drops the unused validate_validate.pb.swift), so this is where the pattern is enforced. init? is strict rather than normalizing; Codable is a bare string so a persisted profile reads as "username": "ted".
  • ProfileIdentifier — the Swift counterpart to the GetProfileRequest oneof, and to Android's sealed interface.
  • Profile — gained userID and username, both optional and defaulted so existing call sites and persisted rows are unaffected.
  • ConversationMember — the second place a profile.v1.UserProfile is decoded. It reads proto.userProfile.* directly rather than going through Profile(_:), so it needed the handle wired separately; usernames now arrive with the conversation and need no extra profile fetch.
  • ProfileService.fetchProfile takes a ProfileIdentifier; FlipClient exposes fetchProfile(userID:owner:) and fetchProfile(username:owner:).
  • ResolverService.resolveUsername / FlipClient.resolveUsername resolve 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 SetUsername RPC yet.

Deliberate divergences from Android

  • Validation placement. Android calls 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 own validate.rules would read as nil here where Android passes it through.
  • Resolver shape. Android introduced a ResolveIdentifier sealed type. Ours already models each arm as a sibling method over a shared private resolve, so resolveUsername follows that pattern rather than converting working code for symmetry.

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.
@bmc08gt
bmc08gt merged commit ae89c93 into main Aug 24, 2026
@bmc08gt
bmc08gt deleted the feat/username-profile-resolver branch August 24, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant