Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Components/NumberPadTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -86,6 +87,7 @@ struct NumberPadTextField: View {
+ Text(viewModel.getPlaceholder(currency: currency))
.foregroundColor(isFocused ? .textSecondary : .textPrimary))
.font(.custom(Fonts.black, size: 44))
.accessibilityIdentifierIfPresent(testIdentifier)
}
}
}
6 changes: 5 additions & 1 deletion Bitkit/Components/TextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct TextField: View {
let font: Font
let axis: Axis
let testIdentifier: String?
let submitLabel: SubmitLabel
@Binding var text: String

init(
Expand All @@ -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
}

Expand All @@ -35,6 +38,7 @@ struct TextField: View {
SwiftUI.TextField("", text: $text, axis: axis)
.accentColor(.brandAccent)
.font(font)
.submitLabel(submitLabel)
.accessibilityIdentifierIfPresent(testIdentifier)
}
.padding()
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
17 changes: 11 additions & 6 deletions Bitkit/Utilities/Lnurl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions Bitkit/Utilities/PaymentNavigationHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 22 additions & 7 deletions Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -264,7 +268,7 @@ extension AppViewModel {
}

selectedWalletToPayFrom = .lightning
lnurlPayData = data
lnurlPayData = normalizedData
}

private func handleLnurlWithdraw(_ data: LnurlWithdrawData) {
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -295,7 +308,7 @@ extension AppViewModel {
return
}

lnurlWithdrawData = data
lnurlWithdrawData = normalizedData
}

private func handleLnurlChannel(_ data: LnurlChannelData) {
Expand Down Expand Up @@ -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"
)
}
}
Expand All @@ -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: _):
Expand Down
24 changes: 23 additions & 1 deletion Bitkit/ViewModels/SheetViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions Bitkit/Views/Sheets/LnurlAuth/LnurlAuthSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
3 changes: 3 additions & 0 deletions Bitkit/Views/Transfer/FundManualSuccessView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ struct FundManualSuccessView: View {
) {
navigation.reset()
}
.accessibilityIdentifier("ExternalSuccess-button")
}
.padding(.horizontal, 16)
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ExternalSuccess")
}
.navigationBarHidden(true)
.interactiveDismissDisabled()
Expand Down
15 changes: 15 additions & 0 deletions Bitkit/Views/Transfer/LnurlChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ struct LnurlChannel: View {
await onConnect()
}
}
.accessibilityIdentifier("ConnectButton")
}
}
.navigationBarHidden(true)
Expand Down Expand Up @@ -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")
}
}
}
20 changes: 13 additions & 7 deletions Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -65,23 +65,29 @@ 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)
}

wallet.lnurlWithdrawAmount = amount

navigationPath.append(.confirm)
onContinue()
}
}
Loading
Loading