Conversation
|
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
166d1c8 to
0961754
Compare
jvsena42
left a comment
There was a problem hiding this comment.
Changes requested — 1 HIGH/MEDIUM finding(s) inline. (Posted as a comment because GitHub does not allow requesting changes on your own PR.)
Inline: MEDIUM — Payment link opened while locked with a sheet open stays queued after the PIN and fires whenever that sheet is later closed; LOW — BottomSheet early return while locked drops state remembered inside the content lambda; the sheet was already under the PIN overlay
Checked: Traced MainActivity.onStop -> AppViewModel.lockOnBackground: it is synchronous (cached isPinEnabled StateFlow, walletExists(), isRecoveryMode.value), skipped on isChangingConfigurations, and a no-op for PIN-disabled users; there is no separate pin-on-launch setting on Android (SettingsStore has only isPinEnabled, isBiometricEnabled, isPinForPaymentsEnabled). The in-app biometric prompt uses BIOMETRIC_STRONG only (BiometricPrompt.kt:114), so no device-credential activity is launched and the prompt cannot stop MainActivity; the USB permission dialog, camera permission dialog and the share chooser are dialog-themed activities that only pause it, while file pickers do stop it, which the PR body lists as accepted. AuthCheckView (MainActivity.kt:177-189) is composed after ContentView (161-175) inside the same Surface with blockPointerInputPassthrough, so it covers the in-window SheetHost (Send/Hardware/Receive) and the BottomSheetOverlayHost at ContentView.kt:760, and payment/NFC deeplinks are deferred by launchScan (AppViewModel.kt:2223-2232) while locked; a payment in flight keeps running under the overlay and its result sheet is in-window. ForgotPinSheet renders through authSheetOverlayState outside the LocalIsAppLocked provider (MainActivity.kt:191-200), so the early return in BottomSheet does not hide it on the PIN screen. Both greptile threads are resolved at 0961754: the lock no longer suspends, and the new test asserts the flag without advanceUntilIdle. No bitkit-ios twin PR was found by title search; the PR body says iOS already clears PIN verification on background.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jvsena42
left a comment
There was a problem hiding this comment.
Approved — no HIGH/MEDIUM findings at head 6a1c0c825. (Posted as a comment because GitHub does not allow approving your own PR.)
Checked: Follow-up on 6a1c0c8: both earlier findings are fixed (lock-deferred link now closes the open sheet on unlock and skips QuickPay; passphrase no longer discarded). Follow-up at 6a1c0c8. MEDIUM (4046216525) is fixed: setIsAuthenticated(true) (AppViewModel.kt:409-422) marks the queued scan, then calls hideSheet() when _currentSheet is non-null, has a deferred scan and is neither high-priority nor QrScanner (425); hideSheet (4753-4791) clears the sheet and flushes, and flushDeferredScan (2406-2427) launches with preserveUntilComplete and the stored suppressQuickPay. Traced Receive/Widgets/Backup/Hardware/TimedSheet open at lock (closed, scan runs to Send Confirm), Send/Gift/Pin open (kept, scan runs on dismiss to Confirm, the option offered in the thread), QrScanner open (kept, existing scanner-result tests pin the flush), two links queued (last wins at enqueueDeferredScan 2364-2389, mark applied to the survivor), link arriving with the PIN pad up (launchScan 2254 enqueues, same path), a job suspended at lock time (deferLockedScan 2337 re-enqueues, mark at unlock covers it), process death (intent re-delivered in onCreate, queued at auth false, flushed by the PIN), and PIN disabled (lockOnBackground 4862 no-ops, scans run unlocked with suppressQuickPay=false). QuickPay: handleQuickPayIfApplicable (3665-3676) returns false only when suppressQuickPay is true, which is set solely by markDeferredScanUnlocked (427-433) or propagated from a queued scan; every user-initiated entry (onScanResult, scanner sheet, clipboard, address continue) passes the default false, so normal QuickPay is unchanged; the only unmarked flush is resetIsAuthenticatedStateInternal with PIN disabled at cold start/post-migration, which is the master behaviour. LOW (4046216534) is fixed by dropping the check from BottomSheet.kt (72-90); all BottomSheet entries render in ContentView's BottomSheetOverlayHost (ContentView.kt:760) through SheetHost/BottomSheetScaffold in-window, ContentView is composed before AuthCheckView inside the same Material3 Surface (MainActivity.kt:161-189), AuthCheckView is fillMaxSize/black/blockPointerInputPassthrough (AuthCheckView.kt:90-95), no Dialog/Popup/ModalBottomSheet hosts exist, and AppAlertDialog keeps its window guard (AppAlertDialog.kt:75); the new LaunchedEffect (MainActivity.kt:165-170) clears focus and hides the IME on lock so a focused TextInput in AddTag or HwPassphrasePromptSheet cannot restore the keyboard over the NumberPad. Widening of the accepted back-press limitation: BottomSheet-based sheets now also stay in composition while locked, so back on the PIN pad reaches their SheetHost BackHandler as it already did for Send/Hardware (PR body lists this as out of scope). Tests: payment deeplink received with a sheet open closes it on unlock and payment deeplink deferred by the lock skips QuickPay on unlock fail at 0961754 (Receive stays open / quickPayData set); the renamed QuickPay test asserts Confirm and fails on the old assertion; the high-priority test passes on both and only pins intended behaviour. Locals.kt is a doc comment. Earlier clean items (synchronous lockOnBackground, isChangingConfigurations skip, amount-warning gate at 4935, transaction sheet gated on isAuthenticated at MainActivity.kt:220) are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jvsena42
left a comment
There was a problem hiding this comment.
Approved — no HIGH/MEDIUM findings at head 5fb363325. (Posted as a comment because GitHub does not allow approving your own PR.)
Inline: LOW — Deeplink step would pass with the deferred-scan fix reverted
Checked: Round 3 (5fb3633, new journey): Round 3 covers only the new journeys/security/pin-lock-on-resume.xml and the two README rows. Every testTag the journey names exists at head: Send (TabBar.kt:135), PinPad (AuthCheckView.kt:143), N0-N9 via NumberPad's default "N$text" tag, ActivityShort-0 (ActivityListSimple.kt:52, rendered on Home), ActivityTag (ActivityDetailScreen.kt:648), TagInput (ActivityAddTagSheet.kt:68) and SendNumberField (SendAmountScreen.kt:260); "Please enter your PIN code" matches security__pin_enter. The preconditions match the lock conditions in lockOnBackground (PIN enabled, wallet exists, not recovery mode), and biometrics off keeps AuthCheckView on the PinPad branch. The uimode toggle really does exercise the isChangingConfigurations skip in MainActivity.onStop, because MainActivity declares no configChanges and is recreated. The payment URI uses a valid regtest bech32 address; with savings above 10 000 sats, onScanOnchain goes to SendRoute.Amount while unlocked, and launchScan defers it while locked. No bitkit-ios twin PR exists, and the cross-platform table records the journey as not ported yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jvsena42
left a comment
There was a problem hiding this comment.
Approved — no blocking HIGH/MEDIUM findings at head 510f4b098. (Posted as a comment because GitHub does not allow approving your own PR.)
Checked: Round 4 (510f4b0): the journey LOW is fixed. Round 4 (510f4b0) covers only journeys/security/pin-lock-on-resume.xml. It answers the earlier LOW (4047397642): after returning Home the journey now taps Receive (testTag "Receive", TabBar.kt:156, which calls showSheet(Sheet.Receive) at ContentView.kt:729-731) and verifies "ReceiveScreen" (ReceiveSheet.kt:144) before backgrounding and firing the bitcoin: URI. After the PIN it asserts "SendNumberField" is shown and "ReceiveScreen" is gone. At head, setIsAuthenticated(true) (AppViewModel.kt:409-423) sees a tracked Receive sheet with a deferred scan, canCloseSheetForDeferredScan (425) returns true because Receive is neither high priority nor QrScanner, and hideSheet() closes it and flushes the scan into Send Amount. With the fix reverted, the scan stays queued behind the open Receive sheet, so both halves of that assertion fail. The description now states why the Add Tag sheet does not count and why the QuickPay path is left to AppViewModelSendFlowTest.kt. No new findings.
…kground # Conflicts: # journeys/README.md
…kground # Conflicts: # journeys/README.md
There was a problem hiding this comment.
Verdict: ✅ Approve
Reaudit: diff 1 file.
Counterpart synonymdev/bitkit-ios#722: not compared.
Findings:
N/A
Audit:
Already done in comment.
QA: emu-1 redroid container on Android 15 (API 35), dev build 2.4.1 (188) at 52e6f0e
-
✅ passed: Backgrounding and reopening showed the PIN pad before wallet content.
-
✅ passed: PIN returned Home; dark-mode recreation stayed unlocked.
-
✅ passed: Returning from Android share kept Receive open without a PIN prompt.
-
✅ passed: PIN hid Add Tag; Trezor Sign With Device returned unchanged after unlock.
-
✅ passed: Unlock restored Add Tag and deferred the payment URI until PIN.
-
✅ passed: PIN hid the balance warning; unlock restored it.
-
⚠️ hardware absent: The redroid device has no physical biometric sensor or service. -
✅ passed: With PIN disabled, backgrounding and reopening returned Home unlocked.
-
✅ passed: Cold relaunch with PIN enabled showed the PIN pad.
Ticked Manual Tests 1a, 1b, 2, 3, 4a, 4b, 5, 6, 7, 9, and 10.
Tip
Test 1 worth a journey:
- Verify the wallet Home screen is visible
- Send Bitkit to the background
- Reopen Bitkit
- Verify the PIN pad appears before wallet content
Test 2 worth a journey:
- Enter the PIN
- Verify the wallet Home screen returns
- Switch dark mode on and then off
- Verify Home stays visible without a PIN prompt
Test 3 worth a journey:
- Open Receive
- Tap Share
- Return from the system share activity
- Verify Receive stays open without a PIN prompt
Test 6 worth a journey:
- Enter the PIN
- Verify the Add Tag sheet returns
- Go back to the wallet Home screen
- Open Receive
- Send Bitkit to the background
- Open the bitcoin payment link
- Verify the PIN pad hides Send
- Enter the PIN
- Verify Send opens with 10 000 and Receive is closed
Test 7 worth a journey:
- Enable the send amount warning
- Open a 160 000 sat bitcoin payment
- Continue to confirmation
- Swipe to pay
- Verify the balance warning remains visible for 30 seconds
- Send Bitkit to the background
- Reopen Bitkit
- Verify the PIN pad hides the warning
- Enter the PIN
- Verify the same warning returns
Test 9 worth a journey:
- Disable the app PIN
- Verify the wallet Home screen is visible
- Send Bitkit to the background
- Reopen Bitkit
- Verify Home returns without a PIN prompt
Test 10 worth a journey:
- Enable the app PIN
- Verify the wallet Home screen is visible
- Force-stop Bitkit
- Launch Bitkit
- Verify the PIN pad appears
Coverage:
QA: 11 of 12 manual tests passed
Reviewed by gpt-5.6-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)










Fixes #724
Related to #1275
This PR locks the app behind the PIN screen whenever it returns from the background, instead of only at launch.
Description
isPinEnabledstate, so the lock is applied beforeonStopreturns and cannot land after the activity has resumed.onStopinstead ofProcessLifecycleOwner, because the manifest removes the androidx startup provider, so the process lifecycle never emitsON_STOPin this app. Bitkit has a single UI activity, so the result is the same.AppAlertDialogis a realDialogand draws in its own window above the PIN overlay. Bottom sheets are left composed:BottomSheetOverlayHostlives insideContentView, which is composed beforeAuthCheckViewin the sameSurface, so the opaque PIN pad already covers them and skipping them would only throw awayrememberstate such as a half-typed Trezor passphrase.isPinForPaymentsEnableddoes not gateSendQuickPayScreen). Scanning while already unlocked still uses QuickPay as before.Out of Scope
MainActivity: pressing back on the PIN pad still reaches the hidden UI and can dismiss an open in-window sheet (Send, Hardware) or cancel a hardware connect behind the lock. ABackHandlerin the overlay is the likely fix; left for a follow-up.MainActivity: toasts are drawn above the PIN overlay and can show while locked.AlertDialogs, the Create Subscription dropdown and tooltip popups are not hidden while locked. None of them move funds.000key, logo and title position); this predates the change.bitkit-ioswith this change.Design
There is no design for when the lock triggers; the PIN screen itself is unchanged.
Preview
Before: HOME then relaunch goes straight to Home with no PIN.
before-no-lock-on-resume.mp4
After: HOME then relaunch shows the PIN pad; unlocking returns to Home.
after-lock-on-resume.mp4
Payment URI opened while backgrounded: PIN first, then Send Amount after unlock.
deeplink-pin-then-send.mp4
QA Notes
Manual Tests
bitcoin:orlightning:URI: PIN pad shows with no Send sheet; after PIN, Send opens with the URI's amount.regression:PIN disabled → HOME → reopen: Home shows with no lock.regression:PIN enabled → kill and relaunch app: PIN pad shows as before.Steps 1-6 and 9 were run on an emulator (debug build). Step 5 stopped at Sign With Device without a connected Trezor, and step 6 used a
bitcoin:URI. Step 8 was not run because no fingerprint was enrolled. The journey below covers steps 1, 2, 4 and 6, and is committed atjourneys/security/pin-lock-on-resume.xml.journeys/security/pin-lock-on-resume.xml
Automated Checks
AppViewModelSendFlowTest.kt:lockOnBackgroundlocks when the PIN is enabled, locks without waiting for a suspension, and does nothing when the PIN is disabled, no wallet exists or recovery mode is on; alightning:URI received after a lock stays queued and opens Send Confirm after unlock; an amount warning confirmation is ignored while locked and accepted after unlock; a URI received with a Receive sheet open closes that sheet on unlock and opens Send Confirm; a URI received with a Pin sheet open leaves it untouched and runs the scan when it is dismissed; a URI deferred by the lock skips QuickPay and opens Send Confirm.AppViewModelSendFlowTest.kt: two warning tests now authenticate first, since confirmations before authentication are rejected;QuickPay eligible scan remains deferred until authenticatedbecomesQuickPay eligible scan remains deferred and confirms after authenticating, since a scan that crossed the lock no longer auto-pays.just compile,just test,just lint.