From 5e530bfc9c584dcb93729b6a21863c97125ffb21 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Sun, 10 Mar 2024 16:37:13 -0600 Subject: [PATCH] ui: Wallet View redesign + Mutiny Wallet integration This patch redesigns the wallet view to more closely match Rob's design. In addition this patch allows users to connect to Mutiny Wallet by clicking a button. iPhone SE (3rd generation) Dark Mode: https://v.nostr.build/K9lk.mp4 iPhone 15 Pro Max Light Mode: https://v.nostr.build/9mKA.mp4 Connected Alby Wallet: https://i.nostr.build/kyd5.png Changelog-Added: Connect to Mutiny Wallet Button Changelog-Changed: Moved paste nwc button to main wallet view Changelog-Changed: Errors with an NWC will show as an alert Changelog-Fixed: Issue where NWC Scanner view would not dismiss after a failed scan/paste Signed-off-by: ericholguin Reviewed-by: William Casarin Link: 20240310223713.4541-1-ericholguin@apache.org Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 8 ++ .../iconography/Image.imageset/Contents.json | 20 +++ .../mutiny.imageset/Contents.json | 12 ++ .../mutiny.imageset/mutiny.png | Bin 0 -> 1655 bytes .../Components/Gradients/MutinyGradient.swift | 15 +++ damus/Views/Buttons/AlbyButton.swift | 11 +- damus/Views/Buttons/MutinyButton.swift | 47 +++++++ damus/Views/Wallet/ConnectWalletView.swift | 121 ++++++++++++++++-- damus/Views/Wallet/NWCScannerView.swift | 38 +----- damus/Views/Wallet/WalletView.swift | 51 +++++++- 10 files changed, 263 insertions(+), 60 deletions(-) create mode 100644 damus/Assets.xcassets/iconography/Image.imageset/Contents.json create mode 100644 damus/Assets.xcassets/mutiny.imageset/Contents.json create mode 100644 damus/Assets.xcassets/mutiny.imageset/mutiny.png create mode 100644 damus/Components/Gradients/MutinyGradient.swift create mode 100644 damus/Views/Buttons/MutinyButton.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 230e76b7..de4189af 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -403,6 +403,8 @@ 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; }; 5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; }; 5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; }; + 5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; }; + 5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; }; 5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; }; 5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; }; 5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; }; @@ -1324,6 +1326,8 @@ 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = ""; }; 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = ""; }; 5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = ""; }; + 5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = ""; }; + 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = ""; }; 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = ""; }; 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = ""; }; 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = ""; }; @@ -2110,6 +2114,7 @@ 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */, 4C687C202A5F7ED00092C550 /* DamusBackground.swift */, 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */, + 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */, ); path = Gradients; sourceTree = ""; @@ -2180,6 +2185,7 @@ 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */, F71694F32A6732B7001F4053 /* GradientFollowButton.swift */, 4C7D09652A0AE62100943473 /* AlbyButton.swift */, + 5C7389B62B9E692E00781E0A /* MutinyButton.swift */, ); path = Buttons; sourceTree = ""; @@ -3049,6 +3055,7 @@ 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4CDD1AE22A6B3074001CD4DF /* NdbTagsIterator.swift in Sources */, + 5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, D7CB5D512B1174D100AD4105 /* FriendFilter.swift in Sources */, D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */, @@ -3341,6 +3348,7 @@ 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */, 4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */, + 5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */, 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, diff --git a/damus/Assets.xcassets/iconography/Image.imageset/Contents.json b/damus/Assets.xcassets/iconography/Image.imageset/Contents.json new file mode 100644 index 00000000..a19a5492 --- /dev/null +++ b/damus/Assets.xcassets/iconography/Image.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/mutiny.imageset/Contents.json b/damus/Assets.xcassets/mutiny.imageset/Contents.json new file mode 100644 index 00000000..30e270a3 --- /dev/null +++ b/damus/Assets.xcassets/mutiny.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mutiny.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/mutiny.imageset/mutiny.png b/damus/Assets.xcassets/mutiny.imageset/mutiny.png new file mode 100644 index 0000000000000000000000000000000000000000..11ade430867476f1072bbf156b2101e2d96b4b57 GIT binary patch literal 1655 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zf+;V6*ddaSW-L^X6`Ro^ZIx@sGu6 zOp^`(~1jADqgW;6C^I&f7c6t11G% z@A|{mV8Voo;=l7IRL^<)?EIt+^Iz?`b~t{TEcf>1X&+ZS`S9e|eW&ls+U>()r@v>s zAZ32rcyr?1>)-y>1pjKT+Bds@&!6a~H^1U_c^VaZ8IVwRC$xG7 zdsYL(2BEyNOS7*%toqw^0EKL^3Zb@tW;H$T3zPI?iqj&ZjzfRe<$(W@f zU+D9f{;tKV_e>2lU}-{g`L}~ zA6WM0+#ZX4zpr0x*IU<}S8C2{<^TOoSCoBE>(cz+%hzYWWipU{GM6Pl^xA$;D??+Z z9jy{fj#7*ca1^yTazS^_l$o)5cT^Rg9sM4E4UCx2j*bD%vzv{eSQCaj#it)1UCj0bS*>b%fFWoPk4pN&f}HqpET=o zW~(ZD>gAH#%d)Drp8Q>qU-hCl&xd6{BT`ZYC)>4~uP(^^_4J5dd6q5vn}6q?_g1aA zv#UbHKfQU+lgK#N*XON*uS7Cjmwvp-GLQM4)$32!TNMSC2r?khNB58BPeOlB+{LS# zv-R?%|L@-gUr6VKzGOFBkMn( zPI*}L@=(qTx%1g`EgqR~e^T*qhnM{UM$a`qC0{`Y?stBDUfT;T zrESjLd1V{F{=*mN`d?Mi*Z!n$d!+i<{2*HN97$_@WMT7j#|IAy=}%mfxE5^qlKVg8 z@x$sJl4t>}X3D*VFYn{Yv)Q^C-j{cTYTM;2GrhR+_vP$l}|9D@?WsGza#&y!bWp{UQs}qj_>}Q({{5nPF`X-loRs&$jaYW z>({bBco!KlfZRe*z7p*t=%{OL$<5nO!vL9 zVjJ#kiq$!^= () + + @Environment(\.colorScheme) var colorScheme + + init(action: @escaping () -> ()) { + self.action = action + } + + var body: some View { + Button(action: { + action() + }) { + HStack { + Image("mutiny") + .resizable() + .frame(width: 45, height: 45) + + Text("Connect to Mutiny Wallet", comment: "Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated.") + .padding() + } + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) + .foregroundColor(DamusColors.white) + .background { + RoundedRectangle(cornerRadius: 12) + .fill(MutinyGradient, strokeBorder: colorScheme == .light ? DamusColors.black.opacity(0.2) : DamusColors.white.opacity(0.2), lineWidth: 1) + } + } + } +} + +struct MutinyButton_Previews: PreviewProvider { + static var previews: some View { + MutinyButton(action: { + print("mutiny button") + }) + } +} diff --git a/damus/Views/Wallet/ConnectWalletView.swift b/damus/Views/Wallet/ConnectWalletView.swift index 8cdd098f..a214ce46 100644 --- a/damus/Views/Wallet/ConnectWalletView.swift +++ b/damus/Views/Wallet/ConnectWalletView.swift @@ -12,14 +12,15 @@ struct ConnectWalletView: View { @ObservedObject var model: WalletModel @State var scanning: Bool = false + @State private var showAlert = false @State var error: String? = nil @State var wallet_scan_result: WalletScanResult = .scanning var nav: NavigationCoordinator var body: some View { MainContent - .navigationTitle(NSLocalizedString("Attach a Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet.")) - .navigationBarTitleDisplayMode(.large) + .navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet.")) + .navigationBarTitleDisplayMode(.inline) .padding() .onChange(of: wallet_scan_result) { res in scanning = false @@ -30,18 +31,29 @@ struct ConnectWalletView: View { self.model.new(url) case .failed: - error = NSLocalizedString("Invalid Nostr wallet connection string", comment: "Error message when an invalid Nostr wallet connection string is provided.") + showAlert.toggle() case .scanning: error = nil } } + .alert(isPresented: $showAlert) { + Alert( + title: Text(NSLocalizedString("Invalid Nostr wallet connection string", comment: "Error message when an invalid Nostr wallet connection string is provided.")), + message: Text("Make sure the wallet you are connecting to supports NWC."), + dismissButton: .default(Text(NSLocalizedString("OK", comment: "Button label indicating user wants to proceed."))) { + wallet_scan_result = .scanning + } + ) + } } func AreYouSure(nwc: WalletConnectURL) -> some View { - VStack { - Text("Are you sure you want to attach this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.") - .font(.title) + VStack(spacing: 25) { + + Text("Are you sure you want to connect this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.") + .fontWeight(.bold) + .multilineTextAlignment(.center) Text(nwc.relay.id) .font(.body) @@ -53,26 +65,73 @@ struct ConnectWalletView: View { .foregroundColor(.gray) } - BigButton(NSLocalizedString("Attach", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) { + Button(action: { model.connect(nwc) + }) { + HStack { + Text("Connect", comment: "Text for button to conect to Nostr Wallet Connect lightning wallet.") + .fontWeight(.semibold) + } + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center) } + .buttonStyle(GradientButtonStyle()) - BigButton(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.")) { + Button(action: { model.cancel() + }) { + HStack { + Text(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning wallet.")) + .padding() + } + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) } + .buttonStyle(NeutralButtonStyle()) } } var ConnectWallet: some View { - VStack { + VStack(spacing: 25) { + AlbyButton() { openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!) } - BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) { - nav.push(route: Route.WalletScanner(result: $wallet_scan_result)) + MutinyButton() { + openURL(URL(string:"https://app.mutinywallet.com/settings/connections")!) } + Button(action: { + if let pasted_nwc = UIPasteboard.general.string { + guard let url = WalletConnectURL(str: pasted_nwc) else { + wallet_scan_result = .failed + return + } + + wallet_scan_result = .success(url) + } + }) { + HStack { + Image("clipboard") + Text("Paste NWC Address", comment: "Text for button to connect a lightning wallet.") + .fontWeight(.semibold) + } + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + + Button(action: { + nav.push(route: Route.WalletScanner(result: $wallet_scan_result)) + }) { + HStack { + Image("qr-code") + Text("Scan NWC Address", comment: "Text for button to connect a lightning wallet.") + .fontWeight(.semibold) + } + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + + if let err = self.error { Text(err) .foregroundColor(.red) @@ -80,14 +139,54 @@ struct ConnectWalletView: View { } } + var TopSection: some View { + HStack(spacing: 0) { + Button(action: {}, label: { + Image("damus-home") + .resizable() + .frame(width: 30, height: 30) + }) + .buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999)) + .disabled(true) + .padding(.horizontal, 30) + + Image("chevron-double-right") + .resizable() + .frame(width: 25, height: 25) + + Button(action: {}, label: { + Image("wallet") + .resizable() + .frame(width: 30, height: 30) + .foregroundStyle(LINEAR_GRADIENT) + }) + .buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999)) + .disabled(true) + .padding(.horizontal, 30) + } + } + + var TitleSection: some View { + VStack(spacing: 25) { + Text("Damus Wallet") + .fontWeight(.bold) + + Text("Securely connect your Damus app to your wallet\nusing Nostr Wallet Connect") + .font(.caption) + .multilineTextAlignment(.center) + } + } + var MainContent: some View { Group { + TopSection switch model.connect_state { case .new(let nwc): AreYouSure(nwc: nwc) case .existing: Text(verbatim: "Shouldn't happen") case .none: + TitleSection ConnectWallet } } diff --git a/damus/Views/Wallet/NWCScannerView.swift b/damus/Views/Wallet/NWCScannerView.swift index 76043cb9..433a1f17 100644 --- a/damus/Views/Wallet/NWCScannerView.swift +++ b/damus/Views/Wallet/NWCScannerView.swift @@ -45,41 +45,6 @@ enum WalletScanResult: Equatable { case scanning } -struct NWCPaste: View { - @Binding var result: WalletScanResult - - @Environment(\.colorScheme) var colorScheme - - init(result: Binding) { - self._result = result - } - - var body: some View { - Button(action: { - if let pasted_nwc = UIPasteboard.general.string { - guard let url = WalletConnectURL(str: pasted_nwc) else { - self.result = .failed - return - } - - self.result = .success(url) - } - }) { - HStack { - Image(systemName: "doc.on.clipboard") - Text("Paste", comment: "Button to paste a Nostr Wallet Connect string to connect the wallet for use in Damus for zaps.") - } - .frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center) - .foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white) - .overlay { - RoundedRectangle(cornerRadius: 24) - .stroke(colorScheme == .light ? DamusColors.black : DamusColors.white, lineWidth: 2) - } - .padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50)) - } - } -} - struct WalletScannerView: View { @Binding var result: WalletScanResult @@ -92,6 +57,7 @@ struct WalletScannerView: View { case .success(let success): guard let url = WalletConnectURL(str: success.string) else { result = .failed + dismiss() return } @@ -102,8 +68,6 @@ struct WalletScannerView: View { dismiss() } - NWCPaste(result: $result) - .padding(.vertical) } } } diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift index ff93a745..096bebdb 100644 --- a/damus/Views/Wallet/WalletView.swift +++ b/damus/Views/Wallet/WalletView.swift @@ -26,19 +26,58 @@ struct WalletView: View { Spacer() } - Text(verbatim: nwc.relay.id) - - if let lud16 = nwc.lud16 { - Text(verbatim: lud16) + VStack(spacing: 5) { + VStack(spacing: 10) { + Text("Wallet Relay") + .fontWeight(.semibold) + .padding(.top) + + Divider() + + RelayView(state: damus_state, relay: nwc.relay.id, showActionButtons: .constant(false)) + } + .frame(maxWidth: .infinity, minHeight: 125, alignment: .top) + .padding(.horizontal, 10) + .background(DamusColors.neutral1) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + + if let lud16 = nwc.lud16 { + VStack(spacing: 10) { + Text("Wallet Address") + .fontWeight(.semibold) + + Divider() + + Text(verbatim: lud16) + } + .frame(maxWidth: .infinity, minHeight: 75, alignment: .center) + .padding(.horizontal, 10) + .background(DamusColors.neutral1) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + } } - BigButton(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")) { + Button(action: { self.model.disconnect() + }) { + HStack { + Text(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")) + } + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center) } + .buttonStyle(GradientButtonStyle()) } .navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view")) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) .padding() }