Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,11 @@ class AppViewModel: ObservableObject {
private let navigationViewModel: NavigationViewModel
private let scanPaymentOperations: ScanPaymentOperations
private let usdtBalance: () -> UInt64?
private var scannedDataHandlingId: UUID?
@Published private var scannedDataHandlingId: UUID?
var isHandlingScannedData: Bool {
scannedDataHandlingId != nil
}
Comment thread
ovitrif marked this conversation as resolved.
Resolved

private var manualEntryValidationSequence: UInt64 = 0

// Combine infrastructure for debounced validation
Expand Down
21 changes: 21 additions & 0 deletions Bitkit/Views/Wallets/Send/SendSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ struct SendSheet: View {
config.preparation != nil && incomingPaymentRequest == nil
}

private var isMissingLnurlPayData: Bool {
guard app.lnurlPayData == nil, !app.isHandlingScannedData, !isSubmittingPayment else { return false }
return Self.isLnurlPayRoute(navigationPath.last ?? currentRoot)
}

private static func isLnurlPayRoute(_ route: SendRoute) -> Bool {
route == .lnurlPayAmount || route == .lnurlPayConfirm
}

private var shouldDismissUnavailableRequest: Bool {
guard let request = incomingPaymentRequest else { return false }
return Self.shouldDismissUnavailableRequest(
Expand Down Expand Up @@ -285,6 +294,10 @@ struct SendSheet: View {
guard shouldDismiss else { return }
sheets.hideSheetIfActive(isEmbedded ? .subscription : .send, reason: "Incoming payment request is no longer available")
}
.onChange(of: isMissingLnurlPayData, initial: true) { _, isMissing in
guard isMissing, isCurrentPresentation else { return }
exitMissingLnurlPayment()
}
.onChange(of: shouldShowSyncOverlay, initial: true) { _, isShowing in
Logger.debug("shouldShowSyncOverlay: \(isShowing) (node: \(wallet.nodeLifecycleState))", context: "SendSheet")
}
Expand Down Expand Up @@ -1249,6 +1262,14 @@ struct SendSheet: View {
navigationPath = [route]
}

private func exitMissingLnurlPayment() {
if !Self.isLnurlPayRoute(currentRoot), let index = navigationPath.firstIndex(where: Self.isLnurlPayRoute) {
navigationPath.removeSubrange(index...)
} else {
sheets.hideSheetIfActive(isEmbedded ? .subscription : .send, reason: "LNURL payment data is no longer available")
}
}

private func replaceRootRoute(with route: SendRoute) {
rootOverride = nil
rootRoute = route
Expand Down
42 changes: 39 additions & 3 deletions BitkitTests/SendSheetLifecycleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,40 @@ final class SendSheetLifecycleTests: XCTestCase {
XCTAssertNil(app.lnurlPayData, "Ending the presentation must still clean up")
}

func testEmbeddedSendExitCleansStateWhileSubscriptionRemainsActive() async throws {
let (app, sheets, prompt, window) = makeHost(isEmbedded: true)
func testMissingLnurlDataClosesItsPresentation() async throws {
let (app, sheets, _, window) = makeHost()
defer { window.isHidden = true; window.rootViewController = nil }
app.lnurlPayData = makeLnurlData()
app.selectedWalletToPayFrom = .lightning
sheets.showSheet(.send, data: SendConfig(view: .lnurlPayConfirm))
try await Task.sleep(for: .seconds(1))
XCTAssertEqual(sheets.activeSheetConfiguration?.id, .send)
app.lnurlPayData = nil
try await Task.sleep(for: .milliseconds(500))
XCTAssertNil(sheets.activeSheetConfiguration, "A confirmation without LNURL data must close instead of staying blank")
}

func testMissingLnurlDataClosesAmountRootedPresentation() async throws {
let (app, sheets, _, window) = makeHost()
defer { window.isHidden = true; window.rootViewController = nil }
app.lnurlPayData = LnurlPayData(
uri: "https://example.com/variable", callback: "https://example.com/callback",
minSendable: 149_500, maxSendable: 200_999,
metadataStr: "[]", commentAllowed: 12, allowsNostr: false, nostrPubkey: nil
)
app.selectedWalletToPayFrom = .lightning
sheets.showSheet(.send, data: SendConfig(view: .lnurlPayAmount))
try await Task.sleep(for: .seconds(1))
XCTAssertEqual(sheets.activeSheetConfiguration?.id, .send)
app.lnurlPayData = nil
try await Task.sleep(for: .milliseconds(500))
XCTAssertNil(sheets.activeSheetConfiguration, "An amount root without LNURL data must close, not stay unusable")
}

func testEmbeddedSendExitCleansStateWhileSubscriptionRemainsActive() async throws {
// Subscriptions route embedded Send to LNURL confirmation only once its data is set.
let (app, sheets, prompt, window) = makeHost(isEmbedded: true, lnurlPayData: makeLnurlData())
defer { window.isHidden = true; window.rootViewController = nil }
try await Task.sleep(for: .seconds(1))
let presentationID = try XCTUnwrap(sheets.activeSheetConfiguration?.presentationID)
XCTAssertEqual(app.lnurlPayData?.minSendable, 222_222)
Expand All @@ -101,10 +130,17 @@ final class SendSheetLifecycleTests: XCTestCase {
commentAllowed: nil, allowsNostr: false, nostrPubkey: nil)
}

private func makeHost(isEmbedded: Bool = false) -> (AppViewModel, SheetViewModel, ScanPromptState, UIWindow) {
private func makeHost(
isEmbedded: Bool = false,
lnurlPayData: LnurlPayData? = nil
) -> (AppViewModel, SheetViewModel, ScanPromptState, UIWindow) {
snapshotAppDefaultsDomain()
snapshotAppGroupDefaults("home_screen_display_currency_code_v1", "home_screen_display_currency_symbol_v1")
let app = AppViewModel()
if let lnurlPayData {
app.lnurlPayData = lnurlPayData
app.selectedWalletToPayFrom = .lightning
}
let wallet = WalletViewModel()
wallet.nodeLifecycleState = .running
let sheets = SheetViewModel()
Expand Down
Loading