Skip to content

feat(native): deferred deep linking through the store install (TASK-20772) - #2557

Closed
kushagrasarathe wants to merge 5 commits into
devfrom
feat/deferred-deep-link
Closed

feat(native): deferred deep linking through the store install (TASK-20772)#2557
kushagrasarathe wants to merge 5 commits into
devfrom
feat/deferred-deep-link

Conversation

@kushagrasarathe

@kushagrasarathe kushagrasarathe commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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:

  • Android — the web side appends &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.
  • iOS — clipboard hand-off: the store-bounce tap copies https://peanut.me/?pnutdl=1&… to the clipboard; first launch reads it via @capacitor/clipboard, gated by the existing prompt-free clipboardHasStrings() so organic installs with an empty clipboard never see the iOS paste prompt.
  • Restore (one-shot, deferredLinkConsumed flag): sets the existing inviteCode/campaignTag cookies (setup flow already reads them), applies the locale live via useAppLocale().setLocale (normalized through resolveLocale — 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.
  • Web side ships helpers + /dev/deferred test page only — the user-facing download modal (TASK-20769) will consume buildDeferredPayload / playStoreUrlWithReferrer / copyIOSHandoff when it lands.

Design notes / accepted trade-offs

  • Consumed flag lives in localStorage (survives Capgo OTA; resets on reinstall — which is exactly fresh-install semantics).
  • iOS clipboard is cleared after a successful consume, after the flag is set, so an interrupted clear can't cause a re-read.
  • The iOS paste prompt is double-gated by prompt-free metadata checks (hasStrings + new detectPatterns probable-web-URL in ClipboardDetectPlugin.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.
  • Restore is deliberately not awaited in native init: a pending paste prompt or a slow referrer service can't block the push-tap listener or splash hide. The Java plugin additionally resolves exactly once with a 5s timeout so its promise can never hang.
  • A deep link that actually navigated wins the landing over the deferred dest (a rejected/off-host launch URL doesn't burn it); cookies/locale are restored regardless. The restored locale applies after the startup locale paints, so the provider's initial swap can't clobber it.
  • Cookies are written with 30-day expiry and normalized (trim/lowercase/strip-@) to match the existing invite/campaign writers — a session cookie died before signup completed, and an uppercase campaign tag would break the case-sensitive badge award.
  • Locale-prefixed marketing paths (/es-419/…) are stripped from dests on both sides — those routes don't exist in the native static export.
  • Organic Play referrer (utm_source=google-play…) is rejected by the pnutdl=1 marker.

Risks / breaking changes

  • Requires a new Android binary (new plugin + Gradle dep) — cannot ship via Capgo OTA. Older binaries no-op safely (plugin bridge throws → caught → null).
  • iOS: adds one prompt-free method to 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.
  • No backend changes. No user-facing web surface changes.

QA

  • 15 unit tests (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:compileDebugJavaWithJavac green with the new plugin.
  • Device plan: iOS e2e via dev build (copy hand-off in Safari → fresh install → paste prompt → /dev/deferred shows 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

  • New Features

    • Added deferred deep-link support to preserve destinations, language, invites, and campaigns across app installation.
    • Restores Android install-referrer and iOS handoff data when the app launches.
    • Added a developer page for inspecting and simulating deferred-link restoration.
    • Added Google Play Store linking and safer iOS clipboard handoff detection.
  • Bug Fixes

    • Prevented deferred links from overriding successfully handled deep links.
    • Improved recovery when handoff data is invalid, unsupported, unavailable, or read more than once.
    • Watchdog checks now handle command failures safely and avoid false pipeline comparisons.
  • Tests

    • Added comprehensive coverage for deferred-link parsing, restoration, locale handling, and native startup behavior.

Hugo0 and others added 3 commits July 28, 2026 22:26
Third fix, same root cause each time: I verified in an environment that was not
the one it runs in.

GitHub runs the step as `bash -e {0}`. Combined with `set -o pipefail` that
means any non-zero anywhere kills the script at that line with NO output — a red
run and an empty log, which is the least debuggable possible failure. It hit
twice: the Actions-API 403, then `grep` returning 1 because a commit touched no
mirrored paths. grep exiting 1 on no-match is a normal answer, not an error, and
it was the very first loop iteration — so the last run produced literally
nothing between the env block and `exit 1`.

`set +e` explicitly. Every branch here decides for itself and reports why; `-e`
adds nothing but the ability to die mid-check without saying so.

Also filter LIVE/TIP/EXPECTED through a 40-hex check. On failure `gh` prints its
error body to stdout, which was landing in those vars and being compared as if
it were a sha; now a broken call resolves to empty and trips the labelled
DEGRADED branch instead.

Verified by extracting the step and running it under `bash -e` against the real
APIs, both paths: healthy -> "production content is current", exit 0, silent;
broken token -> one WATCHDOG DEGRADED line, exit 1. That is the check I should
have been running from the first commit.
…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.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 28, 2026 5:30pm

Request Review

@notion-workspace

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Deferred deep-link flow

Layer / File(s) Summary
Handoff payload construction and parsing
src/utils/deferred-link.ts, src/utils/clipboard-detect.ts, src/constants/general.consts.ts, src/utils/__tests__/deferred-link.test.ts
Adds encoded web-to-store payloads, Android and iOS handoff helpers, probable-URL clipboard detection, parsing validation, and associated tests.
Native restoration and app initialization
src/utils/deferred-link.ts, src/hooks/useNativePlugins.ts, src/hooks/__tests__/useNativePlugins.test.tsx, src/utils/__tests__/deferred-link.test.ts
Restores handoffs once from native sources, applies cookies and locale, sanitizes destinations, avoids overriding successful deep-link navigation, and validates failure and concurrency behavior.
Development inspection and simulation
src/app/dev/deferred/page.tsx
Adds a client-side page for inspecting deferred state, generating handoffs, reading referrers, copying iOS links, and simulating restoration.

Content pipeline watchdog resilience

Layer / File(s) Summary
Watchdog command and SHA evaluation
.github/workflows/content-pipeline-watchdog.yml
Keeps the watchdog running after intermediate command failures and filters captured values to valid commit SHAs before evaluation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebPage
  participant Store
  participant NativeApp
  participant DeferredLink
  WebPage->>DeferredLink: build payload and handoff URL
  DeferredLink->>Store: encode payload in Play referrer or iOS handoff
  Store->>NativeApp: install and launch
  NativeApp->>DeferredLink: restore deferred context
  DeferredLink-->>NativeApp: destination and locale
  NativeApp->>NativeApp: apply locale and navigate when eligible
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: innolope-dev

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: implementing deferred deep linking for native store installs.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deferred-deep-link

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2332 ran, 0 failed, 0 skipped, 40.7s

📊 Coverage (unit)

metric %
statements 61.3%
branches 44.4%
functions 50.6%
lines 61.8%
⏱ 10 slowest test cases
time test
3.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6656.05 → 6671.93 (+15.88)
Findings: +6 net (+14 new, -8 resolved)

🆕 New findings (14)

  • high complexity — src/hooks/useNativePlugins.ts — CC 43, MI 62.5, SLOC 203
  • high complexity — src/utils/deferred-link.ts — CC 42, MI 57.86, SLOC 156
  • medium high-mdd — src/hooks/useNativePlugins.ts:23 — useNativePlugins: MDD 42.5 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/useNativePlugins.ts:23 — useNativePlugins: DLT 39 (calls 39 distinct functions — high context load)
  • medium high-mdd — src/hooks/useNativePlugins.ts:27 — : MDD 39.4 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/useNativePlugins.ts:27 — : DLT 36 (calls 36 distinct functions — high context load)
  • medium high-mdd — src/app/dev/deferred/page.tsx:22 — DeferredLinkDevPage: MDD 29.5 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/useNativePlugins.ts:56 — init: DLT 30 (calls 30 distinct functions — high context load)
  • medium high-mdd — src/hooks/useNativePlugins.ts:56 — init: MDD 24.5 (uses across many lines from declarations)
  • medium complexity — src/app/dev/deferred/page.tsx — CC 16, MI 64.41, SLOC 76
  • low high-dlt — src/app/dev/deferred/page.tsx:22 — DeferredLinkDevPage: DLT 25 (calls 25 distinct functions — high context load)
  • low high-mdd — src/utils/deferred-link.ts:153 — doRestore: MDD 11.4 (uses across many lines from declarations)
  • low missing-return-type — src/app/dev/deferred/page.tsx:22 — DeferredLinkDevPage: exported fn missing return type annotation
  • low missing-return-type — src/hooks/useNativePlugins.ts:23 — useNativePlugins: exported fn missing return type annotation

✅ Resolved (8)

  • src/hooks/useNativePlugins.ts — CC 33, MI 61.76, SLOC 166
  • src/hooks/useNativePlugins.ts:22 — useNativePlugins: MDD 37.2 (uses across many lines from declarations)
  • src/hooks/useNativePlugins.ts:22 — useNativePlugins: DLT 36 (calls 36 distinct functions — high context load)
  • src/hooks/useNativePlugins.ts:25 — : MDD 35.2 (uses across many lines from declarations)
  • src/hooks/useNativePlugins.ts:25 — : DLT 34 (calls 34 distinct functions — high context load)
  • src/hooks/useNativePlugins.ts:46 — init: MDD 25.2 (uses across many lines from declarations)
  • src/hooks/useNativePlugins.ts:46 — init: DLT 27 (calls 27 distinct functions — high context load)
  • src/hooks/useNativePlugins.ts:22 — useNativePlugins: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/deferred-link.ts 0.0 7.8 +7.8
src/app/dev/deferred/page.tsx 0.0 6.8 +6.8
src/utils/clipboard-detect.ts 4.3 4.8 +0.5

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- 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)
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @.github/workflows/content-pipeline-watchdog.yml:
- Around line 94-102: Sanitize every API-derived value before use: apply the
existing only_sha helper to TIP_AT before the date/AGE calculation, ensuring
invalid or failed responses produce the safe empty path rather than reaching
date -d. Update MIRRORED validation to require an exact 40-character lowercase
SHA, matching only_sha, before performing the prefix comparison.

In `@src/app/dev/deferred/page.tsx`:
- Around line 22-59: Gate DeferredLinkDevPage from production access by
restoring the existing /dev proxy restriction; alternatively, remove its
mutating controls and related state-changing actions, including simulate/apply
behavior that can rewrite cookies or clear deferredLinkConsumed. Keep the
diagnostic functionality only where it cannot modify production state.
🪄 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: e2d0ea92-320f-4b98-b2a1-54ad0afa0785

📥 Commits

Reviewing files that changed from the base of the PR and between 134cf01 and b39fffa.

⛔ Files ignored due to path filters (4)
  • android/app/build.gradle is excluded by !android/**
  • android/app/src/main/java/me/peanut/wallet/InstallReferrerPlugin.java is excluded by !android/**
  • android/app/src/main/java/me/peanut/wallet/MainActivity.java is excluded by !android/**
  • ios/App/App/ClipboardDetectPlugin.swift is excluded by !ios/**
📒 Files selected for processing (8)
  • .github/workflows/content-pipeline-watchdog.yml
  • src/app/dev/deferred/page.tsx
  • src/constants/general.consts.ts
  • src/hooks/__tests__/useNativePlugins.test.tsx
  • src/hooks/useNativePlugins.ts
  • src/utils/__tests__/deferred-link.test.ts
  • src/utils/clipboard-detect.ts
  • src/utils/deferred-link.ts

Comment on lines +94 to +102
# On failure `gh` prints its error body to stdout, which would land
# in these vars and read as a bogus sha. Keep only real ones, so a
# broken call resolves to empty and trips the DEGRADED branch below
# instead of being compared as if it were data.
only_sha() { printf '%s' "${1:-}" | grep -Eox '[0-9a-f]{40}' || true; }
LIVE=$(only_sha "$LIVE")
TIP=$(only_sha "$TIP")
EXPECTED=$(only_sha "$EXPECTED")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Sanitize all API-derived values before continuing.

only_sha() protects LIVE, TIP, and EXPECTED, but a failed TIP_AT request can still place gh’s error body into TIP_AT; Line [135] then passes it to date -d, potentially leaving AGE invalid and aborting under set -u. Also validate MIRRORED from Line [103] as an exact 40-character SHA instead of accepting [0-9a-f]+ before the prefix comparison.

🤖 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 @.github/workflows/content-pipeline-watchdog.yml around lines 94 - 102,
Sanitize every API-derived value before use: apply the existing only_sha helper
to TIP_AT before the date/AGE calculation, ensuring invalid or failed responses
produce the safe empty path rather than reaching date -d. Update MIRRORED
validation to require an exact 40-character lowercase SHA, matching only_sha,
before performing the prefix comparison.

Comment on lines +22 to +59
export default function DeferredLinkDevPage() {
const { locale, setLocale } = useAppLocale()
const [state, setState] = useState<Record<string, string>>({})
const [payload, setPayload] = useState('')
const [rawReferrer, setRawReferrer] = useState('(not read)')
const [simulateInput, setSimulateInput] = useState('')
const [simulateResult, setSimulateResult] = useState('')
const [copied, setCopied] = useState(false)

const refresh = useCallback(() => {
setState({
platform: getPlatform(),
appLocale: locale,
consumedFlag: localStorage.getItem(CONSUMED_KEY) ?? '(unset)',
inviteCodeCookie: String(getFromCookie('inviteCode') ?? '(unset)'),
campaignTagCookie: String(getFromCookie('campaignTag') ?? '(unset)'),
})
}, [locale])

useEffect(() => refresh(), [refresh])

const readReferrer = async () => {
setRawReferrer((await readInstallReferrer()) ?? '(null / unavailable)')
}

const simulate = async () => {
const parsed = parseDeferredPayload(simulateInput)
if (!parsed) {
setSimulateResult('rejected (no pnutdl marker)')
} else {
const { dest, locale: restoredLocale } = applyDeferredPayload(parsed)
if (restoredLocale) await setLocale(restoredLocale)
setSimulateResult(
`applied ${JSON.stringify(parsed)} → dest: ${dest ?? 'none'}, locale: ${restoredLocale ?? 'none'}`
)
}
refresh()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f middleware.ts src
rg -n "NODE_ENV|process.env" src/app/dev --type=tsx --type=ts -g '!**/node_modules/**'
fd -t d dev src/app

Repository: peanutprotocol/peanut-ui

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app/dev tree ==\n'
find src/app/dev -maxdepth 3 -type f | sort

printf '\n== app route/layout files near dev ==\n'
find src/app -maxdepth 3 \( -name 'layout.tsx' -o -name 'page.tsx' -o -name 'middleware.ts' -o -name 'route.ts' \) | sort | sed -n '1,200p'

printf '\n== search for dev-only gating ==\n'
rg -n "NODE_ENV|process\.env|DEV|development|production|draftMode|redirect\\(|notFound\\(" src/app src/middleware.ts src -g '!**/node_modules/**' || true

Repository: peanutprotocol/peanut-ui

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/app/dev/deferred/page.tsx ---'
cat -n src/app/dev/deferred/page.tsx | sed -n '1,220p'

echo
echo '--- src/app/dev tree files ---'
find src/app/dev -maxdepth 3 -type f | sort | xargs -I{} sh -c 'echo; echo "### {}"; cat -n "{}" | sed -n "1,220p"'

echo
echo '--- search for route-level gating around dev paths ---'
rg -n "redirect\\(|notFound\\(|draftMode|process\\.env|NODE_ENV|export const dynamic|export const runtime|cookies\\(|headers\\(" src/app/dev src/app -g '!**/node_modules/**' || true

Repository: peanutprotocol/peanut-ui

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/proxy.ts ---'
cat -n src/proxy.ts | sed -n '1,120p'

echo
echo '--- deferred-link utilities ---'
rg -n "export .*applyDeferredPayload|export .*parseDeferredPayload|export .*CONSUMED_KEY|inviteCode|campaignTag|setCookie|cookie" src/utils/deferred-link* src/utils -g '!**/node_modules/**' || true

echo
echo '--- src/utils/deferred-link file(s) ---'
for f in $(git ls-files 'src/utils/deferred-link*'); do
  echo
  echo "### $f"
  cat -n "$f" | sed -n '1,260p'
done

Repository: peanutprotocol/peanut-ui

Length of output: 27478


Gate /dev/deferred behind a production check. /dev/* is already reachable on peanut.me, so this page can still rewrite inviteCode/campaignTag cookies and clear deferredLinkConsumed in production. If it’s meant to stay internal, re-add the /dev block in the proxy or remove the mutating controls from the shipped page.

🤖 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 22 - 59, Gate DeferredLinkDevPage
from production access by restoring the existing /dev proxy restriction;
alternatively, remove its mutating controls and related state-changing actions,
including simulate/apply behavior that can rewrite cookies or clear
deferredLinkConsumed. Keep the diagnostic functionality only where it cannot
modify production state.

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Superseded by #2559 — the rebase onto dev had replayed main's watchdog hotfix (e7c688b) into this branch, polluting the diff with a file outside this feature's scope. #2559 is the identical feature set cherry-picked onto a clean dev base (plus the /dev/deferred prod-wall from the review here). Closing.

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.

2 participants