feat(pos-app): set up merchant ID and API key via dashboard QR code - #626
Conversation
Add a camera-based QR scanner for entering the Customer API key in Settings, so merchants don't have to type long secrets on a POS device. - New full-screen expo-router route `app/scan-api-key.tsx` using expo-camera (one lib across iOS/Android/web); camera bound to `useIsFocused` so it releases on leave; permission + denied states handled. - Viewfinder overlay (dimmed mask + square + corner brackets) built from plain Views via `components/scan-corners.tsx` (pos-app has no react-native-svg). - Scan button (tint-able `scan.png` icon) added to the Customer API key sheet; hidden on camera-less hardware via `hooks/use-has-camera.ts` (web: CameraView.isAvailableAsync; android: device-info isCameraPresent; ios: assume present). - On a successful scan, auto-save through the existing PIN/biometric gate: the value is handed back via a transient store (`store/usePendingApiKeyScanStore.ts`) and run through `handleScannedCustomerApiKey` in `hooks/use-merchant-flow.ts`. QR payload is treated as a plain key string, isolated in `utils/parse-api-key-qr.ts`. - Add expo-camera dep + config plugin (camera permission). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
|
Claude finished @ignaciosantise's task in 2m 4s —— View job Code Review in Progress
|
The merchant dashboard's API key dialog now shows a QR encoding `wpay://setup?apiKey=<key>&merchantId=<id>`. Handle it end to end: - Parse the setup deep link in `utils/parse-setup-qr.ts` (any `wpay` scheme variant, hand-rolled query parsing since RN's URL polyfill lacks searchParams); replaces the plain-key placeholder parser. - Save both values behind a single PIN/biometric check via a new "setup" pending save in `use-merchant-flow`; toast "Credentials saved". - Move scanning to a "Scan credentials QR" row in Settings → Connection (removed from the API key sheet); the scanner rejects non-setup QRs in place and keeps scanning. - Add `app/setup.tsx` as the deep link target for the device camera; it hands the payload to settings and leaves. `initialRouteName: "index"` keeps home under a cold-start deep link. - Reject scans/links in Test Mode and the dashboard iframe session. - PIN prompt names the merchant being saved (`mrch_v2jj…WJnb` via `formatShortId`). - Tests for the parser, the combined save, and `formatShortId`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Pin expo-camera to 56.0.8 (was ~56.0.8) per the repo's exact-version rule. - Camera permission now refers to the credentials QR from the merchant dashboard instead of the Customer API key QR. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ive-examples into feat/qr-scan-api-key
|
@claude review |
|
Claude finished @ignaciosantise's task in 3m 0s —— View job Code Review
🚨 PR Too Large
Found 1 issueIssue 1: Sentry may record API key from deep link route paramsID: setup-sentry-apikey-leak-b3f1 Context:
Recommendation: Scrub the params in Sentry's beforeBreadcrumb(breadcrumb) {
if (
breadcrumb.category === 'navigation' &&
breadcrumb.data?.to === 'setup'
) {
if (breadcrumb.data.params) {
breadcrumb.data.params = { apiKey: '[Filtered]', merchantId: '[Filtered]' };
}
}
return breadcrumb;
},Alternatively, the |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Permission denial can trigger repeated requests, and first-time PIN setup omits the promised merchant confirmation.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds QR-based terminal credential setup through in-app scanning and deep links, protected by existing PIN/biometric flows.
Changes:
- Adds QR parsing, camera scanning, deep-link routing, and credential handoff.
- Saves merchant ID and API key together after authentication.
- Adds camera configuration, UI, documentation, and tests.
| File | Description |
|---|---|
dapps/pos-app/utils/parse-setup-qr.ts |
Parses setup links. |
dapps/pos-app/utils/misc.ts |
Adds shortened ID formatting. |
dapps/pos-app/utils/misc.test.ts |
Tests ID formatting. |
dapps/pos-app/store/usePendingSetupStore.ts |
Holds transient setup payloads. |
dapps/pos-app/package.json |
Adds Expo Camera. |
dapps/pos-app/package-lock.json |
Locks camera dependencies. |
dapps/pos-app/hooks/use-merchant-flow.ts |
Adds combined credential saving. |
dapps/pos-app/hooks/use-has-camera.ts |
Detects camera availability. |
dapps/pos-app/components/scan-corners.tsx |
Adds scanner framing UI. |
dapps/pos-app/app/setup.tsx |
Handles setup deep links. |
dapps/pos-app/app/settings.tsx |
Consumes setup payloads and exposes scanning. |
dapps/pos-app/app/scan-setup-qr.tsx |
Implements camera scanning. |
dapps/pos-app/app/_layout.tsx |
Registers setup routes. |
dapps/pos-app/app.json |
Configures camera permission. |
dapps/pos-app/AGENTS.md |
Documents QR setup architecture. |
dapps/pos-app/__tests__/utils/parse-setup-qr.test.ts |
Tests QR parsing. |
dapps/pos-app/__tests__/hooks/use-merchant-flow.test.ts |
Tests protected combined saves. |
Files not reviewed (1)
- dapps/pos-app/package-lock.json: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Show "Enter your PIN to save these settings." for every protected save, including a scanned credentials QR, and drop the now-unused `formatShortId` helper and `pendingSetupMerchantId`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On web, history breadcrumbs record full URLs, so opening `/setup?apiKey=…` sent the Customer API key to Sentry in the next error's breadcrumbs. Native breadcrumbs only carry route names and were not affected. - Strip query strings and fragments from navigation breadcrumbs (`filterBreadcrumb`), which are still the only ones kept. - Delete `event.request` in `beforeSendTransaction` too, since transactions skip `beforeSend`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks for the review. Issue 1: Sentry may record the API key. Valid on web, but not through the path described. Fixed in c129010.
PR size. Keeping this as one PR. The pieces only work end to end (a parser and route with no way to scan, or a scanner with no parser), and several of the listed files are mechanical. The counts also predate 66e18e2, which removed the 🤖 Generated with Claude Code |
The scanner re-requested whenever permission wasn't granted and `canAskAgain` was true. expo-camera's web implementation always reports `canAskAgain: true`, so a denial looped (~23k getUserMedia calls in 6s) and the denied state never showed; Android re-prompted right after a first denial. Auto-request once, only while the permission is undetermined. After a denial or dismissal show an explicit action: "Allow camera" when the OS or browser can still prompt, otherwise "Open settings". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Parse the setup link query with URLSearchParams instead of a hand-rolled parser (RN 0.85 implements it; catch its throw on malformed percent-encoding). - ScanCorners: fixed sizes for its single caller and corners built from a list (100 → 60 lines); drop the stale "input icon" comment. - Drop parser tests that only exercised Node's URLSearchParams (malformed encoding, repeated params). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#596 changed the params_validation subtitle to "…Try the payment again…" but left this assertion expecting the old copy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>



What
Set up a terminal by scanning the QR code from the merchant dashboard instead of typing the merchant ID and Customer API key by hand.
The dashboard's API key dialog (WalletConnect/pay-merchant-experience#1113) shows a QR encoding:
It reaches the app two ways, and both end in the same PIN/biometric-gated save of both values:
wpay://setupdeep link.How
utils/parse-setup-qr.tsparses the link. It requires both values and reads the query withURLSearchParams(React Native's implementation throws on malformed percent-encoding, which is treated as an invalid link). Suffixedwpay-*schemes are still accepted, so older QR links scan too.app/scan-setup-qr.tsxis the full-screen expo-camera scanner, bound touseIsFocused, with permission and denied states. A non-setup QR shows an error once and the scanner keeps going.app/setup.tsxis the deep link target. It renders nothing: it hands the payload over and usesdismissTo("/settings"), so the key doesn't stay in the route params._layout.tsxsetsinitialRouteName: "index"so a cold-start deep link still has home underneath.store/usePendingSetupStore.tsis a transient hand-off.settings.tsxconsumes it and callshandleScannedSetupinhooks/use-merchant-flow.ts. The pending save is now a typed union, with a newsetupcase that saves the API key first (the async step that can fail), then the merchant ID. Toast: "Credentials saved".utils/sentry.ts): on web, history breadcrumbs record full URLs, so/setup?apiKey=…would have put the key into the next error's breadcrumbs.filterBreadcrumbstrips query strings and fragments from navigation breadcrumbs (still the only ones kept).filterTransactiondeletesevent.request(transactions skipbeforeSend), and it andfilterSpanstrip queries fromurl.full/http.url/route.urlin trace and span data. Native wasn't affected: its navigation breadcrumbs only carry route names, and params and URLs are only attached withsendDefaultPii, which is off. Sentry's default server-side scrubbing would likely also redact these values (its password rule matchesapikeyinside values), but that depends on a project setting, and Sentry recommends scrubbing in the SDK so the key never leaves the device.scan.pngand is hidden on camera-less hardware (hooks/use-has-camera.ts). The viewfinder overlay is plain Views (components/scan-corners.tsx), since pos-app has noreact-native-svg.expo-camera56.0.8and its config plugin (camera permission). pos-app is pure CNG, so this applies at prebuild.Notes
inlineRequires), so cold start is unaffected.wpay, since the dashboard always emitswpay://. With both installed, the OS decides which app opens a device-camera link. The in-app scanner works in either.Testing
__tests__/utils/parse-setup-qr.test.ts,__tests__/hooks/use-merchant-flow.test.ts(combined save and cancel), and__tests__/utils/sentry.test.ts(breadcrumb scrubbing). Also updates a staleparams_validationcopy assertion inutils/payment-errors.test.ts, left over from chore(pos): design review — POS app redesign & polish #596. Full suite: 309/309./setup?apiKey=sk_SECRET…and triggering an error, the key appeared in the breadcrumbs before the fix and in none of the payloads after it.&):adb shell 'am start -a android.intent.action.VIEW -d "wpay://setup?apiKey=test_key_123&merchantId=test-merchant-qr"'🤖 Generated with Claude Code