feat(invite): invite/refer-a-friend mobile client - #679
Open
islandbitcoin wants to merge 8 commits into
Open
Conversation
Re-integrates the invite-a-friend feature (previously on the stale RN-0.72
feat/new-invite-friend branch) onto current main (RN 0.77) as a fresh graft
rather than a conflict-ridden rebase.
New files (verbatim): invite-friend screens (InviteFriend, InviteFriendSuccess,
HandleInviteDeepLink, InviteDeepLinkHandler), contact-picker, Email/PhoneNumber
inputs, settings entry, assets, and the screen test.
Grafted onto main's current versions:
- navigation: register InviteFriend/InviteFriendSuccess routes + param types;
skip invite URLs in the container so InviteDeepLinkHandler owns them.
- graphql: createInvite/redeemInvite mutations + invitePreview query; hand-grafted
the generated types/hooks (codegen can't run — backend invite schema lives only
on the unmerged backend feat/invite branch). Kept npubByUsername (the old branch
diff wrongly deleted it).
- i18n: HomeScreen.invite{Title,Desc}, SettingsScreen.inviteFriend, InviteFriend.*
in en + raw en.json + i18n-types.
- app.tsx: wrap RootStack in InviteDeepLinkHandler (onto main's new provider tree,
without the branch's dropped NostrGroupChatProvider).
- QuickStart: invite quick-action, merged with main's disabled/pending item logic.
- welcome/get-started: pending-invite redemption + invite-token registration routing.
- contact-picker: dropped phantom i18n keys (never existed; literal fallbacks kept),
typed options as AlertButton[], Text type h3->h2.
Native: added react-native-contacts + react-native-permissions (Contacts only) to
Podfile/Info.plist/AndroidManifest/package.json. pod install succeeded and
regenerated Podfile.lock.
Deliberately SKIPPED: the branch's phone-auth pre-fill changes (phone-login/
registration-input, request-phone-code-*). They alter the useRequestPhoneCodeLogin
signature and touch critical shared auth code main has evolved. The feature works
without them — the deep-link handler stores the token and welcome.tsx redeems it;
pre-fill is only a registration UX nicety, deferred pending an auth-flow build.
Verified: full tsc typecheck clean (0 app errors); invite screen test 5/5 pass;
pod install clean. NOT verified here: iOS Xcode build/link, and end-to-end behavior
(gated on the backend feat/invite schema being deployed). react-native-permissions
pinned at ^4.1.5 may want a v5 bump for RN 0.77 New Arch at build time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
Expand invite coverage beyond the InviteFriend screen: InviteFriendSuccess (contact interpolation, fallback, Done->popToTop) and HandleInviteDeepLink (redeemPendingInvite success/no-token/duplicate/error paths, useInviteDeepLink token parsing + store->preview->navigate). 18/18 green (5 existing + 13 new). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
… lint) The grafted invite feature was never CI'd on this branch (RN 0.77): - codegen: add invite types + Mutation/Query fields to the local public-schema.graphql so dev:codegen succeeds; regenerate generated.ts/gql canonically (replaces the earlier hand-graft). - i18n: run update-translations (regenerates i18n-types.ts) and propagate the 12 invite keys into all 23 locales (English fallback) to clear translation-drift. - QuickStart: fix raster-asset detection (typeof !== 'function' — the old 'typeof === number' check crashed under jest's PNG stub); make quick-start spec load the 'en' locale so it matches real titles order-independently. - lint: format the never-linted invite files, drop unused vars, type redeemInviteMutation as RedeemInviteMutationFn, tidy InviteFriendSuccess styles. Gates: jest 222 pass / 0 fail, tsc:check clean, translation-drift clean, lint-changed 0 errors. Native Android/iOS build checks still need a device build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
lint-changed (react-native/no-inline-styles) flagged the graft's spacer
<View style={{ height: 20 }} />. Move it to styles.spacer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
Contributor
Author
🎤 Simon Review — mobile clientAdversarial senior review. Verdict: NEEDS WORKThe Blocking
Should fix
Notes
🤖 Simon-style adversarial review generated with Claude Code. Findings are advisory — verify before acting. |
- Launch URL processed exactly once (useRef guard): no re-storing an
already-redeemed token or spurious 'new users only' alerts on auth flips.
- Token extraction is format-agnostic ([^&#\s]+ + decodeURIComponent);
the backend owns token validation.
- Pending token stored only for unauthenticated sessions, as {token, storedAt}
with a 24h TTL matching backend invite expiry — closes the cross-account
redemption leak.
- Stored token cleared only on success or terminal errors (expired/used/
invalid/own); transient failures keep it for retry. Module-level in-flight
guard collapses concurrent redemptions to one mutation.
- Removed the backend-ignored X-Invite-Recipient header, the dead getStarted
invite branch, and the unused invite route-param extensions.
- Comment now states the real link model: app registers flash:// only; the
backend's https://getflash.io/invite links need the landing redirect or
App/Universal Links (out-of-repo activation dependency).
Tests: HandleInviteDeepLink 10->21 scenarios. Full suite 233/233; tsc,
lint-changed, translation-drift clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
…preview-error triage
- Declare READ_CONTACTS in the Android manifest: the Contacts tab (InviteFriend
default) was permanently dead on Android — request() returns BLOCKED for an
undeclared permission and Settings offers no toggle. iOS half already existed.
- invitePreview errors are now triaged: network errors keep the stored token
and ask to retry; GraphQL errors (the backend throws for malformed/unknown
tokens) clear the token and surface an invalid-invitation alert — no more
doomed tokens lingering for 24h with repeat alerts.
- decodeURIComponent runs inside try with raw fallback (malformed %-encoding
no longer escapes the async handler as an unhandled rejection).
- Dropped phantom nav params (declared undefined, consumed by nothing) and
their as-never casts; redemption rides AsyncStorage as designed.
- TERMINAL_REDEEM_ERRORS gains the resolver's two other permanent errors
('for new users only', 'a different phone number').
- Contact picker awaits Contacts.getAll() so the loading state is real.
Tests: HandleInviteDeepLink 20 -> 24 scenarios. Full suite 237/237; tsc,
lint-changed clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
…light guard 'Unparseable'->'Unparsable' (typos CI); eslint-disable with rationale on the finally reset of redeemInFlight (guard is set synchronously before any await). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
…atches
The backend's one-reward-per-invitee and revoked-invite fixes introduced two
new permanent error strings; recognize both ('already redeemed an invitation'
clears silently, 'no longer valid' clears with notice) so doomed tokens are
not kept for 24h of futile retries. Alert suppression generalized to a
SILENT_REDEEM_ERRORS list; test fixture wording aligned with the resolver's
actual prose. +2 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg
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.
Invite / refer-a-friend — mobile client
The mobile half of the invite/refer feature, freshly grafted onto current
main(RN 0.77). Pairs with the backend lnflash/flash#462 and the ops page lnflash/frappe-flash-admin#57.What's here
InviteFriendscreen (Contacts / WhatsApp / Email methods),InviteFriendSuccess, a contact picker, and email/phone inputs.HandleInviteDeepLink+InviteDeepLinkHandler— parsesflash://invite?token=…, stores the token, previews the invite, and redeems on onboarding (welcome.tsx), routing new users into registration with the invite context.createInvite,redeemInvite,invitePreview), generated types, i18n strings, navigation (routes + deep-link skip), a Settings entry, and a Home-screen QuickStart entry.How it was built
This branch was re-grafted onto current
mainrather than merged from the ~8-month-old original branch (which was pinned to RN 0.72). The graft keptnpubByUsername(the old branch's diff wrongly deleted it), hand-grafted the generated GraphQL types (the backend invite schema isn't in the published schema yet), and added the native bits (contacts permission, a separateflash://inviteAndroid intent that leaves the widget deep-link untouched).Tests
InviteFriend(5),InviteFriendSuccess(3),HandleInviteDeepLink/redeemPendingInvite+ deep-link parsing (10).yarn jest app/screens/invite-friend/tsc --noEmitclean across the app.Notes for review
pod installran clean and regeneratedPodfile.lock.react-native-permissions ^4.1.5may want a v5 bump for RN 0.77's New Architecture at build time (JS API is stable v4→v5, so it doesn't affect the graft).Activation dependency (out of repo)
The backend sends invite links as
https://getflash.io/invite?token=…. The app registers only theflash://invitescheme — HTTPS links require either a getflash.io/invite landing page that redirects toflash://invite?token=…(recommended, no app change) or App Links / Universal Links (assetlinks.json + AASA served on getflash.io). Until one exists, HTTPS invite links open the browser, not the app.Post-review hardening (Simon review, addressed)
[^&#\s]++ decode) — the backend owns validation.{token, storedAt}with a 24h TTL — no cross-account leak.X-Invite-Recipientheader, deadgetStartedinvite branch, and unused invite route params removed.🤖 Generated with Claude Code
https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg