diff --git a/damus/Components/InvoiceView.swift b/damus/Components/InvoiceView.swift index a38f7bfc..cc99f741 100644 --- a/damus/Components/InvoiceView.swift +++ b/damus/Components/InvoiceView.swift @@ -39,7 +39,12 @@ struct InvoiceView: View { if settings.show_wallet_selector { present_sheet(.select_wallet(invoice: invoice.string)) } else { - open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string) + do { + try open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string) + } + catch { + present_sheet(.select_wallet(invoice: invoice.string)) + } } } label: { RoundedRectangle(cornerRadius: 20, style: .circular) @@ -82,21 +87,26 @@ struct InvoiceView: View { } } -func open_with_wallet(wallet: Wallet.Model, invoice: String) { +enum OpenWalletError: Error { + case no_wallet_to_open + case store_link_invalid + case system_cannot_open_store_link +} + +func open_with_wallet(wallet: Wallet.Model, invoice: String) throws { if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } else { guard let store_link = wallet.appStoreLink else { - // TODO: do something here if we don't have an appstore link - return + throw OpenWalletError.no_wallet_to_open } guard let url = URL(string: store_link) else { - return + throw OpenWalletError.store_link_invalid } guard UIApplication.shared.canOpenURL(url) else { - return + throw OpenWalletError.system_cannot_open_store_link } UIApplication.shared.open(url) diff --git a/damus/Components/NoteZapButton.swift b/damus/Components/NoteZapButton.swift index 84f464b1..3feef660 100644 --- a/damus/Components/NoteZapButton.swift +++ b/damus/Components/NoteZapButton.swift @@ -18,6 +18,19 @@ enum ZappingError { case bad_lnurl case canceled case send_failed + + func humanReadableMessage() -> String { + switch self { + case .fetching_invoice: + return NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.") + case .bad_lnurl: + return NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.") + case .canceled: + return NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.") + case .send_failed: + return NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.") + } + } } struct ZappingEvent { diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 2ab1cd68..05a23781 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -448,7 +448,12 @@ struct ContentView: View { present_sheet(.select_wallet(invoice: inv)) } else { let wallet = damus_state!.settings.default_wallet.model - open_with_wallet(wallet: wallet, invoice: inv) + do { + try open_with_wallet(wallet: wallet, invoice: inv) + } + catch { + present_sheet(.select_wallet(invoice: inv)) + } } case .sent_from_nwc: break diff --git a/damus/Models/Wallet.swift b/damus/Models/Wallet.swift index 30682dfc..a90b25f0 100644 --- a/damus/Models/Wallet.swift +++ b/damus/Models/Wallet.swift @@ -51,7 +51,7 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable { switch self { case .system_default_wallet: return .init(index: -1, tag: "systemdefaultwallet", displayName: NSLocalizedString("Local default", comment: "Dropdown option label for system default for Lightning wallet."), - link: "lightning:", appStoreLink: "lightning:", image: "") + link: "lightning:", appStoreLink: nil, image: "") case .strike: return .init(index: 0, tag: "strike", displayName: "Strike", link: "strike:", appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike") diff --git a/damus/Views/Images/ImageContextMenuModifier.swift b/damus/Views/Images/ImageContextMenuModifier.swift index 167fce03..bf0093a7 100644 --- a/damus/Views/Images/ImageContextMenuModifier.swift +++ b/damus/Views/Images/ImageContextMenuModifier.swift @@ -61,7 +61,12 @@ struct ImageContextMenuModifier: ViewModifier { no_link_found.toggle() } else { if qrCodeLink.contains("lnurl") { - open_with_wallet(wallet: settings.default_wallet.model, invoice: qrCodeLink) + do { + try open_with_wallet(wallet: settings.default_wallet.model, invoice: qrCodeLink) + } + catch { + present_sheet(.select_wallet(invoice: qrCodeLink)) + } } else if let _ = URL(string: qrCodeLink) { open_link_confirm.toggle() } diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift index d9cc023a..3de8276e 100644 --- a/damus/Views/ProfileActionSheetView.swift +++ b/damus/Views/ProfileActionSheetView.swift @@ -59,20 +59,7 @@ struct ProfileActionSheetView: View { var zapButton: some View { if let lnurl = self.profile_data()?.lnurl, lnurl != "" { - return AnyView( - VStack(alignment: .center, spacing: 10) { - ProfileZapLinkView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in - Image(reactions_enabled ? "zap.fill" : "zap") - .foregroundColor(reactions_enabled ? .orange : Color.primary) - .profile_button_style(scheme: colorScheme) - } - .buttonStyle(NeutralCircleButtonStyle()) - - Text(NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen")) - .foregroundStyle(.secondary) - .font(.caption) - } - ) + return AnyView(ProfileActionSheetZapButton(damus_state: damus_state, profile: profile, lnurl: lnurl)) } else { return AnyView(EmptyView()) @@ -142,6 +129,146 @@ struct ProfileActionSheetView: View { } } +fileprivate struct ProfileActionSheetZapButton: View { + enum ZappingState: Equatable { + case not_zapped + case zapping + case zap_success + case zap_failure(error: ZappingError) + + func error_message() -> String? { + switch self { + case .zap_failure(let error): + return error.humanReadableMessage() + default: + return nil + } + } + } + + let damus_state: DamusState + @StateObject var profile: ProfileModel + let lnurl: String + @State var zap_state: ZappingState = .not_zapped + @State var show_error_alert: Bool = false + + @Environment(\.colorScheme) var colorScheme + + func receive_zap(zap_ev: ZappingEvent) { + print("Received zap event") + guard zap_ev.target == ZapTarget.profile(self.profile.pubkey) else { + return + } + + switch zap_ev.type { + case .failed(let err): + zap_state = .zap_failure(error: err) + show_error_alert = true + break + case .got_zap_invoice(let inv): + if damus_state.settings.show_wallet_selector { + present_sheet(.select_wallet(invoice: inv)) + } else { + let wallet = damus_state.settings.default_wallet.model + do { + try open_with_wallet(wallet: wallet, invoice: inv) + } + catch { + present_sheet(.select_wallet(invoice: inv)) + } + } + break + case .sent_from_nwc: + zap_state = .zap_success + break + } + } + + var button_label: String { + switch zap_state { + case .not_zapped: + return NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen") + case .zapping: + return NSLocalizedString("Zapping", comment: "Button label indicating that a zap action is in progress (i.e. the user is currently sending a Bitcoin tip via the lightning network to the user shown on-screen) ") + case .zap_success: + return NSLocalizedString("Zapped!", comment: "Button label indicating that a zap action was successful (i.e. the user is successfully sent a Bitcoin tip via the lightning network to the user shown on-screen) ") + case .zap_failure(_): + return NSLocalizedString("Zap failed", comment: "Button label indicating that a zap action was unsuccessful (i.e. the user was unable to send a Bitcoin tip via the lightning network to the user shown on-screen) ") + } + } + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Button( + action: { + send_zap(damus_state: damus_state, target: .profile(self.profile.pubkey), lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) + zap_state = .zapping + }, + label: { + switch zap_state { + case .not_zapped: + Image("zap") + .foregroundColor(Color.primary) + .profile_button_style(scheme: colorScheme) + case .zapping: + ProgressView() + .foregroundColor(Color.primary) + .profile_button_style(scheme: colorScheme) + case .zap_success: + Image("checkmark") + .foregroundColor(Color.green) + .profile_button_style(scheme: colorScheme) + case .zap_failure: + Image("close") + .foregroundColor(Color.red) + .profile_button_style(scheme: colorScheme) + } + + } + ) + .disabled({ + switch zap_state { + case .not_zapped: + return false + default: + return true + } + }()) + .buttonStyle(NeutralCircleButtonStyle()) + + Text(button_label) + .foregroundStyle(.secondary) + .font(.caption) + } + .onReceive(handle_notify(.zapping)) { zap_ev in + receive_zap(zap_ev: zap_ev) + } + .simultaneousGesture(LongPressGesture().onEnded {_ in + present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl)) + }) + .alert(isPresented: $show_error_alert) { + Alert( + title: Text(NSLocalizedString("Zap failed", comment: "Title of an alert indicating that a zap action failed")), + message: Text(zap_state.error_message() ?? ""), + dismissButton: .default(Text(NSLocalizedString("OK", comment: "Button label to dismiss an error dialog"))) + ) + } + .onChange(of: zap_state) { new_zap_state in + switch new_zap_state { + case .zap_success, .zap_failure: + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + withAnimation { + zap_state = .not_zapped + } + } + break + default: + break + } + } + } +} + struct InnerHeightPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = .zero static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { diff --git a/damus/Views/SelectWalletView.swift b/damus/Views/SelectWalletView.swift index 62433904..310e63be 100644 --- a/damus/Views/SelectWalletView.swift +++ b/damus/Views/SelectWalletView.swift @@ -38,7 +38,8 @@ struct SelectWalletView: View { Section(NSLocalizedString("Select a Lightning wallet", comment: "Title of section for selecting a Lightning wallet to pay a Lightning invoice.")) { List{ Button() { - open_with_wallet(wallet: default_wallet.model, invoice: invoice) + // TODO: Handle cases where wallet cannot be opened by the system + try? open_with_wallet(wallet: default_wallet.model, invoice: invoice) } label: { HStack { Text("Default Wallet", comment: "Button to pay a Lightning invoice with the user's default Lightning wallet.").font(.body).foregroundColor(.blue) @@ -47,7 +48,8 @@ struct SelectWalletView: View { List($allWalletModels) { $wallet in if wallet.index >= 0 { Button() { - open_with_wallet(wallet: wallet, invoice: invoice) + // TODO: Handle cases where wallet cannot be opened by the system + try? open_with_wallet(wallet: wallet, invoice: invoice) } label: { HStack { Image(wallet.image).resizable().frame(width: 32.0, height: 32.0,alignment: .center).cornerRadius(5) diff --git a/damus/Views/Zaps/CustomizeZapView.swift b/damus/Views/Zaps/CustomizeZapView.swift index fb4cef39..e7cce83c 100644 --- a/damus/Views/Zaps/CustomizeZapView.swift +++ b/damus/Views/Zaps/CustomizeZapView.swift @@ -194,16 +194,7 @@ struct CustomizeZapView: View { switch zap_ev.type { case .failed(let err): - switch err { - case .fetching_invoice: - model.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.") - case .bad_lnurl: - model.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.") - case .canceled: - model.error = NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.") - case .send_failed: - model.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.") - } + model.error = err.humanReadableMessage() break case .got_zap_invoice(let inv): if state.settings.show_wallet_selector { @@ -212,8 +203,13 @@ struct CustomizeZapView: View { } else { end_editing() let wallet = state.settings.default_wallet.model - open_with_wallet(wallet: wallet, invoice: inv) - dismiss() + do { + try open_with_wallet(wallet: wallet, invoice: inv) + dismiss() + } + catch { + present_sheet(.select_wallet(invoice: inv)) + } } case .sent_from_nwc: dismiss()