Compare commits

...

1 Commits

Author SHA1 Message Date
tyiu 9e49e8c2f1 Refactor profile zaps to reuse same BOLT11 Lightning invoice logic as note zaps, which fixes profile zaps from Cash App and Muun wallets
Changelog-Fixed: Refactor profile zaps to reuse same BOLT11 Lightning invoice logic as note zaps, which fixes profile zaps from Cash App and Muun wallets
Fixes: #1067
2023-06-02 23:10:16 -04:00
9 changed files with 130 additions and 36 deletions
+4
View File
@@ -11,6 +11,7 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
@@ -337,6 +338,7 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonModel.swift; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -903,6 +905,7 @@
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1790,6 +1793,7 @@
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
+26 -18
View File
@@ -10,29 +10,25 @@ import SwiftUI
enum ZappingEventType {
case failed(ZappingError)
case got_zap_invoice(String)
case sent_from_nwc
}
enum ZappingError {
case fetching_invoice
case bad_lnurl
case canceled
case send_failed
}
struct ZappingEvent {
let is_custom: Bool
let type: ZappingEventType
let event: NostrEvent
}
class ZapButtonModel: ObservableObject {
var invoice: String? = nil
@Published var zapping: String = ""
@Published var showing_select_wallet: Bool = false
@Published var showing_zap_customizer: Bool = false
let target: ZapTarget
}
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
let target: ZapTarget
let lnurl: String
@ObservedObject var zaps: ZapsDataModel
@@ -71,7 +67,7 @@ struct ZapButton: View {
func tap() {
guard let our_zap else {
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
return
}
@@ -142,7 +138,7 @@ struct ZapButton: View {
tap()
})
.sheet(isPresented: $button.showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
CustomizeZapView(state: damus_state, target: target, lnurl: lnurl)
}
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
@@ -150,7 +146,7 @@ struct ZapButton: View {
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else {
guard zap_ev.target.id == self.target.id else {
return
}
@@ -169,6 +165,8 @@ struct ZapButton: View {
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
case .sent_from_nwc:
break
}
}
}
@@ -180,7 +178,7 @@ struct ZapButton_Previews: PreviewProvider {
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
let zaps = ZapsDataModel([.pending(pending_zap)])
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps)
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
}
}
@@ -196,14 +194,13 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
return .external(ExtPendingZapState(state: .fetching_invoice))
}
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let keypair = damus_state.keypair.to_full() else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? ""
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
@@ -231,7 +228,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
}
return
@@ -245,7 +242,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
}
return
@@ -258,6 +255,9 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
// don't both continuing, user has canceled
if case .cancel_fetching_invoice = nwc_state.state {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.canceled)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return
}
@@ -276,6 +276,10 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
let typ = ZappingEventType.failed(.send_failed)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return
}
@@ -284,9 +288,13 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
// we don't need to trigger a ZapsDataModel update here
}
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
notify(.zapping, ev)
case .external(let pending_ext):
pending_ext.state = .done
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
notify(.zapping, ev)
}
}
+8
View File
@@ -456,6 +456,11 @@ struct ContentView: View {
let damus_state else {
return
}
if local.type == .profile_zap {
open_profile(id: local.event_id)
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
@@ -471,6 +476,9 @@ struct ContentView: View {
case .mention: fallthrough
case .repost:
open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
+29 -3
View File
@@ -187,7 +187,12 @@ class HomeModel: ObservableObject {
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
switch zap.target {
case .profile(let profile_id):
create_in_app_profile_zap_notification(profiles: profiles, zap: zap, profile_id: profile_id)
case .note(let note_target):
create_in_app_event_zap_notification(profiles: profiles, zap: zap, evId: note_target.note_id)
}
}
}
@@ -1118,7 +1123,28 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
}
}
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
@@ -1202,7 +1228,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
case .dm:
title = displayName
identifier = "myDMNotification"
case .zap:
case .zap, .profile_zap:
// not handled here
break
}
+15
View File
@@ -0,0 +1,15 @@
//
// ZapButtonModel.swift
// damus
//
// Created by Terry Yiu on 6/1/23.
//
import Foundation
class ZapButtonModel: ObservableObject {
var invoice: String? = nil
@Published var zapping: String = ""
@Published var showing_select_wallet: Bool = false
@Published var showing_zap_customizer: Bool = false
}
+1
View File
@@ -44,4 +44,5 @@ enum LocalNotificationType: String {
case mention
case repost
case zap
case profile_zap
}
+1 -1
View File
@@ -88,7 +88,7 @@ struct EventActionBar: View {
if let lnurl = self.lnurl {
Spacer()
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
ZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
}
Spacer()
+33 -7
View File
@@ -106,6 +106,7 @@ struct ProfileView: View {
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state
@@ -244,11 +245,7 @@ struct ProfileView: View {
func lnButton(lnurl: String, profile: Profile) -> some View {
let button_img = profile.reactions == false ? "zap.fill" : "zap"
return Button(action: {
if damus_state.settings.show_wallet_selector {
showing_select_wallet = true
} else {
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
}
zap_button_model.showing_zap_customizer = true
}) {
Image(button_img)
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
@@ -275,8 +272,37 @@ struct ProfileView: View {
}
.cornerRadius(24)
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
.sheet(isPresented: $zap_button_model.showing_zap_customizer) {
CustomizeZapView(state: damus_state, target: ZapTarget.profile(self.profile.pubkey), lnurl: lnurl)
}
.sheet(isPresented: $zap_button_model.showing_select_wallet, onDismiss: {zap_button_model.showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $zap_button_model.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: zap_button_model.invoice ?? "")
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.target.id == self.profile.pubkey else {
return
}
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if damus_state.settings.show_wallet_selector {
zap_button_model.invoice = inv
zap_button_model.showing_select_wallet = true
} else {
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
case .sent_from_nwc:
break
}
}
}
+13 -7
View File
@@ -46,7 +46,7 @@ func satsString(_ count: Int, locale: Locale = Locale.current) -> String {
struct CustomizeZapView: View {
let state: DamusState
let event: NostrEvent
let target: ZapTarget
let lnurl: String
@State var comment: String
@State var custom_amount: String
@@ -71,9 +71,9 @@ struct CustomizeZapView: View {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
init(state: DamusState, event: NostrEvent, lnurl: String) {
init(state: DamusState, target: ZapTarget, lnurl: String) {
self._comment = State(initialValue: "")
self.event = event
self.target = target
self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount)
self._error = State(initialValue: nil)
self._invoice = State(initialValue: "")
@@ -184,7 +184,7 @@ struct CustomizeZapView: View {
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
send_zap(damus_state: state, target: target, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.disabled(custom_amount_sats == 0 || custom_amount.isEmpty)
@@ -208,7 +208,7 @@ struct CustomizeZapView: View {
guard zap_ev.is_custom else {
return
}
guard zap_ev.event.id == event.id else {
guard zap_ev.target.id == target.id else {
return
}
@@ -221,6 +221,10 @@ struct CustomizeZapView: View {
self.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:
self.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:
self.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:
self.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
}
break
case .got_zap_invoice(let inv):
@@ -234,6 +238,8 @@ struct CustomizeZapView: View {
self.showing_wallet_selector = false
dismiss()
}
case .sent_from_nwc:
dismiss()
}
}
@@ -309,7 +315,7 @@ struct CustomizeZapView: View {
}
var ZapPicker: some View {
ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: event.pubkey)
ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: target.pubkey)
}
var MainContent: some View {
@@ -326,7 +332,7 @@ extension View {
struct CustomizeZapView_Previews: PreviewProvider {
static var previews: some View {
CustomizeZapView(state: test_damus_state(), event: test_event, lnurl: "")
CustomizeZapView(state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "")
.frame(width: 400, height: 600)
}
}