commit e498418c2d47c2042d9e27940d2897d7a205697c
parent 33150a42c5a39a121a5dbd97f7b648148662b55d
Author: Daniel D’Aquino <daniel@daquino.me>
Date: Mon, 14 Apr 2025 19:22:41 -0700
Add one-click Coinos wallet setup
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 <daniel@daquino.me>
Diffstat:
5 files changed, 461 insertions(+), 5 deletions(-)
diff --git 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 = "<group>"; };
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
+ D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosDeterministicAccountClient.swift; sourceTree = "<group>"; };
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = "<group>"; };
@@ -3363,6 +3367,7 @@
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
+ D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -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
@@ -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
@@ -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
@@ -14,6 +14,8 @@ struct NWCSettings: View {
@ObservedObject var model: WalletModel
@ObservedObject var settings: UserSettingsStore
+ @Environment(\.dismiss) var dismiss
+
func donation_binding() -> Binding<Double> {
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
@@ -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 {