Repository navigation
fix: keep Paykit inbox observation together - #1070
Conversation
[Medium impact] This PR appears safe to merge; no actionable issues were found. SummaryKeeps automatic Background message receipt and its fresh Payment Request read in one queued operation, after any pending-message drain.
No actionable issues found. Tests were inspected, not run. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Automatic Background refresh] --> B{Full refresh?}
B -->|Yes| C[Drain pending messages]
B -->|No| D[Acquire SDK operation lock]
C --> D
D --> E[Check admission and identity]
E --> F[Receive messages]
F --> G[Record backup changes and intake reports]
G --> H{Still admitted?}
H -->|Yes| I[Read fresh state under the same lock]
H -->|No| J[Stop and allow later polling]
I --> K[Release lock and update request list]
Reviews (1) · Last reviewed commit: "fix: keep Paykit inbox observation toget..." · Reviewed by Greptile |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Automatic Background inbox refreshes now run message receipt and the fresh observeState read in one queued SDK operation, so a queued background publication can no longer land between them; saved-contact forced refreshes drop to Background priority so a payment review can defer them. Reviewed b1d65a7, full tier, reasoned from the code and CI (iOS does not build on this reviewer's Linux host).
What I checked, and 6 candidates I ruled out
Read in full: PaykitSdkService.receiveAndObserveState, withSdk, both withStateRevisionTracking overloads, withBackupStateRevisionTracking, checkRefreshAdmission, PaykitSdkOperationLock (withLock, withoutLock, withWalletWipe, admit/validate), PaykitPaymentRequestService.synchronize, PaykitPaymentRequestManager.refresh (both) and performRefresh, AppScene.refreshIncomingPaykitPaymentRequests and the saved-contacts publisher.
Call sites traced: refresh(... refreshPriority:) from AppScene.swift:682 and :705; synchronize from performRefresh, where priority and messagePriority are always equal, so the combined path is taken exactly for non-forced Background refreshes with an active identity.
CI: SwiftFormat green; Run Tests still pending at review time.
Ruled out
- Observation outside the lock:
operationLock.withoutLockruns inside thewithSdkclosure, so the lock acquired for receive is still held;withoutLockonly adds wipe admission (admit/validate), which is what makes the wipe case throwwallet_wipe_in_progress. - Backup version bumped by the read:
withStateRevisionTracking(sdk:)wraps onlyreceivePrivateMessagesFromLinkedPeers; the observation runs after the revision comparison, and a deferral raised frombeforeOperationpropagates before thedo/catchthat marks the backup changed. - Stale key between receive and observe: no other operation can take the lock in between, and the
sdk ===,cachedPaykitKeypublic key and generation checks plus the post-readobservation.keyGenerationcheck cover an identity error that cleared the cache. - Intake diagnostics or sync time lost when the read fails:
onReceiverunslogIntakeFailuresand takesnow()before the second admission check, matching the old order (receive, log, date, observe). - Saved-contact forced refresh lost during review: at Background it now defers, but forced refreshes keep the separate receive/observe path and ordinary polling picks the request up later; this is the stated intent and the journeys were updated to drop the immediate-arrival assumption.
- Overlapping refresh drops the flag: the recursive upgrade in
refresh(excludingProtectedRequestId:...)forwardscombineReceiveObservation.
The MainActor-hop wipe boundary after the awaited admission check is not forced by a test, as the PR body says; the post-read validate in withoutLock covers it in source.
Merge confidence: 4/5, no findings, but Run Tests had not finished and the change could not be built here.
|
Milestone 3.2.0, matching its Android twin synonymdev/bitkit-android#1645: it is stacked on #1066 and its device measurements are still in progress, so it isn't a 3.1.0 fix. #868 stays on 3.1.0 for now; this PR refers to it rather than closing it. |
There was a problem hiding this comment.
Verdict: ✅ Approve
Findings: None.
QA for b1d65a7: Two iPhone 17 Pro simulators on iOS 26.5, disposable Bitcoin-regtest wallets and a local USDT fork. Recordings show payer left and requester right at 2×.
Test 1: automatic payment request presentation ✅ Cancellation, retry, sheet ownership and insufficient-balance handling passed.
The 21,000-sat request stayed pending after cancellation during preparation, then became ready in the same confirmation on explicit retry. Receive retained ownership while a 5,000-sat request arrived; closing Receive presented it. Both pending rows remained unique and unpaid. The drawer also retained ownership. An unaffordable request showed insufficient-balance feedback once automatically and again on explicit retry, without payment.
A debug gate controlled preparation timing; SDK records and responses were real. Foreground resume was checked in the lifecycle trace before the retained recording.
automatic-presentation.2x.mp4
Preparation cancellation, retry, presentation ownership and insufficient-balance feedback.
Test 2: payment review refresh deferral ✅ Review survived cancellation and app lock, the original paid once, and the USD review required fresh authorization.
On the pristine build, the 1,000-sat confirmation remained unchanged during a 30-second wait and a second, 2,000-sat request. Payment PIN stayed open for another 30 seconds. Cancelling PIN and review sent nothing; ordinary polling then presented the second request. Reopening the original and unlocking the app preserved its amount, prepared 143-sat fee and working details. Authorization completed one payment: the requester received 1,000 sats and recorded “Proof submitted”; the second request remained unpaid. Regtest block decoding confirmed the payment and fee.
The additional USD 1 request used fixed rates btc=0.00001 and usdt=1. Its 1,000-sat Bitcoin confirmation survived app lock, including backgrounding at payment PIN. Unlock returned to unpaid confirmation and required a new swipe and payment PIN. Cancelling left the balance and outgoing Activity unchanged. The issuer used a controlled rate; the payer remained pristine. The local USDT fixture recorded zero submissions.
| Bitcoin review and payment | USD review and app lock |
|---|---|
review-refresh.2x.mp4 |
fixed-price-lock.2x.mp4 |
Both recordings retain the original amounts through app lock and authorization changes.
Test 3: delayed saved-contact callback during review ✅ Background admission deferred, the original review stayed intact, and polling recovered the new request.
A debug gate held the real saved-contact callback before its refresh. With the 21,000-sat confirmation open, the requester sent a 3,000-sat request. Releasing the callback preserved the original review; the admission trace confirmed Background deferral with the three warm requests retained. After cancelling review, ordinary polling presented the new request without restarting or sending a payment. SDK records and responses were real.
saved-contact-callback.2x.mp4
Original review retained during callback release, followed by ordinary polling recovery.
Test 4: pristine build and focused regressions ✅ The unmodified head built and both selected hosted suites passed.
I ran PaykitSdkOperationLockTests and PaykitPaymentRequestServiceTests. The XCTest result bundle confirms execution without failures or skips, including receive/read ordering, receive-only backup accounting, admission, cancellation, identity rejection and later refresh recovery.
focused-tests.2x.mp4
Hosted test execution at 2×; assertion outcomes were checked in the XCTest result bundle.
Full review: 6153563…b1d65a7, all nine changed files and reached callers. All planned cases passed. Wipe, identity, cancellation and backup interleavings have source and automated coverage; latency against the base remains unmeasured. Earlier source report: review.
Pair: bitkit-android#1645 fix: keep incoming request refreshes together at 8564ba2 matches receive/read composition and Background contact-refresh deferral in source.
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 review of the complete PR diff against merge base 6153563f7f73b44ed1a5f99c8606a3fab89e2bf4, at b1d65a7f0787717f03cb59a8922610a017113ed1. No earlier assessment baseline was available.
No new actionable code findings.
Automatic Background inbox refreshes receive linked-peer messages and read fresh Payment Request state in one queued SDK operation, after the existing full-refresh pending-message drain. A queued background publication waits until that pair finishes. Saved-contact forced refreshes stay on the separate receive/read path at Background priority, so an open payment review defers them and a later foreground poll (10-second inbox interval) shows the request. Receipt still records backup changes and intake diagnostics when the following read is deferred, cancelled, rejected for identity or wipe, or fails. Review admission is the send sheet’s confirm, PIN, and hardware-sign routes.
The shared refresh contract matches bitkit-android#1645 at 8564ba2a1a96e4d1ac56c4b084143f619968d48a: combined automatic receive/read, Background contact-refresh deferral, and later polling. iOS still parses requests with the timestamp taken immediately after receipt.
Validation: Run Tests and SwiftFormat succeeded on this head. Those runs and the new lock and service tests were inspected; this review did not execute them. Device testing: not performed in this review.
Ready for device testing. Use the updated automatic-presentation.xml and review-refresh.xml journeys and the saved-contact callback manual case.
Suggested additional test cases
- iOS, two linked regtest wallets, payer confirmation open for a 1,000 sat request. Send a 2,000 sat request, then background and app-lock the payer before that request is shown. Unlock. The restored confirmation still shows 1,000 sats. After ordinary foreground polling, the 2,000 sat request appears once and stays unpaid.
jvsena42
left a comment
There was a problem hiding this comment.
Code scan at b1d65a7: no issues found. Reviewed this PR's own commit on top of #1066 (6153563). Reviewed as a Paykit sync change.
- No deadlock: the SDK lock isn't reentrant, and nothing inside the combined receive+observe hold takes it again.
- Stale-state checks: wipe, runtime and identity generations are checked before the lock, before receive and before observe. The key generation is checked against the observation, which is stricter than the old observe path.
- Narrow scope: only non-forced Background refreshes use the combined path. Forced and explicit refreshes keep the separate calls.
- Partial failure: if observe defers or fails after a successful receive, nothing is lost or shown twice. The refresh generation was already reset, so the next inbox poll still runs.
- No payment path touched.
- Parity: this side moves one forced refresh to Background (
AppScene.swift:682). Android moves two, because only Android forces a refresh after endpoint refresh. See synonymdev/bitkit-android#1645.
Device gate (partial): b1d65a7 — not run yet. Pending:
automatic-presentation.xmlandreview-refresh.xml;- time how long an incoming request takes to appear while a background publication is queued;
- re-check the resync time after closing review.
Device results will follow in a separate review.
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Re-reviewed the full PR diff against merge base 5e801df, at e16d13bc, after the merge base changed since the saved assessment. No prior coverage was inherited.
No new actionable code findings.
Automatic Background inbox refreshes receive linked-peer messages and read fresh Payment Request state in one queued SDK operation, after the full-refresh pending drain. A queued background publication waits until that pair finishes. Saved-contact forced refreshes use Background priority on the separate receive/read path, so an open payment review on confirm, PIN, or hardware sign defers them. The 10-second foreground poll applies the request after review ends. A completed receive still records backup changes and intake diagnostics when the following read is deferred, cancelled, rejected for identity or wipe, or fails. The open confirmation keeps the original request context.
The shared refresh contract matches bitkit-android#1645 at 8564ba2: combined automatic receive/read and Background contact-refresh deferral. Android also defers a forced refresh after endpoint refresh. iOS leaves endpoint refresh on the existing poll schedule.
Validation: SwiftFormat passed on this head. Run Tests was still in progress. The new lock and service tests were inspected and not executed here. Device testing: not performed in this review. A simulator pass is attributed to b1d65a7 in review 5595146723. The feature files are unchanged by the merge onto the current base; this review did not rerun those journeys. Inbox latency against a queued publication was not measured.
Recommended before device testing: wait for the in-progress Run Tests workflow on this head.
jvsena42
left a comment
There was a problem hiding this comment.
Re-reviewed the new commit 6dd46e4 (avoid inbox waits after preparation errors). No findings.
- After a failed
prepareForPayment, the.storedrefresh only re-reads SDK state. - New messages still arrive on the 10 s foreground
.fullpoll, so a request can be delayed by one poll at most, never dropped. - The refresh recursion runs at most once, and a concurrent
.fullpoll still re-runs because its mode ranks higher.
Device gate: not needed for this delta.
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Follow-up at d9566ad8, covering the delta since ac364f01 (baseline review). This pass inspected the recreated confirmation's retained submission flag, its automatic-start and PIN-stage interaction, and the new confirmation tests. Inherited coverage remains the background inbox refresh, saved-contact deferral, receive backup accounting, preparation-failure refresh, refresh coalescing, the payment-request journeys and README limits, and the shared refresh contract in bitkit-android#1645 at bad5ae85.
No new actionable code findings.
A recreated confirmation keeps the swipe busy from the sheet's retained isSubmittingPayment value and leaves submitPayment before another PIN check or performPayment. App unlock during payment PIN entry still sees that flag clear, matching review-refresh.xml: the unpaid review must accept a new swipe and authorization. Request failure retry still closes the sheet and presents again.
SwiftFormat passed for this head. Unit tests were still running; this review did not execute them. The author reports a local unit pass that includes the two new confirmation tests. Device testing: not performed in this review. Earlier device results remain tied to their own revisions.
Recommended before device testing: wait for the in-progress unit-tests run on this head.
Suggested additional test cases
- iOS, with payment PIN enabled, biometrics disabled, and a funded regtest wallet. Start a send that has already passed payment authorization and is still submitting. Background the app and complete app unlock. The confirmation stays busy, does not request payment authorization again, and the original payment completes once.
Reviewer model: grok-4.7
Reasoning effort: high
|
@ben-kaufman Moving this PR to 3.2.0 per the 3.1.0 cut line (https://synonymworkspace.slack.com/archives/C07BJ7DNPCG/p1793307568629859): it sits on #1066, which is in 3.2.0. |
The base branch was changed.
|
@ben-kaufman Retargeted this PR to master: its base, #1066, merged as 888fce9. |
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — loading feedback disappears after unlock during an ordinary send.
Findings:
- LOW —
Bitkit/Views/Wallets/Send/SendConfirmationView.swift:386: an ordinary Amount → Confirm payment restores a blank orange swipe knob after app unlock. The control stays blocked, but its spinner is missing and “Swipe To Pay” remains visible. This reproduced twice; duplicate authorization and the original payment's completion work.
QA for d9566ad: Xcode 27.0, iPhone 17 Pro simulators on iOS 26.5, disposable Bitcoin regtest wallets. App and payment PIN enabled; biometrics disabled. Videos play at 2×. Capture stutter remains after recording retries; the published copies retain all captured changed frames.
App unlock during an authorized payment ❌ The restored ordinary-send control loses its spinner; the second swipe is ignored and the original payment completes once.
I authorized a 1,000-sat ordinary on-chain send through Amount → Confirm. A debug timing gate held the real native operation after durable preparation and immediately before broadcast. The spinner was visible before backgrounding. After entering only app-unlock PIN, the same confirmation returned with a blank orange knob. The ring stayed absent throughout the hold, including after another swipe. No second payment authorization opened. Releasing the original operation reached Bitcoin Sent.
This reproduced in two executions with separate recipient addresses. Each added exactly one outgoing activity and one 1,000-sat recipient output with a 143-sat fee. Raw-transaction decoding and each recipient address's chain history independently verified those payments. The recording shows the second execution; its existing outgoing activity belongs to the first execution.
in-flight.2x.mp4
Visible spinner before lock, blank restored knob, ignored second swipe and original completion.
Dispatch timing test hook
Not pushed. Holds the original native send until an explicit file release. Authorization, submission ownership, the swipe rendering and native broadcast decisions are unchanged.
diff --git a/Bitkit/Services/OnchainSendAttemptService.swift b/Bitkit/Services/OnchainSendAttemptService.swift
index eefaac4f..bd95a8df 100644
--- a/Bitkit/Services/OnchainSendAttemptService.swift
+++ b/Bitkit/Services/OnchainSendAttemptService.swift
@@ -666,6 +666,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-1070-release-dispatch") // test hook
+ let evidence = "txid: \(prepared.txid)\namount: \(prepared.recipientAmountSats)\naddress: \(address)\n" // test hook
+ try evidence.write(to: Env.appStorageUrl.appendingPathComponent("v4-1070-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-30 02:38:38.137 UTC] INFOℹ️: QA dispatch held after authorization and durable preparation
[2026-10-30 02:39:07.181 UTC] INFOℹ️: QA dispatch released
[2026-10-30 02:39:07.256 UTC] INFOℹ️: Onchain send result txid: 99dd4d4a5b06bfec62e408e71feb41e080059b1de20ba2ce9042bbcd3eb8d86b [SendConfirmationView.swift: performPayment(isAutomatic:) line: 1431]
Confirmation submission and app-lock regressions ✅ The signed unmodified head and focused regression suites passed, verified in the Xcode result bundles.
Ran confirmation lifecycle and swipe tests, payment-request service tests, and SDK operation-lock tests. The two new regressions pass: retained submission keeps the loading predicate true, and recreated automatic confirmation requests no additional PIN while preserving navigation and submission ownership. Existing tests cover idle authorization, request app-lock restoration, callback deferral, receive/read ordering, cancellation, identity and wipe admission. The video is the confirmation-only repeat; assertion results were checked in the result bundles. These hosted assertions do not establish the spinner's rendered visibility after native navigation.
confirmation.2x.mp4
Hosted confirmation regression execution; assertion evidence comes from the result bundle.
Check: retained journey coverage ✅ Earlier passing reports remain applicable to unchanged inbox and unpaid-review paths.
The J1, J2 and manual-case-1 report at 974be75 covers request intake, competing sheets, insufficient-balance retry, saved-contact callback deferral and unpaid Bitcoin review. Those paths are unchanged by this delta; unpaid review leaves the submission flag clear. The Amount and ordinary incoming-USDT app-lock report at 7a3cc5a and queued-publication ordering report at 7cd096f also remain applicable. These cases were not repeated.
The parent's incoming-request submission report at aae380f shows the spinner restored on its request-root route. The new device failure above is on this child's ordinary Amount → Confirm route.
Unit/UI CI and SwiftFormat passed. Separate E2E/migration jobs were skipped; they are not passing device evidence.
Coverage: contiguous ac364f0..d9566ad, all four changed files and reached submission, PIN, swipe, retained-presentation and completion callers. No planned case is unrun. Automatic subscription re-entry received hosted coverage; biometric, hardware-signing and funded-USDT flows were not separately driven. This run makes no latency comparison.
Twin: bitkit-android#1645 fix: keep incoming request refreshes together, merged at bad5ae8, matches combined receive/read and Background refresh deferral. This confirmation-recreation change is iOS-specific.
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Verdict: ✅ Approve
Findings: None. The previous spinner finding is fixed in this head.
QA for 85ef8d5: Xcode 27.0, iPhone 17 Pro simulators on iOS 26.5, disposable Bitcoin regtest wallets. App and payment PIN enabled; biometrics disabled. Videos play at 2×.
App unlock during an authorized ordinary payment ✅ The restored spinner stays centered and rotates; another swipe opens no authorization and the original payment completes once.
I authorized a 1,000-sat ordinary on-chain send through Amount → Confirm and held its native operation immediately before broadcast. After backgrounding and entering only app-unlock PIN, the restored confirmation kept its rotating spinner throughout the hold, including after another swipe. Releasing the original operation reached Bitcoin Sent. Payer Activity showed one send; the recipient showed one receipt. Raw-transaction decoding and the fresh recipient address's chain history independently verified one 1,000-sat output and a 143-sat fee.
in-flight-unlock.2x.mp4
Payment authorization, visible progress before and after app unlock, ignored second swipe and original completion.
Dispatch timing test hook
Not pushed. Holds the real native operation after authorization and durable preparation until explicit release. Spinner rendering, submission ownership and broadcast decisions are unchanged.
diff --git a/Bitkit/Services/OnchainSendAttemptService.swift b/Bitkit/Services/OnchainSendAttemptService.swift
index eefaac4f..bd95a8df 100644
--- a/Bitkit/Services/OnchainSendAttemptService.swift
+++ b/Bitkit/Services/OnchainSendAttemptService.swift
@@ -666,6 +666,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-1070-release-dispatch") // test hook
+ let evidence = "txid: \(prepared.txid)\namount: \(prepared.recipientAmountSats)\naddress: \(address)\n" // test hook
+ try evidence.write(to: Env.appStorageUrl.appendingPathComponent("v4-1070-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
}Restored spinner and confirmation regressions ✅ The signed unmodified head and selected confirmation suites passed, verified in the Xcode result bundles.
The new rendering regression passed for initial navigation and a reconstructed confirmation. I inspected its retained screenshots: both show the spinner inside the orange knob. Existing assertions also passed for retained submission, rejected duplicate authorization, fee loading, manual coin selection and confirmation lifecycle. An unchanged repeat passed. The recording shows hosted execution; result bundles establish the assertions. Some capture stutter remains after the recording retry; the published copy retains every captured changed frame.
confirmation-regressions.2x.mp4
Hosted confirmation regression execution on the unmodified head.
Check: retained journey coverage ✅ Earlier passing evidence remains applicable to the unchanged inbox and unpaid-review paths.
- J1, J2 and manual-case-1 report at 974be75: intake, competing sheets, insufficient-balance retry, saved-contact callback deferral and unpaid Bitcoin review.
- Amount and ordinary incoming-USDT app-lock report at 7a3cc5a: retained amount and unpaid authorization context.
- Queued-publication ordering report at 7cd096f: combined receive/read ordering.
These journeys were not repeated. This delta changes the shared spinner's animation; the rendering and native cases above check that change.
Changed-file SwiftFormat passed. Hosted unit/UI and integration were still running at the final check; hosted SwiftFormat passed.
Coverage: contiguous d9566ad..85ef8d5, all three changed files and reached spinner/swipe callers. No planned case is unrun. Other shared-spinner surfaces, biometric prompts, hardware signing and funded-USDT submission were not separately driven. No latency comparison was made.
Twin: bitkit-android#1645 fix: keep incoming request refreshes together, merged at bad5ae8, matches receive/read grouping and Background refresh deferral. This spinner correction is iOS-specific.
Reviewed by gpt-6.1-sol-high
|
@ben-kaufman Merging this PR: approved on head 85ef8d5 with a device run of its cases on that head, checks green and no open threads. It merges before the 3.1.0 cut, so it ships in 3.1.0. |
|
@ben-kaufman Merged, as it was approved and green. |
|
Placing this PR in 3.1.0: it merged before the 3.1.0 cut, so it ships in 3.1.0. |
This PR prevents unrelated background publishing from delaying the request list after an incoming message is received. Automatic message receipt and its fresh Payment Request read now run in the same queued operation.
Follows merged #1066 and targets master. Related to #868. No measured native latency improvement is claimed.
Description
Out of Scope
None.
Design
N/A — no design available.
Preview
Native recording pending; no layout changes.
QA Notes
Setup
Use the journey guide and Payment Request setup at
7cd096f4with disposable regtest wallets and a saved, linked fixture issuer. This source uses Paykit0.1.1-rc1and Core0.8.0-rc10. 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.Journeys
automatic-presentation.xml— foreground resume receives each request once and keeps the current sheet intact.review-refresh.xml— a second request arriving during review leaves the first unchanged and becomes available after cancellation and ordinary synchronization.Manual Tests
Automated Checks
PaykitSdkOperationLockTests.swift— verifies receive/read ordering, receive-only backup accounting, cancellation, review and wipe admission, and identity/runtime rejection.PaykitPaymentRequestServiceTests.swift— preserves the prior pending drain, reports and synchronization time; checks both ordinary and forced Background deferral while explicit recovery remains Ordered.Evidence
Final integration validation
Integration with master
4530ff7bat7cd096f4retains 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. Final-head CI after publication: pending; the previous-head #1070 launch failure remains disclosed below.Gaps
The exact MainActor-hop wipe boundary is rechecked in source after the awaited admission check; tests do not force that interleaving with a new hook. Earlier geo-response/hosted-view messages and one shared-keychain status were disclosed without claiming identical baseline reproduction. At preceding head
e16d13bc, both CI attempts failed with an XCTest app-launch background-assertion timeout; attempt 2 passed all 2,765 unit tests but failed one UI launch case. The cause remains unresolved and is not proven to be infrastructure; a local unit pass does not close this CI gap.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. Cases J1, J2, 1 remain 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
3f4e60097668b5ffb5106a71a6e6e74d201dd8caincorporates 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
ac364f01ff1b6cee95c12c5d978c27aaa3f23f09includes 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
ac364f01ff1b6cee95c12c5d978c27aaa3f23f09, unit/UI run 40412986788 passed on attempt 1: 2,937 unit tests, zero failures/skips; 43 UI executions, 39 passed and four existing prerequisite skips, zero failures. SwiftFormat 40412986734 passed: 0/844 files require formatting, 84 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 29 inherited app-lock guard
Published d9566ad8 carries the four-file R1 correction from #1039 at aae380fc: a recreated confirmation uses the retained submission flag for its busy state and refuses another authorization while the original payment is in flight. This branch retains its existing Core 0.8.0-rc10, LDK 0.7.0-rc.77, Paykit 0.1.1-rc1 and VSS 0.5.23 pins, stacked base and prior history.
On this exact branch candidate, Xcode 27.1 / SDK 27.1 / iOS 26.5 passed the complete selected unit suite: 2,939 passed, 0 failed, 0 skipped, with the six existing unit-workflow integration exclusions. Both new confirmation regression tests passed. Full SwiftFormat 0.64.0 passed: 0/844 files require formatting; 84 existing exclusions. The shared private package copy was corrected before this branch's first test invocation; the sibling #1054's initial build-path failure remains retained. This branch's first full test run passed with unchanged source and dependency pins.
Complete diagnostics were inspected: two AppIntents metadata notices, six native SDK/Pubky/mainline DHT construction QoS warnings, and retained fault-fixture/host output. Forty-five emitter/test files were compared with this branch's pre-fix head; the warning-emitting methods in the two changed files are unchanged. No introduced actionable finding was found. This is source and retained-evidence comparison, not an exact-base runtime replay or warning-free claim. The identical production patch reuses the two independent R1 source reviews and guard negative control from #1039, with the older child callers inspected separately.
All 28 commits in the actual current-parent PR range verified cryptographically locally and report GitHub verified=true / reason=valid after the normal push. New-head hosted checks remain pending and validate their own recorded PR-merge inputs separately from this local older-pin branch.
Existing cases J1, J2 and manual 1 and their documented fixture/admission prerequisites remain unchecked on this new head. The external native R1 report on #1039 reports two successful controlled regtest executions at aae380f; it is attributed parent evidence, not a device run on this child. No new financial journey, binary adoption or final Android206/iOS215 latency/unlock result is claimed.
October 30 restored progress feedback
Fix commit: 85ef8d54.
The repeating animation now applies only to rotation. Navigation restoration can no longer repeatedly displace the indicator’s frame; existing loading predicates, opacity/fade, PIN checks and submission ownership are unchanged.
The same scene-backed, animation-enabled NavigationStack regression failed on the unchanged head and passed with this correction. Its captured initial and restored frames show the ring displaced above the knob before the fix and centered afterward.
Final local unit/UI checks and complete diagnostics: 2,950 distinct passes, 0 failures and 4 explicit fixture skips; configured launch variants expand this to 2,979 passing executions. The four skips are three disabled AI-only Trezor emulator cases and the missing local Arbitrum fork case. Full SwiftFormat 0.64.0 passed 0/844, with 84 existing exclusions. One combined independent correctness, codebase-fit, simplicity and test-quality review found no actionable issue. One rendering regression was added; no new hook, state, fixture or dependency was added. All 29 commits against current master
888fce91verified cryptographically locally before the normal push, and GitHub reportsverified=true / reason=validfor every commit afterward.Complete diagnostics retain nine runtime QoS issues (ten emitted native checker events), newly observed framework layout and shared-keychain notices, and service/environment messages. Their source contexts and unchanged native emitters were inspected; no introduced actionable finding was established. These observations are not presented as warning-free or an exact-base runtime replay. The detailed test list omitted one passing record that the XCTest result and raw log retain; the counts above use the reconciled result.
Current-base hosted unit/UI, integration and E2E checks are running on merge
7b82c927, ordered parents master888fce91and head85ef8d54, tree16ade8d0. This composition inherits master’s Core rc11 / LDK rc78 / Paykit rc2 / VSS 26-rc1 pins; the local branch result above retains rc10 / rc77 / rc1 / VSS 0.5.23. Runtime acceptance and complete diagnostics on those hosted inputs remain pending. The matching E2E branch resolves to the already validated companion bitkit-e2e-tests#288 at0affc996; the actual consumer revision will be checked.This is unfunded hosted-window rendering evidence, not an app-PIN payment or broadcast run. Manual case 2 remains unrun here because the controlled dispatch fixture and financial authorization are unavailable; existing J1, J2 and manual case 1 prerequisites remain unchanged. No native timing, profile-creation latency or final Android206/iOS215 payment/unlock result is claimed.
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)