Repository navigation
fix: restore payment review after app unlock - #1039
Conversation
|
Triage: milestone 3.1.0, with #1025 it is stacked on. It closes no issue, so this PR carries the milestone itself. Not a release blocker. |
[Medium impact] The change reaches the iOS incoming Bitcoin payment review and send-sheet lifecycle. Fix restoration for Bitcoin reviews reached through the amount page before merging. Findings
SummaryPreserves matching unpaid Bitcoin reviews while app lock removes the send view, then reloads setup after unlock.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming Bitcoin request] --> B{Original route}
B -->|confirm| C[Review]
B -->|amount| D[Choose Bitcoin]
D --> E[Review; original route stays amount]
C --> F[App locks]
F --> G[Retain matching unpaid review]
G --> H[Unlock and reload setup]
E --> I[App locks]
I --> J[Retention skipped]
J --> K[Payment state cleared; setup cannot restart]
Reviews (1) · Last reviewed commit: "chore: rename changelog fragment" · Reviewed by Greptile |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Keeps an unpaid Bitcoin payment-request review alive across app lock: the preparation is flagged on background, SendSheet.cleanup skips its reset while the review sits on confirm or PIN, and the remounted sheet re-runs setup; a dismissal in either callback order resets the retained state. Reviewed b93acf7 against fix/paykit-proof-latency, full tier, reasoned from the code and CI (iOS does not build on this reviewer's host).
What I checked, and 6 candidates I ruled out
Read in full: IncomingPaykitPaymentRequestPreparation and the background, presentation and invalidation paths in AppScene.swift; SendSheet .task(id:), cleanup, setUpSend, requestPinCheck; SendConfirmationView.submitPayment/performPayment; SheetViewModel.hideSheet and sendSheetItem; PaykitPaymentRequestManager.finishPayment, markPresentedIfPending, the approval set; both new tests
CI: SwiftFormat and Greptile green; Run Tests still pending at review time
Ruled out
- Lock while on the PIN route leaves the old authorization waiting: the old
pinCheckContinuationsnever resume, butperformPayment(and with itPaykitPaymentActivityand the approval) only starts after PIN success, so nothing is submitted and the remounted sheet starts at confirm. That leak predates this PR. - Disappear-then-dismiss skips
finishPayment: retention requires no submission, andfinishPaymentreturns early for a request not inacceptedRequestIds; a retried accepted request stays inpendingRequestsandpaymentRequestForRetryalready dropped its approval. Your test asserts the request is pending and re-presentable after either order. - Dismiss-then-disappear double reset:
dismiss()clears the request, soownsResolvedContextfails incleanupand the normal reset runs on already-cleared state. - SwiftUI fires the send sheet's
onDismisswhenMainNavViewleaves forAuthCheck, clearing the config before remount: the bug this fixes (a review stuck loading after unlock) shows the sheet config does survive the lock on device. Narrow: only the listed journey, still unexecuted on this head, would show it on a real presenter; the unit test swaps the host's root view instead. setUpSendrefuses the remount:markPresentedIfPendingonly needs the request inpendingRequests, which retention does not change.- Lock on fee-rate, tag or UTXO screens still leaves the review stuck: those routes are not retained and the remount guard returns before setup, but the behaviour is unchanged from the base branch and outside the routes this PR claims.
Merge confidence: 4/5, the remount after a real app lock is covered only by a host-swap test until the updated journey runs, and Run Tests was still pending.
|
@ben-kaufman conflicts |
4022aa7 to
bc2fb60
Compare
b93acf7 to
e9b45c2
Compare
There was a problem hiding this comment.
Verdict: ✅ Approve at e9b45c2.
Findings: None. I verified the amount-selection retention finding is fixed in source and on-device.
QA for e9b45c2: iOS 26.5 simulators, disposable identities and Bitcoin regtest funds. In the paired recordings, payer is left and requester right, aligned at 2×.
Test 1: direct Bitcoin review after unlock ✅ The original review resumed without reopening and completed one freshly authorized payment.
Confirmation and payment PIN each stayed open for over 30 seconds. PIN cancellation and dismissal sent nothing; a later 2,000-sat request arrived without restarting. After reopening the original and locking the app, app-unlock PIN restored its 1,000-sat confirmation, ready swipe and working details with a 143-sat fee. Fresh payment PIN reached success once. The requester showed Proof submitted, while the second request stayed pending. Bitcoin backend data confirmed the exact 1,000-sat output and 143-sat fee; closing success left one outgoing activity.
J1.2x.mp4
Test 2: amount selection and payment-PIN lock ✅ Both locks restored the unpaid Bitcoin review and required fresh payment authorization.
A requester-only test hook issued native SDK terms for 1 USD, accepting Bitcoin and USDT at fixed rates of 0.00001 BTC and 1 USDT. The payer ran the unchanged reviewed binary. Amount showed Savings and 1,000 sats; Continue opened confirmation. Lock/unlock from confirmation and from payment PIN each returned directly to that same Bitcoin review with a prepared 143-sat fee and ready swipe. Entering only app-unlock PIN sent nothing. A new swipe required a new payment PIN; cancelling and closing preserved the balance and the sole earlier outgoing payment. The issuer remained Waiting for payment, and the Bitcoin backend confirmed zero received at this request's recipient address. No USDT payment was made.
J2.2x.mp4
Test 3: hosted lifecycle regressions ✅ App-lock and confirmation lifecycle suites passed.
Verified from the Xcode result: teardown/remount from direct and amount-entry routes, both dismissal callback orders, session/context/sheet ownership and submission exclusions, plus confirmation setup and cancellation. The recording shows hosted test execution; assertions were checked in the test result.
A1.2x.mp4
Coverage: full bc2fb605..e9b45c2, all six changed files and reached callers. Unavailable-node synchronization and hardware/USDT retention exclusions have source or hosted-test coverage only. Inherited proof-delivery and recovery coverage remains in the parent's QA report.
Twin: synonymdev/bitkit-android#1591 fix: deliver completed Paykit proofs promptly shares the review/PIN deferral contract; Android keeps content beneath its lock overlay, so this view-remount fix is iOS-specific.
The full CI unit-test job was still running at completion; the focused local suites above passed.
Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
jvsena42
left a comment
There was a problem hiding this comment.
Approving. No HIGH/MEDIUM. One LOW is inline; it can be fixed here or in a follow-up, your choice.
This fixes the master bug I posted as out-of-scope on #1025 (stuck spinner after a PIN background cycle). The confirmation is retained on app lock, and setup runs again on unlock.
Checked and clean, reviewed as an auth and payment change:
- Retention scope: only an unsubmitted, non-hardware, non-USDT Bitcoin confirmation (or its payment PIN) owned by the same session, sheet, context and request.
- No payment without re-confirmation: after unlock a payment needs a new swipe and a new payment PIN. The old view's PIN continuations are never resumed, and the app-unlock PIN authorizes nothing. A lock during an in-flight payment isn't retained.
- Fresh terms: the amount is re-derived and the fee reloaded. Expired conversion terms return to Amount.
- Session or ownership change while locked: invalidates and dismisses the request (
isIncomingPaymentRequestPreparationInvalid). - Dismissal and the review hold: dismissal in either callback order resets exactly once. The review hold stays balanced across the re-host, with one token per sheet instance. It doesn't worsen my LOW on #1054.
- Android parity: the bug can't happen there, because ContentView stays composed under AuthCheckView. synonymdev/bitkit-android#1591
review-refresh.xmllacks the new lock/unlock steps, so port them, or note the overlay difference in the README, to keep the spec diffable.
Device gate: no run by me — journeys: review-refresh.xml lock/unlock steps already driven at this head; perf: one setUpSend per unlock with a retained review; Figma: n/a; l10n: no UI text changed.
Skipped — already driven this round: payment-requests/review-refresh.xml lock/unlock, direct and amount → confirm entry (iOS, by @ovi-reviewer at e9b45c2, passed: #1039 (review))
The PR body lists SendConfirmationLifecycleTests.swift as updated, but only PaykitPaymentRequestServiceTests.swift changed.
There was a problem hiding this comment.
Verdict: ✅ Approve at 0df5ebf.
Findings: None in the delta. The bitkit-ios#1059 fix: retain fee speed and tags after app unlock remains deferred as agreed in the review thread.
QA for 0df5ebf: iOS 26.5 simulators, disposable identities and Bitcoin regtest funds. The payer ran the unmodified reviewed build. Recordings play at 2×; paired recordings show payer left and requester right.
Test 1: direct Bitcoin review after unlock ✅ The original review resumed directly and completed one freshly authorized payment.
Confirmation and payment PIN each stayed open for over 30 seconds. Cancelling PIN and dismissing sent nothing; a second 2,000-sat request arrived without restarting. After reopening the original and locking the app, app-unlock PIN restored its 1,000-sat confirmation with a ready swipe and working details showing a 143-sat fee. A fresh swipe and payment PIN reached success once. Closing success left one outgoing activity; the requester showed Proof submitted and the second request stayed pending. Bitcoin RPC independently confirmed the funding output stayed unspent before authorization, then the exact 1,000-sat output and 143-sat fee.
J1.2x.mp4
Direct review, cancellation, app unlock and the single authorized payment.
Test 2: amount selection and payment-PIN lock ✅ Both locks restored unpaid Bitcoin confirmation and required fresh payment authorization.
A requester-only fixture hook supplied native SDK terms for 1 USD, accepting Bitcoin and USDT with fixed rates of 0.00001 BTC and 1 USDT; those terms were checked in the hook, and the synchronized requester showed the 1 USD request. The unchanged payer entered Amount with Savings and 1,000 sats, then continued to Bitcoin confirmation. Lock/unlock from confirmation and from payment PIN each returned directly to the same unpaid review; restored details showed a prepared 143-sat fee. App-unlock PIN sent nothing. A new swipe required a new payment PIN; cancelling and closing retained 98,857 sats and the sole earlier outgoing activity. The requester remained Waiting for payment, and Bitcoin RPC confirmed zero received at this fixture-owned destination. No USDT payment was made.
J2.2x.mp4
Fixed-price amount entry and both lock boundaries.
Test 3: biometric app unlock and cancellation ✅ PIN fallback, manual Face ID retry and background interruption preserved the unpaid review.
With simulated Face ID enrolled, a failed match allowed Use PIN code fallback and restored the unpaid 2,000-sat review. Cancelling a later attempt exposed the app's Use Face ID button; a successful manual retry restored that review. Backgrounding another pending challenge and sending a match while backgrounded did not bypass app lock: returning showed a fresh challenge. A new match restored the same amount and a prepared 143-sat fee. Dismissal retained 98,857 sats and the sole earlier outgoing activity; Bitcoin RPC independently confirmed the entire remaining funding output stayed unspent.
J3.2x.mp4
Simulator Face ID failure, fallback, retry and interrupted app unlock.
Check 1: retained hosted regressions ✅ The previous passing app-lock and confirmation-lifecycle assertions remain applicable.
I retained the hosted results from the review at e9b45c2: their test bodies and Bitcoin retention/setup logic are unchanged. They cover both dismissal callback orders, session/context/sheet ownership, active submission, hardware/USDT exclusions and setup cancellation. The changed app-unlock authentication was driven above. These suites were not rerun here.
Coverage: contiguous e9b45c2..0df5ebf, including both integration merges and reached callers. Device coverage above exercises Bitcoin review and app authentication; imported USDT warning/routing, receive-tab, merchant-label and CI changes were inspected in code. Earlier inherited USDT routing and reconciliation evidence is in the parent's report at d702597; it does not prove the later warning integration. Hardware signing and unavailable-node synchronization were not driven in this pass.
Twin: synonymdev/bitkit-android#1591 fix: deliver completed Paykit proofs promptly shares the payment-review deferral contract. Android keeps content beneath its lock overlay; this review-remount fix is iOS-specific.
The full CI unit-test job remained pending at completion.
Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
There was a problem hiding this comment.
Verdict: ✅ Approve at 83ae2ef.
Findings: None in the delta. Fee-speed and tag retention remains deferred to synonymdev/bitkit-ios#1059 fix: retain fee speed and tags after app unlock, as agreed in the review thread.
QA for 83ae2ef: iOS 26.5 simulators, built with Xcode 27.0, disposable identities and Bitcoin regtest funds. The payer ran the unmodified reviewed binary. Paired recordings play at 2×, with payer left and requester right, aligned by recording start time.
Test 1: J1 direct Bitcoin review after unlock ✅ The original review resumed directly and completed one freshly authorized payment.
Confirmation and payment PIN each stayed open for at least 30 seconds. Cancelling PIN and closing review sent nothing; a second 2,000-sat request arrived without restarting. App-unlock PIN restored the original 1,000-sat confirmation with working details and a prepared 143-sat fee. A new swipe required a new payment PIN and completed once. Closing success left one outgoing activity; the requester showed Proof submitted and the second request stayed pending. Bitcoin RPC independently confirmed the funding output stayed unspent before authorization, then the original input, exact 1,000-sat output and 143-sat fee.
J1.2x.mp4
Direct review, cancellation, app unlock and the original authorized payment.
Test 2: J1 amount selection and payment-PIN lock ✅ Both locks restored the same unpaid Bitcoin review and required fresh payment authorization.
A requester-only fixture hook issued a native SDK request for 1 USD, accepting Bitcoin and USDT at fixed rates of 0.00001 BTC and 1 USDT. I checked the decoded stored SDK terms in the diagnostic below; synchronized requester history showed the 1 USD request. The unchanged payer entered Amount with Savings and 1,000 sats. Locking from confirmation and from payment PIN each restored that Bitcoin review directly; details showed a prepared 143-sat fee. App-unlock PIN sent nothing. A fresh swipe required another payment PIN; cancellation and dismissal retained 98,857 sats and the sole earlier outgoing activity. The requester remained Waiting for payment. Bitcoin RPC confirmed zero received at the fixture destination and the entire remaining funding output unspent. No USDT payment was made.
J1b.2x.mp4
Fixed-price amount entry, both lock boundaries and cancellation without payment.
Requester fixture hook
Not pushed. The requester supplies fixed conversion terms its ordinary UI cannot issue, then logs decoded stored terms. The payer is unmodified.
diff --git a/Bitkit/Services/PaykitPaymentRequestService.swift b/Bitkit/Services/PaykitPaymentRequestService.swift
index 888b44b5..9fb75d4f 100644
--- a/Bitkit/Services/PaykitPaymentRequestService.swift
+++ b/Bitkit/Services/PaykitPaymentRequestService.swift
@@ -149,6 +149,11 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
guard record.state != .activeRecurring else { return .failure(.recurringRequest) }
guard let terms = record.terms else { return .failure(.missingTerms) }
+ if CommandLine.arguments.contains("-QA_FIXED"), terms.amount.asset == "usd",
+ let conversion = terms.conversion, case let .fixed(rates) = conversion {
+ let qaRates = rates.map { "\($0.asset)=\($0.value)" }.joined(separator: ",")
+ Logger.info("QA decoded request=\(record.paymentRequestId) amount=\(terms.amount.value) asset=\(terms.amount.asset) endpoints=\(terms.acceptedPaymentEndpointIdentifiers.joined(separator: ",")) rates=\(qaRates)", context: "ReviewLockQA")
+ }
guard terms.recurrence == nil else { return .failure(.recurringRequest) }
let paymentDeadline: PaykitPreciseInstant?
if let deadline = terms.paymentDeadline {
@@ -876,21 +881,25 @@ struct PaykitPaymentRequestService {
let metadataText = String(decoding: metadataData, as: UTF8.self)
let rates = try await PaykitRequestPricing.rates(requested: draft.amount.asset,
endpoints: acceptedPaymentEndpointIdentifiers, market: marketRate(), at: proposalDate)
+ let qaFixed = CommandLine.arguments.contains("-QA_FIXED")
+ let qaUsdt = CommandLine.arguments.first(where: { $0.hasPrefix("-QA_USDT=") })?.dropFirst(9).description ?? ""
+ let qaAddress = CommandLine.arguments.first(where: { $0.hasPrefix("-QA_ADDRESS=") })?.dropFirst(12).description ?? ""
let terms = try Paykit.PaymentRequestTerms(
amount: Paykit.PaymentRequestAmount(
- value: draft.amount.value,
- asset: draft.amount.asset.rawValue
+ value: qaFixed ? "1" : draft.amount.value,
+ asset: qaFixed ? "usd" : draft.amount.asset.rawValue
),
paymentReference: Paykit.PaymentReference(text: "bitkit-\(UUID().uuidString)"),
proposalExpiresAt: Self.timestamp(draft.expiresAt),
recurrence: nil,
- acceptedPaymentEndpointIdentifiers: acceptedPaymentEndpointIdentifiers,
- paymentEndpoints: nil,
+ acceptedPaymentEndpointIdentifiers: qaFixed ? ["btc-regtest-p2wpkh", "usdt-arbitrum-address"] : acceptedPaymentEndpointIdentifiers,
+ paymentEndpoints: qaFixed ? ["btc-regtest-p2wpkh": "{\"value\":\"\(qaAddress)\"}", "usdt-arbitrum-address": "{\"value\":\"\(qaUsdt)\"}"] : nil,
requiredAppId: "bitkit",
- conversion: rates.isEmpty ? nil : .fixed(rates: rates),
+ conversion: qaFixed ? .fixed(rates: [.init(asset: "btc", value: "0.00001"), .init(asset: "usdt", value: "1")]) : (rates.isEmpty ? nil : .fixed(rates: rates)),
paymentDeadline: .at(timestamp: Self.timestamp(draft.expiresAt)),
metadata: Paykit.PrivateJsonObject(text: metadataText)
)
+ if qaFixed { Logger.info("QA fixed proposal terms: \(terms)", context: "ReviewLockQA") }
let record = try await sdk.proposePaymentRequest(
counterparty: target.publicKey,
terms: terms,QA decoded request=456d6b5c-e029-4565-9718-11369461c29a amount=1 asset=usd endpoints=btc-regtest-p2wpkh,usdt-arbitrum-address rates=btc=0.00001,usdt=1
Check 1: retained biometric and hosted lifecycle coverage ✅ Earlier passing results still cover unchanged paths.
I retained biometric cancellation, PIN fallback, manual retry and background-interruption evidence from the review at 0df5ebf: the app-authentication implementation is unchanged. The hosted results at e9b45c2 retain coverage of both dismissal callback orders, session/context/sheet ownership, active submission, hardware/USDT exclusions and setup cancellation. Those test bodies and retention predicates are unchanged; they were not rerun here.
Check 2: inherited integration coverage ✅ Passing parent regressions apply to the identical imported implementation.
The parent report at 2ca5d02 supplies passing focused coverage for imported payment admission, allowance execution, backup, expiry and toast behavior. I inspected the imported allowance UI, expiry toasts and USDT amount layouts in source; these surfaces were not driven as separate device journeys. The changed manual payment path was exercised freshly in Test 1.
Coverage: contiguous 0df5ebf..83ae2ef, all 59 delta files and reached callers, plus the prior open review disposition. Device coverage here is the Bitcoin review and app-lock journey; retained checks above identify the other coverage used.
Twin: synonymdev/bitkit-android#1591 fix: deliver completed Paykit proofs promptly, checked at 766f4a8, matches the shared payment-review deferral contract. Android keeps content beneath its lock overlay; this view-remount fix is iOS-specific.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete pull request diff from merge-base b8b7f3d9 through 88f75ea4. The comparison base moved from 3b27520 to cc5f81fb, and there was no verified coverage baseline to inherit.
No new actionable code findings.
An unpaid Bitcoin confirmation, including one opened from amount selection, stays on the current send sheet across app lock and is set up again on unlock. Amount, USDT, and hardware screens keep that presentation until a real dismissal. Inline acceptance calls claimAndAcceptPaymentRequest, and a blocked peer is still rejected before send. The local proof record is removed after the SDK queues it; recipient delivery runs in the background and does not hold the submission lock. Restoring confirmation still resets fee speed and tags; that remains bitkit-ios#1059.
Validation: unit/UI run 40412990424 and SwiftFormat run 40412990399 passed at this head. E2E run 40447918061 reports success at this head and does not cover the app-lock journey. This review did not execute those workflows or J1. Paykit 0.1.1-rc1 Rust for claimAndAcceptPaymentRequest was not retrieved; the app wrapper and payment-request tests were inspected. Android was not compared at a pinned revision; the journey already records that iOS removes the send view on lock.
Device testing: not performed in this review.
Ready for device testing.
Reviewer model: grok-4.7
Reasoning effort: high
Dismissed per @ovitrif: this review was on an older head, its threads are resolved, and the PR has moved on. @jvsena42 is off until 1 Nov; the current head needs a fresh review from @ben-kaufman or @piotr-iohk.
The merge-base changed after approval.
|
@ben-kaufman Updated this branch with master: the ruleset dismisses approvals on a branch behind its base, so the review approval could not stick. The new head needs its re-review before merge. |
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — the requested in-flight lock check still exposes an active payment swipe after unlock.
Findings: No newly introduced defect confirmed in the delta. The failed QA case below is an existing UI condition: SendConfirmationView.swift and SwipeButton.swift are unchanged from this PR's base, e763422. I am not reporting it as a new regression or evidence of a duplicate debit.
QA for 9073085: iPhone 17 Pro simulator, iOS 26.5, staging identities and Bitcoin regtest. App PIN and payment PIN enabled; biometrics disabled. All planned cases ran.
Test 1: hosted lifecycle regressions (A1) ✅ The focused suite passed; assertions were checked in the Xcode result bundle.
Built the unmodified head and ran payment-request, send-sheet, confirmation, activity and on-chain admission regressions. This includes both app-lock dismissal orders, retained and excluded routes, ownership/session changes, submission exclusions, wallet-switch cancellation and recovered-success state.
The recording shows a short rerun of the three app-lock lifecycle tests; the result bundle, rather than the screen recording, establishes their assertions.
A1-lock.2x.mp4
App-lock regression subset, 2×.
Test 2: direct Bitcoin review and fresh authorization (J1a) ✅ Timed accessibility captures and chain decoding confirmed retained review, fresh payment authorization and one payment.
On the unmodified payer, the 1,000-sat confirmation and payment PIN each remained open for over 30 seconds. Cancelling and dismissing left no outgoing payment. A later 2,000-sat request arrived without restarting the app. Reopening the original request, backgrounding and entering app-unlock PIN restored its unpaid 1,000-sat confirmation and 143-sat fee. A fresh swipe and payment PIN completed it once; closing success retained the activity.
The verification clip shows the sole outgoing payment. Separate requester accessibility captures showed Proof submitted for that request and the later request still waiting. Independent raw-transaction decoding verified the original funding input, a 1,000-sat recipient output and 143-sat fee: 0b41005bd12e06233c1c6eee0a2087ac0b23eab5c443a0754581c7cc3eaa3878.
J1a-verification.2x.mp4
Subsequent activity and balance verification, 2×; the timed holds and unlock steps were checked with accessibility captures.
Test 3: fixed-USD Bitcoin review and payment-PIN lock (J1b) ✅ Accessibility captures confirmed both locks restored the same unpaid 1,000-sat review; recipient chain history remained empty.
The requester-only hook issued a real signed Paykit request for USD 1, accepting Bitcoin and USDT with fixed rates of BTC 0.00001/USD and USDT 1/USD. The payer ran the unmodified head. Choosing Savings showed 1,000 sats and a 143-sat fee. Locking at confirmation and again at payment PIN restored the same unpaid confirmation. App unlock sent nothing; a new swipe still required payment PIN. Cancelling and dismissing left the outgoing activity unchanged, and the quoted Bitcoin destination had zero balance and no transaction history.
The shorter verification recording repeats the confirmation lock and fresh-PIN boundary on the unmodified payer. The payment-PIN lock boundary was checked in the original recording and retained accessibility captures.
J1b-verification.2x.mp4
Fixed-quote confirmation, app unlock and fresh payment PIN, 2×.
Requester test hook
Not pushed. Only the requester used this hook; it supplies the fixed-price request shape and logs the stored terms. No USDT payment was attempted.
diff --git a/Bitkit/Services/PaykitPaymentRequestService.swift b/Bitkit/Services/PaykitPaymentRequestService.swift
index 888b44b5..af0c9e71 100644
--- a/Bitkit/Services/PaykitPaymentRequestService.swift
+++ b/Bitkit/Services/PaykitPaymentRequestService.swift
@@ -149,6 +149,13 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
guard record.state != .activeRecurring else { return .failure(.recurringRequest) }
guard let terms = record.terms else { return .failure(.missingTerms) }
+ #if DEBUG || E2E_BUILD // test hook
+ if CommandLine.arguments.contains("-QA_FIXED"), terms.amount.asset == "usd",
+ let conversion = terms.conversion, case let .fixed(rates) = conversion {
+ let qaRates = rates.map { "\($0.asset)=\($0.value)" }.joined(separator: ",")
+ Logger.info("QA decoded request=\(record.paymentRequestId) amount=\(terms.amount.value) asset=\(terms.amount.asset) endpoints=\(terms.acceptedPaymentEndpointIdentifiers.joined(separator: ",")) rates=\(qaRates)", context: "ReviewLockQA")
+ }
+ #endif // test hook
guard terms.recurrence == nil else { return .failure(.recurringRequest) }
let paymentDeadline: PaykitPreciseInstant?
if let deadline = terms.paymentDeadline {
@@ -876,21 +883,29 @@ struct PaykitPaymentRequestService {
let metadataText = String(decoding: metadataData, as: UTF8.self)
let rates = try await PaykitRequestPricing.rates(requested: draft.amount.asset,
endpoints: acceptedPaymentEndpointIdentifiers, market: marketRate(), at: proposalDate)
+ #if DEBUG || E2E_BUILD // test hook
+ let qaFixed = CommandLine.arguments.contains("-QA_FIXED") // test hook
+ #else // test hook
+ let qaFixed = false // test hook
+ #endif // test hook
+ let qaUsdt = CommandLine.arguments.first(where: { $0.hasPrefix("-QA_USDT=") })?.dropFirst(9).description ?? ""
+ let qaAddress = CommandLine.arguments.first(where: { $0.hasPrefix("-QA_ADDRESS=") })?.dropFirst(12).description ?? ""
let terms = try Paykit.PaymentRequestTerms(
amount: Paykit.PaymentRequestAmount(
- value: draft.amount.value,
- asset: draft.amount.asset.rawValue
+ value: qaFixed ? "1" : draft.amount.value,
+ asset: qaFixed ? "usd" : draft.amount.asset.rawValue
),
paymentReference: Paykit.PaymentReference(text: "bitkit-\(UUID().uuidString)"),
proposalExpiresAt: Self.timestamp(draft.expiresAt),
recurrence: nil,
- acceptedPaymentEndpointIdentifiers: acceptedPaymentEndpointIdentifiers,
- paymentEndpoints: nil,
+ acceptedPaymentEndpointIdentifiers: qaFixed ? ["btc-regtest-p2wpkh", "usdt-arbitrum-address"] : acceptedPaymentEndpointIdentifiers,
+ paymentEndpoints: qaFixed ? ["btc-regtest-p2wpkh": "{\"value\":\"\(qaAddress)\"}", "usdt-arbitrum-address": "{\"value\":\"\(qaUsdt)\"}"] : nil,
requiredAppId: "bitkit",
- conversion: rates.isEmpty ? nil : .fixed(rates: rates),
+ conversion: qaFixed ? .fixed(rates: [.init(asset: "btc", value: "0.00001"), .init(asset: "usdt", value: "1")]) : (rates.isEmpty ? nil : .fixed(rates: rates)),
paymentDeadline: .at(timestamp: Self.timestamp(draft.expiresAt)),
metadata: Paykit.PrivateJsonObject(text: metadataText)
)
+ if qaFixed { Logger.info("QA fixed proposal terms: \(terms)", context: "ReviewLockQA") }
let record = try await sdk.proposePaymentRequest(
counterparty: target.publicKey,
terms: terms,[2026-10-29 04:02:25.962 UTC] INFOℹ️: QA decoded request=628bb443-e517-4cd8-ac92-ae2191d3abac amount=1 asset=usd endpoints=btc-regtest-p2wpkh,usdt-arbitrum-address rates=btc=0.00001,usdt=1 - ReviewLockQA [PaykitPaymentRequestService.swift: parse(record:expectedRole:now:network:requiresActionableRequest:) line: 156]
Test 4: app lock after payment authorization (R1) ❌ The active swipe reopened payment PIN while the original send was held; logs and accessibility captures confirmed the prior authorization and app unlock.
This drives the requested authorized-send check. After the real payment PIN succeeded, a debug hook held the original on-chain operation after durable transaction preparation and immediately before broadcast. While held, I backgrounded the app, reopened it and entered only app-unlock PIN. The same request returned with an arrow and active Swipe To Pay; another swipe opened payment PIN again.
I cancelled that second PIN without authorizing another payment. Releasing the original operation completed successfully. The activity list gained exactly one outgoing payment for this case, and requester accessibility capture showed Proof submitted. Raw transaction decoding confirmed one 1,000-sat recipient output and a 143-sat fee: 1a3c9f5a1f7278071286bc7503906530cb4159b8b87ee9dfb3df0deb49e60a3e. No duplicate debit was observed.
The failed invariant is the active payment control while the original operation is running. Source comparison confirms the condition predates this PR: the manual swipe does not receive the retained isSubmittingPayment state, and submitPayment() does not check that flag before asking for PIN. The changed cleanup path preserves the pre-existing in-flight operation. There is no introduced diff line for an inline finding.
R1-verification.2x.mp4
After app unlock: active swipe, second PIN cancelled and the original send completes, 2×. Hook logs establish when the hold began and ended; accessibility captures establish the unlock boundary, and chain decoding and activity captures establish the one-payment result.
Dispatch timing test hook
Not pushed. Holds after real authorization and durable preparation, then releases through a test-owned file. It leaves request checks, financial guards and native broadcast intact.
--- a/Bitkit/Services/OnchainSendAttemptService.swift
+++ b/Bitkit/Services/OnchainSendAttemptService.swift
@@ -666,6 +666,17 @@
throw OnchainSendAttemptError.preDispatch(preDispatchError)
}
+ #if DEBUG // test hook
+ if CommandLine.arguments.contains("-QA_HOLD_DISPATCH") { // test hook
+ let gate = Env.appStorageUrl.appendingPathComponent("v4-1039-release-dispatch") // test hook
+ let evidence = "txid: \(prepared.txid)\namount: \(prepared.recipientAmountSats)\naddress: \(address)\nrequest: \(String(describing: requestId))\ninputs: \(prepared.inputs)\n" // test hook
+ try evidence.write(to: Env.appStorageUrl.appendingPathComponent("v4-1039-held.yaml"), atomically: true, encoding: .utf8) // test hook
+ Logger.info("QA dispatch held after authorization and durable preparation", context: "ReviewLockQA") // test hook
+ while !FileManager.default.fileExists(atPath: gate.path) { try await Task.sleep(for: .milliseconds(200)) } // test hook
+ Logger.info("QA dispatch released", context: "ReviewLockQA") // test hook
+ } // test hook
+ #endif // test hook
+
if let winner = try winningResult(attemptId: attemptId) {
return winner
}[2026-10-29 04:18:26.942 UTC] INFOℹ️: QA dispatch held after authorization and durable preparation - ReviewLockQA [OnchainSendAttemptService.swift: send(using:address:amountSats:satsPerVbyte:utxosToSpend:isMaxAmount:requestId:orderId:paymentIdentity:followupContext:transferContext:paymentDeadline:beforeBroadcastAttempt:onPreDispatchFailure:) line: 671]
[2026-10-29 04:19:18.566 UTC] INFOℹ️: QA dispatch released - ReviewLockQA [OnchainSendAttemptService.swift: send(using:address:amountSats:satsPerVbyte:utxosToSpend:isMaxAmount:requestId:orderId:paymentIdentity:followupContext:transferContext:paymentDeadline:beforeBroadcastAttempt:onPreDispatchFailure:) line: 673]
Check 1: retained evidence, review scope and twin
Reviewed the contiguous delta 69b4181..9073085 and prior feedback, including imported master changes and their reached payment, recovery, backup and Blocktank callers. Native journeys here cover payment-review restoration and authorized sending; imported USDT recovery, backup, allowance execution and Blocktank migration flows were inspected in source but did not receive separate device journeys.
- Retained the unchanged Amount-screen lock, Savings/amount retention and unpaid dismissal evidence from the passing review at 69b4181. The changed confirmation paths received the current-head tests above.
- Fee-speed and tag retention remains explicitly deferred to bitkit-ios#1059 fix: retain fee speed and tags after app unlock.
- Twin: bitkit-android#1591 fix: deliver completed Paykit proofs promptly. The payment-review deferral and fresh-authorization intent matches; iOS reconstructs its sheet after app lock, whereas Android retains content beneath its authentication UI.
Current-head CI reports unit, integration, E2E, formatting and translation checks successful; optional migration was skipped. No test in this run's plan remains unrun.
Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete pull request diff from merge base e7634224 through 90730855. No coverage baseline was supplied after the comparison base changed, so nothing earlier was inherited. The verified merge base is the current base.
No new actionable code findings.
An unpaid Bitcoin confirmation, including one opened from amount selection, stays on the current send sheet across app lock. Unlock runs setup again only while the same session and payment context still own that sheet; a real dismissal still clears the request. Amount, USDT, and hardware steps keep the request, amount, and terms until dismissal. Restoring confirmation still resets the fee speed and clears tags, which remains deferred in bitkit-ios#1059 (thread). The active swipe after unlock during an authorized in-flight send is the existing recreated swipe control: submission state is local to that control, and this diff does not retain an in-flight payment as a new review. The latest device check cancelled the second PIN and recorded one payment.
This review inspected the app-lock tests and did not run them. On this revision, unit tests and SwiftFormat passed. Device testing: not performed. Android a56c75be draws the lock screen over the existing content, so it does not recreate the send sheet; the iOS journey already records that difference, and the Android review-refresh.xml at that revision does not include the iOS lock steps.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict: ✅ Approve — the previously failing in-flight lock case now keeps the payment control busy and blocks another authorization.
Findings: None in 9073085..aae380f.
QA for aae380f: Xcode 27.0, iPhone 17 Pro simulator, iOS 26.5, disposable staging identities and Bitcoin regtest funds. App PIN and payment PIN enabled; biometrics disabled.
App unlock during an authorized payment (manual 1) ✅ The restored spinner ignored another swipe, and releasing the original send completed one payment and requester proof.
After real payment-PIN authorization, the disclosed debug hook held the native on-chain operation after durable transaction preparation and immediately before broadcast. Backgrounding and entering only app-unlock PIN restored the same 1,000-sat confirmation with its busy spinner. Dragging the swipe control did not open another PIN prompt. Releasing the hold once reached Bitcoin Sent.
This passed twice with separate requests. Each added exactly one outgoing activity and one 1,000-sat recipient output with a 143-sat fee; each recipient's chain history contained only its matching transaction. The requester showed Proof submitted for each request. The video is the shorter second execution; its existing outgoing activity belongs to the first execution. Activity snapshots and raw-transaction decoding independently verified both payments.
| Payer | Requester |
|---|---|
R1-repeat-p2x.2x.mp4 |
![]() |
Payer: authorization, app unlock, ignored second swipe and completion, 2×. Requester: proof for the recorded second request.
Dispatch timing test hook
Not pushed. Only the payer used this hook. It holds the real operation until an explicit release; payment validation, submission state, native broadcast and completion remain unchanged.
diff --git a/Bitkit/Services/OnchainSendAttemptService.swift b/Bitkit/Services/OnchainSendAttemptService.swift
index f222a4d8..fc04320a 100644
--- a/Bitkit/Services/OnchainSendAttemptService.swift
+++ b/Bitkit/Services/OnchainSendAttemptService.swift
@@ -663,6 +663,17 @@ actor OnchainSendAttemptService {
throw OnchainSendAttemptError.preDispatch(preDispatchError)
}
+ #if DEBUG // test hook
+ if CommandLine.arguments.contains("-QA_HOLD_DISPATCH") { // test hook
+ let gate = Env.appStorageUrl.appendingPathComponent("v4-1039-release-dispatch") // test hook
+ let evidence = "txid: \(prepared.txid)\namount: \(prepared.recipientAmountSats)\naddress: \(address)\nrequest: \(String(describing: requestId))\ninputs: \(prepared.inputs)\n" // test hook
+ try evidence.write(to: Env.appStorageUrl.appendingPathComponent("v4-1039-held.yaml"), atomically: true, encoding: .utf8) // test hook
+ Logger.info("QA dispatch held after authorization and durable preparation", context: "ReviewLockQA") // test hook
+ while !FileManager.default.fileExists(atPath: gate.path) { try await Task.sleep(for: .milliseconds(200)) } // test hook
+ Logger.info("QA dispatch released", context: "ReviewLockQA") // test hook
+ } // test hook
+ #endif // test hook
+
if let winner = try winningResult(attemptId: attemptId) {
return winner
}
[2026-10-29 08:45:36.238 UTC] INFOℹ️: QA dispatch held after authorization and durable preparation
[2026-10-29 08:45:51.686 UTC] INFOℹ️: QA dispatch released
Confirmation submission and app-lock regressions ✅ The focused suites passed on the unmodified head; assertions were verified in the Xcode result bundle.
Ran both confirmation lifecycle/swipe suites and the three request app-lock regressions. They cover retained busy state, recreated confirmation refusing another PIN, unchanged navigation and submission ownership, idle authorization, teardown/remount, both dismissal orders, and session/context/submission exclusions. The Amount/USDT-flag regression is a hosted exclusion check, not a funded USDT test.
A1-t2x.2x.mp4
Hosted regression execution, 2×; assertion evidence comes from the result bundle.
Check: retained journey evidence and scope ✅ Earlier passing reports cover the unchanged unpaid-review paths.
J1 direct and fixed-USD entry, confirmation/payment-PIN lock, cancellation, fresh authorization and single-payment evidence remain in the review at 9073085. Its failed authorized-send case is replaced by the passing current-head case above. The new guard leaves idle submission unchanged, also checked in the focused suite.
The Amount-screen report at 69b4181 still covers Savings/amount retention, continuation, unpaid dismissal and reopening: Amount, preparation and cleanup are unchanged in this delta. These retained device cases were not repeated. Fee-speed and tag retention remain deferred to bitkit-ios#1059 fix: retain fee speed and tags after app unlock.
Reviewed the contiguous 9073085..aae380f delta and reached swipe, authorization, retained-presentation and completion callers. Native testing covers the changed in-flight manual payment path; automatic subscription re-entry received hosted coverage. Biometric-specific authorization, hardware and USDT flows were not separately driven in this delta. No planned case remains unrun.
Twin: bitkit-android#1591 fix: deliver completed Paykit proofs promptly, checked at 2f0e03d, matches the shared intent. Android retains its loading state beneath the authentication overlay; iOS reconstructs the confirmation and now derives its busy state from the retained submission flag.
Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete PR diff at aae380fc against verified base and merge base e7634224, as explicitly requested. No earlier coverage was inherited.
No new actionable code findings.
The retained submission flag now controls both the restored swipe's busy state and its authorization entry guard. The earlier amount-entry and nonretained-screen cleanup concerns are fixed in source. Restoring an unpaid confirmation still resets selected fee speed and tags; this remains tracked in #1059.
Validation: diff whitespace checks and changed-journey XML parsing passed locally. Native tests and SwiftFormat were not run locally because Swift/Xcode are unavailable. The CI unit workflow passed on merge 63700e5f, which combines this head with base 383491f2, rather than the supplied assessment base. Android comparison at a56c75be confirms that its content remains beneath the lock overlay; visual equivalence was not tested.
Device testing: not performed in this review. A separate current-head report records manual case 1 passing with a disclosed pre-broadcast timing hook; its unpaid-review cases rely on older revisions.
Suggested additional test cases
- iOS: with app PIN enabled and a short-lived, unpaid Bitcoin request on confirmation, background until its deadline expires, then unlock. Expect dismissal or unavailable-request feedback, with no payable stale review and no outgoing payment.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
|
@ben-kaufman Merging this PR on the approvals of head aae380f, with ovi-reviewer's device run on that head, checks green and no open threads. The PR stacked on this branch is retargeted to master first. |
|
@ben-kaufman Merged, as it was approved and green. |

This PR restores incoming Bitcoin payment review after app unlock and keeps an in-flight payment busy when the confirmation is recreated. Parent #1025 is merged; this PR now targets
master.Description
Retain the current Bitcoin confirmation even when the request originally opened on the amount/payment-method screen. Restore the confirmation route and reload fees and funding setup after unlock under the existing session and payment-context checks.
Keep amount selection available during synchronization, while confirmation and PIN routes retain their synchronization safeguards.
Preserve the current request, amount and terms while app lock temporarily hides Amount or another send step outside the retained Bitcoin confirmation flow. Actual dismissal still clears the request.
Clear dismissed reviews in either callback order. Preserve the existing handling of submitted payments, hardware signing, USDT and Pending recovery.
Keep the restored pay control busy during an in-flight payment and ignore another authorization entry while the original operation completes.
Out of Scope
Preserve fee speed and tags across app-lock restoration: #1059, tracked for 3.2.0 (review thread).
New USDT and hardware review restoration behavior beyond preserving the existing presentation, financial retries and authorization changes.
The proof-delivery and Pending recovery-prompt changes inherited from fix: deliver Paykit payment proofs promptly #1025.
Design
N/A — reuses the existing confirmation screen.
Preview
Updated device journey recording pending.
QA Notes
Setup
Use the journey guide and Payment Request setup at
aae380fcwith disposable regtest wallets and a saved, linked fixture issuer. The current source pins Paykit0.1.1-rc2and Core0.8.0-rc11; resolve the committed Package.resolved without updating versions. Keep ordinary payment authorization enabled. The controlled transport/admission faults named below require additional fixture control; compatible issuer/homeserver access and staging merchant/Locks unlock must be established separately. Missing prerequisites leave the affected cases blocked.For manual case 1, use the controlled post-prepare/pre-broadcast hold described in review R1, with an independently authorized disposable regtest fixture. No hold hook or funding permission is added by this PR; without that controlled fixture the native case remains blocked.
Journeys
review-refresh.xml— open a request through amount/payment-method selection, choose Bitcoin, lock/unlock during confirmation, then cancel PIN and complete a later single authorized payment. The original request, ready control and fee review must remain available. Also open a fresh unpaid 1,000-sat request on Amount, lock/unlock, check the same amount and Savings wallet, continue to confirmation, then close it without paying and confirm the request remains pending.Manual Tests
Automated Checks
PaykitPaymentRequestServiceTests.swift— actual teardown/remount restores both direct-confirmation and amount→confirmation routes; dismissal works in either callback order. Session, context, sheet ownership and active-submission changes prevent retention; hardware and USDT exclusions remain. The added regression preserves Amount and a synthetic USDT-exclusion case through teardown/remount, then verifies real-dismissal cleanup and no acceptance; this is not a native USDT payment test.SendConfirmationLifecycleTests.swift— retain required setup/synchronization checks, one successful coin preparation and cancellation of an earlier task when a second enters.Evidence
October 29 in-flight submission fix at
aae380fcAddresses review R1: the retained submission flag now drives the swipe spinner and blocks another authorization entry while the original operation retains completion ownership. This fixes an existing UI condition exposed by reconstruction; the preceding reviewer cancelled the second PIN and observed one payment, with no duplicate debit.
Author validation on Xcode 27.1 / SDK 27.1 / iOS 26.5, with the committed dependency pins:
BitkitTests, with the current four CI integration exclusions: 2,832 passed, zero failed, one skipped.UsdtNativeRecoveryIntegrationTests/testEncryptedRecoveryAgainstLocalProtocolFixtureskipped because its separate loopback recovery fixture was unavailable; its source is unchanged from the pre-fix head and actual master. That coverage remains unverified.git diff --checkpassed. No translation source changed.Earlier local attempts stalled before test execution in asset compilation or simulator startup. Their failures were retained; the passing runs above followed Ben's explicitly approved shared Simulator-service restart. Hosted checks for this published head are monitored separately; no manual CI rerun was requested.
Remaining native acceptance: manual case 1 has not been run by this author on
aae380fcand requires the authorized controlled transport fixture. Prior J1a/J1b and R1 recordings remain attributed to90730855. No new payment, funded USDT, physical-hardware, field latency, merchant-unlock or final Android206/iOS215 measurement is claimed. Existing pending human requests and review opinions are preserved.Current follow-up at
69b41810— October 26Fixes the app-lock state-loss finding. The current presentation now returns before setup cancellation and final cleanup when retained Bitcoin confirmation restoration does not apply. The suspension marker remains until remount or actual dismissal so existing session/context invalidation still works.
Validation on Xcode 26.1 / SDK 26.1 / iOS 26.5:
8e9a0341f5314e60777f874a6757760623ce10d3: 19-target build and 2,924 canonical hosted unit tests passed, zero failures/skips. These are two full suites, not seven independent runs. Both select BitkitTests with the exact six existing CI integration exclusions.Current-head hosted CI is pending after publication. The new native journey steps remain unrun; no payment, physical-hardware, final latency or merchant-unlock result is claimed. The prior #1025 hardware-transfer and #1054 preparation/UI-launch CI incidents remain unresolved; this correction does not establish their causes or fixes. #1098 remains a separate unmerged dependency.
Historical integration validation at
0df5ebfbIntegration with master
4530ff7bat0df5ebfbretains both USDT-warning cancellation and the payment-review callback. Bottom #1025: all-target build passed (19 targets), 378 focused tests passed, 0 failures/skips. One combined #1070 + #1081 + #1084 + #1085 composition, tree8f1e72d3from #10707cd096f4plus #10818967dff8, #1084b7561c08and #1085310e689a, covers the stack and those sibling changes: all-target build passed (19 targets) and 2,779 tests passed, 0 failures/skips using the six existing CI exclusions. These are not eight independent full-suite runs.All eight heads passed SwiftFormat. Translation checks report 0 errors and 15 warnings, all matching master by key, language, file and occurrence count, including
security__use_pin. Final toolchain/runtime and complete diagnostic disposition: Xcode 26.1 / iOS 27.0 simulator, serial tests; complete diagnostics inspected, with six SDK-construction QoS warnings and retained fixture/appearance messages matching baseline evidence; no introduced finding. CI was pending at that historical publication; see the current follow-up above for this head.Gaps
The updated app-lock journey remains unexecuted. The diagnostic CI rerun passed at preceding head
c1e06c20; it does not verify this new integration head.The final Android 206 / iOS 215 comparison cohort was blocked before financial authorization, with no debit. It supplies no final payment-latency result or native benefit claim. Case J1 remains unchecked and unexecuted on this integrated head. Native USDT-warning, PIN and biometric flows have not been verified by this integration validation.
October 26 master integration
Published integration head
c77f1179a0a0cc135e84f90c2dc6414f93e73222incorporates actual master2ce210252be8ae7ea130d2422a2fa704a191ef7dthrough ordinary signed merges, preserving the stacked base and prior child changes. The two parent conflict resolutions retain review-state ownership, root-safe subscription Back navigation, one outer USDT status task, prompt recipient-scoped proof reconciliation and the terminal-refresh exit. No local assertion/timeout weakening or new test suppression was introduced.Validation: two 19-target builds and two canonical hosted unit suites passed: 2,910 on the parent and 2,937 on the combined tree, zero failures/skips with the exact six CI integration exclusions. This PR is covered by the combined stack/sibling tree
f471e8a6(2,937 units), including this branch; these are two suites, not eight independent runs. Full SwiftFormat0.64 passed on all eight final trees (0/844); translations had zero errors and 15 warnings matching execution on complete actual-master source. Independent frozen integration review found no actionable issue. Local environment: Xcode26.1/SDK26.1, iOS26.5 simulator.Complete diagnostics retained: two App Intents notices per build; parent1,082/combined1,099 runtime scan records, including four/six SDK/Pubky/DHT QoS stacks, fault fixtures and hosted appearance/identity notices. Source comparisons are not exact-base runtime replay or warning-free evidence. New-head hosted CI follows publication. Master's #1130 retired Blocktank skips and E2E285 channel/migration omissions remain coverage gaps, not fixes for original #1109. Historical #1054 preparation/UI-launch causes remain unresolved. Existing QA case IDs and native gaps remain: no new payment, funded USDT, physical hardware, latency or merchant-unlock journey was run by this integration.
October 26 lifecycle integration and camera fixture
Published head
88f75ea4b66ef2e8b9b79f44e4892497502ab632includes approved and merged #1098 through masterb8b7f3d9902de5669c025bb839232c1d7e98d2df. The overlapping lifecycle test retains one owned switch task per current context, holds resumed preparation until confirmation is visible, and keeps the exact-one-success assertion, cancellation/context guards and retained presentation/app-lock behavior. Seven ordinary signed merges preserve stacked bases and prior child changes; #1084/#1085 also retain merged #1081 through #1070.The existing unfunded USDT UI case now resets camera permission before launch, explicitly waits for the camera alert, allows it and waits for dismissal before manual entry. This adds six test-only lines and two assertions; no test, assertion or timeout was removed or weakened. Production camera behavior and the merged Receive blank-band checks are preserved.
Final validation passed two 19-target builds, 2,913 parent canonical units and 2,940 units on combined tree
db653be56e228dec55f4e6c0fd57e42e7d8475c2, with zero failures/skips and the exact six CI integration exclusions. This branch is included in that combined composition. These are two suites, not seven independent runs. The combined unfunded UI case passed, including both Receive blank-band swipes and explicit camera handling, using runner-only loopback endpoint settings. Full SwiftFormat 0.64 passed on all seven final trees (0/844); translations are unchanged. Independent final review found no actionable issue. All 151 distinct commits across local current/prospective base ranges verified cryptographically; all 129 distinct commits across the final published GitHub ranges report verified=true/reason=valid.Complete diagnostics remain retained: parent 1,107/combined 1,125 broad records, including four/six SDK/Pubky/DHT QoS stacks, App Intents notices, hosted appearance and fault-fixture messages. The combined shared-keychain -25300 publication diagnostic has unchanged emitter/test source; its exact runtime cause remains unestablished. Source comparison is not exact-base runtime replay or warning-free evidence.
The old Receive-dismissal failure in #1070 run 40394372963 did not reproduce in the controlled original-head run; that run passed and used XCTest's default camera interruption handler. The camera-fixture comparison passed with an identical 157-file production app bundle. Neither result establishes the Receive failure's cause or repair. Local Xcode 26.1/SDK 26.1/iOS 26.5 differs from hosted CI; the completed hosted results are recorded below. The daemon launch issue #1110, original hardware-transfer issue #1109 and earlier coverage gaps remain. No new payment, funded USDT, physical-hardware, final latency or merchant-unlock result is claimed. João, Piotr and Ovi remain requested; existing pending requests are preserved without duplicate notification.
October 26 hosted CI confirmation
For published head
88f75ea4b66ef2e8b9b79f44e4892497502ab632, unit/UI run 40412990424 passed on attempt 1: 2,916 unit tests, zero failures/skips; 43 UI executions, 39 passed and four existing prerequisite skips, zero failures. SwiftFormat 40412990399 passed: 0/844 files require formatting, 80 skipped. These workflows tested GitHub's PR merge commit using Xcode 16.4/SDK 18.5 on iOS 26.2.Complete workflow diagnostics were inspected. The 571 duplicate-symbol records match the retained passing #1025 run 40376172874; workflow/cache-action sources match actual master. This is retained-log/source comparison, not an exact-base runtime replay or warning-free result. The remaining red aggregate contains cancelled duplicate jobs; the replacement unit/UI and formatting workflows passed.
No manual CI rerun was requested. Historical Receive-dismissal/daemon-launch causes and original hardware-transfer issue #1109 remain unresolved; existing skipped/deferred coverage and final Android 206/iOS 215 payment/unlock measurement remain incomplete. Existing human review requests are preserved.
October 27 E2E after retargeting to master
For unchanged head
88f75ea4b66ef2e8b9b79f44e4892497502ab632, the automatic retarget workflow dispatched E2E run 40447918061. It passed on attempt 1: 13 cases passed, one existing hardware-transfer case skipped, zero failures. The first test step passed; retry steps 2 and 3 were skipped. The app build used CI merge86ee3bcb980dc0649d35dfd2859dd1fbdd40238eover masterb666ccb183837d9b49cf01809dad41166bd04702, Xcode 26.2/SDK 26.2 and iPhone 17/iOS 26.2. E2E source wasec01c02ff611797c112e65b5c87c936d25720bac; the existing transfer-to-spending skip does not resolve #1109.Complete logs and diagnostics were inspected: 68 stale-element warnings, five npm deprecations, Buffer/build/setup notices and four log-capture restarts (143/137/124/124). The 82 diagnostic records use the same message groups retained from the earlier passing E2E log, with two fewer stale-element warnings and no path-filter base-input warning; 11 additional App Intents informational notices are retained separately. E2E/retarget workflow, project and package-pin blobs match the exact head, CI merge and current master. This is retained-log/source comparison, not exact-base runtime replay or warning-free output; log-capture interruptions limit absence-of-error conclusions.
No manual rerun was requested. Existing native app-lock/PIN/biometric journeys, historical Receive/daemon/hardware-transfer causes and final payment/unlock measurements remain outside this result. João's effective changes request and all three pending human requests are preserved.
Models used
gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:xhigh)gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:xhigh)gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:high)