diff --git a/apps/ios/Koins/AppModel.swift b/apps/ios/Koins/AppModel.swift index c4cb097..49078e5 100644 --- a/apps/ios/Koins/AppModel.swift +++ b/apps/ios/Koins/AppModel.swift @@ -41,6 +41,27 @@ final class AppModel: ObservableObject { var symbol: String { chain.symbol } + // MARK: - Cache + + /// Restores the last-known state for the chain on screen instantly; the + /// refresh that follows replaces it with live data. + private func showCached(_ forChain: Chain) { + guard let cached = WalletStore.cachedState(forChain) else { return } + balance = cached.balance + transactions = cached.transactions + if let accounts = cached.accounts { moneroAccounts = accounts } + } + + private func persistState(for forChain: Chain) { + guard (forChain == .xrp ? xrpWallet : xmrWallet) != nil else { return } + WalletStore.saveCachedState( + ChainCache(balance: balance, + transactions: transactions, + accounts: forChain == .xmr ? moneroAccounts : nil, + savedAt: Date()), + chain: forChain) + } + // MARK: - Bootstrap func bootstrap() async { @@ -57,6 +78,7 @@ final class AppModel: ObservableObject { if xrpWallet != nil || moneroReady { chain = xrpWallet != nil ? .xrp : .xmr } + showCached(chain) await refresh(force: true) startAutoRefresh() } @@ -101,7 +123,9 @@ final class AppModel: ObservableObject { statusMessage = nil balance = Balance(total: "0", unlocked: "", funded: false) transactions = [] + moneroAccounts = [] syncStatus = nil + showCached(newChain) if newChain == .xmr, let xmrWallet, !moneroOpened { await openMonero(afterRestore: false, wallet: xmrWallet) @@ -217,6 +241,7 @@ final class AppModel: ObservableObject { guard !isStale(token, requestChain) else { return } balance = fetchedBalance transactions = fetchedTransactions + persistState(for: requestChain) } catch { guard !isStale(token, requestChain) else { return } errorMessage = error.localizedDescription @@ -251,6 +276,7 @@ final class AppModel: ObservableObject { balance = fetchedBalance transactions = fetchedTransactions moneroAccounts = fetchedAccounts + persistState(for: requestChain) } } catch { guard !isStale(token, requestChain) else { return } diff --git a/apps/ios/Koins/Models.swift b/apps/ios/Koins/Models.swift index 05a6d49..46572a7 100644 --- a/apps/ios/Koins/Models.swift +++ b/apps/ios/Koins/Models.swift @@ -18,12 +18,12 @@ struct WalletRecord: Codable, Identifiable, Equatable { var createdAt: Date } -enum Direction { +enum Direction: String, Codable { case incoming case outgoing } -struct TransactionRecord: Identifiable { +struct TransactionRecord: Identifiable, Codable { let id: String let amount: String let timestamp: Date? @@ -44,12 +44,21 @@ struct TransactionRecord: Identifiable { } -struct Balance { +struct Balance: Codable { var total: String var unlocked: String var funded: Bool } +/// Persisted per chain so a switch or relaunch shows the last-known state +/// instantly while the network catches up. +struct ChainCache: Codable { + var balance: Balance + var transactions: [TransactionRecord] + var accounts: [MoneroAccount]? + var savedAt: Date +} + struct SyncStatus { var height: UInt64 var daemonHeight: UInt64 @@ -78,7 +87,7 @@ enum TransactionPriority: Int { } } -struct MoneroSubaddress: Identifiable { +struct MoneroSubaddress: Identifiable, Codable { let accountIndex: Int let index: Int let address: String @@ -90,7 +99,7 @@ struct MoneroSubaddress: Identifiable { var id: String { "\(accountIndex)/\(index)" } } -struct MoneroAccount: Identifiable { +struct MoneroAccount: Identifiable, Codable { let index: Int let address: String let balance: String diff --git a/apps/ios/Koins/Support/WalletStore.swift b/apps/ios/Koins/Support/WalletStore.swift index 07c6f8e..8898874 100644 --- a/apps/ios/Koins/Support/WalletStore.swift +++ b/apps/ios/Koins/Support/WalletStore.swift @@ -63,9 +63,24 @@ enum WalletStore { static func removeWallet(_ chain: Chain) { defaults.removeObject(forKey: walletKey(chain)) + defaults.removeObject(forKey: cacheKey(chain)) Keychain.remove(secretKey(chain)) } + // MARK: - Cached state (balance, transactions, accounts) + + private static func cacheKey(_ chain: Chain) -> String { "koins.cache.\(chain.rawValue)" } + + static func cachedState(_ chain: Chain) -> ChainCache? { + guard let data = defaults.data(forKey: cacheKey(chain)) else { return nil } + return try? JSONDecoder().decode(ChainCache.self, from: data) + } + + static func saveCachedState(_ state: ChainCache, chain: Chain) { + guard let data = try? JSONEncoder().encode(state) else { return } + defaults.set(data, forKey: cacheKey(chain)) + } + static func secret(_ chain: Chain) -> String? { Keychain.get(secretKey(chain)) }