diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 5a7b4ce7..a5783d7b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -108,7 +108,7 @@ jobs: # - { name: onchain_boost_receive_widgets, grep: "@onchain|@boost|@receive|@widgets" } # - { name: settings, grep: "@settings" } # - { name: security, grep: "@security" } - - { name: e2e, grep: '@lightning|@backup|@onboarding|@onchain_1|@onchain_2|@numberpad|@widgets|@boost|@receive|@settings|@security' } + - { name: e2e, grep: '@lnurl|@lightning|@backup|@onboarding|@onchain_1|@onchain_2|@numberpad|@widgets|@boost|@receive|@settings|@security' } name: e2e-tests - ${{ matrix.shard.name }} diff --git a/Bitkit/Components/NumberPadTextField.swift b/Bitkit/Components/NumberPadTextField.swift index 4faac76c..92c282ec 100644 --- a/Bitkit/Components/NumberPadTextField.swift +++ b/Bitkit/Components/NumberPadTextField.swift @@ -7,6 +7,7 @@ struct NumberPadTextField: View { var showConversion: Bool = true var isFocused: Bool = true + var testIdentifier: String? private let springAnimation = Animation.spring(response: 0.3, dampingFraction: 0.8) @@ -86,6 +87,7 @@ struct NumberPadTextField: View { + Text(viewModel.getPlaceholder(currency: currency)) .foregroundColor(isFocused ? .textSecondary : .textPrimary)) .font(.custom(Fonts.black, size: 44)) + .accessibilityIdentifierIfPresent(testIdentifier) } } } diff --git a/Bitkit/Components/TextField.swift b/Bitkit/Components/TextField.swift index 4919e6fa..e4277552 100644 --- a/Bitkit/Components/TextField.swift +++ b/Bitkit/Components/TextField.swift @@ -6,6 +6,7 @@ struct TextField: View { let font: Font let axis: Axis let testIdentifier: String? + let submitLabel: SubmitLabel @Binding var text: String init( @@ -14,13 +15,15 @@ struct TextField: View { backgroundColor: Color = .white10, font: Font = .custom(Fonts.semiBold, size: 15), axis: Axis = .horizontal, - testIdentifier: String? = nil + testIdentifier: String? = nil, + submitLabel: SubmitLabel = .return ) { self.placeholder = placeholder self.backgroundColor = backgroundColor self.font = font self.axis = axis self.testIdentifier = testIdentifier + self.submitLabel = submitLabel _text = text } @@ -35,6 +38,7 @@ struct TextField: View { SwiftUI.TextField("", text: $text, axis: axis) .accentColor(.brandAccent) .font(font) + .submitLabel(submitLabel) .accessibilityIdentifierIfPresent(testIdentifier) } .padding() diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index 18c3d06b..b2d3db66 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -15,6 +15,7 @@ enum Env { static let isE2E = ProcessInfo.processInfo.environment["E2E"] == "true" #endif static let dustLimit = 547 + static let msatsPerSat: UInt64 = 1000 #if CHECK_GEOBLOCK static let isGeoblockingEnabled = true diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 1e1c1df1..cf522206 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -83,7 +83,7 @@ struct MainNavView: View { .sheet( item: $sheets.lnurlWithdrawSheetItem, onDismiss: { - sheets.hideSheet() + sheets.hideSheetIfActive(.lnurlWithdraw, reason: "LNURL withdraw sheet dismissed") } ) { config in LnurlWithdrawSheet(config: config) @@ -116,7 +116,7 @@ struct MainNavView: View { .sheet( item: $sheets.scannerSheetItem, onDismiss: { - sheets.hideSheet() + sheets.hideSheetIfActive(.scanner, reason: "Scanner sheet dismissed") } ) { config in ScannerSheet(config: config) @@ -141,7 +141,7 @@ struct MainNavView: View { .sheet( item: $sheets.sendSheetItem, onDismiss: { - sheets.hideSheet() + sheets.hideSheetIfActive(.send, reason: "Send sheet dismissed") } ) { config in SendSheet(config: config) diff --git a/Bitkit/Utilities/Lnurl.swift b/Bitkit/Utilities/Lnurl.swift index a8f3ab15..11279d8c 100644 --- a/Bitkit/Utilities/Lnurl.swift +++ b/Bitkit/Utilities/Lnurl.swift @@ -175,13 +175,18 @@ struct LnurlHelper { params: LnurlChannelData, nodeId: String ) async throws { - let queryItems = [ - URLQueryItem(name: "k1", value: params.k1), - URLQueryItem(name: "remoteid", value: nodeId), - URLQueryItem(name: "private", value: "1"), // Private channel - ] + let callbackUrlString = try createChannelRequestUrl( + k1: params.k1, + callback: params.callback, + localNodeId: nodeId, + isPrivate: true, + cancel: false + ) + + guard let callbackURL = URL(string: callbackUrlString) else { + throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback URL"]) + } - let callbackURL = try buildUrl(baseUrl: params.callback, queryItems: queryItems) let responseString = try await makeHttpGetRequest(url: callbackURL) let channelResponse = try parseJsonResponse(responseString, as: LnurlChannelResponse.self) diff --git a/Bitkit/Utilities/PaymentNavigationHelper.swift b/Bitkit/Utilities/PaymentNavigationHelper.swift index 78cdaf5f..acc87dea 100644 --- a/Bitkit/Utilities/PaymentNavigationHelper.swift +++ b/Bitkit/Utilities/PaymentNavigationHelper.swift @@ -103,6 +103,14 @@ struct PaymentNavigationHelper { currency: CurrencyViewModel, settings: SettingsViewModel ) -> SendRoute { + if let lnurlWithdrawData = app.lnurlWithdrawData { + if lnurlWithdrawData.minWithdrawable == lnurlWithdrawData.maxWithdrawable { + return .lnurlWithdrawConfirm + } else { + return .lnurlWithdrawAmount + } + } + let shouldUseQuickpay = shouldUseQuickpay(app: app, settings: settings, currency: currency) // Handle Lightning address / LNURL pay diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 5c8b6c15..2d54ff20 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -252,9 +252,13 @@ extension AppViewModel { return } + var normalizedData = data + normalizedData.minSendable = max(1, normalizedData.minSendable / Env.msatsPerSat) + normalizedData.maxSendable = max(normalizedData.minSendable, normalizedData.maxSendable / Env.msatsPerSat) + // Check if user has enough lightning balance to pay the minimum amount let lightningBalance = lightningService.balances?.totalLightningBalanceSats ?? 0 - if lightningBalance < data.minSendable { + if lightningBalance < normalizedData.minSendable { toast( type: .warning, title: t("other__lnurl_pay_error"), @@ -264,7 +268,7 @@ extension AppViewModel { } selectedWalletToPayFrom = .lightning - lnurlPayData = data + lnurlPayData = normalizedData } private func handleLnurlWithdraw(_ data: LnurlWithdrawData) { @@ -274,8 +278,11 @@ extension AppViewModel { return } + let minMsats = data.minWithdrawable ?? Env.msatsPerSat + let maxMsats = data.maxWithdrawable + // Check if minWithdrawable > maxWithdrawable - if (data.minWithdrawable ?? 1000) > data.maxWithdrawable { + if minMsats > maxMsats { toast( type: .warning, title: t("other__lnurl_withdr_error"), @@ -284,9 +291,15 @@ extension AppViewModel { return } + var normalizedData = data + let minSats = max(1, minMsats / Env.msatsPerSat) + let maxSats = max(minSats, maxMsats / Env.msatsPerSat) + normalizedData.minWithdrawable = minSats + normalizedData.maxWithdrawable = maxSats + // Check if we have enough receiving capacity let lightningBalance = lightningService.balances?.totalLightningBalanceSats ?? 0 - if lightningBalance < (data.minWithdrawable ?? 1000) / 1000 { + if lightningBalance < minSats { toast( type: .warning, title: t("other__lnurl_withdr_error"), @@ -295,7 +308,7 @@ extension AppViewModel { return } - lnurlWithdrawData = data + lnurlWithdrawData = normalizedData } private func handleLnurlChannel(_ data: LnurlChannelData) { @@ -415,7 +428,8 @@ extension AppViewModel { type: .lightning, title: t("lightning__channel_opened_title"), description: t("lightning__channel_opened_msg"), - visibilityTime: 5.0 + visibilityTime: 5.0, + accessibilityIdentifier: "SpendingBalanceReadyToast" ) } } @@ -424,7 +438,8 @@ extension AppViewModel { type: .lightning, title: t("lightning__channel_opened_title"), description: t("lightning__channel_opened_msg"), - visibilityTime: 5.0 + visibilityTime: 5.0, + accessibilityIdentifier: "SpendingBalanceReadyToast" ) } case .channelClosed(channelId: _, userChannelId: _, counterpartyNodeId: _, reason: _): diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index 2e2d37b8..529b76b5 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -38,6 +38,7 @@ class SheetViewModel: ObservableObject { DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in guard let self else { return } + Logger.debug("Showing sheet \(id.rawValue) after delay", context: "SheetViewModel") activeSheetConfiguration = SheetConfiguration(id: id, data: data) playHaptics(for: id) @@ -48,6 +49,7 @@ class SheetViewModel: ObservableObject { } } else { // If no sheet is open, show the new sheet immediately + Logger.debug("Showing sheet \(id.rawValue)", context: "SheetViewModel") activeSheetConfiguration = SheetConfiguration(id: id, data: data) playHaptics(for: id) @@ -58,7 +60,16 @@ class SheetViewModel: ObservableObject { } } - func hideSheet() { + func hideSheet(reason: String? = nil, file: String = #file, function: String = #function, line: Int = #line) { + if let config = activeSheetConfiguration { + let fallback = "\(URL(fileURLWithPath: file).lastPathComponent):\(line) \(function)" + let reasonText = " reason: \(reason ?? fallback)" + Logger.debug("Hiding sheet \(config.id.rawValue)\(reasonText)", context: "SheetViewModel") + } else { + let fallback = "\(URL(fileURLWithPath: file).lastPathComponent):\(line) \(function)" + let reasonText = " reason: \(reason ?? fallback)" + Logger.debug("hideSheet called with no active sheet\(reasonText)", context: "SheetViewModel") + } activeSheetConfiguration = nil // Notify timed sheet manager @@ -67,6 +78,17 @@ class SheetViewModel: ObservableObject { } } + func hideSheetIfActive(_ id: SheetID, reason: String? = nil, file: String = #file, function: String = #function, line: Int = #line) { + guard activeSheetConfiguration?.id == id else { + let fallback = "\(URL(fileURLWithPath: file).lastPathComponent):\(line) \(function)" + let reasonText = " reason: \(reason ?? fallback)" + let activeId = activeSheetConfiguration?.id.rawValue ?? "none" + Logger.debug("hideSheetIfActive skipped for \(id.rawValue) (active: \(activeId))\(reasonText)", context: "SheetViewModel") + return + } + hideSheet(reason: reason, file: file, function: function, line: line) + } + var isAnySheetOpen: Bool { return activeSheetConfiguration != nil } diff --git a/Bitkit/Views/Sheets/LnurlAuth/LnurlAuthSheet.swift b/Bitkit/Views/Sheets/LnurlAuth/LnurlAuthSheet.swift index 5e24e6ae..1d14ccb5 100644 --- a/Bitkit/Views/Sheets/LnurlAuth/LnurlAuthSheet.swift +++ b/Bitkit/Views/Sheets/LnurlAuth/LnurlAuthSheet.swift @@ -85,16 +85,19 @@ struct LnurlAuthSheet: View { ) { onCancel() } + .accessibilityIdentifier("LnurlAuthCancel") CustomButton(title: actionText) { Task { await onContinue() } } + .accessibilityIdentifier("LnurlAuthContinue") } .padding(.top, 32) } .padding(.horizontal, 16) + .accessibilityElement(children: .contain) .accessibilityIdentifier("LnurlAuth") } } diff --git a/Bitkit/Views/Transfer/FundManualSuccessView.swift b/Bitkit/Views/Transfer/FundManualSuccessView.swift index 62043a14..1343bfe3 100644 --- a/Bitkit/Views/Transfer/FundManualSuccessView.swift +++ b/Bitkit/Views/Transfer/FundManualSuccessView.swift @@ -41,8 +41,11 @@ struct FundManualSuccessView: View { ) { navigation.reset() } + .accessibilityIdentifier("ExternalSuccess-button") } .padding(.horizontal, 16) + .accessibilityElement(children: .contain) + .accessibilityIdentifier("ExternalSuccess") } .navigationBarHidden(true) .interactiveDismissDisabled() diff --git a/Bitkit/Views/Transfer/LnurlChannel.swift b/Bitkit/Views/Transfer/LnurlChannel.swift index 994ed94b..81135cf9 100644 --- a/Bitkit/Views/Transfer/LnurlChannel.swift +++ b/Bitkit/Views/Transfer/LnurlChannel.swift @@ -122,6 +122,7 @@ struct LnurlChannel: View { await onConnect() } } + .accessibilityIdentifier("ConnectButton") } } .navigationBarHidden(true) @@ -166,10 +167,24 @@ struct LnurlChannel: View { self.channelInfo = channelInfo isLoadingChannelInfo = false } + + await connectToPeerIfNeeded(channelInfo: channelInfo) } catch { await MainActor.run { isLoadingChannelInfo = false } } } + + private func connectToPeerIfNeeded(channelInfo: LnurlChannelData) async { + guard let peer = try? LnPeer(connection: channelInfo.uri) else { + return + } + + do { + try await wallet.connectPeer(peer) + } catch { + Logger.error(error, context: "Failed to connect LNURL peer") + } + } } diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift index 2567df6a..ba2c91df 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift @@ -4,16 +4,16 @@ struct LnurlWithdrawAmount: View { @EnvironmentObject var app: AppViewModel @EnvironmentObject var currency: CurrencyViewModel @EnvironmentObject var wallet: WalletViewModel - @Binding var navigationPath: [LnurlWithdrawRoute] + let onContinue: () -> Void @StateObject private var amountViewModel = AmountInputViewModel() var minAmount: Int { - Int((app.lnurlWithdrawData!.minWithdrawable ?? 1000) / 1000) + Int(app.lnurlWithdrawData!.minWithdrawable ?? 1) } var maxAmount: Int { - Int((app.lnurlWithdrawData!.maxWithdrawable) / 1000) + Int(app.lnurlWithdrawData!.maxWithdrawable) } var amount: UInt64 { @@ -29,7 +29,7 @@ struct LnurlWithdrawAmount: View { SheetHeader(title: t("wallet__lnurl_w_title"), showBackButton: true) VStack(alignment: .leading, spacing: 0) { - NumberPadTextField(viewModel: amountViewModel) + NumberPadTextField(viewModel: amountViewModel, testIdentifier: "SendNumberField") .onTapGesture { amountViewModel.togglePrimaryDisplay(currency: currency) } @@ -65,16 +65,22 @@ struct LnurlWithdrawAmount: View { } CustomButton(title: t("common__continue"), isDisabled: !isValid) { - onContinue() + handleContinue() } + .accessibilityIdentifier("ContinueAmount") } } .navigationBarHidden(true) .padding(.horizontal, 16) .sheetBackground() + .onAppear { + if amountViewModel.amountSats == 0 { + amountViewModel.updateFromSats(UInt64(minAmount), currency: currency) + } + } } - private func onContinue() { + private func handleContinue() { // If minimum is above the amount the user entered, automatically set amount to that minimum if amount < minAmount { amountViewModel.updateFromSats(UInt64(minAmount), currency: currency) @@ -82,6 +88,6 @@ struct LnurlWithdrawAmount: View { wallet.lnurlWithdrawAmount = amount - navigationPath.append(.confirm) + onContinue() } } diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift index 23322b80..e90f3889 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift @@ -5,13 +5,13 @@ struct LnurlWithdrawConfirm: View { @EnvironmentObject var currency: CurrencyViewModel @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel - @Binding var navigationPath: [LnurlWithdrawRoute] + let onFailure: (UInt64) -> Void @State private var isLoading = false var amount: UInt64 { // Fixed amount if app.lnurlWithdrawData!.maxWithdrawable == app.lnurlWithdrawData!.minWithdrawable { - return app.lnurlWithdrawData!.maxWithdrawable / 1000 + return app.lnurlWithdrawData!.maxWithdrawable } // For variable amount, use the amount from the previous screen @@ -22,7 +22,7 @@ struct LnurlWithdrawConfirm: View { VStack(spacing: 0) { SheetHeader(title: t("wallet__lnurl_w_title"), showBackButton: true) - MoneyStack(sats: Int(amount), showSymbol: true) + MoneyStack(sats: Int(amount), showSymbol: true, testIdPrefix: "WithdrawAmount") .padding(.top, 16) .padding(.bottom, 42) @@ -42,6 +42,7 @@ struct LnurlWithdrawConfirm: View { CustomButton(title: t("wallet__lnurl_w_button"), isLoading: isLoading) { performWithdraw() } + .accessibilityIdentifier("WithdrawConfirmButton") } .navigationBarHidden(true) .padding(.horizontal, 16) @@ -77,12 +78,13 @@ struct LnurlWithdrawConfirm: View { description: t("other__lnurl_withdr_success_msg") ) isLoading = false - sheets.hideSheet() + sheets.hideSheetIfActive(.send, reason: "LNURL withdraw completed") + sheets.hideSheetIfActive(.lnurlWithdraw, reason: "LNURL withdraw completed") } } catch { await MainActor.run { - navigationPath.append(.failure(amount: amount)) + onFailure(amount) isLoading = false } } diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawFailure.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawFailure.swift index 0f24ae60..c880d521 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawFailure.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawFailure.swift @@ -4,7 +4,6 @@ struct LnurlWithdrawFailure: View { @EnvironmentObject var currency: CurrencyViewModel @EnvironmentObject var navigation: NavigationViewModel @EnvironmentObject var sheets: SheetViewModel - @Binding var navigationPath: [LnurlWithdrawRoute] let amount: UInt64 // TODO: add localized strings diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawSheet.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawSheet.swift index 4e770804..529dbc1a 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawSheet.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawSheet.swift @@ -43,11 +43,15 @@ struct LnurlWithdrawSheet: View { private func viewForRoute(_ route: LnurlWithdrawRoute) -> some View { switch route { case .amount: - LnurlWithdrawAmount(navigationPath: $navigationPath) + LnurlWithdrawAmount { + navigationPath.append(.confirm) + } case .confirm: - LnurlWithdrawConfirm(navigationPath: $navigationPath) + LnurlWithdrawConfirm { amount in + navigationPath.append(.failure(amount: amount)) + } case let .failure(amount): - LnurlWithdrawFailure(navigationPath: $navigationPath, amount: amount) + LnurlWithdrawFailure(amount: amount) } } } diff --git a/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift b/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift index f747c956..a3d4ed33 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift @@ -27,7 +27,7 @@ struct LnurlPayAmount: View { SheetHeader(title: t("wallet__lnurl_p_title"), showBackButton: true) VStack(alignment: .leading, spacing: 0) { - NumberPadTextField(viewModel: amountViewModel) + NumberPadTextField(viewModel: amountViewModel, testIdentifier: "SendNumberField") .onTapGesture { amountViewModel.togglePrimaryDisplay(currency: currency) } @@ -71,6 +71,7 @@ struct LnurlPayAmount: View { CustomButton(title: t("common__continue"), isDisabled: !isValid) { onContinue() } + .accessibilityIdentifier("ContinueAmount") } } .navigationBarHidden(true) @@ -84,7 +85,8 @@ struct LnurlPayAmount: View { if amount < minSendable { app.toast( type: .error, title: t("wallet__lnurl_pay__error_min__title"), - description: t("wallet__lnurl_pay__error_min__description", variables: ["amount": "\(minSendable)"]) + description: t("wallet__lnurl_pay__error_min__description", variables: ["amount": "\(minSendable)"]), + accessibilityIdentifier: "LnurlPayAmountTooLowToast" ) return } diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index 076be8d2..cd39e4d1 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -16,6 +16,7 @@ struct LnurlPayConfirm: View { @State private var showingBiometricError = false @State private var biometricErrorMessage = "" @State private var comment = "" + @FocusState private var isCommentFocused: Bool private var biometryTypeName: String { switch Env.biometryType { @@ -43,8 +44,12 @@ struct LnurlPayConfirm: View { SheetHeader(title: t("wallet__lnurl_p_title"), showBackButton: true) VStack(alignment: .leading) { - MoneyStack(sats: Int(wallet.sendAmountSats ?? app.lnurlPayData!.minSendable), showSymbol: true) - .padding(.bottom, 32) + MoneyStack( + sats: Int(wallet.sendAmountSats ?? app.lnurlPayData!.minSendable), + showSymbol: true, + testIdPrefix: "ReviewAmount" + ) + .padding(.bottom, 32) VStack(spacing: 0) { VStack(alignment: .leading) { @@ -77,7 +82,7 @@ struct LnurlPayConfirm: View { Divider() - if let commentAllowed = app.lnurlPayData?.commentAllowed { + if let commentAllowed = app.lnurlPayData?.commentAllowed, commentAllowed > 0 { VStack(alignment: .leading) { CaptionMText(t("wallet__lnurl_pay_confirm__comment")) .padding(.bottom, 8) @@ -85,8 +90,12 @@ struct LnurlPayConfirm: View { TextField( t("wallet__lnurl_pay_confirm__comment_placeholder"), text: $comment, - axis: .vertical + axis: .vertical, + testIdentifier: "CommentInput", + submitLabel: .done ) + .focused($isCommentFocused) + .dismissKeyboardOnReturn(text: $comment, isFocused: $isCommentFocused) .lineLimit(3 ... 3) .onChange(of: comment) { newValue in let maxLength = Int(commentAllowed) diff --git a/Bitkit/Views/Wallets/Send/SendAmountView.swift b/Bitkit/Views/Wallets/Send/SendAmountView.swift index 33d3ff50..8943849a 100644 --- a/Bitkit/Views/Wallets/Send/SendAmountView.swift +++ b/Bitkit/Views/Wallets/Send/SendAmountView.swift @@ -50,7 +50,7 @@ struct SendAmountView: View { SheetHeader(title: t("wallet__send_amount"), showBackButton: true) VStack(alignment: .leading, spacing: 0) { - NumberPadTextField(viewModel: amountViewModel) + NumberPadTextField(viewModel: amountViewModel, testIdentifier: "SendNumberField") .onTapGesture { amountViewModel.togglePrimaryDisplay(currency: currency) } diff --git a/Bitkit/Views/Wallets/Send/SendSheet.swift b/Bitkit/Views/Wallets/Send/SendSheet.swift index d6b0d4a3..e5d8ffb1 100644 --- a/Bitkit/Views/Wallets/Send/SendSheet.swift +++ b/Bitkit/Views/Wallets/Send/SendSheet.swift @@ -14,6 +14,9 @@ enum SendRoute: Hashable { case failure case lnurlPayAmount case lnurlPayConfirm + case lnurlWithdrawAmount + case lnurlWithdrawConfirm + case lnurlWithdrawFailure(amount: UInt64) } struct SendConfig { @@ -96,6 +99,16 @@ struct SendSheet: View { LnurlPayAmount(navigationPath: $navigationPath) case .lnurlPayConfirm: LnurlPayConfirm(navigationPath: $navigationPath) + case .lnurlWithdrawAmount: + LnurlWithdrawAmount { + navigationPath.append(.lnurlWithdrawConfirm) + } + case .lnurlWithdrawConfirm: + LnurlWithdrawConfirm { amount in + navigationPath.append(.lnurlWithdrawFailure(amount: amount)) + } + case let .lnurlWithdrawFailure(amount): + LnurlWithdrawFailure(amount: amount) } } }