Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -100,23 +102,32 @@ final class AppState {
walletCache[chain] = w
notFoundChains.remove(chain)
await fetchBalance()
await loadSigners()
} catch {
walletErrorMessage = error.userMessage
}

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)
}
Expand Down Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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))
}
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) } }
)
}
}
}
Expand Down Expand Up @@ -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)
Expand Down
Loading