feat(username): claim a handle, and reach a tip card by it - #1330
Merged
Conversation
Usernames get an entry screen, a balance-gated nudge on the You tab, and a
place in every surface that until now showed only an opaque user id.
- `Username` joins the update-profile stack as a conditional step alongside
display name (node 9491:6296), reachable from the You tab's progress card
(node 9491:6295) and a new My Account row (node 9491:6297).
- `usernameGate` reads the `usernameMinBalance` flag locally, so the card shows
how far off the balance is instead of letting someone type a handle into the
server's rejection. Rejections show as info rather than error: each describes
what was typed and is fixed by typing something else. Length is checked here
so "too short" and "too long" name the problem rather than arriving as a
generic INVALID_USERNAME.
- `TipCardOwner` replaces the bare `ID` at the deeplink, session, and Linkify
seams, so a card can be addressed by id or by handle. A vanity link renders
whole; only an opaque id is abbreviated (node 9442:3673).
- `flipcash.com/{username}` is claimed as a verified App Link, narrowed to the
handle charset by `pathAdvancedPattern`. Android verifies per host and not
per path, so the website's own paths (/download, /privacy, /terms) are listed
in `AppRouter` and handed back to a browser through
`DeeplinkAction.OpenExternally` rather than dead-ending on the home screen.
- Following your own handle answers with the You tab. Both handle-based
self-checks are blind until this account's profile has loaded, which is
exactly the window a cold-started link lands in, so `TippingCoordinator`
compares ids after the fetch and raises `OwnTipCard` before the resolve arms
the tip modal.
The new host needs `assetlinks.json` served from
`https://flipcash.com/.well-known/` before Android will verify it.
The handle comes from `LOGIN_USERNAME` rather than being written into the
flows, so the suite doesn't carry a real account's handle and a rotated test
account is a one-line `.env` change. `run.sh` forwards it alongside the other
credentials.
- `vanity_deeplink_self.yaml` — the account that owns the handle follows its own
`flipcash.com/{handle}` link and lands on the You tab. Opened from cold state
deliberately: the handle-based self-checks can't answer until the account's
profile has loaded, and a link tapped from outside the app arrives inside that
window.
- `vanity_deeplink_tip.yaml` — a brand-new account follows the same link and
gets that handle's owner card, asserted on the `@handle` drawn under the name.
The name alone would only prove some card opened. Tagged `creates-account`,
which the runner excludes by default.
…ccount row
`pathAdvancedPattern` is a case-sensitive PatternMatcher glob, so
`/[a-z0-9_]{2,15}` doesn't match `flipcash.com/Sally_Streamer` — the link opens
in the browser on API 31+ and the app's own lowercasing in `isVanityProfile`
never runs. Below 31 the attribute is ignored and the same link works, so one
handle behaved two ways across the minSdk range. A-Z in the set closes that;
mixed-case website pages it now also claims (`/Download`) classify as unrouted
and bounce back out via `OpenExternally`, which is what already happens below 31.
The My Account default-state test still expected three rows, and `ChangeUsername`
made it four — the failing assertion on CI.
`reservedVanityPaths` held eight of the website's pages; the AASA served from `flipcash.com/.well-known/apple-app-site-association` excludes twenty. The ten handle-shaped ones it had and this list didn't — /app, /api, /assets, /fonts, /icons, /js, /pool, /v1, /wallet, /currencycreator — were captured by the App Link filter, classified as vanity profiles, looked up over the wire, and dead-ended on "username not found" instead of opening the page. The two lists are the same statement made twice, so the test now walks the AASA's handle-shaped excludes rather than spot-checking three of them.
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.
Usernames get an entry screen, a balance-gated nudge on the You tab, and a place in every surface that until now showed only an opaque user id. Figma nodes 9491:6295 through 9491:6299.
Claiming a handle
UpdateProfileStep.Usernamejoins the update-profile stack as a conditional step alongside display name, so the same screen serves first-time setup and a later change.AppRoute.UpdateUserProfilegainsincludeUsername, threaded intobuildUpdateUserProfileStack(includeName, includeUsername, includePhoto). Two entry points: the You tab's progress card (node 9491:6295) and a new Change Username row in My Account (node 9491:6297).The server enforces a minimum balance on submit (
SetUsernameError.InsufficientBalance).usernameGate(username, minimum, balance)reads the sameusernameMinBalanceflag locally so the card can show the distance rather than let someone type a handle into a rejection. It fails open on a zero or unresolved minimum, and clamps its progress fraction to0f..1f— a negative balance previously drew the bar backwards.Every rejection surfaces through
showInfo, notshowError: taken, too short, too long, reserved, moderated all describe what was typed and are fixed by typing something else, so the destructive style would read as a fault in the app. Only a failed check stays an error. Length is checked locally so "Too Short" and "Too Long" name the actual problem instead of arriving as a genericINVALID_USERNAME.Addressing a tip card by handle
TipCardOwneris a sealed type —ById(userId)orByUsername(username)— and replaces the bareIDat three seams:DeeplinkAction.PresentTipCard,SessionController.resolveTipCard, andLinkify.tipcard. Resolving a handle is a server round trip, so it stays a handle from the router all the way to the session, which owns the fetch.The link row renders
flipcash.com/sally_streamerwhole (node 9442:3673) and abbreviates only an opaque id toapp.flipcash.com/tip/b0ced…(node 9276:4753) — the handle is the point of claiming one, and cutting it would hide the part that identifies the person. The test is the handle's own shape, so nothing else is ever truncated.flipcash.com/{username}deeplinksA new
autoVerifyintent-filter claimsflipcash.comandwww.flipcash.com, narrowed to the handle charset withpathAdvancedPattern="/[a-z0-9_]{2,15}".Android verifies App Links per host, not per path —
assetlinks.jsonhas no path scoping — andpathAdvancedPatternis API 31+ with no negative lookahead. So the website's own single-segment paths (/download,/privacy,/terms) satisfy the filter too.AppRouterkeeps the reserved list and answers those withDeeplinkAction.OpenExternally, which hands the URL to a named browser rather than dead-ending the tap on the home screen. It has to be named: a package-lessACTION_VIEWon a host we are a verified handler for resolves straight back to us and loops.This differs from iOS deliberately — their AASA supports path exclusions, so the same list lives in their apple-app-site-association file rather than in code. iOS also makes no
www.flipcash.comclaim; if that stays true the two platforms will disagree aboutwwwlinks.Following your own handle
Two self-checks already existed, in
AppRouterand inTipCardOwner.isSelf, and both compare handles — which they cannot do until this account's own profile has loaded. That is exactly the window a cold-started link lands in, so a link to your own handle got past both.TippingCoordinator.resolveTipCardnow compares the id off the wire after the fetch and raisesOwnTipCard, whichTipCardDelegateanswers withTipCardEvent.OwnCardScanned— the same answer as the pre-dispatch guard. Raised as a failure rather than returning a card so the resolve stops short of its side effects (arming the tip modal, vibrating) for a card nobody will tip. Mirrors iOSTipFlow.prepare's secondguard.Elsewhere
ChatParticipant.handleexposes the handle to chat rows;Handle.ktcentralises the@prefix, the length bounds, and the shape regex that all three consumers depend on.Analytics.AddMoneySource.UsernameShortfallreports a deposit prompted by the gate, so it is not counted as a deliberate visit to Add Money. The wire string matches iOS.Depends on
assetlinks.jsonmust be served fromhttps://flipcash.com/.well-known/before Android will verify the new host. Until it is, the links open in a browser instead of the app.