Skip to content

[Home refresh] Unified Scan / Receive screen#944

Merged
CassioMG merged 20 commits into
mainfrom
cg-scan-receive-tabs
Jul 23, 2026
Merged

[Home refresh] Unified Scan / Receive screen#944
CassioMG merged 20 commits into
mainfrom
cg-scan-receive-tabs

Conversation

@CassioMG

@CassioMG CassioMG commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Part of the Home refresh design work.

What this does

Introduces a single Scan / Receive screen that replaces the two separate screens we had for scanning a QR code and showing your account's QR code. A tab control in the header switches between:

  • Scan — the camera scanner (send to an address / connect a dApp via WalletConnect)
  • Receive — your account's QR code + "Copy wallet address"

Switching tabs does a smooth fade, and the camera stays warm underneath so there's no flicker when you jump back to Scan.

🎨 Figma: Scan / Receive design

Where it opens from

Entry point Opens on
Home header scan button (top-right) Scan tab
Home ⋯ → My QR code Receive tab
Add Funds → Transfer from another account Receive tab
Send → scan button (top-right) Scan-only view

Also included

  • A new reusable /sds/Tabs design-system component (hug / full-width / fixed-width sizing) used in the header. It replaces the old SegmentedControl, which is removed.
  • The old AccountQRCodeScreen is removed (its consumers now use the Receive tab). The Send-flow scanner is untouched — it keeps its own screen.
  • Existing analytics are preserved (per-tab screen views + QR scan success/error).

📹 Demo video

One video covering all four scanner flows:

  1. Home → scan button → unified Scan tab
  2. Home → ⋯ → My QR code → unified Receive tab
  3. Add Funds → Transfer from another account → unified Receive tab
  4. Send flow → scan a recipient address (existing Send-flow scanner, unchanged — included for full coverage)
scan-receive-ux.mov
🔧 Technical details (for reviewers / agents)

Structure

New src/components/screens/ScanReceiveScreen/:

  • ScanReceiveScreen.tsx — orchestrator. Holds activeTab (from route.params.initialTab, default "scan"). Renders a floating header (X close + centered Tabs) over two absolute-fill layers: the scanner and an opaque Receive layer whose opacity is animated (Animated, ~200ms) to crossfade over the camera.
  • ScanTabView.tsx — headerless scanner body. Reuses the existing useQRCodeScreenScanner(HOME_SCANNER) hook + QRScanner, so all address / WalletConnect handling and QR analytics are unchanged. Also hosts the dev-only WalletConnect manual-input overlay.
  • ReceiveTabView.tsx — avatar + name + truncated address, QR (react-native-qrcode-svg), "Stellar" chip, helper text, full-width tertiary "Copy wallet address" button.

Camera lifecycle (why it doesn't blink)

Toggling vision-camera's isActive off/on tears down and re-initializes the capture session, which shows a black frame (the blink). Instead:

  • The camera stays active while the screen is focused (useIsFocused), across both tabs — the opaque Receive layer just fades over the live feed.
  • Scanning is gated to the Scan tab (isScanning) so the still-running camera behind Receive can't trigger a navigation.
  • Camera pauses on blur (navigating away / backgrounding).
  • QRScanner gained an optional isActive prop (default true) for this.

Platform note: on Android the preview still shows a brief blank on first camera warm-up (CameraX/SurfaceView cold start) — a platform difference from iOS, not the re-init blink. Can be masked later with a placeholder-until-ready if desired.

/sds/Tabs

  • sizing: "hug" (default, all pills sized to the widest label via an onLayout measure pass, hidden for the single measuring frame to avoid a width jump), "fill", or fixed width.
  • Replaced SegmentedControl; the two DeveloperBackendConfig usages migrated to <Tabs sizing="fill" />.
  • Centering gotcha: hug mode uses alignSelf: flex-start, so a consumer that wants it centered must center it on the main axis (flex-row justify-center), not the cross axis (items-center). The header does this.

Analytics (per-tab, preserved)

  • The unified route is added to ROUTES_WITHOUT_ANALYTICS so it does not auto-fire a generic view event.
  • Instead, a useEffect([activeTab]) fires VIEW_SCAN_QR_CODE (new enum, "loaded screen: scan qr code") on Scan and VIEW_ACCOUNT_QR_CODE on Receive — matching the previous per-screen tracking on both initial open and tab switches.
  • QR_SCAN_SUCCESS / QR_SCAN_ERROR are untouched (reused hook + QRScanner).

Navigation / cleanup

  • New route SCAN_RECEIVE_SCREEN (+ ScanReceiveTab type), registered in RootNavigator and (mirroring how AccountQRCodeScreen was) in the Add Funds stack so it opens within that stack.
  • AccountQRCodeScreen + its test deleted; ACCOUNT_QR_CODE_SCREEN route/params removed.
  • Home header (useHomeHeaders) and AddFundsScreen now navigate to the unified screen.

Testing / infra

  • New tests: ScanReceiveScreen (initial tab, per-tab analytics, close), ReceiveTabView (renders address, copies public key), plus analyticsConfig assertions. /sds/Tabs has its own unit tests.
  • Full suite: 196 suites / 2685 tests pass.
  • Infra: jest.config.js now ignores nested .claude/ git worktrees, which were colliding via Haste and breaking the suite.

🤖 Generated with Claude Code

CassioMG and others added 11 commits July 21, 2026 15:03
Reusable segmented tab control matching the Figma Tabs designs. Supports
three sizing modes: hug-content (default, all pills sized to the widest
label), full-width, and fixed width. nativewind-styled; hug mode measures
pill widths and stays hidden for the single measuring frame to avoid a
width jump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the two DeveloperBackendConfig usages to <Tabs sizing="fill" />
and remove the now-redundant SegmentedControl component.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nested git worktrees under .claude/ (created by Claude Code sessions) are
full repo copies; Jest's Haste scan of their node_modules/__mocks__ collided
with the main project and broke the suite. Exclude .claude/ from module and
test path resolution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mera

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m auto analytics

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er hook

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…per-tab analytics

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…emove AccountQRCodeScreen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Stellar logo, tertiary copy button

- Center the Scan/Receive tabs on screen (the Tabs hug container's
  self-start was defeating the parent's items-center; use a row with
  justify-center instead).
- Lift the dev-only WalletConnect manual-input overlay above the bottom
  safe-area inset instead of pinning it to the screen edge.
- Render the Stellar network chip logo as a circle.
- Make the Copy wallet address button tertiary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CassioMG CassioMG changed the title [WIP] Unified Scan / Receive screens [WIP] Unified Scan / Receive screen Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-f1c51182b8ef2d576dc8 (SDF collaborators only — install instructions in the release description)

CassioMG and others added 5 commits July 22, 2026 10:22
…ide margins

- Keep the camera session active while the screen is focused (both tabs)
  instead of toggling it per-tab, so switching Receive -> Scan no longer
  blinks from the capture session re-initializing. Scanning is gated to the
  Scan tab so the still-running camera behind Receive can't trigger a
  navigation; the camera still pauses on blur (useIsFocused).
- Give the dev-only WalletConnect overlay 20px horizontal margins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… overlay

Match the app-wide bottom spacing (insets.bottom + DEFAULT_PADDING, as
BaseLayout applies on the Asset Details and Swap screens) for the Receive
copy button and the dev WalletConnect overlay, instead of the ad-hoc 16px.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n title

Pin the copy icon to the right of the label and color it with the tertiary
button's text color (near-black) so it matches the title instead of using the
theme-dependent foreground color.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Helper text uses md (Inter Medium 16, secondary) per Figma Text/MD/500,
  was sm.
- Copy icon size 18px.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CassioMG CassioMG changed the title [WIP] Unified Scan / Receive screen [Home refresh] Unified Scan / Receive screen Jul 22, 2026
@CassioMG
CassioMG marked this pull request as ready for review July 22, 2026 15:07
Copilot AI review requested due to automatic review settings July 22, 2026 15:07

Copilot AI 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.

Pull request overview

Unifies scanning and receiving into one tabbed screen while preserving navigation, analytics, and send-flow scanning.

Changes:

  • Adds the Scan/Receive screen with persistent camera and QR display.
  • Introduces reusable SDS Tabs and migrates existing controls.
  • Updates navigation, translations, analytics, and tests.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/navigators/RootNavigator.tsx Registers the unified screen.
src/navigators/AddFundsNavigator.tsx Adds the screen to Add Funds.
src/i18n/locales/pt/translations.json Adds Portuguese strings.
src/i18n/locales/en/translations.json Adds English strings.
src/hooks/useHomeHeaders.tsx Redirects Home entry points.
src/helpers/navigationOptions.tsx Generalizes transition documentation.
src/config/routes.ts Defines unified route parameters.
src/config/analyticsConfig.ts Adds per-tab analytics handling.
src/components/sds/Tabs/index.tsx Implements the Tabs control.
src/components/sds/SegmentedControl.tsx Removes the superseded control.
src/components/screens/ScanReceiveScreen/ScanTabView.tsx Adds the scanner tab body.
src/components/screens/ScanReceiveScreen/ScanReceiveScreen.tsx Implements tab orchestration and animation.
src/components/screens/ScanReceiveScreen/ReceiveTabView.tsx Adds account QR and copy action.
src/components/screens/ScanReceiveScreen/index.ts Exports the unified screen.
src/components/screens/ChangeNetworkScreen/DeveloperBackendConfig.tsx Migrates controls to Tabs.
src/components/screens/AddFundsScreen/AddFundsScreen.tsx Opens the Receive tab.
src/components/screens/AccountQRCodeScreen/index.ts Removes the old export.
src/components/screens/AccountQRCodeScreen/AccountQRCodeScreen.tsx Removes the old QR screen.
src/components/QRScanner.tsx Adds camera activation control.
jest.setup.js Mocks navigation focus.
jest.config.js Excludes nested worktrees.
__tests__/Tabs.test.tsx Tests Tabs behavior.
__tests__/config/analyticsConfig.test.ts Tests analytics exclusions.
__tests__/components/screens/ScanReceiveScreen/ScanReceiveScreen.test.tsx Tests tabs, analytics, and closing.
__tests__/components/screens/ScanReceiveScreen/ReceiveTabView.test.tsx Tests account display and copying.
__tests__/components/screens/AddFundsScreen/AddFundsScreen.test.tsx Updates navigation expectations.
__tests__/components/screens/AccountQRCodeScreen/AccountQRCodeScreen.test.tsx Removes obsolete tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/screens/ScanReceiveScreen/ScanReceiveScreen.tsx Outdated
Comment thread src/components/screens/ScanReceiveScreen/ScanReceiveScreen.tsx Outdated
Comment thread src/components/screens/ScanReceiveScreen/ScanTabView.tsx Outdated
Comment thread src/components/sds/Tabs/index.tsx
Comment thread src/components/screens/ScanReceiveScreen/ScanReceiveScreen.tsx Outdated
@CassioMG CassioMG added the don't review yet Work in Progress / Draft PR / Code Review adjustments being worked on label Jul 22, 2026
@CassioMG CassioMG self-assigned this Jul 22, 2026
…ating, a11y

- Mount the scanner lazily on the first Scan visit so opening straight to
  Receive (My QR code / Add Funds) never triggers a camera permission prompt;
  keep it mounted afterwards.
- Combine useIsFocused with AppState so the camera also pauses when the app is
  backgrounded, not just on route blur.
- Add a real scanEnabled gate to QRScanner that drops detections before the
  debounce/dedup logic, instead of a no-op onRead that left codes cached and
  unscannable for up to 10s after returning to Scan.
- Expose accessibilityRole="tab" and accessibilityState on Tabs touch targets.
- Correct the ScanReceiveScreen camera-lifecycle doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 3 comments.

Comment thread src/components/screens/ScanReceiveScreen/ScanReceiveScreen.tsx
Comment thread src/components/screens/ScanReceiveScreen/ScanReceiveScreen.tsx
Comment thread src/i18n/locales/en/translations.json Outdated
…e, add tests

Copilot review:
- Hide the inactive Scan/Receive layer from screen readers (accessibilityElements
  Hidden + importantForAccessibility) so VoiceOver/TalkBack can't reach content
  behind the visible tab.
- Add the missing article: "the Stellar network." (en).

Additional review findings:
- Delete the orphaned accountQRCodeScreen translation blocks (en + pt).
- Add QRScanner tests for the scanEnabled drop-before-debounce gate.
- Add accessibilityRole="tablist" to the Tabs container.
- Add Tabs tests for hug-mode equalization and fill mode.
- Drop the vestigial transition param / withTransitionOverride / useClearTransition
  Param plumbing on the unified screen (no caller passes transition).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CassioMG CassioMG removed the don't review yet Work in Progress / Draft PR / Code Review adjustments being worked on label Jul 22, 2026
@minkyeongshin

minkyeongshin commented Jul 22, 2026

Copy link
Copy Markdown
  1. If I remember correctly, we decided to remove the WalletConnect functionality from the Scan. would you mind comfirming it? @JakeUrban

  2. Are the icons and notification components planned to be updated later? @CassioMG

Other than that, everything looks good to me!

@JakeUrban

Copy link
Copy Markdown
Contributor

If I remember correctly, we decided to remove the WalletConnect functionality from the Scan. would you mind comfirming it?

I think we decided to remove the component that allows users to paste the wallet connect URI, but we want to keep support for scanning wallet connect QR codes generally

@CassioMG

Copy link
Copy Markdown
Contributor Author
  1. If I remember correctly, we decided to remove the WalletConnect functionality from the Scan. would you mind comfirming it? @JakeUrban

@minkyeongshin the WC scan functionality is removed just on the Send flow, the other flows should keep it. The video shows it.

We can ignore though the "Paste WalletConnect URI" box at the bottom of the Scan screen since that's only shown on the Dev build, the Prod build won't have it.

  1. Are the icons and notification components planned to be updated later? @CassioMG

The notification component should be discussed and implemented as a separate task (we've raised it last year). The Figma file is using a new notification style which doesn't support "title + message" as it only shows a single "title" string. We use "title + message" in lots of places in the code so this needs some discussion. The Notification component on SDS components also have support for "title + message" as reference.

As for the icons, do you see some icon that is not matching Figma designs?

Thanks!

@JakeUrban

Copy link
Copy Markdown
Contributor

This is likely unrelated but I noticed we dont have a $0.00 amount when the account is unfunded.

Screenshot 2026-07-22 at 12 14 41 PM

@CassioMG

Copy link
Copy Markdown
Contributor Author

This is likely unrelated but I noticed we dont have a $0.00 amount when the account is unfunded.

@JakeUrban yeah I also noticed it while testing, this is an existing Prod behavior. I'll adjust it together with the Home refresh 👍

@piyalbasu

Copy link
Copy Markdown
Contributor

Nit: "camera lifecycle" isn't actually covered by the ScanReceiveScreen tests

Nit / test-coverage gap — not a merge blocker.

TL;DR: The headline behavior of this screen — keeping the camera warm underneath the Receive layer and pausing it when the app is backgrounded or the screen loses focus — has no test asserting it. The current tests only check when the scanner is mounted, not that it actually pauses/resumes. If that pause-on-background logic silently regressed, the suite would stay green. Worth a follow-up test; fine to merge as-is.


Detailed explanation (for agents)

Root cause: ScanTabView is fully mocked to a static stub in the test file, so the cameraActive / isScanning props it receives are never inspected:

jest.mock("components/screens/ScanReceiveScreen/ScanTabView", () => {
const { View: MockView } = jest.requireActual("react-native");
return {
ScanTabView: () => <MockView testID="scan-tab-view" />,

Meanwhile the real wiring being claimed as "camera lifecycle" is here:

<ScanTabView
cameraActive={isFocused && isAppActive}
isScanning={isScan}
/>

Because the stub ignores its props, nothing asserts that cameraActive={isFocused && isAppActive} or isScanning={isScan} are passed correctly. The existing mount tests (scan-tab-view present/absent) verify the hasMountedScanner sticky-mount behavior — genuinely useful — but that is a different concern from the camera pausing when isFocused/isAppActive flip to false. A regression that dropped the isFocused && isAppActive guard (camera stays live while backgrounded) would not fail any current test.

Suggested fixes (in increasing order of depth):

  1. Cheap: give the ScanTabView mock a prop-capturing stub (e.g. render the prop values into testID/text, or push them to a spy) and assert cameraActive is false when useIsFocused() returns false or AppState is backgrounded, and isScanning tracks the active tab.
  2. Root cause: add a focused test that toggles the useIsFocused mock and the AppState listener and asserts the props handed to ScanTabView change accordingly — this locks in the "keep camera warm but paused" contract described in the component's own doc comment.

@piyalbasu piyalbasu 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.

This is looking good to me on Simulator! Confirmed that the Paste Wallectconnect URI is gated behind a dev build. Just left 1 nit test improvement

claude and others added 2 commits July 23, 2026 12:51
The existing tests fully mocked ScanTabView to a static stub, so nothing
verified the "keep camera warm but paused" contract described in the
component doc comment. A regression dropping the `isFocused && isAppActive`
guard (camera stays live while backgrounded) or the tab-gated scanning would
have kept the suite green.

Capture the props handed to ScanTabView and add camera-lifecycle tests:
- camera active + scanning on the Scan tab
- camera stays warm but scanning is gated off behind the Receive tab
- camera pauses when the screen is not focused
- camera pauses when the app is backgrounded and resumes on return

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UW7CohZ9tsQjuqpddG5DBd
@CassioMG
CassioMG merged commit d18aae5 into main Jul 23, 2026
32 of 35 checks passed
@CassioMG
CassioMG deleted the cg-scan-receive-tabs branch July 23, 2026 13:57
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.

6 participants