1
0
mirror of git://jb55.com/damus synced 2024-09-16 02:03:45 +00:00

zaps: Implement single-tap zap on profile action sheet and fix zap fallthrough on default settings

This commit implements a single-tap zap on the profile action sheet and fixes an issue where zapping would silently fail on default settings if the user had no lightning wallet installed in their system.

Testing
-------

Configurations:
- iPhone 13 Mini (physical device) on iOS 17.0.2 with NWC wallet attached
- iPhone 15 Pro (simulator) on iOS 17.0.1 with no lightning wallet nor NWC

Damus: This commit

Coverage:
- Zapping using NWC connected wallet: PASS (Zaps and shows UX feedback of the completed action)
- Zapping under default settings and no lightning wallet: PASS (Shows the wallet selector invoice view)
- Long press on zap button brings custom zap view

Regression testing
------------------

Preconditions: iPhone 15 Pro (simulator) on iOS 17.0.1 with no lightning wallet nor NWC

Coverage:
- Zapping user on their full profile shows wallet selector. PASS
- On-post invoice shows wallet selector. PASS

Closes: https://github.com/damus-io/damus/issues/1634
Changelog-Changed: Zap button on profile action sheet now zaps with a single click, while a long press brings custom zap view
Changelog-Fixed: Fixed an issue where zapping would silently fail on default settings if the user does not have a lightning wallet preinstalled on their device.
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino 2023-10-23 23:32:55 +00:00 committed by William Casarin
parent 139df33cb7
commit 692d29942b
8 changed files with 195 additions and 37 deletions

View File

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

View File

@ -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 {

View File

@ -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

View File

@ -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")

View File

@ -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()
}

View File

@ -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) {

View File

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

View File

@ -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()