feat(client): native clipboard via expo-clipboard (#218) #247

Merged
james merged 1 commit from 218-native-clipboard into main 2026-06-23 16:05:47 +00:00
Owner

Summary

Closes #218.

Adds a write-only platform shim for the clipboard so the PAT "Copy" button on /account works on native, not just on the PWA. Inline navigator.clipboard was wrapped in a Platform-check that no-op'd on Android.

Shim shape

apps/client/lib/clipboard.ts:

import * as Clipboard from "expo-clipboard";
import { Platform } from "react-native";

export async function copyToClipboard(text: string): Promise<void> {
  if (Platform.OS === "web") {
    await navigator.clipboard.writeText(text);
    return;
  }
  await Clipboard.setStringAsync(text);
}
  • Web (PWA + Tauri webview): navigator.clipboard.writeText.
  • Native: expo-clipboard's setStringAsync — bridges to ClipboardManager.setPrimaryClip on Android (no manifest permission required, confirmed in upstream docs) and UIPasteboard on iOS.

Call sites swept

  • apps/client/app/(app)/account.tsx — the RevealBanner "Copy" button on the PAT plaintext reveal. The plaintext stays on screen as a manual-copy fallback in case writeText / setStringAsync rejects.

grep -rn "navigator.clipboard" on apps/client/ is otherwise clean.

Dep

  • expo-clipboard@~56.0.4 — matches the Expo SDK 56 line (latest dist-tag for the SDK-56 branch).

Test plan

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/client typecheck / lint / test / export:web
  • pnpm -F @carol/api typecheck / lint / test
  • pnpm -F @carol/api-client typecheck / lint / test / check
  • New tests/clipboard.test.ts exercises both branches via mocks (expo-clipboard mocked on native; navigator.clipboard redefined via Object.defineProperty on web).
  • Manual on Android via Expo Go: mint a PAT, tap Copy, paste into another app — round-trips. (To run before merge; documenting here so the manual gate isn't lost.)
  • Web (PWA) unchanged — the Platform.OS === "web" branch still calls navigator.clipboard.writeText, and the export still bundles clean.
  • No new Android manifest entries (confirmed via upstream docs; expo-clipboard doesn't add a permission).

Acceptance

  • Acceptance criteria from #218 are inline above.
## Summary Closes #218. Adds a write-only platform shim for the clipboard so the PAT "Copy" button on `/account` works on native, not just on the PWA. Inline `navigator.clipboard` was wrapped in a Platform-check that no-op'd on Android. ## Shim shape `apps/client/lib/clipboard.ts`: ```ts import * as Clipboard from "expo-clipboard"; import { Platform } from "react-native"; export async function copyToClipboard(text: string): Promise<void> { if (Platform.OS === "web") { await navigator.clipboard.writeText(text); return; } await Clipboard.setStringAsync(text); } ``` - Web (PWA + Tauri webview): `navigator.clipboard.writeText`. - Native: `expo-clipboard`'s `setStringAsync` — bridges to `ClipboardManager.setPrimaryClip` on Android (no manifest permission required, confirmed in upstream docs) and `UIPasteboard` on iOS. ## Call sites swept - `apps/client/app/(app)/account.tsx` — the `RevealBanner` "Copy" button on the PAT plaintext reveal. The plaintext stays on screen as a manual-copy fallback in case `writeText` / `setStringAsync` rejects. `grep -rn "navigator.clipboard"` on `apps/client/` is otherwise clean. ## Dep - `expo-clipboard@~56.0.4` — matches the Expo SDK 56 line (latest dist-tag for the SDK-56 branch). ## Test plan - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/client typecheck` / `lint` / `test` / `export:web` - [x] `pnpm -F @carol/api typecheck` / `lint` / `test` - [x] `pnpm -F @carol/api-client typecheck` / `lint` / `test` / `check` - [x] New `tests/clipboard.test.ts` exercises both branches via mocks (`expo-clipboard` mocked on native; `navigator.clipboard` redefined via `Object.defineProperty` on web). - [ ] Manual on Android via Expo Go: mint a PAT, tap Copy, paste into another app — round-trips. (To run before merge; documenting here so the manual gate isn't lost.) - [x] Web (PWA) unchanged — the `Platform.OS === "web"` branch still calls `navigator.clipboard.writeText`, and the export still bundles clean. - [x] No new Android manifest entries (confirmed via upstream docs; `expo-clipboard` doesn't add a permission). ## Acceptance - Acceptance criteria from #218 are inline above.
feat(client): native clipboard via expo-clipboard (#218)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 12s
PR / OSV-Scanner (pull_request) Successful in 18s
Secrets / gitleaks (pull_request) Successful in 15s
PR / Lint (pull_request) Successful in 39s
PR / Package age policy (soft) (pull_request) Successful in 27s
PR / Coverage (soft) (pull_request) Successful in 55s
PR / Trivy (image) (pull_request) Successful in 1m55s
PR / pnpm audit (pull_request) Failing after 10m2s
PR / Test (sqlite) (pull_request) Failing after 10m2s
PR / Test (postgres) (pull_request) Failing after 10m2s
PR / Client (web export smoke) (pull_request) Failing after 10m4s
PR / Build (pull_request) Failing after 10m4s
PR / Static analysis (pull_request) Failing after 10m5s
PR / OpenAPI (pull_request) Failing after 10m6s
PR / Typecheck (pull_request) Failing after 10m6s
771cd7d68b
Add a write-only platform shim at apps/client/lib/clipboard.ts —
navigator.clipboard.writeText on web (PWA + Tauri webview),
expo-clipboard's setStringAsync on native. Swap the one inline call
site (the PAT reveal banner on /account) to use it so the Copy button
finally works on Android, not just on the PWA.

- Add expo-clipboard@~56.0.4 (SDK 56 line, no manifest permission
  required per upstream docs).
- New tests/clipboard.test.ts mocks both branches under the existing
  vitest "node" environment.
- README documents the shim and the no-permission stance.

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 83.3% ≥ 50%
Branches 75.7% ≥ 75%
Functions 91.9% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 83.3% ✅ | ≥ 50% | | Branches | 75.7% ✅ | ≥ 75% | | Functions | 91.9% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james force-pushed 218-native-clipboard from 771cd7d68b
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 12s
PR / OSV-Scanner (pull_request) Successful in 18s
Secrets / gitleaks (pull_request) Successful in 15s
PR / Lint (pull_request) Successful in 39s
PR / Package age policy (soft) (pull_request) Successful in 27s
PR / Coverage (soft) (pull_request) Successful in 55s
PR / Trivy (image) (pull_request) Successful in 1m55s
PR / pnpm audit (pull_request) Failing after 10m2s
PR / Test (sqlite) (pull_request) Failing after 10m2s
PR / Test (postgres) (pull_request) Failing after 10m2s
PR / Client (web export smoke) (pull_request) Failing after 10m4s
PR / Build (pull_request) Failing after 10m4s
PR / Static analysis (pull_request) Failing after 10m5s
PR / OpenAPI (pull_request) Failing after 10m6s
PR / Typecheck (pull_request) Failing after 10m6s
to 55fc1fc583
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 8s
PR / OSV-Scanner (pull_request) Successful in 1m37s
PR / pnpm audit (pull_request) Successful in 1m54s
PR / Static analysis (pull_request) Successful in 1m54s
PR / OpenAPI (pull_request) Successful in 2m38s
PR / Test (postgres) (pull_request) Failing after 2m44s
PR / Client (web export smoke) (pull_request) Successful in 2m58s
PR / Build (pull_request) Successful in 3m3s
PR / Package age policy (soft) (pull_request) Successful in 1m7s
PR / Test (sqlite) (pull_request) Successful in 3m7s
PR / Lint (pull_request) Successful in 3m14s
PR / Typecheck (pull_request) Successful in 3m17s
Secrets / gitleaks (pull_request) Successful in 42s
PR / Coverage (soft) (pull_request) Successful in 1m57s
PR / Trivy (image) (pull_request) Successful in 2m26s
2026-06-23 15:37:42 +00:00
Compare
james merged commit b479d9c572 into main 2026-06-23 16:05:47 +00:00
james deleted branch 218-native-clipboard 2026-06-23 16:05:47 +00:00
Sign in to join this conversation.
No description provided.