feat(native): deferred deep linking through the store install (TASK-20772) - #2559
feat(native): deferred deep linking through the store install (TASK-20772)#2559kushagrasarathe wants to merge 5 commits into
Conversation
…0772) Carry locale + invite code + campaign tag + destination path from mobile web through the app-store hop. Android rides the Play Install Referrer (app-local InstallReferrerPlugin); iOS rides a clipboard hand-off gated by the prompt-free clipboardHasStrings check. One-shot restore on first launch applies the existing inviteCode/campaignTag cookies, persists preferredLocale for future in-app i18n, and navigates only when the launch wasn't already a real deep link.
dev's in-app i18n landed while this branched off main — the restore now returns the normalized AppLocale and applies it via useAppLocale's setLocale (live switch + native Preferences persistence) instead of a parallel localStorage key. unsupported tags return null so a garbage payload can never override the device language.
- 30-day normalized cookies (session cookies died before signup; uppercase
campaign tags broke the case-sensitive badge award)
- strip marketing locale prefix from dest (native export has no /{locale})
- InstallReferrerPlugin: resolve exactly once + disconnect handler + 5s
timeout so the PluginCall can never hang app init
- restore no longer awaited in init: iOS paste prompt / slow referrer
service can't block the push listener or splash hide
- dest yields only to a deep link that actually navigated, not to a
rejected launch url; locale applies after startup locale paints
- iOS prompt gated by prompt-free UIPasteboard.detectPatterns probable-URL
check: unrelated clipboard text never triggers the paste alert
- in-flight guard: strict-mode double-effects share one read
- hook wiring tests (dest precedence, locale apply, failure isolation)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds deferred deep-link payload generation and parsing, Android/iOS native restoration, locale-aware routing safeguards, comprehensive tests, and a Capacitor-only development page for inspecting and simulating handoffs. ChangesDeferred deep-linking
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant WebPage
participant PlayStore
participant NativeApp
participant DeferredLink
participant Router
WebPage->>DeferredLink: build deferred payload
WebPage->>PlayStore: open handoff URL with referrer
NativeApp->>DeferredLink: restore install context
DeferredLink-->>NativeApp: restored destination and locale
NativeApp->>Router: navigate when no deep link already handled
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Code-analysis diffPainscore total: 6656.05 → 6672.29 (+16.24) 🆕 New findings (14)
✅ Resolved (8)
📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/dev/deferred/page.tsx`:
- Line 31: Update the deferred page’s copied state and clipboard handler so
success is tracked for the current payload only. Reset copied whenever the
payload changes, and associate each asynchronous copy attempt with its payload
so a completion from an older payload cannot set copied for the new one.
- Around line 63-67: Update the gate in the deferred page to use the build-time
native flag rather than the runtime isCapacitor() bridge check, while preserving
the BASE_URL condition and existing notFound() behavior for web production. Keep
the change scoped to this post-hooks access guard.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7b5fc5a3-641f-4b0f-ac7d-f667a9a187ff
⛔ Files ignored due to path filters (4)
android/app/build.gradleis excluded by!android/**android/app/src/main/java/me/peanut/wallet/InstallReferrerPlugin.javais excluded by!android/**android/app/src/main/java/me/peanut/wallet/MainActivity.javais excluded by!android/**ios/App/App/ClipboardDetectPlugin.swiftis excluded by!ios/**
📒 Files selected for processing (7)
src/app/dev/deferred/page.tsxsrc/constants/general.consts.tssrc/hooks/__tests__/useNativePlugins.test.tsxsrc/hooks/useNativePlugins.tssrc/utils/__tests__/deferred-link.test.tssrc/utils/clipboard-detect.tssrc/utils/deferred-link.ts
| const [rawReferrer, setRawReferrer] = useState('(not read)') | ||
| const [simulateInput, setSimulateInput] = useState('') | ||
| const [simulateResult, setSimulateResult] = useState('') | ||
| const [copied, setCopied] = useState(false) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Track clipboard success per payload.
Building a new payload leaves copied true; a pending copy of an older payload can also later mark the new one as copied. The UI can claim success for content never copied.
Proposed fix
- const [copied, setCopied] = useState(false)
+ const [copiedPayload, setCopiedPayload] = useState<string | null>(null)
...
- onClick={() => setPayload(buildDeferredPayload('/home'))}
+ onClick={() => {
+ setPayload(buildDeferredPayload('/home'))
+ setCopiedPayload(null)
+ }}
...
await copyIOSHandoff(payload)
- setCopied(true)
+ setCopiedPayload(payload)
...
- {copied ? 'copied ✓' : 'copy ios hand-off to clipboard'}
+ {copiedPayload === payload ? 'copied ✓' : 'copy ios hand-off to clipboard'}Also applies to: 94-116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dev/deferred/page.tsx` at line 31, Update the deferred page’s copied
state and clipboard handler so success is tracked for the current payload only.
Reset copied whenever the payload changes, and associate each asynchronous copy
attempt with its payload so a completion from an older payload cannot set copied
for the new one.
| // same wall as (mobile-ui)/dev/layout.tsx, with a native exception: the | ||
| // capacitor build ships with BASE_URL=peanut.me but needs this page for | ||
| // on-device verification. web prod stays blocked. (after hooks — throwing | ||
| // before them would break the rules of hooks.) | ||
| if (BASE_URL === 'https://peanut.me' && !isCapacitor()) notFound() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
# Inspect the platform detector's server-side path.
fd -a '^capacitor\.(ts|tsx|js|jsx)$' src/utils
rg -n -C 6 '\bisCapacitor\b' src/utils
# Check whether the app is configured for static output/prerendering.
fd -a '^next\.config\.(js|mjs|ts)$' .
rg -n -C 4 'output|static|export' next.config.* 2>/dev/null || trueRepository: peanutprotocol/peanut-ui
Length of output: 46428
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '1,140p' src/app/dev/deferred/page.tsx
printf '\n---\n'
sed -n '1,120p' src/utils/capacitor.ts
printf '\n---\n'
sed -n '1,120p' next.config.native.jsRepository: peanutprotocol/peanut-ui
Length of output: 11760
🏁 Script executed:
#!/bin/bash
set -eu
rg -n -C 4 'export const BASE_URL|BASE_URL =' src/constants src/utils
sed -n '1,120p' src/constants/general.consts.tsRepository: peanutprotocol/peanut-ui
Length of output: 9130
Defer this gate until runtime
In the native static export, the server render sees isCapacitor() as false, so this branch can turn the page into a 404 before the WebView loads. Use the build-time native flag here instead of the runtime bridge check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dev/deferred/page.tsx` around lines 63 - 67, Update the gate in the
deferred page to use the build-time native flag rather than the runtime
isCapacitor() bridge check, while preserving the BASE_URL condition and existing
notFound() behavior for web production. Keep the change scoped to this
post-hooks access guard.
|
Superseded by #2560 — this needs to base on |
Summary
TASK-20772 — deferred deep linking v1 (DIY). When a mobile-web user is bounced to the Play Store / App Store, their context (locale, invite code, campaign tag, destination path) now survives the install into the native app:
&referrer=<payload>to the Play URL; a new app-local Capacitor plugin (InstallReferrerPlugin.java, Play Install Referrer library) reads it back on first launch. Deterministic, first-party.https://peanut.me/?pnutdl=1&…to the clipboard; first launch reads it via@capacitor/clipboard, gated by the existing prompt-freeclipboardHasStrings()so organic installs with an empty clipboard never see the iOS paste prompt.deferredLinkConsumedflag): sets the existinginviteCode/campaignTagcookies (setup flow already reads them), applies the locale live viauseAppLocale().setLocale(normalized throughresolveLocale— unsupported tags are dropped so a garbage payload can never override the device language), and navigates to the destination only when the launch wasn't already a real deep link./dev/deferredtest page only — the user-facing download modal (TASK-20769) will consumebuildDeferredPayload/playStoreUrlWithReferrer/copyIOSHandoffwhen it lands.Design notes / accepted trade-offs
localStorage(survives Capgo OTA; resets on reinstall — which is exactly fresh-install semantics).hasStrings+ newdetectPatternsprobable-web-URL inClipboardDetectPlugin.swift): organic installs with unrelated clipboard text (a password, a draft) are never prompted — only a clipboard that plausibly holds the hand-off URL is worth the alert./es-419/…) are stripped from dests on both sides — those routes don't exist in the native static export.utm_source=google-play…) is rejected by thepnutdl=1marker.Risks / breaking changes
ClipboardDetectPlugin.swift— rides the next IPA (iOS is mid-submission; no binaries in the field). On a binary without the method the JS skips the clipboard read entirely: no prompt, no restore, status quo.QA
deferred-link.test.ts): build→parse round-trip, Play URL encoding hop, iOS hand-off form, organic-referrer rejection, one-shot consume, off-origin dest rejection, locale normalization.:app:compileDebugJavaWithJavacgreen with the new plugin./dev/deferredshows restored state); Android via Play internal testing track with a&referrer=install link; sideloaded builds verified to no-op.Screenshots: N/A (no visible change — dev-only page + native plumbing)
Summary by CodeRabbit