feat: PWA hook typing, browser share adapter, camera stream hook, IndexedDB repository#532
Conversation
…exedDB repository Closes OlufunbiIK#487 Closes OlufunbiIK#488 Closes OlufunbiIK#489 Closes OlufunbiIK#490
|
@ogazboiz is attempting to deploy a commit to the olufunbiik's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR addresses four linked infrastructure improvements: typing and cleanup of the PWA install hook with new typed interfaces, introduction of a centralized browser share/clipboard utility to replace inline API usage, creation of a reusable camera stream hook for unified media lifecycle management, and new typed offline storage infrastructure for splits and queued payments using IndexedDB with typed repositories. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
frontend/src/hooks/useCameraStream.ts (1)
51-54: Hoist the default constraints object.When callers omit
constraints, this object literal is recreated on every render, souseCallback([constraints])givesstartStream/restartStreamnew identities even when nothing changed. That can churn downstream effects and memoized consumers.♻️ Suggested fix
+const DEFAULT_CAMERA_CONSTRAINTS: MediaStreamConstraints = { + video: { facingMode: 'environment' }, + audio: false, +}; + export function useCameraStream({ - constraints = { video: { facingMode: 'environment' }, audio: false }, + constraints = DEFAULT_CAMERA_CONSTRAINTS, autoStart = false, }: UseCameraStreamOptions = {}): UseCameraStreamReturn {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/useCameraStream.ts` around lines 51 - 54, The default constraints object is recreated on every render causing useCallback hooks like startStream and restartStream (which depend on constraints) to get new identities; hoist the literal by declaring a top-level constant (e.g., DEFAULT_CONSTRAINTS) and use that as the default parameter in useCameraStream instead of the inline object, then ensure references to constraints in startStream/restartStream dependency arrays still work with the stable default so those callbacks stop changing identity when callers omit constraints.frontend/src/components/Split/ShareModal.tsx (1)
17-27: Handle adapter outcomes to provide user feedback.The handlers at lines 18 and 22 ignore return values from
copyToClipboardandshareOrCopy. Both functions return explicit success/failure metadata ({ success: true/false; error?: Error }and{ method, success }respectively), but these outcomes are not inspected. Copy and share failures silently fail without user feedback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/Split/ShareModal.tsx` around lines 17 - 27, Update handleCopy and handleShare to inspect the adapter return values from copyToClipboard and shareOrCopy and surface success/failure to the user: call copyToClipboard(splitLink) and check its {success, error} result and display a success or error message (e.g., trigger existing toast/notification or set local error state) when success is false; similarly call shareOrCopy(...) and inspect its {method, success} result and notify the user if sharing failed (or confirm which method succeeded). Modify the handlers (handleCopy, handleShare) to await the result, branch on the returned success flag, and use the component’s existing UI feedback mechanism to inform the user of success or show the error details from the adapter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/Payment/QRCodeGenerator.tsx`:
- Around line 50-59: The try block in handleCopyUri is missing a catch/finally,
causing a syntax error; wrap the await copyToClipboard(paymentUri) call inside
try { ... } and add a catch (err) { setError(err?.message || String(err)); } (or
a finally) so errors are handled and the block is syntactically correct;
reference the handleCopyUri function, the copyToClipboard call, and the state
setters setCopied and setError when adding the catch/finally.
In `@frontend/src/hooks/useCameraStream.ts`:
- Around line 90-95: The catch in useCameraStream currently hardcodes setError({
type: 'unknown', message }), losing structured camera error details; update the
handler to preserve the original camera error type when available (e.g., read
(err as CameraError).type or a cameraErrorType helper) and pass that into
setError instead of 'unknown', while still using
getUserFriendlyErrorMessage(err) for message and guarding with
mountedRef.current before calling setStatus and setError.
- Around line 61-102: Pending camera requests can resolve after
stopStream/unmount and reassign streamRef or setStatus to 'active'; to fix, add
a cancellable request token (e.g., requestIdRef or a local closure id inside
startStream) and check it before mutating state or streamRef in startStream's
success/error paths, and increment/clear that token inside stopStream and the
unmount cleanup to invalidate outstanding requests; use the existing identifiers
requestCameraPermission, startStream, stopStream, streamRef, and mountedRef when
implementing the token check so late-resolving promises do nothing.
In `@frontend/src/hooks/usePWA.ts`:
- Around line 29-32: The install flow can race with the appinstalled handler
which clears promptRef.current; update installApp to copy promptRef.current into
a local variable (e.g., savedPrompt = promptRef.current) before calling await
savedPrompt.prompt() and use that savedPrompt for subsequent checks and
showPrompt calls to avoid null dereference; do the same for the second
occurrence where promptRef.current is used after an await, and leave
handleAppInstalled as-is (it should still clear promptRef.current).
In `@frontend/src/utils/offlineRepository.ts`:
- Around line 75-80: markSynced and recordAttempt perform separate read and
write awaits which can race and clobber concurrent updates; change both to
perform get+modify+put inside a single readwrite transaction (use getDb(), start
a transaction on STORES.splits, call tx.store.get(id), update the record and
tx.store.put(updated) before committing) so the read/modify/write is atomic, and
update recordAttempt to only clear lastError when an explicit null/empty
indicator is provided (otherwise preserve existing lastError) while incrementing
the attempt counter using the value read inside the same transaction; reference
functions: markSynced, recordAttempt, getDb, STORES.splits, and OfflineSplit.
---
Nitpick comments:
In `@frontend/src/components/Split/ShareModal.tsx`:
- Around line 17-27: Update handleCopy and handleShare to inspect the adapter
return values from copyToClipboard and shareOrCopy and surface success/failure
to the user: call copyToClipboard(splitLink) and check its {success, error}
result and display a success or error message (e.g., trigger existing
toast/notification or set local error state) when success is false; similarly
call shareOrCopy(...) and inspect its {method, success} result and notify the
user if sharing failed (or confirm which method succeeded). Modify the handlers
(handleCopy, handleShare) to await the result, branch on the returned success
flag, and use the component’s existing UI feedback mechanism to inform the user
of success or show the error details from the adapter.
In `@frontend/src/hooks/useCameraStream.ts`:
- Around line 51-54: The default constraints object is recreated on every render
causing useCallback hooks like startStream and restartStream (which depend on
constraints) to get new identities; hoist the literal by declaring a top-level
constant (e.g., DEFAULT_CONSTRAINTS) and use that as the default parameter in
useCameraStream instead of the inline object, then ensure references to
constraints in startStream/restartStream dependency arrays still work with the
stable default so those callbacks stop changing identity when callers omit
constraints.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 591515ac-a421-4447-af60-d02cdfb704bb
📒 Files selected for processing (10)
frontend/src/components/InstallPrompt.tsxfrontend/src/components/Payment/QRCodeGenerator.tsxfrontend/src/components/Split/ShareModal.tsxfrontend/src/components/SplitCalculator/SplitCalculator.tsxfrontend/src/hooks/useCameraStream.tsfrontend/src/hooks/usePWA.tsfrontend/src/types/offline.tsfrontend/src/types/pwa.tsfrontend/src/utils/browserShare.tsfrontend/src/utils/offlineRepository.ts
| try { | ||
| await navigator.clipboard.writeText(paymentUri); | ||
| setCopied(true); | ||
| window.setTimeout(() => setCopied(false), 1500); | ||
| } catch (copyError) { | ||
| setError((copyError as Error).message); | ||
| } | ||
| // Issue #488 — use browserShare adapter | ||
| const result = await copyToClipboard(paymentUri); | ||
| if (result.success) { | ||
| setCopied(true); | ||
| window.setTimeout(() => setCopied(false), 1500); | ||
| } else { | ||
| setError(result.error.message); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Inspecting handleCopyUri block:"
nl -ba frontend/src/components/Payment/QRCodeGenerator.tsx | sed -n '45,65p'
python - <<'PY'
from pathlib import Path
import re, sys
p = Path("frontend/src/components/Payment/QRCodeGenerator.tsx")
text = p.read_text(encoding="utf-8")
m = re.search(r'const handleCopyUri = async \(\) => \{([\s\S]*?)\n \};', text)
if not m:
print("FAIL: handleCopyUri block not found")
sys.exit(1)
block = m.group(1)
has_try = "try {" in block
has_catch = "catch (" in block or "catch{" in block
has_finally = "finally {" in block or "finally{" in block
if has_try and not (has_catch or has_finally):
print("FAIL: try without catch/finally detected in handleCopyUri")
sys.exit(2)
print("PASS: no malformed try block detected in handleCopyUri")
PYRepository: OlufunbiIK/StellarSplit
Length of output: 160
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Reading lines 45-65 from QRCodeGenerator.tsx ==="
if [ -f "frontend/src/components/Payment/QRCodeGenerator.tsx" ]; then
sed -n '45,65p' "frontend/src/components/Payment/QRCodeGenerator.tsx" | cat -n
else
echo "File not found"
exit 1
fi
echo ""
echo "=== Checking try-catch structure in handleCopyUri ==="
python3 - <<'PY'
from pathlib import Path
import re, sys
p = Path("frontend/src/components/Payment/QRCodeGenerator.tsx")
if not p.exists():
print("FAIL: File does not exist")
sys.exit(1)
text = p.read_text(encoding="utf-8")
# Search for handleCopyUri function
m = re.search(r'const handleCopyUri = async \(\) => \{([\s\S]*?)\n \};', text)
if not m:
print("FAIL: handleCopyUri block not found with expected pattern")
# Try alternative pattern
m = re.search(r'const handleCopyUri[^}]*\{([\s\S]*?)\n\s*\};', text)
if not m:
print("Alternative pattern also failed")
sys.exit(1)
block = m.group(0)
print(f"Found handleCopyUri block:\n{block[:200]}...")
# Check for try-catch-finally
has_try = "try {" in block or "try{" in block
has_catch = "catch" in block
has_finally = "finally" in block
print(f"\nhas_try: {has_try}, has_catch: {has_catch}, has_finally: {has_finally}")
if has_try and not (has_catch or has_finally):
print("CRITICAL: try without catch/finally detected!")
sys.exit(2)
elif has_try and (has_catch or has_finally):
print("OK: try block properly closed with catch/finally")
else:
print("OK: no try block found")
PYRepository: OlufunbiIK/StellarSplit
Length of output: 1212
Fix parse-breaking try block in handleCopyUri.
At lines 50-59, try block is missing required catch or finally clause, causing syntax error.
Proposed fix
const handleCopyUri = async () => {
if (!paymentUri) {
return;
}
-
- try {
- // Issue `#488` — use browserShare adapter
- const result = await copyToClipboard(paymentUri);
- if (result.success) {
- setCopied(true);
- window.setTimeout(() => setCopied(false), 1500);
- } else {
- setError(result.error.message);
- }
+ // Issue `#488` — use browserShare adapter
+ const result = await copyToClipboard(paymentUri);
+ if (result.success) {
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1500);
+ } else {
+ setError(result.error.message);
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await navigator.clipboard.writeText(paymentUri); | |
| setCopied(true); | |
| window.setTimeout(() => setCopied(false), 1500); | |
| } catch (copyError) { | |
| setError((copyError as Error).message); | |
| } | |
| // Issue #488 — use browserShare adapter | |
| const result = await copyToClipboard(paymentUri); | |
| if (result.success) { | |
| setCopied(true); | |
| window.setTimeout(() => setCopied(false), 1500); | |
| } else { | |
| setError(result.error.message); | |
| } | |
| }; | |
| const handleCopyUri = async () => { | |
| if (!paymentUri) { | |
| return; | |
| } | |
| // Issue `#488` — use browserShare adapter | |
| const result = await copyToClipboard(paymentUri); | |
| if (result.success) { | |
| setCopied(true); | |
| window.setTimeout(() => setCopied(false), 1500); | |
| } else { | |
| setError(result.error.message); | |
| } | |
| }; |
🧰 Tools
🪛 Biome (2.4.13)
[error] 59-59: Expected a catch clause but instead found ';'.
(parse)
🪛 GitHub Actions: CI
[error] 59-59: ESLint parsing error: 'catch' or 'finally' expected
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/Payment/QRCodeGenerator.tsx` around lines 50 - 59,
The try block in handleCopyUri is missing a catch/finally, causing a syntax
error; wrap the await copyToClipboard(paymentUri) call inside try { ... } and
add a catch (err) { setError(err?.message || String(err)); } (or a finally) so
errors are handled and the block is syntactically correct; reference the
handleCopyUri function, the copyToClipboard call, and the state setters
setCopied and setError when adding the catch/finally.
| useEffect(() => { | ||
| mountedRef.current = true; | ||
| return () => { | ||
| mountedRef.current = false; | ||
| // Always release hardware on unmount | ||
| if (streamRef.current) { | ||
| stopCameraStream(streamRef.current); | ||
| streamRef.current = null; | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const stopStream = useCallback(() => { | ||
| if (streamRef.current) { | ||
| stopCameraStream(streamRef.current); | ||
| streamRef.current = null; | ||
| } | ||
| if (mountedRef.current) setStatus('stopped'); | ||
| }, []); | ||
|
|
||
| const startStream = useCallback(async () => { | ||
| if (!mountedRef.current) return; | ||
| setStatus('requesting'); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const stream = await requestCameraPermission(constraints); | ||
| streamRef.current = stream; | ||
| if (mountedRef.current) setStatus('active'); | ||
| } catch (err) { | ||
| const message = getUserFriendlyErrorMessage(err as Error); | ||
| if (mountedRef.current) { | ||
| setStatus('error'); | ||
| setError({ type: 'unknown', message }); | ||
| } | ||
| } | ||
| }, [constraints]); | ||
|
|
||
| const restartStream = useCallback(async () => { | ||
| stopStream(); | ||
| await startStream(); | ||
| }, [stopStream, startStream]); |
There was a problem hiding this comment.
Invalidate pending camera requests on stop/unmount.
requestCameraPermission() can resolve after stopStream() or the unmount cleanup already ran. In that case the hook still writes the stream into streamRef.current and can flip back to active, leaking camera access after teardown.
🔒 Recommended fix
export function useCameraStream({
constraints = { video: { facingMode: 'environment' }, audio: false },
autoStart = false,
}: UseCameraStreamOptions = {}): UseCameraStreamReturn {
+ const requestIdRef = useRef(0);
const [status, setStatus] = useState<CameraStreamStatus>('idle');
const [error, setError] = useState<CameraStreamError | null>(null);
const streamRef = useRef<MediaStream | null>(null);
@@
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
+ requestIdRef.current += 1;
// Always release hardware on unmount
if (streamRef.current) {
stopCameraStream(streamRef.current);
streamRef.current = null;
}
@@
const stopStream = useCallback(() => {
+ requestIdRef.current += 1;
if (streamRef.current) {
stopCameraStream(streamRef.current);
streamRef.current = null;
}
if (mountedRef.current) setStatus('stopped');
}, []);
@@
const startStream = useCallback(async () => {
if (!mountedRef.current) return;
+ const requestId = ++requestIdRef.current;
setStatus('requesting');
setError(null);
try {
const stream = await requestCameraPermission(constraints);
+ if (!mountedRef.current || requestId !== requestIdRef.current) {
+ stopCameraStream(stream);
+ return;
+ }
streamRef.current = stream;
if (mountedRef.current) setStatus('active');
} catch (err) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useCameraStream.ts` around lines 61 - 102, Pending camera
requests can resolve after stopStream/unmount and reassign streamRef or
setStatus to 'active'; to fix, add a cancellable request token (e.g.,
requestIdRef or a local closure id inside startStream) and check it before
mutating state or streamRef in startStream's success/error paths, and
increment/clear that token inside stopStream and the unmount cleanup to
invalidate outstanding requests; use the existing identifiers
requestCameraPermission, startStream, stopStream, streamRef, and mountedRef when
implementing the token check so late-resolving promises do nothing.
| } catch (err) { | ||
| const message = getUserFriendlyErrorMessage(err as Error); | ||
| if (mountedRef.current) { | ||
| setStatus('error'); | ||
| setError({ type: 'unknown', message }); | ||
| } |
There was a problem hiding this comment.
Preserve the structured camera error type.
The catch block always stores type: 'unknown', which throws away the permission-denied / not-found / not-secure detail already exposed by cameraPermissions.ts. Consumers of this hook can't branch on the actual failure mode anymore.
💡 Suggested fix
} catch (err) {
- const message = getUserFriendlyErrorMessage(err as Error);
+ const cameraError = err as Error & { permissionError?: CameraStreamError };
+ const message = getUserFriendlyErrorMessage(cameraError);
if (mountedRef.current) {
setStatus('error');
- setError({ type: 'unknown', message });
+ setError({
+ type: cameraError.permissionError?.type ?? 'unknown',
+ message,
+ });
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useCameraStream.ts` around lines 90 - 95, The catch in
useCameraStream currently hardcodes setError({ type: 'unknown', message }),
losing structured camera error details; update the handler to preserve the
original camera error type when available (e.g., read (err as CameraError).type
or a cameraErrorType helper) and pass that into setError instead of 'unknown',
while still using getUserFriendlyErrorMessage(err) for message and guarding with
mountedRef.current before calling setStatus and setError.
| const handleAppInstalled = () => { | ||
| promptRef.current = null; | ||
| setCanInstall(false); | ||
| }; |
There was a problem hiding this comment.
Cache the prompt event before awaiting.
installApp() reads promptRef.current again after prompt() resolves, but appinstalled clears that ref independently. If the event fires in between, this becomes a null dereference and breaks the install flow.
🔧 Proposed fix
const installApp = useCallback(async (): Promise<InstallOutcome | null> => {
- if (!promptRef.current) return null;
- await promptRef.current.prompt();
- const { outcome } = await promptRef.current.userChoice;
+ const prompt = promptRef.current;
+ if (!prompt) return null;
+ await prompt.prompt();
+ const { outcome } = await prompt.userChoice;
promptRef.current = null;
setCanInstall(false);
return outcome;
}, []);Also applies to: 47-54
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/usePWA.ts` around lines 29 - 32, The install flow can race
with the appinstalled handler which clears promptRef.current; update installApp
to copy promptRef.current into a local variable (e.g., savedPrompt =
promptRef.current) before calling await savedPrompt.prompt() and use that
savedPrompt for subsequent checks and showPrompt calls to avoid null
dereference; do the same for the second occurrence where promptRef.current is
used after an await, and leave handleAppInstalled as-is (it should still clear
promptRef.current).
| async markSynced(id: string): Promise<void> { | ||
| const db = await getDb(); | ||
| const split = await db.get(STORES.splits, id) as OfflineSplit | undefined; | ||
| if (split) { | ||
| await db.put(STORES.splits, { ...split, pendingSync: false }); | ||
| } |
There was a problem hiding this comment.
Make the record updates atomic.
markSynced and recordAttempt both read a record and write back a mutated copy in separate awaits, so concurrent callers can clobber newer fields or lose an increment. recordAttempt also clears lastError whenever error is omitted, which drops the previous failure context.
🔧 Proposed fix
async markSynced(id: string): Promise<void> {
const db = await getDb();
- const split = await db.get(STORES.splits, id) as OfflineSplit | undefined;
- if (split) {
- await db.put(STORES.splits, { ...split, pendingSync: false });
- }
+ const tx = db.transaction(STORES.splits, 'readwrite');
+ const store = tx.objectStore(STORES.splits);
+ const split = await store.get(id) as OfflineSplit | undefined;
+ if (split) {
+ await store.put({ ...split, pendingSync: false });
+ }
+ await tx.done;
}
@@
async recordAttempt(id: string, error?: string): Promise<void> {
const db = await getDb();
- const payment = await db.get(STORES.queuedPayments, id) as QueuedPayment | undefined;
- if (payment) {
- await db.put(STORES.queuedPayments, {
- ...payment,
- attempts: payment.attempts + 1,
- lastError: error,
- });
- }
+ const tx = db.transaction(STORES.queuedPayments, 'readwrite');
+ const store = tx.objectStore(STORES.queuedPayments);
+ const payment = await store.get(id) as QueuedPayment | undefined;
+ if (payment) {
+ await store.put({
+ ...payment,
+ attempts: payment.attempts + 1,
+ ...(error !== undefined ? { lastError: error } : {}),
+ });
+ }
+ await tx.done;
}Also applies to: 112-120
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/utils/offlineRepository.ts` around lines 75 - 80, markSynced and
recordAttempt perform separate read and write awaits which can race and clobber
concurrent updates; change both to perform get+modify+put inside a single
readwrite transaction (use getDb(), start a transaction on STORES.splits, call
tx.store.get(id), update the record and tx.store.put(updated) before committing)
so the read/modify/write is atomic, and update recordAttempt to only clear
lastError when an explicit null/empty indicator is provided (otherwise preserve
existing lastError) while incrementing the attempt counter using the value read
inside the same transaction; reference functions: markSynced, recordAttempt,
getDb, STORES.splits, and OfflineSplit.
Summary
PWA Install Prompt Hook Typing and Cleanup #487 —
src/types/pwa.ts:BeforeInstallPromptEvent,InstallOutcome,PWAHooktypes.src/hooks/usePWA.ts: replacedanywith typedBeforeInstallPromptEventstored in a ref, added stable named handlers for all four window events (online,offline,beforeinstallprompt,appinstalled) with full cleanup on unmount, exposedcanInstall: booleaninstead of the raw event,installApp()returnsInstallOutcome | null.InstallPrompt.tsxupdated tocanInstall.Clipboard and Share Capability Adapter #488 —
src/utils/browserShare.ts:canNativeShare(),canWriteClipboard()capability detectors;shareOrCopy(data)tries native share → clipboard fallback → unsupported with typedShareOutcome;copyToClipboard(text)for clipboard-only flows. UpdatedShareModal.tsx,QRCodeGenerator.tsx, andSplitCalculator.tsxto use the adapter — no more inlinenavigator.share/navigator.clipboardbranching.QR Scanner and Camera Stream Unification #489 —
src/hooks/useCameraStream.ts: shared hook owningrequestCameraPermission,stopCameraStream, stream ref, status machine (idle → requesting → active → error → stopped),startStream(),stopStream(),restartStream(), and cleanup on unmount viamountedRef. QRCodeScanner and CameraCapture can consume this hook to eliminate duplicate stream management.IndexedDB Offline Repository Hardening #490 —
src/types/offline.ts:OfflineSplit,QueuedPayment, store name constants,DB_VERSION = 2.src/utils/offlineRepository.ts:splitRepository(save,get,getAll,getPendingSync,markSynced,delete,clear) andqueuedPaymentRepository(enqueue,get,getAll,recordAttempt,dequeue,clear) — all fully typed, noany. Versionedupgradehandler migrates from v1 schema.Test plan
usePWA— noanyin state,beforeinstallpromptlistener removed on unmount,canInstallflips tofalseafterinstallApp()resolvesshareOrCopy— native share available → callsnavigator.share; share rejected → falls through to clipboard; clipboard only → writes textcopyToClipboard— returns{ success: false, error }when clipboard API unavailableuseCameraStream—startStreamsets statusactive;stopStreamreleases tracks; cleanup stops stream on unmountsplitRepository.getPendingSync— returns only records withpendingSync: truequeuedPaymentRepository.recordAttempt— incrementsattemptsand setslastErrorCloses #487
Closes #488
Closes #489
Closes #490
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Refactor