Skip to content

fix(native): stop the deep-link mapper from dropping the URL fragment - #2584

Merged
kushagrasarathe merged 1 commit into
devfrom
fix/native-deeplink-fragment
Jul 30, 2026
Merged

fix(native): stop the deep-link mapper from dropping the URL fragment#2584
kushagrasarathe merged 1 commit into
devfrom
fix/native-deeplink-fragment

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Summary

mapDeepLink() builds its result from parsed.pathname + parsed.search and never reads parsed.hash, so every deep link loses its fragment on the way into the native app.

That matters for one link type in particular: a claim link's password lives in the fragment (/claim?c=&v=&i=#p=<password>) and is deliberately never sent to the server — peanut-link.utils.ts parses it client-side and claim.ts only ever relays a signature. So today, tapping a claim link on a device with the app installed opens /claim?c=&v=&i= without the password: the page loads, and there is no way to claim from it. sanitizeRedirectURL already preserves .hash, so the loss was entirely in this mapper.

Fix

Re-attach the fragment once in mapDeepLink, around a new mapDeepLinkPath that keeps the existing branch logic untouched:

return `${mapDeepLinkPath(parsed)}${parsed.hash}`

Deliberately not appended inside each branch — there are six return points and a seventh is one route away. Doing it at the boundary is the only shape a future branch can't forget. parsed.hash is '' when absent (and for a bare trailing #), so no stray # is introduced.

Behaviour is otherwise unchanged: off-host links still return null, and malformed percent-escapes still degrade to null via the existing guard rather than throwing during render.

Tests

Added to src/utils/__tests__/native-routes.test.ts (there was no fragment coverage at all):

  • claim-link password preserved, full App-Links URL and bare push-payload path
  • fragment carried through a dynamic-route rewrite (/send/bob#p=x/send?recipient=bob#p=x)
  • no stray # when the link has no fragment
  • off-host link with a fragment still rejected
  • web mode preserves the fragment too

Notes for reviewers

mapDeepLink built its result from pathname + search and never read
parsed.hash, so every deep link lost its fragment entering the app.

That breaks exactly one thing badly: a claim link carries its password in
the fragment (#p=<password>) and deliberately never sends it to the
server — peanut-link.utils.ts parses it client-side and claim.ts only
relays a signature. So tapping a claim link with the app installed opened
/claim?c=&v=&i= with no password: a claim page that cannot claim.
sanitizeRedirectURL already preserved .hash, so the loss was here.

Re-attach it once at the boundary, around an extracted mapDeepLinkPath,
instead of in each of the six existing return points — a seventh is one
route away and would silently reintroduce the bug.
@innolope-dev innolope-dev self-assigned this Jul 29, 2026
@vercel

vercel Bot commented Jul 29, 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 Jul 29, 2026 10:49pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bd83a0e5-58d8-4702-abf2-d8f7eadba7a1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6739.08 → 6739.16 (+0.08)
Findings: 0 net (+6 new, -6 resolved)

🆕 New findings (6)

  • critical complexity — src/utils/native-routes.ts — CC 67, MI 60.17, SLOC 152
  • medium method-complexity — src/utils/native-routes.ts:110 — mapDeepLinkPath CC 19 SLOC 48
  • medium method-complexity — src/utils/native-routes.ts:158 — rewriteMethodPath CC 16 SLOC 31
  • low high-dlt — src/utils/native-routes.ts:110 — mapDeepLinkPath: DLT 18 (calls 18 distinct functions — high context load)
  • low high-mdd — src/utils/native-routes.ts:110 — mapDeepLinkPath: MDD 11.6 (uses across many lines from declarations)
  • low high-mdd — src/utils/native-routes.ts:158 — rewriteMethodPath: MDD 11.7 (uses across many lines from declarations)

✅ Resolved (6)

  • src/utils/native-routes.ts — CC 66, MI 59.68, SLOC 149
  • src/utils/native-routes.ts:97 — mapDeepLink CC 20 SLOC 52
  • src/utils/native-routes.ts:150 — rewriteMethodPath CC 16 SLOC 31
  • src/utils/native-routes.ts:97 — mapDeepLink: DLT 19 (calls 19 distinct functions — high context load)
  • src/utils/native-routes.ts:150 — rewriteMethodPath: MDD 11.7 (uses across many lines from declarations)
  • src/utils/native-routes.ts:97 — mapDeepLink: MDD 11.2 (uses across many lines from declarations)

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2330 ran, 0 failed, 0 skipped, 42.0s

📊 Coverage (unit)

metric %
statements 61.0%
branches 44.3%
functions 50.4%
lines 61.5%
⏱ 10 slowest test cases
time test
4.0s 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.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
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/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@kushagrasarathe
kushagrasarathe merged commit edc163d into dev Jul 30, 2026
20 checks passed
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