damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 5e530bfc9c584dcb93729b6a21863c97125ffb21
parent 0719e94fbcb49b6b313d41c32d6c3592675f8068
Author: ericholguin <ericholguin@apache.org>
Date:   Sun, 10 Mar 2024 16:37:13 -0600

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 <ericholguin@apache.org>
Reviewed-by: William Casarin <jb55@jb55.com>
Link: 20240310223713.4541-1-ericholguin@apache.org
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++++++
Adamus/Assets.xcassets/iconography/Image.imageset/Contents.json | 20++++++++++++++++++++
Adamus/Assets.xcassets/mutiny.imageset/Contents.json | 12++++++++++++
Adamus/Assets.xcassets/mutiny.imageset/mutiny.png | 0
Adamus/Components/Gradients/MutinyGradient.swift | 15+++++++++++++++
Mdamus/Views/Buttons/AlbyButton.swift | 11+++++------
Adamus/Views/Buttons/MutinyButton.swift | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Wallet/ConnectWalletView.swift | 119++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mdamus/Views/Wallet/NWCScannerView.swift | 38+-------------------------------------
Mdamus/Views/Wallet/WalletView.swift | 51+++++++++++++++++++++++++++++++++++++++++++++------
10 files changed, 262 insertions(+), 59 deletions(-)

diff --git 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 = "<group>"; }; 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; }; 5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; }; + 5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = "<group>"; }; + 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = "<group>"; }; 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = "<group>"; }; 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = "<group>"; }; 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = "<group>"; }; @@ -2110,6 +2114,7 @@ 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */, 4C687C202A5F7ED00092C550 /* DamusBackground.swift */, 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */, + 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */, ); path = Gradients; sourceTree = "<group>"; @@ -2180,6 +2185,7 @@ 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */, F71694F32A6732B7001F4053 /* GradientFollowButton.swift */, 4C7D09652A0AE62100943473 /* AlbyButton.swift */, + 5C7389B62B9E692E00781E0A /* MutinyButton.swift */, ); path = Buttons; sourceTree = "<group>"; @@ -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 @@ -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 @@ -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 Binary files differ. diff --git a/damus/Components/Gradients/MutinyGradient.swift b/damus/Components/Gradients/MutinyGradient.swift @@ -0,0 +1,15 @@ +// +// MutinyGradient.swift +// damus +// +// Created by eric on 3/9/24. +// + +import SwiftUI + +fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161) +fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56) +fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1] + +let MutinyGradient: LinearGradient = + LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom) diff --git a/damus/Views/Buttons/AlbyButton.swift b/damus/Views/Buttons/AlbyButton.swift @@ -23,16 +23,15 @@ struct AlbyButton: View { HStack { Image("alby") - Text("Attach Alby Wallet", comment: "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.") + Text("Connect to Alby Wallet", comment: "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.") + .padding() } - .offset(x: -25) - .frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center) + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) .foregroundColor(DamusColors.black) .background { - RoundedRectangle(cornerRadius: 24) - .fill(AlbyGradient, strokeBorder: colorScheme == .light ? DamusColors.black : DamusColors.white, lineWidth: 2) + RoundedRectangle(cornerRadius: 12) + .fill(AlbyGradient, strokeBorder: colorScheme == .light ? DamusColors.black.opacity(0.2) : DamusColors.white, lineWidth: 1) } - .padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50)) } } } diff --git a/damus/Views/Buttons/MutinyButton.swift b/damus/Views/Buttons/MutinyButton.swift @@ -0,0 +1,47 @@ +// +// MutinyButton.swift +// damus +// +// Created by eric on 3/9/24. +// + +import SwiftUI + +struct MutinyButton: View { + let action: () -> () + + @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 @@ -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,25 +65,72 @@ 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.")) { + 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) @@ -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 @@ -45,41 +45,6 @@ enum WalletScanResult: Equatable { case scanning } -struct NWCPaste: View { - @Binding var result: WalletScanResult - - @Environment(\.colorScheme) var colorScheme - - init(result: Binding<WalletScanResult>) { - 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 @@ -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() }