From 800808469851fae687033b7fd12f80710599f100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 14 Apr 2025 19:22:41 -0700 Subject: [PATCH 1/2] Add one-click Coinos wallet setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a one-click Coinos wallet setup. This was implemented using the Coinos API, and using account details that are deterministically generated from the user's private key. Closes: https://github.com/damus-io/damus/issues/2961 Changelog-Added: Added one-click Coinos wallet setup Signed-off-by: Daniel D’Aquino --- damus.xcodeproj/project.pbxproj | 8 + .../CoinosDeterministicAccountClient.swift | 340 ++++++++++++++++++ damus/Views/Wallet/ConnectWalletView.swift | 111 +++++- damus/Views/Wallet/NWCSettings.swift | 3 + damus/Views/Wallet/WalletView.swift | 4 +- 5 files changed, 461 insertions(+), 5 deletions(-) create mode 100644 damus/Util/CoinosDeterministicAccountClient.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 6f452ea85..0d7515399 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -1649,6 +1649,9 @@ D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; }; D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; }; D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; + D7D09AB52DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; }; + D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; }; + D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; }; D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; }; D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; }; D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; }; @@ -2554,6 +2557,7 @@ D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = ""; }; D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = ""; }; D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = ""; }; + D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosDeterministicAccountClient.swift; sourceTree = ""; }; D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = ""; }; D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = ""; }; D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = ""; }; @@ -3363,6 +3367,7 @@ D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */, D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */, D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */, + D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */, ); path = Util; sourceTree = ""; @@ -4859,6 +4864,7 @@ 4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4C1253582A76C9060004F4B8 /* PresentSheetNotify.swift in Sources */, + D7D09AB52DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */, D773BC5F2C6D538500349F0A /* CommentItem.swift in Sources */, 4C363A962827096D006E126D /* PostBlock.swift in Sources */, 4CA9275F2A2902B20098A105 /* LongformPreview.swift in Sources */, @@ -5247,6 +5253,7 @@ 82D6FB652CD99F7900C925F4 /* CollectionExtension.swift in Sources */, 82D6FB662CD99F7900C925F4 /* ZapDataModel.swift in Sources */, 82D6FB672CD99F7900C925F4 /* Zaps+.swift in Sources */, + D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */, 82D6FB682CD99F7900C925F4 /* WalletConnect+.swift in Sources */, 82D6FB692CD99F7900C925F4 /* DamusPurpleNotificationManagement.swift in Sources */, 82D6FB6A2CD99F7900C925F4 /* DamusPurple.swift in Sources */, @@ -5996,6 +6003,7 @@ D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */, D73E5F852C6AA628007EB227 /* LoadScript.swift in Sources */, D703D74E2C6709DA00A400EA /* Pubkey.swift in Sources */, + D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */, D703D7802C670C2500A400EA /* NIP05.swift in Sources */, D703D7AA2C670E5D00A400EA /* verifier.c in Sources */, D73E5E1D2C6A9680007EB227 /* PreviewCache.swift in Sources */, diff --git a/damus/Util/CoinosDeterministicAccountClient.swift b/damus/Util/CoinosDeterministicAccountClient.swift new file mode 100644 index 000000000..2d99334ed --- /dev/null +++ b/damus/Util/CoinosDeterministicAccountClient.swift @@ -0,0 +1,340 @@ +// +// CoinosDeterministicClient.swift +// damus +// +// Created by Daniel D’Aquino on 2025-04-14. +// + +import Foundation + +/// Implements a client that can talk to the Coinos API server with a deterministic account derived from the user's private key. +/// +/// This is NOT a general-purpose Coinos client, and only works with the user's own deterministic "one-click setup" Coinos wallet account. +class CoinosDeterministicAccountClient { + // MARK: - State + + /// The user's normal keypair for using Nostr + private let userKeypair: FullKeypair + /// The JWT authentication token with Coinos + private var jwtAuthToken: String? = nil + + + // MARK: - Computed properties for a deterministic wallet + + /// A deterministic keypair for the NWC connection derived from the user's private key + private var nwcKeypair: FullKeypair? { + let nwcPrivateKey: Privkey = Privkey(sha256(self.userKeypair.privkey.id)) // SHA256 is an irreversible operation, user's nsec should not be deriveable from this new private key + return FullKeypair(privkey: nwcPrivateKey) + } + + /// A deterministic username for a Coinos account + private var username: String? { + // Derive from private key because deriving from a pubkey would mean that anyone could compute the username and take that username before our user + // Add some prefix so that we can ensure this will NOT match the password nor the NWC keypair + guard let fullText = sha256Hex(text: "coinos_username:" + self.userKeypair.privkey.hex()) else { return nil } + // There is very little risk of a birthday attack on getting only the first 16 characters, because: + // 1. before this user creates an account, no one else knows the private key in order to know the expected username and create an account before them + // 2. after the account is created and username is revealed, finding collisions is pointless as duplicate usernames will be rejected by Coinos + // + // In terms of the risk of an accidental collision due to the birthday problem, 16 characters should be enough to pragmatically avoid any collision. + // According to `https://en.wikipedia.org/wiki/Birthday_problem#Probability_table`, + // even if we have 610 million Damus users connected to Coinos, the probability of even a single collision is still as low as 1%. + return String(fullText.prefix(16)) + } + + /// A deterministic password for a Coinos account + private var password: String? { + // Add some prefix so that we can ensure this will NOT match the user nor the NWC private key + return sha256Hex(text: "coinos_password:" + self.userKeypair.privkey.hex()) + } + + /// A deterministic NWC app connection name + private var nwcConnectionName: String { return "Damus" } + + + // MARK: - Initialization + + /// Initializes the client with the user's keypair + init(userKeypair: FullKeypair) { + self.userKeypair = userKeypair + } + + + // MARK: - Authentication and registration + + /// Tries to login to the user's deterministic account. If it cannot be found, it will register for one and log into that. + func loginOrRegister() async throws { + do { + // Check if client has an account + try await self.login() + } + catch { + guard let error = error as? CoinosDeterministicAccountClient.ClientError, error == .unauthorized else { throw error } + // Client does not seem to have an account, create one + try await self.register() + try await self.login() + } + } + + /// Registers for a Coinos account using deterministic account details. + /// + /// It succeeds if it returns without throwing errors. + func register() async throws { + guard let username, let password else { throw ClientError.errorFormingRequest } + let registerPayload = RegisterRequest(user: UserCredentials(username: username, password: password)) + let jsonData = try JSONEncoder().encode(registerPayload) + + let url = URL(string: "https://coinos.io/api/register")! + let (data, response) = try await makeRequest(method: .post, url: url, payload: jsonData, payload_type: .json) + + if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { + return + } else { + throw ClientError.unexpectedHTTPResponse(status_code: (response as? HTTPURLResponse)?.statusCode ?? -1, response: data) + } + } + + /// Logs into the deterministic account, if an auth token is not present + func loginIfNeeded() async throws { + if self.jwtAuthToken == nil { try await self.login() } + } + + /// Logs into to our deterministic account. + /// + /// Succeeds if it returns without returning errors. + /// + /// Mutating function, will update the client's internal state. + func login() async throws { + self.jwtAuthToken = try await sendLoginRequest().token + } + + /// Sends the login request and return the response + /// + /// Does NOT update the internal login state. + private func sendLoginRequest() async throws -> AuthResponse { + guard let url = URL(string: "https://coinos.io/api/login") else { throw ClientError.errorFormingRequest } + guard let username, let password else { throw ClientError.errorFormingRequest } + let credentials = UserCredentials(username: username, password: password) + let jsonData = try JSONEncoder().encode(credentials) + + let (data, response) = try await makeRequest(method: .post, url: url, payload: jsonData, payload_type: .json) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: return try JSONDecoder().decode(AuthResponse.self, from: data) + case 401: throw ClientError.unauthorized + default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data) + } + } + throw ClientError.errorProcessingResponse + } + + + // MARK: - Managing NWC connections + + /// Creates a new NWC connection + /// + /// Note: Account must exist before calling this endpoint + func createNWCConnection() async throws -> WalletConnectURL { + guard let nwcKeypair else { throw ClientError.errorFormingRequest } + guard let urlEndpoint = URL(string: "https://coinos.io/api/app") else { throw ClientError.errorFormingRequest } + + try await self.loginIfNeeded() + + let config = try defaultWalletConnectionConfig() + let configData = try encode_json_data(config) + + let (data, response) = try await self.makeAuthenticatedRequest( + method: .post, + url: urlEndpoint, + payload: configData, + payload_type: .json + ) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: + guard let nwc = try await self.getNWCUrl() else { throw ClientError.errorProcessingResponse } + return nwc + case 401: throw ClientError.unauthorized + default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data) + } + } + throw ClientError.errorProcessingResponse + } + + /// Returns the default wallet connection config + private func defaultWalletConnectionConfig() throws -> NewWalletConnectionConfig { + guard let nwcKeypair else { throw ClientError.errorFormingRequest } + return NewWalletConnectionConfig( + name: self.nwcConnectionName, + secret: nwcKeypair.privkey.hex(), + pubkey: nwcKeypair.pubkey.hex(), + max_amount: 30000, // 30K sats per week maximum + budget_renewal: .weekly + ) + } + + /// Gets the NWC URL for the deterministic NWC app connection + /// + /// Account must already exist before calling this + /// + /// Returns `nil` if no NWC url is found, (e.g. if app connection has not been configured yet) + func getNWCUrl() async throws -> WalletConnectURL? { + guard let connectionConfig = try await self.getNWCAppConnectionConfig(), let nwc = connectionConfig.nwc else { return nil } + return WalletConnectURL(str: nwc) + } + + /// Gets the deterministic NWC app connection configuration details, if it exists + /// + /// Account must already exist before calling this + /// + /// Returns `nil` if no connection is found, (e.g. if app connection has not been configured yet) + func getNWCAppConnectionConfig() async throws -> WalletConnectionConfig? { + guard let nwcKeypair else { throw ClientError.errorFormingRequest } + guard let url = URL(string: "https://coinos.io/api/app/" + nwcKeypair.pubkey.hex()) else { throw ClientError.errorFormingRequest } + + try await self.loginIfNeeded() + + let (data, response) = try await self.makeAuthenticatedRequest( + method: .get, + url: url, + payload: nil, + payload_type: nil + ) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: return try JSONDecoder().decode(WalletConnectionConfig.self, from: data) + case 401: throw ClientError.unauthorized + case 404: return nil + default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data) + } + } + throw ClientError.errorProcessingResponse + } + + + // MARK: - Lower level request convenience functions + + /// Makes a request without any authorization + func makeRequest(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?) async throws -> (data: Data, response: URLResponse) { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.httpBody = payload + + if let payload_type { + request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type") + } + return try await URLSession.shared.data(for: request) + } + + /// Makes an authenticated request with our JWT auth token. + /// + /// Client must be logged-in before calling this, otherwise an error will be thrown. + func makeAuthenticatedRequest(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?) async throws -> (data: Data, response: URLResponse) { + guard let jwtAuthToken else { throw ClientError.errorFormingRequest } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.httpBody = payload + + request.setValue("Bearer " + jwtAuthToken, forHTTPHeaderField: "Authorization") + if let payload_type { + request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type") + } + return try await URLSession.shared.data(for: request) + } + + + // MARK: - Helper structures + + /// Payload for registering for a new Coinos account + struct RegisterRequest: Codable { + /// New user credentials + let user: UserCredentials + } + + /// Payload for user credentials (sign-up and login) + struct UserCredentials: Codable { + /// The username + let username: String + /// The user password + let password: String + } + + /// A successful response to a login auth endpoint + struct AuthResponse: Codable { + /// The JWT token to be applied to any authenticated API calls + let token: String + } + + /// Used by the client to define new NWC configurations + struct NewWalletConnectionConfig: Codable { + /// The name of the connection + let name: String + /// 32 Hex-encoded bytes containing a shared private key secret + let secret: String + /// 32 Hex-encoded bytes containing the pubkey for the secret + let pubkey: String + /// Max amount that can be spent in each renewal period (measured in sats) + let max_amount: UInt64 + /// The period of time it takes for the budget limits to reset + let budget_renewal: BudgetRenewalPeriod + } + + /// The NWC connection configuration details + /// + /// ## Implementation notes + /// + /// - All items defined as optionals because the Coinos API may change in the future, so this may help increase future compatibility. + struct WalletConnectionConfig: Codable { + /// The name of the connection + let name: String? + /// 32 Hex-encoded bytes containing a shared private key secret + let secret: String? + /// 32 Hex-encoded bytes containing the pubkey for the secret + let pubkey: String? + /// Max amount that can be spent in every renewal period (measured in sats) + let max_amount: UInt64? + /// The NWC url generated by the server + let nwc: String? + /// Budget renewal information + let budget_renewal: BudgetRenewalPeriod? + } + + /// A period of time it takes for budget limits to be reset + enum BudgetRenewalPeriod: String, Codable { + /// Resets once a week + case weekly + } + + /// A client error occured + enum ClientError: Error, Equatable { + /// Received an unexpected HTTP response + /// + /// Could be for a variety of reasons. + case unexpectedHTTPResponse(status_code: Int, response: Data) + /// Error forming the request, generally due to missing or inconsistent internal data + /// + /// Probably caused by a programming error. + case errorFormingRequest + /// The client could not process the response from the server + /// + /// Might be a sign of an incompatibility bug + case errorProcessingResponse + /// The action performed is not authorized + /// Generally thrown if user does not exist, credentials do not match what Coinos has on file, or programming error + case unauthorized + /// Client not logged in on a call that expected login + case notLoggedIn + } +} + +/// Computes a SHA256 hash digest from a piece of UTF-8 text, and returns the result as a "hex" string +/// +/// When working only with strings, this can be more convenient than transforming text to data, and data back to text +fileprivate func sha256Hex(text: String) -> String? { + guard let data = text.data(using: .utf8) else { return nil } + return sha256(data).toHexString() +} diff --git a/damus/Views/Wallet/ConnectWalletView.swift b/damus/Views/Wallet/ConnectWalletView.swift index 01beeb3d3..675fea6fb 100644 --- a/damus/Views/Wallet/ConnectWalletView.swift +++ b/damus/Views/Wallet/ConnectWalletView.swift @@ -16,7 +16,9 @@ struct ConnectWalletView: View { @State var error: String? = nil @State var wallet_scan_result: WalletScanResult = .scanning @State var show_introduction: Bool = true + @State var show_coinos_options: Bool = false var nav: NavigationCoordinator + let userKeypair: Keypair var body: some View { MainContent @@ -147,8 +149,7 @@ struct ConnectWalletView: View { Spacer() CoinosButton() { - show_introduction = false - openURL(URL(string:"https://coinos.io/settings/nostr")!) + self.show_coinos_options = true } .padding() } @@ -161,6 +162,110 @@ struct ConnectWalletView: View { .padding(2) // Avoids border clipping on the sides ) .padding(.top, 20) + .sheet(isPresented: $show_coinos_options, content: { + CoinosConnectionOptionsSheet + }) + } + + var CoinosConnectionOptionsSheet: some View { + VStack(spacing: 20) { + Text("How would you like to connect to your Coinos wallet?", comment: "Question for the user when connecting a Coinos wallet.") + .font(.title3) + .bold() + .multilineTextAlignment(.center) + .padding(.bottom, 10) + .lineLimit(2) + + Spacer() + + VStack(spacing: 5) { + Button( + action: { self.oneClickSetup() }, + label: { + HStack { + Spacer() + VStack { + HStack { + Image(systemName: "wand.and.sparkles") + Text("One-click setup", comment: "Button label for users to do a one-click Coinos wallet setup.") + } + // I have to hide this on npub logins, because otherwise SwiftUI will start truncating text + if self.userKeypair.privkey != nil { + Text("Also click here if you had a one-click setup before.", comment: "Button description hint for users who may want to do a one-click setup.") + .font(.caption) + } + } + Spacer() + } + } + ) + .frame(maxWidth: .infinity) + .buttonStyle(GradientButtonStyle()) + .opacity(self.userKeypair.privkey == nil ? 0.5 : 1.0) + .disabled(self.userKeypair.privkey == nil) + + if self.userKeypair.privkey == nil { + Text("You must be logged in with your nsec to use this option.", comment: "Warning text for users who cannot create a Coinos account via the one-click setup without being logged in with their nsec.") + .font(.caption) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + + Text("Your profile will not be shared with Coinos.", comment: "Label text for users to reassure them that their nsec is not shared with a third party.") + .font(.caption) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + } + + Button( + action: { + show_introduction = false + show_coinos_options = false + openURL(URL(string:"https://coinos.io/settings/nostr")!) + }, + label: { + HStack { + Spacer() + + VStack { + HStack { + Image(systemName: "arrow.up.right") + Text("Connect via the website", comment: "Button label for users who are setting up a Coinos wallet and would like to connect via the website") + } + Text("Click here if you have a Coinos username and password.", comment: "Button description hint for users who may want to connect via the website.") + .font(.caption) + } + + Spacer() + } + } + ) + .frame(maxWidth: .infinity) + } + .padding() + .presentationDetents([.height(300)]) + } + + func oneClickSetup() { + Task { + show_coinos_options = false + do { + guard let fullKeypair = self.userKeypair.to_full() else { + throw CoinosDeterministicAccountClient.ClientError.errorFormingRequest + } + let client = CoinosDeterministicAccountClient(userKeypair: fullKeypair) + try await client.loginOrRegister() + let nwcURL = try await client.createNWCConnection() + model.connect(nwcURL) // Connect directly, to make it a true one-click setup + } + catch { + present_sheet(.error(.init( + user_visible_description: NSLocalizedString("Something went wrong when performing the one-click Coinos wallet setup.", comment: "Error label when user tries the one-click Coinos wallet setup but fails for some generic reason."), + tip: NSLocalizedString("Check your internet connection and try again. If the error persists, contact support.", comment: "Error tip when user tries to create the one-click Coinos wallet setup but fails for a generic reason."), + technical_info: error.localizedDescription + ))) + } + } } var ManualSetup: some View { @@ -270,7 +375,7 @@ struct ConnectWalletView: View { struct ConnectWalletView_Previews: PreviewProvider { static var previews: some View { - ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init()) + ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init(), userKeypair: test_keypair) .previewDisplayName("Main Wallet Connect View") ConnectWalletView.AreYouSure(nwc: get_test_nwc(), show_introduction: .constant(false), model: WalletModel(settings: test_damus_state.settings)) .previewDisplayName("Are you sure screen") diff --git a/damus/Views/Wallet/NWCSettings.swift b/damus/Views/Wallet/NWCSettings.swift index c4e801b30..ae2bb94dd 100644 --- a/damus/Views/Wallet/NWCSettings.swift +++ b/damus/Views/Wallet/NWCSettings.swift @@ -14,6 +14,8 @@ struct NWCSettings: View { @ObservedObject var model: WalletModel @ObservedObject var settings: UserSettingsStore + @Environment(\.dismiss) var dismiss + func donation_binding() -> Binding { return Binding(get: { @@ -136,6 +138,7 @@ struct NWCSettings: View { Button(action: { self.model.disconnect() + dismiss() }) { HStack { Text("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.") diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift index 6d78e3427..2dae37419 100644 --- a/damus/Views/Wallet/WalletView.swift +++ b/damus/Views/Wallet/WalletView.swift @@ -39,9 +39,9 @@ struct WalletView: View { var body: some View { switch model.connect_state { case .new: - ConnectWalletView(model: model, nav: damus_state.nav) + ConnectWalletView(model: model, nav: damus_state.nav, userKeypair: self.damus_state.keypair) case .none: - ConnectWalletView(model: model, nav: damus_state.nav) + ConnectWalletView(model: model, nav: damus_state.nav, userKeypair: self.damus_state.keypair) case .existing(let nwc): MainWalletView(nwc: nwc) .toolbar { From 957cb5c540d634e333eb718b9770ad103e8db7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 18 Apr 2025 16:43:48 -0700 Subject: [PATCH 2/2] Add disclaimer to Coinos button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog-Changed: Added disclaimer to clarify that Coinos is a third-party service Signed-off-by: Daniel D’Aquino --- damus/Views/Wallet/ConnectWalletView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/damus/Views/Wallet/ConnectWalletView.swift b/damus/Views/Wallet/ConnectWalletView.swift index 675fea6fb..28b9be481 100644 --- a/damus/Views/Wallet/ConnectWalletView.swift +++ b/damus/Views/Wallet/ConnectWalletView.swift @@ -148,8 +148,14 @@ struct ConnectWalletView: View { Spacer() - CoinosButton() { - self.show_coinos_options = true + VStack(spacing: 5) { + CoinosButton() { + self.show_coinos_options = true + } + Text("Coinos is a service operated by a third-party. We have no access to your Coinos wallet.", comment: "Small caption with a disclaimer that Damus does not own or have access to Coinos wallets, Coinos is a third-party service.") + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) } .padding() }