diff --git a/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/AppState.swift b/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/AppState.swift index 8926505..8539182 100644 --- a/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/AppState.swift +++ b/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/AppState.swift @@ -32,6 +32,7 @@ final class AppState { // Signer selection (shared across Transfer/Signing/Signers) private(set) var selectedSignerLocator: String? + private(set) var delegatedSigners: [WalletDelegatedSignerConfigApiModel] = [] private let sdk: CrossmintSDK = .shared @@ -75,6 +76,7 @@ final class AppState { // Only update UI state if still on the same chain if chain == selectedChain { await fetchBalance() + await loadSigners() preloadOtherChains(email: email) } } else { @@ -100,6 +102,7 @@ final class AppState { walletCache[chain] = w notFoundChains.remove(chain) await fetchBalance() + await loadSigners() } catch { walletErrorMessage = error.userMessage } @@ -107,16 +110,24 @@ final class AppState { isCreatingWallet = false } + func loadSigners() async { + delegatedSigners = (try? await wallet?.signers()) ?? [] + guard selectedSignerLocator == nil, let locator = firstSelectableLocator() else { return } + await selectSigner(locator: locator) + } + /// Switches chain without re-fetching if the wallet is already cached. func switchChain(_ chain: SupportedChain, email: String) async { guard chain != selectedChain else { return } selectedChain = chain selectedSignerLocator = nil + delegatedSigners = [] walletErrorMessage = nil balance = nil if walletCache[chain] != nil { await fetchBalance() + await loadSigners() } else if !notFoundChains.contains(chain) { await loadWallet(email: email) } @@ -198,6 +209,13 @@ final class AppState { } } + private func firstSelectableLocator() -> String? { + if let recovery = recoveryLocator, signerConfig(for: recovery) != nil { return recovery } + return delegatedSigners + .compactMap { $0.locator ?? $0.signer } + .first { signerConfig(for: $0) != nil } + } + private func signerConfig(for locator: String) -> SignerConfig? { if locator.hasPrefix("device:") { return .device } if locator.hasPrefix("api-key:") { return .apiKey } diff --git a/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignerPicker.swift b/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignerPicker.swift index 4444c2a..636ad26 100644 --- a/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignerPicker.swift +++ b/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignerPicker.swift @@ -7,10 +7,9 @@ import CrossmintClient import SwiftUI /// A form section with a "Sign with" picker. -/// Only renders when the wallet has more than one selectable signer. +/// Renders whenever the wallet has at least one selectable signer. struct SignerPicker: View { @Environment(AppState.self) private var appState - @State private var delegatedSigners: [WalletDelegatedSignerConfigApiModel] = [] private struct Option: Identifiable { var id: String { locator } @@ -25,8 +24,8 @@ struct SignerPicker: View { if let recovery = appState.recoveryLocator, isSelectable(recovery) { result.append(Option(locator: recovery, typeLabel: SignerRow.typeLabel(for: recovery), isRecovery: true)) } - for signer in delegatedSigners { - if let locator = signer.locator, isSelectable(locator) { + for signer in appState.delegatedSigners { + if let locator = signer.locator ?? signer.signer, isSelectable(locator) { result.append(Option(locator: locator, typeLabel: SignerRow.typeLabel(for: locator), isRecovery: false)) } } @@ -35,33 +34,24 @@ struct SignerPicker: View { var body: some View { let opts = options - Group { - if opts.count > 1 { - Section { - Picker("Sign with", selection: Binding( - get: { appState.selectedSignerLocator ?? opts.first?.id ?? "" }, - set: { new in Task { await appState.selectSigner(locator: new) } } - )) { - ForEach(opts) { opt in - Button { } label: { - Text(opt.typeLabel + (opt.isRecovery ? " (Recovery)" : "")) - Text(opt.id) - .font(.caption) - .foregroundStyle(.secondary) - } - .tag(opt.id) + if !opts.isEmpty { + Section { + Picker("Sign with", selection: Binding( + get: { appState.selectedSignerLocator ?? opts.first?.id ?? "" }, + set: { new in Task { await appState.selectSigner(locator: new) } } + )) { + ForEach(opts) { opt in + Button { } label: { + Text(opt.typeLabel + (opt.isRecovery ? " (Recovery)" : "")) + Text(opt.id) + .font(.caption) + .foregroundStyle(.secondary) } + .tag(opt.id) } } } } - .task(id: appState.wallet?.address) { - guard let wallet = appState.wallet else { return } - delegatedSigners = (try? await wallet.signers()) ?? [] - if appState.selectedSignerLocator == nil, let first = options.first { - await appState.selectSigner(locator: first.id) - } - } } private func isSelectable(_ locator: String) -> Bool { diff --git a/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignersView.swift b/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignersView.swift index 68914ad..58efd6f 100644 --- a/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignersView.swift +++ b/Examples/SmartWalletsDemo/SmartWalletsDemo/Playground/Signers/SignersView.swift @@ -11,37 +11,41 @@ struct SignersView: View { @Environment(\.dismiss) private var dismiss @State private var showAddSigner = false @State private var removingSignerLocator: String? - @State private var fetchedSigners: [WalletDelegatedSignerConfigApiModel] = [] @State private var isLoadingSigners = false - - private var isRemovingSigner: Bool { removingSignerLocator != nil } @State private var alertTitle = "" @State private var alertMessage = "" @State private var showAlert = false + private var isRemovingSigner: Bool { removingSignerLocator != nil } + + private var signerItems: [(locator: String, model: WalletDelegatedSignerConfigApiModel)] { + appState.delegatedSigners.compactMap { signer in + guard let locator = signer.locator ?? signer.signer else { return nil } + return (locator: locator, model: signer) + } + } + var body: some View { NavigationStack { List { recoverySection() Section("Signers") { - if fetchedSigners.isEmpty && !isLoadingSigners { + if signerItems.isEmpty && !isLoadingSigners { ContentUnavailableView( "No Signers", systemImage: "person.badge.key", description: Text("No delegated signers are registered on this wallet.") ) } else { - ForEach(fetchedSigners, id: \.locator) { signer in - if let locator = signer.locator { - SignerRow( - locator: locator, - isRemoving: removingSignerLocator == locator, - canRemove: true, - onSelect: {}, - onRemove: { Task { await removeSigner(signer) } } - ) - } + ForEach(signerItems, id: \.locator) { item in + SignerRow( + locator: item.locator, + isRemoving: removingSignerLocator == item.locator, + canRemove: true, + onSelect: {}, + onRemove: { Task { await removeSigner(item.model, locator: item.locator) } } + ) } } } @@ -93,14 +97,14 @@ struct SignersView: View { } private func loadSigners() async { - guard let wallet = appState.wallet else { return } + guard appState.wallet != nil else { return } isLoadingSigners = true - fetchedSigners = (try? await wallet.signers()) ?? [] + await appState.loadSigners() isLoadingSigners = false } - private func removeSigner(_ signer: WalletDelegatedSignerConfigApiModel) async { - guard let wallet = appState.wallet, let locator = signer.locator else { return } + private func removeSigner(_ signer: WalletDelegatedSignerConfigApiModel, locator: String) async { + guard let wallet = appState.wallet else { return } removingSignerLocator = locator do { _ = try await wallet.removeSigner(locator: locator)