Repository navigation
fix(mobile): swap expo-clipboard for @react-native-clipboard/clipboard — kills Android-12 ANR (#16) - #329
Conversation
#16) expo-clipboard registers an always-on native clipboard listener that calls getPrimaryClipDescription on the main thread on Android 12 -> ANR (Sentry ORBIT-MOBILE-3). Removing the package entirely (the listener lives in native module init, so non-use is not enough) and using @react-native-clipboard/clipboard@1.16.3 eliminates it. Migrated the 3 setStringAsync call sites to the sync setString API; dropped the now-vestigial async/void markers. Native autolinking activates on the next dev-client/EAS build. Mobile-only; no backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Code Review: PR #329
Recommendation: NEEDS WORK
Summary
This PR removes expo-clipboard (which registers a main-thread native listener that triggers an ANR on Android 12) and replaces it with @react-native-clipboard/clipboard@1.16.3. Three call sites are migrated from async setStringAsync to sync Clipboard.setString. The fix is correct and complete for the mobile app itself. However, one stale reference to expo-clipboard survives in the root package.json overrides block, which keeps the package resolved by npm and defeats the purpose of this PR.
[HIGH] Stale expo-clipboard override in root package.json
dimension: Dead / stale code (rubric #2)
location: package.json:45
issue: "expo-clipboard": "55.0.12" remains in the root overrides block after the package was removed from apps/mobile/package.json. npm overrides pins a transitive resolution — leaving it means npm still resolves the package at that version for any transitive consumer, keeps expo-clipboard in node_modules, and the native module initialises on startup.
risk: The ANR the PR is designed to eliminate may still occur on Android 12 because the native listener is registered at init time by whichever package resolves expo-clipboard — even if no JS ever imports it. The fix is silently incomplete.
fix: Remove the "expo-clipboard": "55.0.12" line from the overrides block in root package.json, then re-run npm install to regenerate the lockfile without expo-clipboard.
reference: CLAUDE.md rule 2 (delete unused code immediately)
Subagents
| Agent | Verdict |
|---|---|
| parity-checker | PAIRED — web mirrors use navigator.clipboard natively; no web change required |
| i18n-syncer | N/A — no new user-facing strings touched |
| contract-aligner | N/A — no shared types or orbit-api changes |
| security-reviewer | N/A — no orbit-api code changed |
Validation
| Check | Result |
|---|---|
| Lint | PASS (per PR body) |
| Type check | PASS (per PR body) |
| Tests | PASS (635 passed, per PR body) |
| Build (api) | N/A |
What's good
- Root-cause diagnosis is correct: the ANR is in native module init, not the JS call site — removing the package entirely is the right fix.
- All three JS call sites cleanly migrated; the
async/voidcruft properly stripped. - Test mock updated to match the new default-export module shape (
{ default: { setString: vi.fn() } }). - Parity correctly scoped to mobile only — web already uses
navigator.clipboardnatively. - PR body is clear about peer-dependency ranges and autolinking implications.
Recommendation
Remove "expo-clipboard": "55.0.12" from the overrides block in root package.json (line 45) and regenerate the lockfile (npm install). That one-line removal completes the ANR fix.
Generated with Claude Code
expo-clipboard was removed from apps/mobile/package.json, but a stale "expo-clipboard": "55.0.12" remained in the root overrides block. npm install confirms the lockfile is unchanged (the override was inert: expo-clipboard already absent from the lockfile + both node_modules trees), so this is a dead-config cleanup (rule 2) that also removes the latent re-introduction risk. RN hoisting junction verified intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed in |
There was a problem hiding this comment.
Code Review: PR #329
Scope: PR #329 — fix(mobile): swap expo-clipboard for @react-native-clipboard/clipboard — kills Android-12 ANR (#16)
Recommendation: APPROVE
Summary
This PR replaces expo-clipboard with @react-native-clipboard/clipboard across all three mobile call sites (advanced.tsx, referral-drawer.tsx, create-api-key-modal.tsx) to eliminate the Android-12 ANR caused by expo-clipboard's always-on native clipboard listener calling getPrimaryClipDescription on the main thread. The swap is clean, complete, and correct: every setStringAsync becomes synchronous setString, every async/void marker is dropped, the test mock is updated, and zero expo-clipboard references survive in source or lockfile. No web-side change is required because apps/web already uses navigator.clipboard natively.
Findings
Critical
None
High
None
Medium
None
Low / Info
None
Subagents
| Agent | Verdict |
|---|---|
| parity-checker | PAIRED — all three mobile files have web mirrors; web mirrors use navigator.clipboard (platform adapter — no sync needed per root CLAUDE.md "Allowed differences: platform adapters only") |
| i18n-syncer | N/A — no user-facing strings added or removed |
| contract-aligner | N/A — no packages/shared/src/types/* or endpoints.ts changes; no orbit-api changes |
| security-reviewer | N/A — no orbit-api code changed |
Validation
| Check | Result |
|---|---|
| Lint | PASS (author-reported: 0 errors) |
| Type check | PASS (author-reported: clean) |
| Tests | PASS (author-reported: 635 passed) |
| Build (api) | N/A — mobile-only change |
Deferred — N/A dimensions & files not verdicted
- DESIGN.md / AI-slop (#8): no new UI markup added; the changed lines are all import + function-body plumbing with no style/layout code.
- Contract drift + backward-compat guard (#11): no
packages/sharedZod schemas or orbit-api DTOs touched; guard is N/A. - Backend hard rules (#13): no orbit-api code in diff.
- Security (#12, API side): no orbit-api code in diff; frontend-side clipboard access carries no injection or auth risk.
- i18n (#10): no user-facing string keys added or removed.
- Native module install:
@react-native-clipboard/clipboardis a native module activated via autolinking on the next EAS/dev-client build — not verifiable in this CI context (expected for any native module swap).
What's good
- Root cause fixed correctly: removing the package entirely rather than merely not calling it (the ANR fires at native module init, not at the call site — the PR body correctly identifies this).
- All three call sites updated atomically in a single PR; no stragglers.
- Sync
setStringis the right API for fire-and-forget copy — no reason to hold a promise. voidoperators andasyncmarkers cleaned up consistently across all four handlers.- Test mock updated to match the new default-export shape (
{ default: { setString: vi.fn() } }). - Peer dependency range (
react-native >= 0.61.5) is satisfied by the project's RN 0.83.4. - Lockfile integrity hash is well-formed (88-char base64, matching the sha512 standard used throughout the lockfile).
- Zero
expo-clipboardreferences remain in source, config, or lockfile (confirmed by grep).
Recommendation
No issues found. The change is minimal, targeted, and correctly executed.
🤖 Generated with Claude Code
|
A bare #329 in this repository resolves to orbit-ui-mobile PR 329, the clipboard pull request, not the private Progresso ticket. Qualified all three occurrences. This is the same cross-repo ambiguity recorded in orbit-tickets #438, seen from the other side: verify-delivery accepts only a bare #N, which is precisely the form that points at the wrong repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVEELVD8mRz6BQYZmqZkvU
* docs: record the Progresso multi-day gap question and its API blocker (#329) The #329 worker stopped rather than guess: the existing repair endpoint only repairs yesterday and spends exactly one freeze, so the canvas's Spend N freezes action for a multi-day gap has no endpoint to call. Filed the API work as orbit-tickets #442 and recorded #329 as blocked by it. The question in this file is what Progresso should show for a multi-day gap it cannot repair, because the drawing's no-freeze line would blame an empty bank that is not empty, and the alternative is a state nobody drew. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVEELVD8mRz6BQYZmqZkvU * docs: qualify the ticket references so they resolve to orbit-tickets A bare #329 in this repository resolves to orbit-ui-mobile PR 329, the clipboard pull request, not the private Progresso ticket. Qualified all three occurrences. This is the same cross-repo ambiguity recorded in orbit-tickets #438, seen from the other side: verify-delivery accepts only a bare #N, which is precisely the form that points at the wrong repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVEELVD8mRz6BQYZmqZkvU --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(progress): build the stage 2 account streak block (#329) * fix(progress): retain account stats behind the legacy bank gate * fix(progress): validate protected dates before formatting * fix(progress): fit all fourteen streak days within the row * fix(progress): trust account freeze dates and stage announcements * fix(progress): match protected today using account timezone * fix(progress): use UTC for unavailable account timezones * fix: anchor streak timeline to account date * fix: serialize account dates from named parts * fix: keep streak timezone snapshots coherent * fix(progress): load streaks for UTC fallback accounts * fix(progress): reconcile streak checks after base merge
* feat(progress): build the stage 2 account streak block (#329) * fix(progress): retain account stats behind the legacy bank gate * fix(progress): validate protected dates before formatting * fix(progress): fit all fourteen streak days within the row * fix(progress): trust account freeze dates and stage announcements * fix(progress): match protected today using account timezone * fix(progress): use UTC for unavailable account timezones * feat(progress): align stage 4 goal cards and reordering with canvas * fix(progress): consume mobile drag press suppression once * fix(progress): clear press suppression when goal drag is cancelled
* feat(progress): build the stage 2 account streak block (#329) * fix(progress): retain account stats behind the legacy bank gate * fix(progress): validate protected dates before formatting * fix(progress): fit all fourteen streak days within the row * fix(progress): trust account freeze dates and stage announcements * fix(progress): match protected today using account timezone * fix(progress): use UTC for unavailable account timezones * feat(progress): align stage 4 goal cards and reordering with canvas * fix(progress): consume mobile drag press suppression once * feat(progress): rebuild goal detail for stage 5 of ticket 329 * test(web): scope SVG geometry mock to DOM environments * fix(progress): clear press suppression when goal drag is cancelled * fix(progress): align goal completion with server behavior
* feat(progress): finish XP and achievements * chore(progress): refresh the generated architecture map Stage 7 changed module structure without regenerating the map, so the drift gate went red. Output of node tools/arch-map.mjs, nothing hand edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBADebHkHd4QMHhTqFzsm3 * test(progress): pin achievement states * fix(progress): make achievement groups accessible --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three conflicts, two mechanical and one that needed both sides read. `architecture.json` and `architecture.html` are DELETED on `redesign/main` and modified here. thomasluizon/orbit-tickets#470 stopped committing the generated map and `.gitignore` now carries both paths citing it, so the deletion is the incoming intent and this branch's edits are a regenerated artifact. Taken as deleted. `apps/mobile/__tests__/screens/progress-screen.test.tsx` is the real one, and the two sides are INCOMPATIBLE rather than composable. `redesign/main` (8cb6bb1, Progresso #329) mocks `@/lib/theme` so `createTokensV2` returns a Proxy handing back `#status-done`, `#fg-1` and `#111111`. This branch (bd8bb43, #482) DELETED that mock on purpose and switched to the real `createTokensV2('purple', mode)` with a hoisted `theme.mode`, because a contrast assertion measured against a Proxy that returns one constant for every token proves nothing. Resolved toward this branch's intent, which is also the merge's goal: no theme mock. The one assertion that depended on the mock's literal now reads the REAL token instead of `'#status-done'`, so Progresso's intent survives too and is strictly stronger: it still proves the earned mark is painted with the `statusDone` role, now against the value the app actually renders. One piece of merge fallout, not a conflict. `progressScreen.goals.progress` gained interpolation params on `redesign/main`, so the mocked `t` returns `key:{json}` and an equality selector stopped matching. Switched to the `startsWith` idiom this file already uses two tests below. npx vitest run __tests__/screens/progress-screen.test.tsx Test Files 1 passed (1) Tests 51 passed (51) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012LguAmjM5WjvsPonsJh4tJ
* feat(progress): add atomic streak gap repair * fix(progress): align gap repair presentation * fix(progress): defer multi-day repairs to server data * Restore streak repair error status color * test(progress): cover goal list stage 4 * test(progress): prove reorder release and filter guards Assert filtered goal cards expose no drag semantics and release writes one order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VrpPfdSqgp5twko5kzTvf8 * test: detect goal card adornments --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>



Kill the Android-12 clipboard ANR (Sentry ORBIT-MOBILE-3)
expo-clipboardregisters an always-on native clipboard listener that, on Android 12, callsgetPrimaryClipDescriptionon the main thread → ANR. The fix is to remove the package entirely (merely not using it isn't enough — the listener is in native module init) and use@react-native-clipboard/clipboard, which has no such listener.Change
expo-clipboard, added@react-native-clipboard/clipboard@1.16.3(peer rangereact-native >= 0.61.5, satisfied by RN 0.83.4).setStringAsync) to the syncClipboard.setString(...)—advanced.tsx,referral-drawer.tsx,create-api-key-modal.tsx. The handlers became synchronous, so the vestigialasync/voidmarkers were dropped.vi.mockfor the new default-export module.Notes
@react-native-clipboard/clipboardis a native module — autolinking activates it on the next dev-client/EAS build (not Expo Go). Mobile-only (web uses the browser clipboard API). No backend change.Validation
type-checkclean,lint0 errors, mobile 635 tests passed. Zero remainingexpo-clipboardreferences (code + lockfile).🤖 Generated with Claude Code