diff --git a/FRW.xcodeproj/project.pbxproj b/FRW.xcodeproj/project.pbxproj index 828014c5..d9817179 100644 --- a/FRW.xcodeproj/project.pbxproj +++ b/FRW.xcodeproj/project.pbxproj @@ -893,6 +893,8 @@ 15DFD3332CD4576A004B0DB8 /* CGPoint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DFD3272CD4576A004B0DB8 /* CGPoint+Extensions.swift */; }; 15DFD3342CD4576A004B0DB8 /* BlobLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DFD3262CD4576A004B0DB8 /* BlobLayer.swift */; }; 15DFD3352CD4576A004B0DB8 /* ResizableLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DFD32A2CD4576A004B0DB8 /* ResizableLayer.swift */; }; + 15DFD34C2CE197F9004B0DB8 /* AppExternalLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DFD34B2CE197F9004B0DB8 /* AppExternalLinks.swift */; }; + 15DFD34D2CE197F9004B0DB8 /* AppExternalLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DFD34B2CE197F9004B0DB8 /* AppExternalLinks.swift */; }; 15ECAE3928C4FCE600B79453 /* WalletConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15CC8A6B28C4F817001D2696 /* WalletConnectView.swift */; }; 15ECAE3A28C4FCE700B79453 /* WalletConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15CC8A6B28C4F817001D2696 /* WalletConnectView.swift */; }; 15ECAE3B28C4FCEB00B79453 /* WalletConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15CC8A6C28C4F836001D2696 /* WalletConnectViewModel.swift */; }; @@ -1138,6 +1140,8 @@ 4E9532392CD3E4B300AAECD1 /* CustomTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9532372CD3E4B300AAECD1 /* CustomTokenManager.swift */; }; 4E95323E2CD5501C00AAECD1 /* CustomTokenDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E95323D2CD5501C00AAECD1 /* CustomTokenDetailView.swift */; }; 4E95323F2CD5501C00AAECD1 /* CustomTokenDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E95323D2CD5501C00AAECD1 /* CustomTokenDetailView.swift */; }; + 4E9532412CDA18FB00AAECD1 /* AddTokenSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9532402CDA18FB00AAECD1 /* AddTokenSheetView.swift */; }; + 4E9532422CDA18FB00AAECD1 /* AddTokenSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9532402CDA18FB00AAECD1 /* AddTokenSheetView.swift */; }; 4E9621D52B984EF3006859AD /* CadenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9621D42B984EF3006859AD /* CadenceManager.swift */; }; 4E9621D62B984EF3006859AD /* CadenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9621D42B984EF3006859AD /* CadenceManager.swift */; }; 4E9621D82B9850CF006859AD /* FRWAPI+Cadence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9621D72B9850CF006859AD /* FRWAPI+Cadence.swift */; }; @@ -2314,6 +2318,7 @@ 15DFD3282CD4576A004B0DB8 /* FluidGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidGradient.swift; sourceTree = ""; }; 15DFD3292CD4576A004B0DB8 /* FluidGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidGradientView.swift; sourceTree = ""; }; 15DFD32A2CD4576A004B0DB8 /* ResizableLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizableLayer.swift; sourceTree = ""; }; + 15DFD34B2CE197F9004B0DB8 /* AppExternalLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExternalLinks.swift; sourceTree = ""; }; 15EC9DF1274FD1FD00F70CD9 /* FRW_App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRW_App.swift; sourceTree = ""; }; 15EC9DF5274FD1FD00F70CD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 15EC9DF8274FD1FD00F70CD9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -2437,6 +2442,7 @@ 4E9532342CD2196400AAECD1 /* SectionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionItem.swift; sourceTree = ""; }; 4E9532372CD3E4B300AAECD1 /* CustomTokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTokenManager.swift; sourceTree = ""; }; 4E95323D2CD5501C00AAECD1 /* CustomTokenDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTokenDetailView.swift; sourceTree = ""; }; + 4E9532402CDA18FB00AAECD1 /* AddTokenSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTokenSheetView.swift; sourceTree = ""; }; 4E9621D42B984EF3006859AD /* CadenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CadenceManager.swift; sourceTree = ""; }; 4E9621D72B9850CF006859AD /* FRWAPI+Cadence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FRWAPI+Cadence.swift"; sourceTree = ""; }; 4E9621DA2B985BAB006859AD /* cloudfunctions.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = cloudfunctions.json; sourceTree = ""; }; @@ -4951,6 +4957,7 @@ 4E9532312CD2089C00AAECD1 /* AddCustomTokenViewModel.swift */, 4E95323D2CD5501C00AAECD1 /* CustomTokenDetailView.swift */, 4E9532372CD3E4B300AAECD1 /* CustomTokenManager.swift */, + 4E9532402CDA18FB00AAECD1 /* AddTokenSheetView.swift */, ); path = CustomToken; sourceTree = ""; @@ -5802,6 +5809,7 @@ 6AAD4E6828BC6E9900AEAB1F /* TransactionModels.swift */, 6AC476E728F3C993008503E6 /* WebBookmark.swift */, 6A2C560A290BE46800306A6C /* Currency.swift */, + 15DFD34B2CE197F9004B0DB8 /* AppExternalLinks.swift */, ); path = Model; sourceTree = ""; @@ -6726,6 +6734,7 @@ 6A164F742845F1CB0026B31E /* EnvironmentValues+IndexBarInsets.swift in Sources */, 15DC20CD27819C56000B187A /* VNavigationLink.swift in Sources */, 15ADAE2B28F51EBB0014B722 /* SymmetricEncryption.swift in Sources */, + 15DFD34C2CE197F9004B0DB8 /* AppExternalLinks.swift in Sources */, 4E31380B2C658FB2003A73E5 /* CappedCollection.swift in Sources */, 15DFD3312CD4576A004B0DB8 /* FluidGradientView.swift in Sources */, 15DFD3322CD4576A004B0DB8 /* FluidGradient.swift in Sources */, @@ -7050,6 +7059,7 @@ 154866F428D0FE8900D012B8 /* ViewCondition.swift in Sources */, 6A46754B28F2D50500F705A8 /* DBManager.swift in Sources */, 4EFBE5DE2BCE61FF0012968A /* WalletConnectEVMHandler.swift in Sources */, + 4E9532422CDA18FB00AAECD1 /* AddTokenSheetView.swift in Sources */, 15DC20DB27819C56000B187A /* VPrimaryButtonState.swift in Sources */, 4E5646622C06073900890E61 /* WalletAccount.swift in Sources */, 6A5D99AF2A4D5E7400C43D36 /* DAppsListViewModel.swift in Sources */, @@ -7521,6 +7531,7 @@ 15C58AB82868A4EE00BD4FC6 /* EnvironmentValues+IndexBarInsets.swift in Sources */, 15C58AB92868A4EE00BD4FC6 /* VNavigationLink.swift in Sources */, 15ADAE2A28F51EBB0014B722 /* SymmetricEncryption.swift in Sources */, + 15DFD34D2CE197F9004B0DB8 /* AppExternalLinks.swift in Sources */, 4E31380A2C658FB2003A73E5 /* CappedCollection.swift in Sources */, 15DFD32C2CD4576A004B0DB8 /* FluidGradientView.swift in Sources */, 15DFD32D2CD4576A004B0DB8 /* FluidGradient.swift in Sources */, @@ -7845,6 +7856,7 @@ 6A46754A28F2D50500F705A8 /* DBManager.swift in Sources */, 1565FBB728B173960086A652 /* ExploreTabScreen.swift in Sources */, 4EFBE5DD2BCE61FF0012968A /* WalletConnectEVMHandler.swift in Sources */, + 4E9532412CDA18FB00AAECD1 /* AddTokenSheetView.swift in Sources */, 6A5D99AE2A4D5E7400C43D36 /* DAppsListViewModel.swift in Sources */, 4E5646612C06073900890E61 /* WalletAccount.swift in Sources */, 15C58B572868A4EE00BD4FC6 /* ThemeManager.swift in Sources */, diff --git a/FRW/App/AppDelegate.swift b/FRW/App/AppDelegate.swift index 683156fd..7f2877dd 100644 --- a/FRW/App/AppDelegate.swift +++ b/FRW/App/AppDelegate.swift @@ -103,28 +103,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_: UIApplication, continue userActivity: NSUserActivity, restorationHandler _: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { if let url = userActivity.webpageURL { - if url.absoluteString.hasPrefix("https://fcw-link.lilico.app") { - var uri = url.absoluteString.deletingPrefix("https://fcw-link.lilico.app/wc?uri=") - uri = uri.deletingPrefix("fcw://") - WalletConnectManager.shared.onClientConnected = { - WalletConnectManager.shared.connect(link: uri) - } - WalletConnectManager.shared.connect(link: uri) - } else if url.absoluteString.hasPrefix("https://frw-link.lilico.app") { - var uri = url.absoluteString.deletingPrefix("https://frw-link.lilico.app/wc?uri=") - uri = uri.deletingPrefix("frw://") - WalletConnectManager.shared.onClientConnected = { - WalletConnectManager.shared.connect(link: uri) - } - WalletConnectManager.shared.connect(link: uri) - } else { - var uri = url.absoluteString.deletingPrefix("https://link.lilico.app/wc?uri=") - uri = uri.deletingPrefix("lilico://") - WalletConnectManager.shared.onClientConnected = { - WalletConnectManager.shared.connect(link: uri) - } + let uri = AppExternalLinks.exactWCLink(link: url.absoluteString) + WalletConnectManager.shared.onClientConnected = { WalletConnectManager.shared.connect(link: uri) } + WalletConnectManager.shared.connect(link: uri) } return true } diff --git a/FRW/App/Env/Prod/InstabugConfig.swift b/FRW/App/Env/Prod/InstabugConfig.swift index aabec84a..10cc662b 100644 --- a/FRW/App/Env/Prod/InstabugConfig.swift +++ b/FRW/App/Env/Prod/InstabugConfig.swift @@ -1,5 +1,5 @@ // -// Instabug.swift +// InstabugConfig.swift // FRW // // Created by cat on 2024/1/12. diff --git a/FRW/App/Env/ServiceConfig.swift b/FRW/App/Env/ServiceConfig.swift index b9fab8c5..1dd735e3 100644 --- a/FRW/App/Env/ServiceConfig.swift +++ b/FRW/App/Env/ServiceConfig.swift @@ -8,21 +8,30 @@ import Foundation import Instabug +// MARK: - ServiceConfig + class ServiceConfig { - static let shared = ServiceConfig() - private let dict: [String: String] + // MARK: Lifecycle init() { guard let filePath = Bundle.main.path(forResource: "ServiceConfig", ofType: "plist") else { fatalError("fatalError ===> Can't find ServiceConfig.plist") } - dict = NSDictionary(contentsOfFile: filePath) as? [String: String] ?? [:] + self.dict = NSDictionary(contentsOfFile: filePath) as? [String: String] ?? [:] } + // MARK: Internal + + static let shared = ServiceConfig() + static func configure() { ServiceConfig.shared.setupInstabug() ServiceConfig.shared.setupMixPanel() } + + // MARK: Private + + private let dict: [String: String] } // MARK: instabug config @@ -35,7 +44,7 @@ extension ServiceConfig { InstabugConfig.start(token: token) } - + private func setupMixPanel() { guard let token = dict["MixPanelToken"] else { fatalError("fatalError ===> Can't find MixPanel Token at ServiceConfig.plist") diff --git a/FRW/Foundation/Define/AppPlaceholder.swift b/FRW/Foundation/Define/AppPlaceholder.swift index 2de4ae85..d8f2af0f 100644 --- a/FRW/Foundation/Define/AppPlaceholder.swift +++ b/FRW/Foundation/Define/AppPlaceholder.swift @@ -7,7 +7,7 @@ import Foundation -struct AppPlaceholder { +enum AppPlaceholder { static var image: String = "https://lilico.app/placeholder-2.0.png" static var imageURL = URL(string: AppPlaceholder.image)! } diff --git a/FRW/Foundation/Model/AddressBookInfoModel.swift b/FRW/Foundation/Model/AddressBookInfoModel.swift index bc657393..4b0b77a9 100644 --- a/FRW/Foundation/Model/AddressBookInfoModel.swift +++ b/FRW/Foundation/Model/AddressBookInfoModel.swift @@ -18,6 +18,8 @@ extension Contact { case flowns = 2 case meow = 3 + // MARK: Internal + var domain: String { switch self { case .unknown: @@ -38,10 +40,10 @@ extension Contact { } } -// MARK: - AddressBook +// MARK: - Contact struct Contact: Codable, Identifiable { - enum WalletType: String,Codable { + enum WalletType: String, Codable { case flow case evm case link @@ -56,7 +58,7 @@ struct Contact: Codable, Identifiable { var walletType: WalletType? = .flow var needShowLocalAvatar: Bool { - return contactType == .domain + contactType == .domain } var localAvatar: String? { @@ -99,6 +101,6 @@ struct Contact: Codable, Identifiable { } var uniqueId: String { - return "\(address ?? "")-\(domain?.domainType?.rawValue ?? 0)-\(name)-\(contactType?.rawValue ?? 0)" + "\(address ?? "")-\(domain?.domainType?.rawValue ?? 0)-\(name)-\(contactType?.rawValue ?? 0)" } } diff --git a/FRW/Foundation/Model/AppExternalLinks.swift b/FRW/Foundation/Model/AppExternalLinks.swift new file mode 100644 index 00000000..f7b72bff --- /dev/null +++ b/FRW/Foundation/Model/AppExternalLinks.swift @@ -0,0 +1,42 @@ +// +// AppExternalLinks.swift +// FRW +// +// Created by Hao Fu on 11/11/2024. +// + +import Foundation + +enum AppExternalLinks: String, CaseIterable { + case frw = "frw://" + case fcw = "fcw://" + case lilico = "lilico://" + case frwUL = "https://frw-link.lilico.app" + case fcwUL = "https://fcw-link.lilico.app" + + // MARK: Internal + + static var allLinks: [String] { + AppExternalLinks.allCases.map(\.rawValue) + } + + var isUniversalLink: Bool { + switch self { + case .frwUL, .fcwUL: + return true + default: + return false + } + } + + static func exactWCLink(link: String) -> String { + let newLink = link + .replacingOccurrences(of: "wc%2Fwc", with: "wc") + .replacingOccurrences(of: "wc/wc", with: "wc") + + return newLink + .deletingPrefixes(allLinks.map { link in "\(link)/wc?uri=" }) + .deletingPrefixes(allLinks.map { link in "\(link)wc?uri=" }) + .deletingPrefixes(allLinks) + } +} diff --git a/FRW/Foundation/Model/WalletModels.swift b/FRW/Foundation/Model/WalletModels.swift index ae7f7f37..cb09ddc7 100644 --- a/FRW/Foundation/Model/WalletModels.swift +++ b/FRW/Foundation/Model/WalletModels.swift @@ -1,21 +1,23 @@ // -// TokenModel.swift +// WalletModels.swift // Flow Wallet // // Created by Hao Fu on 30/4/2022. // +import BigInt import Flow import Foundation -import BigInt -// MARK: - Coin +// MARK: - QuoteMarket enum QuoteMarket: String { case binance case kraken case huobi + // MARK: Internal + var flowPricePair: String { switch self { case .kraken: @@ -35,10 +37,12 @@ enum QuoteMarket: String { } var iconName: String { - return rawValue + rawValue } } +// MARK: - ListedToken + enum ListedToken: String, CaseIterable { case flow case fusd @@ -46,6 +50,19 @@ enum ListedToken: String, CaseIterable { case usdc case other + // MARK: Lifecycle + + init?(rawValue: String) { + if let item = ListedToken.allCases + .first(where: { $0.rawValue.lowercased() == rawValue.lowercased() }) { + self = item + } else { + self = .other + } + } + + // MARK: Internal + enum PriceAction { case fixed(price: Decimal) case query(String) @@ -66,16 +83,10 @@ enum ListedToken: String, CaseIterable { return .fixed(price: 0.0) } } - - init?(rawValue: String) { - if let item = ListedToken.allCases.first(where: { $0.rawValue.lowercased() == rawValue.lowercased() }) { - self = item - } else { - self = .other - } - } } +// MARK: - TokenModel + struct TokenModel: Codable, Identifiable, Mockable { let name: String var address: FlowNetworkModel @@ -94,7 +105,7 @@ struct TokenModel: Codable, Identifiable, Mockable { } var isFlowCoin: Bool { - return symbol?.lowercased() ?? "" == ListedToken.flow.rawValue + symbol?.lowercased() ?? "" == ListedToken.flow.rawValue } var contractId: String { @@ -126,11 +137,39 @@ struct TokenModel: Codable, Identifiable, Mockable { } var id: String { - return symbol ?? "" + symbol ?? "" + } + + var isActivated: Bool { + if let symbol = symbol { + return WalletManager.shared.isTokenActivated(symbol: symbol) + } + + return false + } + + static func mock() -> TokenModel { + TokenModel( + name: "mockname", + address: FlowNetworkModel( + mainnet: nil, + testnet: nil, + crescendo: nil, + previewnet: nil + ), + contractName: "contractname", + storagePath: FlowTokenStoragePath(balance: "", vault: "", receiver: ""), + decimal: 999, + icon: nil, + symbol: randomString(), + website: nil, + evmAddress: nil, + flowIdentifier: nil + ) } func getAddress() -> String? { - return address.addressByNetwork(LocalUserDefaults.shared.flowNetwork.toFlowType()) + address.addressByNetwork(LocalUserDefaults.shared.flowNetwork.toFlowType()) } func getPricePair(market: QuoteMarket) -> String { @@ -143,27 +182,6 @@ struct TokenModel: Codable, Identifiable, Mockable { return market.flowPricePair // TODO: #six Need to confirm } } - - var isActivated: Bool { - if let symbol = symbol { - return WalletManager.shared.isTokenActivated(symbol: symbol) - } - - return false - } - - static func mock() -> TokenModel { - return TokenModel(name: "mockname", - address: FlowNetworkModel(mainnet: nil, testnet: nil, crescendo: nil, previewnet: nil), - contractName: "contractname", - storagePath: FlowTokenStoragePath(balance: "", vault: "", receiver: ""), - decimal: 999, - icon: nil, - symbol: randomString(), - website: nil, - evmAddress: nil, - flowIdentifier: nil) - } } extension TokenModel { @@ -182,6 +200,8 @@ extension TokenModel { } } +// MARK: - FlowNetworkModel + struct FlowNetworkModel: Codable { let mainnet: String? var testnet: String? @@ -202,12 +222,16 @@ struct FlowNetworkModel: Codable { } } +// MARK: - FlowTokenStoragePath + struct FlowTokenStoragePath: Codable { let balance: String let vault: String let receiver: String } +// MARK: - SingleTokenResponse + struct SingleTokenResponse: Codable { let name: String let network: String? @@ -221,6 +245,8 @@ struct SingleTokenResponse: Codable { } } +// MARK: - SingleToken + struct SingleToken: Codable { let chainId: Int let address: String @@ -237,17 +263,41 @@ struct SingleToken: Codable { func toTokenModel(network: LocalUserDefaults.FlowNetworkType) -> TokenModel { let logo = URL(string: logoURI ?? "") - let model = TokenModel(name: name, - address: FlowNetworkModel(mainnet: network == .mainnet ? address : nil, testnet: network == .testnet ? address : nil, crescendo: nil, previewnet: network == .previewnet ? address : nil), - contractName: contractName ?? "", storagePath: path ?? FlowTokenStoragePath(balance: "", vault: "", receiver: ""), decimal: decimals, icon: logo, symbol: symbol, website: extensions?.website, evmAddress: evmAddress, flowIdentifier: flowIdentifier) + let model = TokenModel( + name: name, + address: FlowNetworkModel( + mainnet: network == .mainnet ? address : nil, + testnet: network == .testnet ? address : nil, + crescendo: nil, + previewnet: network == .previewnet ? address : nil + ), + contractName: contractName ?? "", + storagePath: path ?? + FlowTokenStoragePath(balance: "", vault: "", receiver: ""), + decimal: decimals, + icon: logo, + symbol: symbol, + website: extensions?.website, + evmAddress: evmAddress, + flowIdentifier: flowIdentifier + ) return model } } +// MARK: - TokenExtension + struct TokenExtension: Codable { - let website: URL? - let twitter: URL? - let discord: URL? + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.website = try? container.decode(URL.self, forKey: .website) + self.twitter = try? container.decode(URL.self, forKey: .twitter) + self.discord = try? container.decode(URL.self, forKey: .discord) + } + + // MARK: Internal enum CodingKeys: String, CodingKey { case website @@ -255,10 +305,7 @@ struct TokenExtension: Codable { case discord } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - website = try? container.decode(URL.self, forKey: .website) - twitter = try? container.decode(URL.self, forKey: .twitter) - discord = try? container.decode(URL.self, forKey: .discord) - } + let website: URL? + let twitter: URL? + let discord: URL? } diff --git a/FRW/Modules/Backup/BackupPasswordView.swift b/FRW/Modules/Backup/BackupPasswordView.swift index d2a3b059..76789486 100644 --- a/FRW/Modules/Backup/BackupPasswordView.swift +++ b/FRW/Modules/Backup/BackupPasswordView.swift @@ -8,23 +8,30 @@ import SwiftUI import SwiftUIX -struct BackupPasswordView: RouteableView { - @StateObject var vm: BackupPasswordViewModel +// MARK: - BackupPasswordView - @State var isTick: Bool = false - @State var highlight: VTextFieldHighlight = .none - @State var confrimHighlight: VTextFieldHighlight = .none - @State var text: String = "" - @State var confrimText: String = "" +struct BackupPasswordView: RouteableView { + // MARK: Lifecycle - var title: String { - return "" + init(backupType: BackupManager.BackupType) { + _vm = StateObject(wrappedValue: BackupPasswordViewModel(backupType: backupType)) } - func backButtonAction() { - UIApplication.shared.endEditing() - Router.pop() - } + // MARK: Internal + + @StateObject + var vm: BackupPasswordViewModel + + @State + var isTick: Bool = false + @State + var highlight: VTextFieldHighlight = .none + @State + var confrimHighlight: VTextFieldHighlight = .none + @State + var text: String = "" + @State + var confrimText: String = "" var model: VTextFieldModel = { var model = TextFieldStyle.primary @@ -32,6 +39,10 @@ struct BackupPasswordView: RouteableView { return model }() + var title: String { + "" + } + var canGoNext: Bool { if confrimText.count < 8 || text.count < 8 { return false @@ -41,11 +52,7 @@ struct BackupPasswordView: RouteableView { } var buttonState: VPrimaryButtonState { - return canGoNext ? .enabled : .disabled - } - - init(backupType: BackupManager.BackupType) { - _vm = StateObject(wrappedValue: BackupPasswordViewModel(backupType: backupType)) + canGoNext ? .enabled : .disabled } var body: some View { @@ -72,49 +79,66 @@ struct BackupPasswordView: RouteableView { Spacer() VStack(spacing: 25) { - VTextField(model: model, - type: .secure, - highlight: highlight, - placeholder: "backup_password".localized, - footerTitle: "minimum_8_char".localized, - text: $text, - onChange: {}) - - VTextField(model: model, - type: .secure, - highlight: confrimHighlight, - placeholder: "confirm_password".localized, - footerTitle: "", - text: $confrimText, - onChange: {}, - onReturn: .returnAndCustom {}) + VTextField( + model: model, + type: .secure, + highlight: highlight, + placeholder: "backup_password".localized, + footerTitle: "minimum_8_char".localized, + text: $text, + onChange: {} + ) + + VTextField( + model: model, + type: .secure, + highlight: confrimHighlight, + placeholder: "confirm_password".localized, + footerTitle: "", + text: $confrimText, + onChange: {}, + onReturn: .returnAndCustom {} + ) }.padding(.bottom, 25) - VCheckBox(model: CheckBoxStyle.secondary, - isOn: $isTick) { - VText(type: .oneLine, - font: .footnote, - color: Color.LL.rebackground, - title: "can_not_recover_pwd_tips".localized) + VCheckBox( + model: CheckBoxStyle.secondary, + isOn: $isTick + ) { + VText( + type: .oneLine, + font: .footnote, + color: Color.LL.rebackground, + title: "can_not_recover_pwd_tips".localized + ) } .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) - VPrimaryButton(model: ButtonStyle.primary, - state: buttonState, - action: { - UIApplication.shared.endEditing() - vm.backupToCloudAction(password: confrimText) - }, - title: "secure_backup".localized) - .padding(.bottom, 20) + VPrimaryButton( + model: ButtonStyle.primary, + state: buttonState, + action: { + UIApplication.shared.endEditing() + vm.backupToCloudAction(password: confrimText) + }, + title: "secure_backup".localized + ) + .padding(.bottom, 20) } .padding(.horizontal, 28) .backgroundFill(Color.LL.background) .applyRouteable(self) } + + func backButtonAction() { + UIApplication.shared.endEditing() + Router.pop() + } } +// MARK: - BackupPasswordView_Previews + struct BackupPasswordView_Previews: PreviewProvider { static var previews: some View { BackupPasswordView(backupType: .googleDrive) diff --git a/FRW/Modules/Backup/ManualBackupView.swift b/FRW/Modules/Backup/ManualBackupView.swift index b45737ff..a493a386 100644 --- a/FRW/Modules/Backup/ManualBackupView.swift +++ b/FRW/Modules/Backup/ManualBackupView.swift @@ -7,17 +7,23 @@ import SwiftUI +// MARK: - EnumeratedForEach + struct EnumeratedForEach: View { - let data: [ItemType] - let content: (Int, ItemType) -> ContentView + // MARK: Lifecycle init(_ data: [ItemType], @ViewBuilder content: @escaping (Int, ItemType) -> ContentView) { self.data = data self.content = content } + // MARK: Internal + + let data: [ItemType] + let content: (Int, ItemType) -> ContentView + var body: some View { - ForEach(Array(self.data.enumerated()), id: \.offset) { idx, item in + ForEach(Array(data.enumerated()), id: \.offset) { idx, item in self.content(idx, item) } } @@ -35,13 +41,9 @@ extension ManualBackupView { } } -struct ManualBackupView: RouteableView { - @StateObject var viewModel = ManualBackupViewModel() - - var title: String { - return "" - } +// MARK: - ManualBackupView +struct ManualBackupView: RouteableView { struct BackupModel: Identifiable { let id = UUID() let position: Int @@ -49,19 +51,18 @@ struct ManualBackupView: RouteableView { let list: [String] } - @State var selectArray: [Int?] = [nil, nil, nil, nil] + @StateObject + var viewModel = ManualBackupViewModel() - var isAllPass: Bool { - if case let .render(dataSource) = viewModel.state { - return dataSource.map { $0.correct } == selectArray - } - return false - } + @State + var selectArray: [Int?] = [nil, nil, nil, nil] var model: VSegmentedPickerModel = { var model = VSegmentedPickerModel() - model.colors.background = .init(enabled: .LL.bgForIcon, - disabled: .LL.bgForIcon) + model.colors.background = .init( + enabled: .LL.bgForIcon, + disabled: .LL.bgForIcon + ) model.fonts.rows = .LL.body.weight(.semibold) model.layout.height = 64 @@ -72,21 +73,15 @@ struct ManualBackupView: RouteableView { return model }() - func getColor(selectIndex: Int?, - item: String, - list: [String], - currentListIndex _: Int, - correct: Int) -> Color - { - guard let selectIndex = selectIndex else { - return .LL.text - } + var title: String { + "" + } - guard let index = list.firstIndex(of: item), selectIndex == index else { - return .LL.text + var isAllPass: Bool { + if case let .render(dataSource) = viewModel.state { + return dataSource.map { $0.correct } == selectArray } - - return selectIndex == correct ? Color.LL.success : Color.LL.error + return false } var body: some View { @@ -123,30 +118,39 @@ struct ManualBackupView: RouteableView { } .font(.LL.body) - VSegmentedPicker(model: model, - selectedIndex: $selectArray[index], - data: element.list) { item in - VText(type: .oneLine, - font: model.fonts.rows, - color: getColor(selectIndex: selectArray[index], - item: item, - list: element.list, - currentListIndex: index, - correct: element.correct), - title: item) + VSegmentedPicker( + model: model, + selectedIndex: $selectArray[index], + data: element.list + ) { item in + VText( + type: .oneLine, + font: model.fonts.rows, + color: getColor( + selectIndex: selectArray[index], + item: item, + list: element.list, + currentListIndex: index, + correct: element.correct + ), + title: item + ) } } .padding(.bottom) } } - VPrimaryButton(model: ButtonStyle.primary, - state: isAllPass ? .enabled : .disabled, - action: { - viewModel.trigger(.backupSuccess) - }, title: "Next") - .padding(.top, 20) - .padding(.bottom) + VPrimaryButton( + model: ButtonStyle.primary, + state: isAllPass ? .enabled : .disabled, + action: { + viewModel.trigger(.backupSuccess) + }, + title: "Next" + ) + .padding(.top, 20) + .padding(.bottom) } } .padding(.horizontal, 28) @@ -156,8 +160,28 @@ struct ManualBackupView: RouteableView { } .applyRouteable(self) } + + func getColor( + selectIndex: Int?, + item: String, + list: [String], + currentListIndex _: Int, + correct: Int + ) -> Color { + guard let selectIndex = selectIndex else { + return .LL.text + } + + guard let index = list.firstIndex(of: item), selectIndex == index else { + return .LL.text + } + + return selectIndex == correct ? Color.LL.success : Color.LL.error + } } +// MARK: - ManualBackupView_Previews + struct ManualBackupView_Previews: PreviewProvider { static var previews: some View { ManualBackupView() diff --git a/FRW/Modules/Backup/RecoveryPhraseView.swift b/FRW/Modules/Backup/RecoveryPhraseView.swift index b5cbfe76..40d01be5 100644 --- a/FRW/Modules/Backup/RecoveryPhraseView.swift +++ b/FRW/Modules/Backup/RecoveryPhraseView.swift @@ -21,17 +21,24 @@ extension RecoveryPhraseView { } } +// MARK: - RecoveryPhraseView + struct RecoveryPhraseView: RouteableView { - @StateObject var viewModel = RecoveryPhraseViewModel() - @State var isBlur: Bool = true - private var isInBackupMode = false + // MARK: Lifecycle init(backupMode: Bool) { - isInBackupMode = backupMode + self.isInBackupMode = backupMode } + // MARK: Internal + + @StateObject + var viewModel = RecoveryPhraseViewModel() + @State + var isBlur: Bool = true + var title: String { - return "" + "" } var copyBtn: some View { @@ -50,7 +57,7 @@ struct RecoveryPhraseView: RouteableView { Text("recovery".localized) .bold() .foregroundColor(Color.LL.text) - + Text("phrase".localized) .bold() .foregroundColor(Color.LL.orange) @@ -142,42 +149,59 @@ struct RecoveryPhraseView: RouteableView { .padding(.top) .padding(.bottom) - VPrimaryButton(model: ButtonStyle.primary, - state: viewModel.state.icloudLoading ? .loading : .enabled, - action: { - viewModel.trigger(.icloudBackup) - }, title: "backup_to_icloud".localized) - .padding(.top, 20) - .visibility(isInBackupMode ? .gone : .visible) - - VPrimaryButton(model: ButtonStyle.primary, - action: { - viewModel.trigger(.googleBackup) - }, title: "backup_to_gd".localized) - .padding(.top, 8) - .visibility(isInBackupMode ? .gone : .visible) - - VPrimaryButton(model: ButtonStyle.border, - action: { - viewModel.trigger(.manualBackup) - }, title: "backup_manually".localized) - .padding(.top, 8) - .padding(.bottom, 20) - .visibility(isInBackupMode ? .gone : .visible) + VPrimaryButton( + model: ButtonStyle.primary, + state: viewModel.state.icloudLoading ? .loading : .enabled, + action: { + viewModel.trigger(.icloudBackup) + }, + title: "backup_to_icloud".localized + ) + .padding(.top, 20) + .visibility(isInBackupMode ? .gone : .visible) + + VPrimaryButton( + model: ButtonStyle.primary, + action: { + viewModel.trigger(.googleBackup) + }, + title: "backup_to_gd".localized + ) + .padding(.top, 8) + .visibility(isInBackupMode ? .gone : .visible) + + VPrimaryButton( + model: ButtonStyle.border, + action: { + viewModel.trigger(.manualBackup) + }, + title: "backup_manually".localized + ) + .padding(.top, 8) + .padding(.bottom, 20) + .visibility(isInBackupMode ? .gone : .visible) } } .padding(.horizontal, 28) .backgroundFill(Color.LL.background) .applyRouteable(self) } + + // MARK: Private + + private var isInBackupMode = false } +// MARK: - RecoveryPhraseView_Previews + struct RecoveryPhraseView_Previews: PreviewProvider { static var previews: some View { RecoveryPhraseView(backupMode: false) } } +// MARK: - WordListView + struct WordListView: View { struct WordItem: Identifiable { var id: Int diff --git a/FRW/Modules/Backup/View/PinStackView.swift b/FRW/Modules/Backup/View/PinStackView.swift index 5fa18435..fa94df54 100644 --- a/FRW/Modules/Backup/View/PinStackView.swift +++ b/FRW/Modules/Backup/View/PinStackView.swift @@ -1,5 +1,5 @@ // -// TestView.swift +// PinStackView.swift // Flow Wallet // // Created by Hao Fu on 6/1/22. @@ -9,6 +9,8 @@ import Introspect import SwiftUI import SwiftUIX +// MARK: - PinStackView + struct PinStackView: View { var maxDigits: Int var emptyColor: Color @@ -44,8 +46,10 @@ struct PinStackView: View { var body: some View { ZStack { - VBaseTextField(state: $focuse, - text: $pin) { + VBaseTextField( + state: $focuse, + text: $pin + ) { handler(pin, pin.count == maxDigits) } .onReceive(pin.publisher.collect()) { @@ -70,7 +74,7 @@ struct PinStackView: View { // } HStack(spacing: 24) { - ForEach(0 ..< maxDigits) { digit in + ForEach(0..() + // MARK: Lifecycle - let trustProvider = TrustWeb3Provider.flowConfig() + deinit { + observation = nil + } - private var commonColor: UIColor? = UIColor(named: "bg.silver") + // MARK: Public - private lazy var contentView: UIView = { - let view = UIView() - view.backgroundColor = commonColor - return view - }() + public var shouldHideActionBar: Bool = false - private lazy var bgMaskLayer: CAShapeLayer = { - let layer = CAShapeLayer() - return layer - }() + // MARK: Internal + + let trustProvider = TrustWeb3Provider.flowConfig() lazy var webView: WKWebView = { let view = WKWebView(frame: .zero, configuration: generateWebViewConfiguration()) @@ -44,9 +40,9 @@ class BrowserViewController: UIViewController { view.allowsLinkPreview = true view.layer.masksToBounds = true #if DEBUG - if #available(iOS 16.4, *) { - view.isInspectable = true - } + if #available(iOS 16.4, *) { + view.isInspectable = true + } #endif return view }() @@ -63,6 +59,46 @@ class BrowserViewController: UIViewController { return obj }() + override var preferredStatusBarStyle: UIStatusBarStyle { + .lightContent + } + + override func viewDidLoad() { + super.viewDidLoad() + setup() + setupObserver() + hero.isEnabled = true + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(true, animated: true) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + reloadBgPaths() + } + + // MARK: Private + + private var observation: NSKeyValueObservation? + private var actionBarIsHiddenFlag: Bool = false + private var cancelSets = Set() + + private var commonColor: UIColor? = UIColor(named: "bg.silver") + + private lazy var contentView: UIView = { + let view = UIView() + view.backgroundColor = commonColor + return view + }() + + private lazy var bgMaskLayer: CAShapeLayer = { + let layer = CAShapeLayer() + return layer + }() + private lazy var actionBarView: BrowserActionBarView = { let view = BrowserActionBarView() @@ -86,31 +122,6 @@ class BrowserViewController: UIViewController { return view }() - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - deinit { - observation = nil - } - - override func viewDidLoad() { - super.viewDidLoad() - setup() - setupObserver() - hero.isEnabled = true - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(true, animated: true) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - reloadBgPaths() - } - private func setup() { view.backgroundColor = commonColor @@ -146,11 +157,15 @@ class BrowserViewController: UIViewController { } private func setupObserver() { - observation = webView.observe(\.estimatedProgress, options: .new, changeHandler: { [weak self] _, _ in - DispatchQueue.main.async { - self?.reloadActionBarView() + observation = webView.observe( + \.estimatedProgress, + options: .new, + changeHandler: { [weak self] _, _ in + DispatchQueue.main.async { + self?.reloadActionBarView() + } } - }) + ) NotificationCenter.default.publisher(for: .networkChange) .receive(on: DispatchQueue.main) @@ -162,7 +177,11 @@ class BrowserViewController: UIViewController { private func reloadBgPaths() { bgMaskLayer.frame = contentView.bounds - let path = UIBezierPath(roundedRect: contentView.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 20.0, height: 20.0)) + let path = UIBezierPath( + roundedRect: contentView.bounds, + byRoundingCorners: [.topLeft, .topRight], + cornerRadii: CGSize(width: 20.0, height: 20.0) + ) bgMaskLayer.path = path.cgPath } } @@ -219,7 +238,8 @@ extension BrowserViewController { // } } - @objc private func onBackBtnClick() { + @objc + private func onBackBtnClick() { if webView.canGoBack { webView.goBack() return @@ -228,15 +248,18 @@ extension BrowserViewController { onHomeBtnClick() } - @objc private func onHomeBtnClick() { + @objc + private func onHomeBtnClick() { Router.pop() } - @objc private func onReloadBtnClick() { + @objc + private func onReloadBtnClick() { webView.reload() } - @objc private func onMoveAssets() { + @objc + private func onMoveAssets() { if MoveAssetsAction.shared.allowMoveAssets { let vc = PresentHostingController(rootView: MoveAssetsView()) navigationController?.present(vc, completion: nil) @@ -245,7 +268,8 @@ extension BrowserViewController { } } - @objc private func onAddressBarClick() { + @objc + private func onAddressBarClick() { showSearchInputView() } @@ -273,6 +297,25 @@ extension BrowserViewController { private func onClearCookie() { BrowserViewController.deleteCookie() } + + private func handleNavigationAction(navigationAction: WKNavigationAction) { + guard let url = navigationAction.request.url else { + return + } + + if !url.absoluteString.hasPrefixes(AppExternalLinks.allLinks) { + if navigationAction.targetFrame == nil { + UIApplication.shared.open(url) + } + return + } + + let uri = AppExternalLinks.exactWCLink(link: url.absoluteString) + WalletConnectManager.shared.onClientConnected = { + WalletConnectManager.shared.connect(link: uri) + } + WalletConnectManager.shared.connect(link: uri) + } } // MARK: - Search Recommend @@ -293,7 +336,7 @@ extension BrowserViewController { } } -// MARK: - Delegate +// MARK: WKNavigationDelegate extension BrowserViewController: WKNavigationDelegate { func webView(_: WKWebView, didStartProvisionalNavigation _: WKNavigation!) { @@ -319,48 +362,32 @@ extension BrowserViewController: WKNavigationDelegate { reloadActionBarView() } - func webView(_: WKWebView, decidePolicyFor _: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - decisionHandler(.allow) + func webView( + _: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction + ) async -> WKNavigationActionPolicy { + handleNavigationAction(navigationAction: navigationAction) reloadActionBarView() + return .allow } } -extension BrowserViewController: WKUIDelegate { - func webView(_: WKWebView, createWebViewWith _: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures) -> WKWebView? { - if navigationAction.targetFrame == nil, let url = navigationAction.request.url { - if url.absoluteString.hasPrefix("https://fcw-link.lilico.app") { - var uri = url.absoluteString.deletingPrefix("https://fcw-link.lilico.app/wc?uri=") - uri = uri.deletingPrefix("fcw://") - WalletConnectManager.shared.onClientConnected = { - WalletConnectManager.shared.connect(link: uri) - } - WalletConnectManager.shared.connect(link: uri) - } else if url.absoluteString.hasPrefix("https://frw-link.lilico.app") { - var uri = url.absoluteString.deletingPrefix("https://frw-link.lilico.app/wc?uri=") - uri = uri.deletingPrefix("frw://") - WalletConnectManager.shared.onClientConnected = { - WalletConnectManager.shared.connect(link: uri) - } - WalletConnectManager.shared.connect(link: uri) - } else if url.absoluteString.hasPrefix("https://link.lilico.app") { - var uri = url.absoluteString.deletingPrefix("https://link.lilico.app/wc?uri=") - uri = uri.deletingPrefix("lilico://") - WalletConnectManager.shared.onClientConnected = { - WalletConnectManager.shared.connect(link: uri) - } - WalletConnectManager.shared.connect(link: uri) - } else if url.description.lowercased().range(of: "http://") != nil || - url.description.lowercased().range(of: "https://") != nil || - url.description.lowercased().range(of: "mailto:") != nil - { - UIApplication.shared.openURL(url) - } - } +// MARK: WKUIDelegate +extension BrowserViewController: WKUIDelegate { + func webView( + _: WKWebView, + createWebViewWith _: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures _: WKWindowFeatures + ) -> WKWebView? { + handleNavigationAction(navigationAction: navigationAction) return nil } } +// MARK: UIScrollViewDelegate + extension BrowserViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview) @@ -376,17 +403,22 @@ extension BrowserViewController { static func deleteCookie() { HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) let dispatch_group = DispatchGroup() - WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in - records.forEach { record in - dispatch_group.enter() - WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: { - dispatch_group.leave() - }) - #if DEBUG + WKWebsiteDataStore.default() + .fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in + for record in records { + dispatch_group.enter() + WKWebsiteDataStore.default().removeData( + ofTypes: record.dataTypes, + for: [record], + completionHandler: { + dispatch_group.leave() + } + ) + #if DEBUG print("WKWebsiteDataStore record deleted:", record) - #endif + #endif + } + dispatch_group.notify(queue: DispatchQueue.main) {} } - dispatch_group.notify(queue: DispatchQueue.main) {} - } } } diff --git a/FRW/Modules/Browser/Handler/JSMessageHandler.swift b/FRW/Modules/Browser/Handler/JSMessageHandler.swift index 6403f245..84fa8213 100644 --- a/FRW/Modules/Browser/Handler/JSMessageHandler.swift +++ b/FRW/Modules/Browser/Handler/JSMessageHandler.swift @@ -10,25 +10,37 @@ import TrustWeb3Provider import UIKit import WebKit +// MARK: - JSMessageType + enum JSMessageType: String { case ready = "FCL:VIEW:READY" case response = "FCL:VIEW:READY:RESPONSE" } +// MARK: - JSMessageHandler + class JSMessageHandler: NSObject { + // MARK: Internal + + private(set) var processingAuthzTransaction: AuthzTransaction? + weak var webVC: BrowserViewController? + + // MARK: Private + private var processingMessage: String? private var processingServiceType: FCLServiceType? private var processingFCLResponse: FCLResponseProtocol? private var readyToSignEnvelope: Bool = false - private(set) var processingAuthzTransaction: AuthzTransaction? private weak var processingLinkAccountViewModel: ChildAccountLinkViewModel? - - weak var webVC: BrowserViewController? } +// MARK: WKScriptMessageHandler + extension JSMessageHandler: WKScriptMessageHandler { func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { + let url = message.frameInfo.request.url ?? webVC?.webView.url + log.debug("did receive message") if message.name == TrustWeb3Provider.scriptHandlerName { @@ -42,7 +54,7 @@ extension JSMessageHandler: WKScriptMessageHandler { return } - handleMessage(msgString) + handleMessage(msgString, url: url) case .flowTransaction: guard let msgString = message.body as? String else { log.error("JSListenerType.flowTransaction body invalid") @@ -60,7 +72,10 @@ extension JSMessageHandler { private func handleTransaction(_ message: String) { do { guard let msgData = message.data(using: .utf8), - let jsonDict = try JSONSerialization.jsonObject(with: msgData, options: .mutableContainers) as? [String: AnyObject], + let jsonDict = try JSONSerialization.jsonObject( + with: msgData, + options: .mutableContainers + ) as? [String: AnyObject], let tid = jsonDict["txId"] as? String else { log.error("invalid message") @@ -72,7 +87,8 @@ extension JSMessageHandler { return } - guard let processingAuthzTransaction = processingAuthzTransaction, let data = try? JSONEncoder().encode(processingAuthzTransaction) else { + guard let processingAuthzTransaction = processingAuthzTransaction, + let data = try? JSONEncoder().encode(processingAuthzTransaction) else { log.error("no processingAuthzTransaction") return } @@ -80,7 +96,11 @@ extension JSMessageHandler { log.debug("handle transaction", context: message) let id = Flow.ID(hex: tid) - let holder = TransactionManager.TransactionHolder(id: id, type: .fclTransaction, data: data) + let holder = TransactionManager.TransactionHolder( + id: id, + type: .fclTransaction, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) if let linkAccountVM = processingLinkAccountViewModel { @@ -93,7 +113,7 @@ extension JSMessageHandler { } extension JSMessageHandler { - private func handleMessage(_ message: String) { + private func handleMessage(_ message: String, url: URL?) { if message.isEmpty || processingMessage == message { return } @@ -108,14 +128,16 @@ extension JSMessageHandler { do { if let msgData = message.data(using: .utf8), - let jsonDict = try JSONSerialization.jsonObject(with: msgData, options: .mutableContainers) as? [String: AnyObject] - { + let jsonDict = try JSONSerialization.jsonObject( + with: msgData, + options: .mutableContainers + ) as? [String: AnyObject] { if messageIsServce(jsonDict) { log.debug("will handle service") handleService(message) } else if jsonDict["type"] as? String == JSMessageType.response.rawValue { log.debug("will handle view ready response") - handleViewReadyResponse(message) + handleViewReadyResponse(message, url: url) } else { log.warning("unknown message", context: message) } @@ -179,7 +201,7 @@ extension JSMessageHandler { // MARK: - Response extension JSMessageHandler { - private func handleViewReadyResponse(_ message: String) { + private func handleViewReadyResponse(_ message: String, url: URL?) { do { guard let data = message.data(using: .utf8) else { log.error("decode message failed") @@ -190,10 +212,15 @@ extension JSMessageHandler { if !fcl.networkIsMatch { let current = LocalUserDefaults.shared.flowNetwork - log.warning("network mismatch, current: \(current), prefer: \(fcl.network ?? "unknown")") + log + .warning( + "network mismatch, current: \(current), prefer: \(fcl.network ?? "unknown")" + ) finishService() - if let network = fcl.network, let toNetwork = LocalUserDefaults.FlowNetworkType(rawValue: network.lowercased()) { + if let network = fcl.network, + let toNetwork = LocalUserDefaults + .FlowNetworkType(rawValue: network.lowercased()) { Router.route(to: RouteMap.Explore.switchNetwork(current, toNetwork, nil)) } @@ -201,7 +228,10 @@ extension JSMessageHandler { } if processingServiceType != fcl.serviceType { - log.error("service not same (old: \(String(describing: processingServiceType)), new: \(fcl.serviceType))") + log + .error( + "service not same (old: \(String(describing: processingServiceType)), new: \(fcl.serviceType))" + ) return } @@ -210,13 +240,13 @@ extension JSMessageHandler { switch fcl.serviceType { case .authn: log.debug("will handle authn") - handleAuthn(message) + handleAuthn(message, url: url) case .authz: log.debug("will handle authz") - handleAuthz(message) + handleAuthz(message, url: url) case .userSignature: log.debug("will handle user signature") - handleUserSignature(message) + handleUserSignature(message, url: url) default: log.error("unsupport service type", context: fcl.serviceType) } @@ -225,7 +255,7 @@ extension JSMessageHandler { } } - private func handleAuthn(_ message: String) { + private func handleAuthn(_ message: String, url: URL?) { do { guard let data = message.data(using: .utf8) else { log.error("decode message failed") @@ -246,15 +276,18 @@ extension JSMessageHandler { let title = authnResponse.config?.app?.title ?? webVC?.webView.title ?? "unknown" let network = authnResponse.config?.client?.network ?? "" let chainID = Flow.ChainID(name: network) - let vm = BrowserAuthnViewModel(title: title, - url: webVC?.webView.url?.host ?? "unknown", - logo: authnResponse.config?.app?.icon, - walletAddress: WalletManager.shared.getPrimaryWalletAddress(), - network: chainID) { [weak self] result in + let vm = BrowserAuthnViewModel( + title: title, + url: url?.host ?? "unknown", + logo: authnResponse.config?.app?.icon, + walletAddress: WalletManager.shared + .getPrimaryWalletAddress(), + network: chainID + ) { [weak self] result in guard let self = self else { return } - + if result { self.didConfirmAuthn(response: authnResponse) } else { @@ -282,7 +315,7 @@ extension JSMessageHandler { } } - private func handleAuthz(_ message: String) { + private func handleAuthz(_ message: String, url: URL?) { do { guard let data = message.data(using: .utf8) else { log.error("decode message failed") @@ -301,13 +334,13 @@ extension JSMessageHandler { if readyToSignEnvelope, authzResponse.isSignEnvelope { log.debug("will sign envelope") - signEnvelope(authzResponse) + signEnvelope(authzResponse, url: url) return } if authzResponse.isLinkAccount { log.debug("will link account") - linkAccount(authzResponse) + linkAccount(authzResponse, url: url) return } @@ -317,13 +350,13 @@ extension JSMessageHandler { if authzResponse.isSignAuthz { log.debug("will sign authz") - signAuthz(authzResponse) + signAuthz(authzResponse, url: url) return } if authzResponse.isSignPayload { log.debug("will sign payload") - signPayload(authzResponse) + signPayload(authzResponse, url: url) return } @@ -333,7 +366,7 @@ extension JSMessageHandler { } } - private func handleUserSignature(_ message: String) { + private func handleUserSignature(_ message: String, url: URL?) { do { guard let data = message.data(using: .utf8) else { log.error("decode message failed") @@ -351,8 +384,13 @@ extension JSMessageHandler { log.debug("handle user signature, uid: \(response.uniqueId())") let title = response.config?.app?.title ?? webVC?.webView.title ?? "unknown" - let url = webVC?.webView.url?.host ?? "unknown" - let vm = BrowserSignMessageViewModel(title: title, url: url, logo: response.config?.app?.icon, cadence: response.body?.message ?? "") { [weak self] result in + let url = url?.host ?? "unknown" + let vm = BrowserSignMessageViewModel( + title: title, + url: url, + logo: response.config?.app?.icon, + cadence: response.body?.message ?? "" + ) { [weak self] result in guard let self = self else { return } @@ -372,19 +410,27 @@ extension JSMessageHandler { } extension JSMessageHandler { - private func signAuthz(_ authzResponse: FCLAuthzResponse) { + private func signAuthz(_ authzResponse: FCLAuthzResponse, url: URL?) { let title = authzResponse.config?.app?.title ?? webVC?.webView.title ?? "unknown" - let url = webVC?.webView.url?.host ?? "unknown" - let vm = BrowserAuthzViewModel(title: title, url: url, logo: authzResponse.config?.app?.icon, - cadence: authzResponse.body.cadence, - arguments: authzResponse.body.voucher.arguments) { [weak self] result in + let urlHost = url?.host ?? "unknown" + let vm = BrowserAuthzViewModel( + title: title, + url: urlHost, + logo: authzResponse.config?.app?.icon, + cadence: authzResponse.body.cadence, + arguments: authzResponse.body.voucher.arguments + ) { [weak self] result in guard let self = self else { return } DispatchQueue.main.async { if result { - self.processingAuthzTransaction = AuthzTransaction(url: self.webVC?.webView.url?.absoluteString, title: self.webVC?.webView.title, voucher: authzResponse.body.voucher) + self.processingAuthzTransaction = AuthzTransaction( + url: url?.absoluteString, + title: self.webVC?.webView.title, + voucher: authzResponse.body.voucher + ) self.didConfirmSignPayload(authzResponse) } } @@ -394,12 +440,16 @@ extension JSMessageHandler { Router.route(to: RouteMap.Explore.authz(vm)) } - private func linkAccount(_ authzResponse: FCLAuthzResponse) { + private func linkAccount(_ authzResponse: FCLAuthzResponse, url: URL?) { let title = authzResponse.config?.app?.title ?? webVC?.webView.title ?? "unknown" - let url = webVC?.webView.url?.host ?? "unknown" + let url = url?.host ?? "unknown" let logo = authzResponse.config?.app?.icon ?? "" - let vm = ChildAccountLinkViewModel(fromTitle: title, url: url, logo: logo) { [weak self] result in + let vm = ChildAccountLinkViewModel( + fromTitle: title, + url: url, + logo: logo + ) { [weak self] result in guard let self = self else { return } @@ -416,12 +466,16 @@ extension JSMessageHandler { Router.route(to: RouteMap.Explore.linkChildAccount(vm)) } - private func signPayload(_ authzResponse: FCLAuthzResponse) { + private func signPayload(_ authzResponse: FCLAuthzResponse, url: URL?) { let title = authzResponse.config?.app?.title ?? webVC?.webView.title ?? "unknown" - let url = webVC?.webView.url?.host ?? "unknown" - let vm = BrowserAuthzViewModel(title: title, url: url, logo: authzResponse.config?.app?.icon, - cadence: authzResponse.body.cadence, - arguments: authzResponse.body.voucher.arguments) { [weak self] result in + let url = url?.host ?? "unknown" + let vm = BrowserAuthzViewModel( + title: title, + url: url, + logo: authzResponse.config?.app?.icon, + cadence: authzResponse.body.cadence, + arguments: authzResponse.body.voucher.arguments + ) { [weak self] result in guard let self = self else { return } @@ -449,19 +503,26 @@ extension JSMessageHandler { } } - private func signEnvelope(_ authzResponse: FCLAuthzResponse) { - let url = webVC?.webView.url?.absoluteString + private func signEnvelope(_ authzResponse: FCLAuthzResponse, url: URL?) { let title = webVC?.webView.title Task { - let request = SignPayerRequest(transaction: authzResponse.body.voucher.toFCLVoucher(), message: .init(envelopeMessage: authzResponse.body.message)) - let signature: SignPayerResponse = try await Network.requestWithRawModel(FirebaseAPI.signAsPayer(request)) + let request = SignPayerRequest( + transaction: authzResponse.body.voucher.toFCLVoucher(), + message: .init(envelopeMessage: authzResponse.body.message) + ) + let signature: SignPayerResponse = try await Network + .requestWithRawModel(FirebaseAPI.signAsPayer(request)) let sign = signature.envelopeSigs DispatchQueue.main.async { self.webVC?.postAuthzEnvelopeSignResponse(sign: sign) - let authzTransaction = AuthzTransaction(url: url, title: title, voucher: authzResponse.body.voucher) + let authzTransaction = AuthzTransaction( + url: url?.absoluteString, + title: title, + voucher: authzResponse.body.voucher + ) self.processingAuthzTransaction = authzTransaction self.readyToSignEnvelope = false diff --git a/FRW/Modules/Browser/Input/BrowserSearchInputViewController.swift b/FRW/Modules/Browser/Input/BrowserSearchInputViewController.swift index 53d4e99b..a6bf7795 100644 --- a/FRW/Modules/Browser/Input/BrowserSearchInputViewController.swift +++ b/FRW/Modules/Browser/Input/BrowserSearchInputViewController.swift @@ -11,14 +11,51 @@ import UIKit private let RecommendCellHeight: CGFloat = 50 private let DAppCellHeight: CGFloat = 60 +// MARK: - Section + private enum Section: Int { case dapp case recommend } +// MARK: - BrowserSearchInputViewController + class BrowserSearchInputViewController: UIViewController { + // MARK: Public + + public func setSearchText(text: String? = "") { + searchingText = text ?? "" + inputBar.textField.text = text + inputBar.reloadView() + if let str = text, !str.isEmpty { + inputBar.textField.becomeFirstResponder() + inputBar.textField.selectAll(self) + } + } + + // MARK: Internal + var selectTextCallback: ((String) -> Void)? + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + reloadBgPaths() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if !inputBar.textField.isFirstResponder { + inputBar.textField.becomeFirstResponder() + } + } + + // MARK: Private + private var recommendArray: [RecommendItemModel] = [] private var remoteDAppList: [DAppModel] = [] private var dappArray: [DAppModel] = [] @@ -61,8 +98,14 @@ class BrowserSearchInputViewController: UIViewController { view.delegate = self view.dataSource = self - view.register(BrowserSearchItemCell.self, forCellWithReuseIdentifier: "BrowserSearchItemCell") - view.register(BrowserSearchDAppItemCell.self, forCellWithReuseIdentifier: "BrowserSearchDAppItemCell") + view.register( + BrowserSearchItemCell.self, + forCellWithReuseIdentifier: "BrowserSearchItemCell" + ) + view.register( + BrowserSearchDAppItemCell.self, + forCellWithReuseIdentifier: "BrowserSearchDAppItemCell" + ) return view }() @@ -71,11 +114,6 @@ class BrowserSearchInputViewController: UIViewController { return layer }() - override func viewDidLoad() { - super.viewDidLoad() - setup() - } - private func setup() { view.backgroundColor = .black @@ -100,42 +138,25 @@ class BrowserSearchInputViewController: UIViewController { make.bottom.equalTo(inputBar.snp.top) } - collectionView.transform = CGAffineTransform(rotationAngle: -(CGFloat)(Double.pi)) + collectionView.transform = CGAffineTransform(rotationAngle: -CGFloat(Double.pi)) hero.isEnabled = true } - public func setSearchText(text: String? = "") { - searchingText = text ?? "" - inputBar.textField.text = text - inputBar.reloadView() - if let str = text, !str.isEmpty { - inputBar.textField.becomeFirstResponder() - inputBar.textField.selectAll(self) - } - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - reloadBgPaths() - } - private func reloadBgPaths() { contentViewBgMaskLayer.frame = contentView.bounds - let cPath = UIBezierPath(roundedRect: contentView.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 24.0, height: 24.0)) + let cPath = UIBezierPath( + roundedRect: contentView.bounds, + byRoundingCorners: [.topLeft, .topRight], + cornerRadii: CGSize(width: 24.0, height: 24.0) + ) contentViewBgMaskLayer.path = cPath.cgPath } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if !inputBar.textField.isFirstResponder { - inputBar.textField.becomeFirstResponder() - } - } } extension BrowserSearchInputViewController { - @objc private func onCancelBtnClick() { + @objc + private func onCancelBtnClick() { close() } @@ -186,7 +207,8 @@ extension BrowserSearchInputViewController { let engine = "https://www.google.com/search?q=" - urlString = urlString.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! + urlString = urlString + .addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! urlString = "\(engine)\(urlString)" return urlString @@ -197,7 +219,8 @@ extension BrowserSearchInputViewController { Task { do { - let result: [RecommendItemModel] = try await Network.requestWithRawModel(FRWAPI.Browser.recommend(currentText)) + let result: [RecommendItemModel] = try await Network + .requestWithRawModel(FRWAPI.Browser.recommend(currentText)) if self.searchingText != currentText { // outdate result @@ -235,7 +258,12 @@ extension BrowserSearchInputViewController { return } - var result = list.filter { $0.name.lowercased().contains(currentText.lowercased()) || $0.url.absoluteString.lowercased().contains(currentText.lowercased()) } + var result = list + .filter { + $0.name.lowercased().contains(currentText.lowercased()) || $0.url + .absoluteString + .lowercased().contains(currentText.lowercased()) + } if result.count > 5 { // max 5 @@ -258,7 +286,13 @@ extension BrowserSearchInputViewController { private func startTimer() { stopTimer() - let t = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(onTimer), userInfo: nil, repeats: false) + let t = Timer.scheduledTimer( + timeInterval: 0.2, + target: self, + selector: #selector(onTimer), + userInfo: nil, + repeats: false + ) RunLoop.main.add(t, forMode: .common) timer = t } @@ -270,7 +304,8 @@ extension BrowserSearchInputViewController { } } - @objc private func onTimer() { + @objc + private func onTimer() { doSearch() doSearchDApp() } @@ -282,9 +317,12 @@ extension BrowserSearchInputViewController { } } -extension BrowserSearchInputViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { +// MARK: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource + +extension BrowserSearchInputViewController: UICollectionViewDelegateFlowLayout, + UICollectionViewDataSource { func numberOfSections(in _: UICollectionView) -> Int { - return 2 + 2 } func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int { @@ -298,17 +336,26 @@ extension BrowserSearchInputViewController: UICollectionViewDelegateFlowLayout, } } - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { switch Section(rawValue: indexPath.section) { case .recommend: let model = recommendArray[indexPath.item] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BrowserSearchItemCell", for: indexPath) as! BrowserSearchItemCell + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: "BrowserSearchItemCell", + for: indexPath + ) as! BrowserSearchItemCell cell.config(model, inputText: searchingText) cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi) return cell case .dapp: let model = dappArray[indexPath.item] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BrowserSearchDAppItemCell", for: indexPath) as! BrowserSearchDAppItemCell + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: "BrowserSearchDAppItemCell", + for: indexPath + ) as! BrowserSearchDAppItemCell cell.config(model) cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi) return cell @@ -330,12 +377,22 @@ extension BrowserSearchInputViewController: UICollectionViewDelegateFlowLayout, } } - func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { switch Section(rawValue: indexPath.section) { case .recommend: - return CGSize(width: Router.coordinator.window.bounds.size.width, height: RecommendCellHeight) + return CGSize( + width: Router.coordinator.window.bounds.size.width, + height: RecommendCellHeight + ) case .dapp: - return CGSize(width: Router.coordinator.window.bounds.size.width, height: DAppCellHeight) + return CGSize( + width: Router.coordinator.window.bounds.size.width, + height: DAppCellHeight + ) default: return .zero } diff --git a/FRW/Modules/Browser/View/BrowserAuthnViewModel.swift b/FRW/Modules/Browser/View/BrowserAuthnViewModel.swift index 2993ce2e..249fa8e9 100644 --- a/FRW/Modules/Browser/View/BrowserAuthnViewModel.swift +++ b/FRW/Modules/Browser/View/BrowserAuthnViewModel.swift @@ -8,44 +8,60 @@ import Flow import SwiftUI +// MARK: - BrowserAuthnViewModel.Callback + extension BrowserAuthnViewModel { typealias Callback = (Bool) -> Void } +// MARK: - BrowserAuthnViewModel + class BrowserAuthnViewModel: ObservableObject { - @Published var title: String - @Published var urlString: String - @Published var walletAddress: String? - @Published var logo: String? - @Published var network: Flow.ChainID? - private var callback: BrowserAuthnViewModel.Callback? + // MARK: Lifecycle - init(title: String, - url: String, - logo: String?, - walletAddress: String?, - network: Flow.ChainID? = nil, - callback: @escaping BrowserAuthnViewModel.Callback) - { + init( + title: String, + url: String, + logo: String?, + walletAddress: String?, + network: Flow.ChainID? = nil, + callback: @escaping BrowserAuthnViewModel.Callback + ) { self.title = title - urlString = url + self.urlString = url self.logo = logo self.network = network self.walletAddress = walletAddress self.callback = callback } + deinit { + callback?(false) + WalletConnectManager.shared.reloadPendingRequests() + } + + // MARK: Internal + + @Published + var title: String + @Published + var urlString: String + @Published + var walletAddress: String? + @Published + var logo: String? + @Published + var network: Flow.ChainID? + func didChooseAction(_ result: Bool) { Router.dismiss { [weak self] in guard let self else { return } callback?(result) callback = nil } - } - deinit { - callback?(false) - WalletConnectManager.shared.reloadPendingRequests() - } + // MARK: Private + + private var callback: BrowserAuthnViewModel.Callback? } diff --git a/FRW/Modules/Browser/View/BrowserSignMessageView.swift b/FRW/Modules/Browser/View/BrowserSignMessageView.swift index 2839a975..50ffbabc 100644 --- a/FRW/Modules/Browser/View/BrowserSignMessageView.swift +++ b/FRW/Modules/Browser/View/BrowserSignMessageView.swift @@ -8,14 +8,21 @@ import Kingfisher import SwiftUI +// MARK: - BrowserSignMessageView + struct BrowserSignMessageView: View { - @StateObject var vm: BrowserSignMessageViewModel + // MARK: Lifecycle init(vm: BrowserSignMessageViewModel) { _vm = StateObject(wrappedValue: vm) UITextView.appearance().backgroundColor = UIColor(hex: "#313131") } + // MARK: Internal + + @StateObject + var vm: BrowserSignMessageViewModel + var body: some View { normalView .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -121,11 +128,15 @@ struct BrowserSignMessageView: View { } } +// MARK: - BrowserSignMessageView_Previews + struct BrowserSignMessageView_Previews: PreviewProvider { - static let vm = BrowserSignMessageViewModel(title: "Test title", - url: "https://lilico.app", - logo: "", - cadence: "464f4f") { _ in + static let vm = BrowserSignMessageViewModel( + title: "Test title", + url: "https://lilico.app", + logo: "", + cadence: "464f4f" + ) { _ in } static var previews: some View { diff --git a/FRW/Modules/Browser/View/BrowserSignTypedMessageView.swift b/FRW/Modules/Browser/View/BrowserSignTypedMessageView.swift index 63df301f..059e025e 100644 --- a/FRW/Modules/Browser/View/BrowserSignTypedMessageView.swift +++ b/FRW/Modules/Browser/View/BrowserSignTypedMessageView.swift @@ -5,19 +5,24 @@ // Created by cat on 2024/10/8. // -import SwiftUI import Kingfisher +import SwiftUI + +// MARK: - BrowserSignTypedMessageView struct BrowserSignTypedMessageView: View { - - @StateObject var viewModel: BrowserSignTypedMessageViewModel - + // MARK: Lifecycle + init(viewModel: BrowserSignTypedMessageViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } - + + // MARK: Internal + + @StateObject + var viewModel: BrowserSignTypedMessageViewModel + var body: some View { - VStack { titleView VStack { @@ -37,14 +42,14 @@ struct BrowserSignTypedMessageView: View { .cornerRadius(16) } } - + Spacer() actionView } .padding(18) .background(Color.Theme.Background.bg2) } - + var titleView: some View { HStack(alignment: .top, spacing: 18) { HStack { @@ -82,7 +87,7 @@ struct BrowserSignTypedMessageView: View { } } } - + var actionView: some View { WalletSendButtonView(allowEnable: .constant(true), buttonText: "hold_to_sign".localized) { viewModel.didChooseAction(true) @@ -90,20 +95,19 @@ struct BrowserSignTypedMessageView: View { } } +// MARK: BrowserSignTypedMessageView.Card extension BrowserSignTypedMessageView { - struct Card: View { - var model: JSONValue - + var body: some View { VStack { titleView contentView() } } - + var titleView: some View { HStack { Text(model.title.uppercasedFirstLetter()) @@ -117,11 +121,11 @@ extension BrowserSignTypedMessageView { .foregroundStyle(Color.Theme.Text.black) } } - + func contentView() -> some View { VStack { if let subValue = model.subValue { - if case .object(let dictionary) = subValue { + if case let .object(dictionary) = subValue { let keys = Array(dictionary.keys) ForEach(0.. some View { VStack { - if case .object(let dictionary) = item { + if case let .object(dictionary) = item { let keys = Array(dictionary.keys) ForEach(0.. Void - - @Published var title: String - @Published var urlString: String - @Published var logo: String? - @Published var rawString: String - - @Published var sections: [BrowserSignTypedMessageViewModel.Section] = [] - - @Published var list: [JSONValue] = [] - - private var callback: BrowserSignTypedMessageViewModel.Callback? - - init(title: String, urlString: String, logo: String? = nil, rawString: String, callback: BrowserSignTypedMessageViewModel.Callback? = nil) { + // MARK: Lifecycle + + init( + title: String, + urlString: String, + logo: String? = nil, + rawString: String, + callback: BrowserSignTypedMessageViewModel.Callback? = nil + ) { self.title = title self.urlString = urlString self.logo = logo @@ -30,7 +26,51 @@ class BrowserSignTypedMessageViewModel: ObservableObject { self.callback = callback parseRawString() } - + + deinit { + callback?(false) + WalletConnectManager.shared.reloadPendingRequests() + } + + // MARK: Internal + + typealias Callback = (Bool) -> Void + + @Published + var title: String + @Published + var urlString: String + @Published + var logo: String? + @Published + var rawString: String + + @Published + var sections: [BrowserSignTypedMessageViewModel.Section] = [] + + @Published + var list: [JSONValue] = [] + + func didChooseAction(_ result: Bool) { + Router.dismiss { [weak self] in + guard let self else { return } + callback?(result) + callback = nil + } + } + + func onCloseAction() { + Router.dismiss { [weak self] in + guard let self else { return } + callback?(false) + callback = nil + } + } + + // MARK: Private + + private var callback: BrowserSignTypedMessageViewModel.Callback? + private func parseRawString() { guard let data = rawString.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data, options: []), @@ -39,34 +79,40 @@ class BrowserSignTypedMessageViewModel: ObservableObject { return } let primaryType = (dict["primaryType"] as? String) ?? "" - let headerSection = Section(title: "Message", content: nil, items: [.init(tag: "Primary Type", content: primaryType)]) - self.sections.append(headerSection) - + let headerSection = Section( + title: "Message", + content: nil, + items: [.init(tag: "Primary Type", content: primaryType)] + ) + sections.append(headerSection) + guard let message = dict["message"] as? [String: Any] else { return } var tmpSection: [Section] = [] - + let result = JSONValue.parse(jsonString: rawString) switch result { - case .object(let dictionary): + case let .object(dictionary): for (key, value) in dictionary { if key.lowercased() == "primaryType".lowercased() { - list.insert(JSONValue.object( ["Message": JSONValue.object([key: value])]), at: 0) - }else if key.lowercased() == "message" { - if case .object(let mDic) = value { + list.insert( + JSONValue.object(["Message": JSONValue.object([key: value])]), + at: 0 + ) + } else if key.lowercased() == "message" { + if case let .object(mDic) = value { for (mKey, mValue) in mDic { - list.append(JSONValue.object([mKey : mValue])) + list.append(JSONValue.object([mKey: mValue])) } } } } default: break - } log.debug(result ?? "") - + for (key, value) in message { if let item = value as? [String: String] { var items: [Section.Item] = [] @@ -76,48 +122,28 @@ class BrowserSignTypedMessageViewModel: ObservableObject { } let section = Section(title: key, content: nil, items: items) tmpSection.append(section) - } - else if let item = value as? String { + } else if let item = value as? String { let section = Section(title: key, content: item, items: []) tmpSection.append(section) } } - self.sections.append(contentsOf: tmpSection) - } - - func didChooseAction(_ result: Bool) { - Router.dismiss { [weak self] in - guard let self else { return } - callback?(result) - callback = nil - } - } - - func onCloseAction() { - Router.dismiss { [weak self] in - guard let self else { return } - callback?(false) - callback = nil - } - } - - deinit { - callback?(false) - WalletConnectManager.shared.reloadPendingRequests() + sections.append(contentsOf: tmpSection) } } +// MARK: BrowserSignTypedMessageViewModel.Section + extension BrowserSignTypedMessageViewModel { struct Section { struct Item { let tag: String let content: String } - + let title: String let content: String? let items: [Section.Item] - + func showTitle() -> String { let tit = title.uppercasedFirstLetter() if !tit.hasPrefix(":") { @@ -126,5 +152,4 @@ extension BrowserSignTypedMessageViewModel { return tit } } - } diff --git a/FRW/Modules/EVM/View/EVMTagView.swift b/FRW/Modules/EVM/View/EVMTagView.swift index df365c84..b04012e8 100644 --- a/FRW/Modules/EVM/View/EVMTagView.swift +++ b/FRW/Modules/EVM/View/EVMTagView.swift @@ -7,6 +7,8 @@ import SwiftUI +// MARK: - EVMTagView + struct EVMTagView: View { var body: some View { Text("EVM") @@ -19,11 +21,13 @@ struct EVMTagView: View { } } +// MARK: - TagView + struct TagView: View { var type: Contact.WalletType = .flow - + var body: some View { - return HStack { + HStack { if type != .flow { Text(title) .font(.inter(size: 9)) @@ -35,9 +39,8 @@ struct TagView: View { .cornerRadius(8) } } - } - + var title: String { switch type { case .flow: @@ -48,15 +51,15 @@ struct TagView: View { return "Linked" } } - + var BGColor: Color { switch type { case .flow: - .clear + .clear case .evm: - .Theme.evm + .Theme.evm case .link: - .Theme.Accent.blue + .Theme.Accent.blue } } } diff --git a/FRW/Modules/Login/ChooseAccountView.swift b/FRW/Modules/Login/ChooseAccountView.swift index e39d0b49..7c71fe25 100644 --- a/FRW/Modules/Login/ChooseAccountView.swift +++ b/FRW/Modules/Login/ChooseAccountView.swift @@ -1,5 +1,5 @@ // -// ChooseAccount.swift +// ChooseAccountView.swift // Flow Wallet // // Created by Hao Fu on 31/12/21. @@ -15,17 +15,27 @@ private func createFakeItem() -> BackupManager.DriveItem { extension ChooseAccountView { var title: String { - return "" + "" } } +// MARK: - ChooseAccountView + struct ChooseAccountView: RouteableView { - @StateObject var vm: ChooseAccountViewModel + // MARK: Lifecycle init(driveItems: [BackupManager.DriveItem], backupType: BackupManager.BackupType) { - _vm = StateObject(wrappedValue: ChooseAccountViewModel(driveItems: driveItems, backupType: backupType)) + _vm = StateObject(wrappedValue: ChooseAccountViewModel( + driveItems: driveItems, + backupType: backupType + )) } + // MARK: Internal + + @StateObject + var vm: ChooseAccountViewModel + var body: some View { VStack(spacing: 10) { headerView diff --git a/FRW/Modules/Login/EnterRestorePasswordView.swift b/FRW/Modules/Login/EnterRestorePasswordView.swift index 7e886137..a91b300d 100644 --- a/FRW/Modules/Login/EnterRestorePasswordView.swift +++ b/FRW/Modules/Login/EnterRestorePasswordView.swift @@ -1,5 +1,5 @@ // -// EnterPasswordView.swift +// EnterRestorePasswordView.swift // Flow Wallet // // Created by Hao Fu on 31/12/21. @@ -10,29 +10,42 @@ import SwiftUIX extension EnterRestorePasswordView { var title: String { - return "" + "" } } -struct EnterRestorePasswordView: RouteableView { - @StateObject var vm: EnterRestorePasswordViewModel +// MARK: - EnterRestorePasswordView - @State var text: String = "" - @State var textStatus: LL.TextField.Status = .normal - @State var state: VTextFieldState = .enabled +struct EnterRestorePasswordView: RouteableView { + // MARK: Lifecycle - var buttonState: VPrimaryButtonState { - text.count >= 8 ? .enabled : .disabled + init(driveItem: BackupManager.DriveItem, backupType: BackupManager.BackupType) { + _vm = StateObject(wrappedValue: EnterRestorePasswordViewModel( + driveItem: driveItem, + backupType: backupType + )) } + // MARK: Internal + + @StateObject + var vm: EnterRestorePasswordViewModel + + @State + var text: String = "" + @State + var textStatus: LL.TextField.Status = .normal + @State + var state: VTextFieldState = .enabled + var model: VTextFieldModel = { var model = TextFieldStyle.primary model.misc.textContentType = .password return model }() - init(driveItem: BackupManager.DriveItem, backupType: BackupManager.BackupType) { - _vm = StateObject(wrappedValue: EnterRestorePasswordViewModel(driveItem: driveItem, backupType: backupType)) + var buttonState: VPrimaryButtonState { + text.count >= 8 ? .enabled : .disabled } var body: some View { @@ -54,24 +67,29 @@ struct EnterRestorePasswordView: RouteableView { } .frame(maxWidth: .infinity, alignment: .leading) - VTextField(model: model, - type: .secure, - state: $state, - placeholder: "enter_your_password".localized, - footerTitle: "minimum_8_char".localized, - text: $text) {} + VTextField( + model: model, + type: .secure, + state: $state, + placeholder: "enter_your_password".localized, + footerTitle: "minimum_8_char".localized, + text: $text + ) {} .frame(height: 120) .padding(.top, 80) Spacer() - VPrimaryButton(model: ButtonStyle.primary, - state: buttonState, - action: { - state = .enabled - vm.restoreAction(password: text) - }, title: "restore_account".localized) - .padding(.bottom, 20) + VPrimaryButton( + model: ButtonStyle.primary, + state: buttonState, + action: { + state = .enabled + vm.restoreAction(password: text) + }, + title: "restore_account".localized + ) + .padding(.bottom, 20) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 28) diff --git a/FRW/Modules/Login/RestoreWalletView.swift b/FRW/Modules/Login/RestoreWalletView.swift index 8b0b3e27..1c103364 100644 --- a/FRW/Modules/Login/RestoreWalletView.swift +++ b/FRW/Modules/Login/RestoreWalletView.swift @@ -1,5 +1,5 @@ // -// RestoreWallet.swift +// RestoreWalletView.swift // Flow Wallet // // Created by Hao Fu on 31/12/21. @@ -7,8 +7,10 @@ import SwiftUI +// MARK: - RestoreWalletView + struct RestoreWalletView: RouteableView { - private var viewModel = RestoreWalletViewModel() + // MARK: Internal var body: some View { VStack(spacing: 10) { @@ -31,33 +33,48 @@ struct RestoreWalletView: RouteableView { Spacer() - VPrimaryButton(model: ButtonStyle.primary, - action: { - viewModel.restoreWithCloudAction(type: .icloud) - }, title: "restore_with_icloud".localized) - VPrimaryButton(model: ButtonStyle.primary, - action: { - viewModel.restoreWithCloudAction(type: .googleDrive) - }, title: "restore_with_gd".localized) + VPrimaryButton( + model: ButtonStyle.primary, + action: { + viewModel.restoreWithCloudAction(type: .icloud) + }, + title: "restore_with_icloud".localized + ) + VPrimaryButton( + model: ButtonStyle.primary, + action: { + viewModel.restoreWithCloudAction(type: .googleDrive) + }, + title: "restore_with_gd".localized + ) - VPrimaryButton(model: ButtonStyle.border, - action: { - viewModel.restoreWithManualAction() - }, title: "restore_with_recovery_phrase".localized) + VPrimaryButton( + model: ButtonStyle.border, + action: { + viewModel.restoreWithManualAction() + }, + title: "restore_with_recovery_phrase".localized + ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 28) .backgroundFill(Color.LL.background) .applyRouteable(self) } + + // MARK: Private + + private var viewModel = RestoreWalletViewModel() } extension RestoreWalletView { var title: String { - return "" + "" } } +// MARK: - RestoreWalletView_Previews + struct RestoreWalletView_Previews: PreviewProvider { static var previews: some View { RestoreWalletView() diff --git a/FRW/Modules/Login/SyncAccountView.swift b/FRW/Modules/Login/SyncAccountView.swift index 31cf98ad..60140e17 100644 --- a/FRW/Modules/Login/SyncAccountView.swift +++ b/FRW/Modules/Login/SyncAccountView.swift @@ -1,5 +1,5 @@ // -// SyncAccount.swift +// SyncAccountView.swift // FRW // // Created by cat on 2023/11/24. @@ -8,7 +8,8 @@ import SwiftUI struct SyncAccountView: RouteableView { - @ObservedObject var viewModel: SyncAccountViewModel = .init() + @ObservedObject + var viewModel: SyncAccountViewModel = .init() var title: String { "sync_flow_reference".localized diff --git a/FRW/Modules/Login/SyncAddDeviceView.swift b/FRW/Modules/Login/SyncAddDeviceView.swift index c1b19a45..dde9c4d3 100644 --- a/FRW/Modules/Login/SyncAddDeviceView.swift +++ b/FRW/Modules/Login/SyncAddDeviceView.swift @@ -1,5 +1,5 @@ // -// AddSyncDeviceView.swift +// SyncAddDeviceView.swift // FRW // // Created by cat on 2023/11/29. @@ -11,12 +11,17 @@ import SwiftUI import SwiftUIX struct SyncAddDeviceView: View { - @StateObject var viewModel: SyncAddDeviceViewModel + // MARK: Lifecycle init(viewModel: SyncAddDeviceViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } + // MARK: Internal + + @StateObject + var viewModel: SyncAddDeviceViewModel + var body: some View { VStack { HStack(alignment: .center) { @@ -53,10 +58,16 @@ struct SyncAddDeviceView: View { .frame(height: 6) VStack(spacing: 8) { - DeviceInfoItem(title: "application_tag".localized, detail: viewModel.model.deviceInfo.userAgent ?? "") - .frame(height: 24) - DeviceInfoItem(title: "ip_address_tag".localized, detail: viewModel.model.deviceInfo.ip ?? "") - .frame(height: 24) + DeviceInfoItem( + title: "application_tag".localized, + detail: viewModel.model.deviceInfo.userAgent ?? "" + ) + .frame(height: 24) + DeviceInfoItem( + title: "ip_address_tag".localized, + detail: viewModel.model.deviceInfo.ip ?? "" + ) + .frame(height: 24) DeviceInfoItem(title: "location".localized, detail: location) .frame(height: 24) } @@ -64,7 +75,10 @@ struct SyncAddDeviceView: View { Spacer() - WalletSendButtonView(allowEnable: .constant(true), buttonText: "hold_to_sync".localized) { + WalletSendButtonView( + allowEnable: .constant(true), + buttonText: "hold_to_sync".localized + ) { viewModel.addDevice() } } @@ -79,25 +93,30 @@ struct SyncAddDeviceView: View { if viewModel.model.deviceInfo.city != nil && !viewModel.model.deviceInfo.city!.isEmpty { res += viewModel.model.deviceInfo.city! } - if viewModel.model.deviceInfo.country != nil && !viewModel.model.deviceInfo.country!.isEmpty { + if viewModel.model.deviceInfo.country != nil && !viewModel.model.deviceInfo.country! + .isEmpty { res += ",\(viewModel.model.deviceInfo.country!)" } return res } func region() -> MKCoordinateRegion { - let region = MKCoordinateRegion(center: coordinate(), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)) + let region = MKCoordinateRegion( + center: coordinate(), + span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) + ) return region } func annotations() -> [CLLocationCoordinate2D] { - return [ + [ coordinate(), ] } func coordinate() -> CLLocationCoordinate2D { - guard let latitude = viewModel.model.deviceInfo.lat, let longitude = viewModel.model.deviceInfo.lon else { + guard let latitude = viewModel.model.deviceInfo.lat, + let longitude = viewModel.model.deviceInfo.lon else { return CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0) } return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) diff --git a/FRW/Modules/Login/ViewModel/EnterRestorePasswordViewModel.swift b/FRW/Modules/Login/ViewModel/EnterRestorePasswordViewModel.swift index 4e25a0ee..e84a7cdf 100644 --- a/FRW/Modules/Login/ViewModel/EnterRestorePasswordViewModel.swift +++ b/FRW/Modules/Login/ViewModel/EnterRestorePasswordViewModel.swift @@ -1,5 +1,5 @@ // -// EnterPasswordViewModel.swift +// EnterRestorePasswordViewModel.swift // Flow Wallet // // Created by Hao Fu on 10/1/22. @@ -8,25 +8,34 @@ import SwiftUI class EnterRestorePasswordViewModel: ObservableObject { - private let item: BackupManager.DriveItem - private let backupType: BackupManager.BackupType + // MARK: Lifecycle init(driveItem: BackupManager.DriveItem, backupType: BackupManager.BackupType) { - item = driveItem + self.item = driveItem self.backupType = backupType } + // MARK: Internal + func restoreAction(password: String) { let mnemonicHexString = item.data.trim() do { - let mnemoincString = try BackupManager.shared.decryptMnemonic(mnemonicHexString, password: password) + let mnemoincString = try BackupManager.shared.decryptMnemonic( + mnemonicHexString, + password: password + ) restoreLogin(mnemonic: mnemoincString) } catch { HUD.error(title: "decrypt_failed".localized) } } + // MARK: Private + + private let item: BackupManager.DriveItem + private let backupType: BackupManager.BackupType + private func restoreLogin(mnemonic: String) { HUD.loading() @@ -35,7 +44,8 @@ class EnterRestorePasswordViewModel: ObservableObject { try await UserManager.shared.restoreLogin(withMnemonic: mnemonic) DispatchQueue.main.async { - if let uid = UserManager.shared.activatedUID, MultiAccountStorage.shared.getBackupType(uid) == .none { + if let uid = UserManager.shared.activatedUID, + MultiAccountStorage.shared.getBackupType(uid) == .none { MultiAccountStorage.shared.setBackupType(self.backupType, uid: uid) } } diff --git a/FRW/Modules/Login/ViewModel/SyncAccountViewModel.swift b/FRW/Modules/Login/ViewModel/SyncAccountViewModel.swift index b452aa57..e2bc7bab 100644 --- a/FRW/Modules/Login/ViewModel/SyncAccountViewModel.swift +++ b/FRW/Modules/Login/ViewModel/SyncAccountViewModel.swift @@ -11,11 +11,7 @@ import WalletConnectPairing import WalletConnectSign class SyncAccountViewModel: ObservableObject { - @Published var uriString: String? - @Published var isConnect: Bool = false - private var publishers = [AnyCancellable]() - - private var topic: String? + // MARK: Lifecycle init() { Task { @@ -25,7 +21,7 @@ class SyncAccountViewModel: ObservableObject { .debounce(for: .seconds(1), scheduler: DispatchQueue.main) .receive(on: DispatchQueue.main) .sink { sessions in - sessions.forEach { session in + for session in sessions { if session.pairingTopic == self.topic { self.isConnect = true } @@ -34,6 +30,13 @@ class SyncAccountViewModel: ObservableObject { }.store(in: &publishers) } + // MARK: Internal + + @Published + var uriString: String? + @Published + var isConnect: Bool = false + func setupInitialState() async throws { uriString = nil do { @@ -51,4 +54,10 @@ class SyncAccountViewModel: ObservableObject { log.error("[sync device] create uri error:\(error)") } } + + // MARK: Private + + private var publishers = [AnyCancellable]() + + private var topic: String? } diff --git a/FRW/Modules/Login/ViewModel/SyncAddDeviceViewModel.swift b/FRW/Modules/Login/ViewModel/SyncAddDeviceViewModel.swift index 1f5fe125..79bc85fb 100644 --- a/FRW/Modules/Login/ViewModel/SyncAddDeviceViewModel.swift +++ b/FRW/Modules/Login/ViewModel/SyncAddDeviceViewModel.swift @@ -1,5 +1,5 @@ // -// AddSyncDeviceViewModel.swift +// SyncAddDeviceViewModel.swift // FRW // // Created by cat on 2023/11/30. @@ -10,36 +10,59 @@ import Foundation import WalletConnectSign import WalletConnectUtils +// MARK: - SyncAddDeviceViewModel.Callback + extension SyncAddDeviceViewModel { typealias Callback = (Bool) -> Void } -class SyncAddDeviceViewModel: ObservableObject { - var model: SyncInfo.DeviceInfo - private var callback: BrowserAuthzViewModel.Callback? +// MARK: - SyncAddDeviceViewModel - @Published var result: String = "" +class SyncAddDeviceViewModel: ObservableObject { + // MARK: Lifecycle init(with model: SyncInfo.DeviceInfo, callback: @escaping SyncAddDeviceViewModel.Callback) { self.model = model self.callback = callback } + deinit { + NotificationCenter().removeObserver(self) + } + + // MARK: Internal + + var model: SyncInfo.DeviceInfo + @Published + var result: String = "" + func addDevice() { Task { let address = WalletManager.shared.address - let accountKey = Flow.AccountKey(publicKey: Flow.PublicKey(hex: model.accountKey.publicKey), - signAlgo: Flow.SignatureAlgorithm(index: model.accountKey.signAlgo), - hashAlgo: Flow.HashAlgorithm(cadence: model.accountKey.hashAlgo), - weight: 1000) + let accountKey = Flow.AccountKey( + publicKey: Flow.PublicKey(hex: model.accountKey.publicKey), + signAlgo: Flow.SignatureAlgorithm(index: model.accountKey.signAlgo), + hashAlgo: Flow.HashAlgorithm(cadence: model.accountKey.hashAlgo), + weight: 1000 + ) do { let flowAccount = try await findFlowAccount(at: WalletManager.shared.keyIndex) let sequenceNumber = flowAccount?.sequenceNumber ?? 0 - let flowId = try await FlowNetwork.addKeyWithMulti(address: address, keyIndex: WalletManager.shared.keyIndex, sequenceNum: sequenceNumber, accountKey: accountKey, signers: WalletManager.shared.defaultSigners) + let flowId = try await FlowNetwork.addKeyWithMulti( + address: address, + keyIndex: WalletManager.shared.keyIndex, + sequenceNum: sequenceNumber, + accountKey: accountKey, + signers: WalletManager.shared.defaultSigners + ) guard let data = try? JSONEncoder().encode(model) else { return } - let holder = TransactionManager.TransactionHolder(id: flowId, type: .addToken, data: data) + let holder = TransactionManager.TransactionHolder( + id: flowId, + type: .addToken, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) HUD.loading() @@ -64,9 +87,13 @@ class SyncAddDeviceViewModel: ObservableObject { func sendSuccessStatus() { Task { do { - let response: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.User.syncDevice(self.model)) + let response: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.User.syncDevice(self.model)) if response.httpCode != 200 { - log.info("[Sync Device] add device failed. publicKey: \(self.model.accountKey.publicKey)") + log + .info( + "[Sync Device] add device failed. publicKey: \(self.model.accountKey.publicKey)" + ) DispatchQueue.main.async { self.result = "add device failed." } @@ -99,7 +126,7 @@ class SyncAddDeviceViewModel: ObservableObject { return flowAccountKey } - deinit { - NotificationCenter().removeObserver(self) - } + // MARK: Private + + private var callback: BrowserAuthzViewModel.Callback? } diff --git a/FRW/Modules/Migration/Migration.swift b/FRW/Modules/Migration/Migration.swift index 2281ca63..63fb5604 100644 --- a/FRW/Modules/Migration/Migration.swift +++ b/FRW/Modules/Migration/Migration.swift @@ -5,32 +5,40 @@ // Created by cat on 2024/4/26. // +import FlowWalletCore import Foundation import KeychainAccess -import FlowWalletCore + +// MARK: - Migration struct Migration { - private let remoteKeychain: Keychain - private let localKeychain: Keychain - private let mnemonicPrefix = "lilico.mnemonic." + // MARK: Lifecycle init() { let remoteService = (Bundle.main.bundleIdentifier ?? "com.flowfoundation.wallet") - remoteKeychain = Keychain(service: remoteService) + self.remoteKeychain = Keychain(service: remoteService) .label("Lilico app backup") .accessibility(.whenUnlocked) let localService = remoteService + ".local" - localKeychain = Keychain(service: localService) + self.localKeychain = Keychain(service: localService) .label("Flow Wallet Backup") .accessibility(.whenUnlocked) } + // MARK: Internal + func start() { fetchiCloudRemoteList() WallectSecureEnclave.Store.migrationFromLilicoTag() try? WallectSecureEnclave.Store.twoBackupIfNeed() } + + // MARK: Private + + private let remoteKeychain: Keychain + private let localKeychain: Keychain + private let mnemonicPrefix = "lilico.mnemonic." } // MARK: iCloud Migration @@ -44,7 +52,7 @@ extension Migration { defer { data = Data() } - UserManager.shared.loginUIDList.forEach { uid in + for uid in UserManager.shared.loginUIDList { do { var encodedData = try WalletManager.encryptionChaChaPoly(key: uid, data: data) let newKey = "lilico.pinCode.\(uid)" @@ -78,7 +86,10 @@ extension Migration { continue } let uid = key.removePrefix(mnemonicPrefix) - if let decryptedData = try? WalletManager.decryptionChaChaPoly(key: uid, data: value), let mnemonic = String(data: decryptedData, encoding: .utf8), !mnemonic.isEmpty { + if let decryptedData = try? WalletManager.decryptionChaChaPoly( + key: uid, + data: value + ), let mnemonic = String(data: decryptedData, encoding: .utf8), !mnemonic.isEmpty { do { try localKeychain.comment("Lilico user uid: \(uid)").set(value, key: key) try remoteKeychain.remove(key) diff --git a/FRW/Modules/MultiBackup/Manager/MultiBackupManager.swift b/FRW/Modules/MultiBackup/Manager/MultiBackupManager.swift index e1cf3dcf..983f1223 100644 --- a/FRW/Modules/MultiBackup/Manager/MultiBackupManager.swift +++ b/FRW/Modules/MultiBackup/Manager/MultiBackupManager.swift @@ -16,6 +16,8 @@ import GoogleSignIn import GTMSessionFetcherCore import WalletCore +// MARK: - MultiBackupTarget + protocol MultiBackupTarget { var uploadedItem: MultiBackupManager.StoreItem? { get set } var registeredDeviceInfo: SyncInfo.DeviceInfo? { get set } @@ -25,28 +27,43 @@ protocol MultiBackupTarget { func removeItem(password: String) async throws } +// MARK: - MultiBackupManager + class MultiBackupManager: ObservableObject { + // MARK: Lifecycle + + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(onTransactionManagerChanged), + name: .transactionManagerDidChanged, + object: nil + ) + } + + // MARK: Internal + static let shared = MultiBackupManager() - private let gdTarget = MultiBackupGoogleDriveTarget() - private let iCloudTarget = MultiBackupiCloudTarget() - private let phraseTarget = MultiBackupPhraseTarget() - private let passkeyTarget = MultiBackupPasskeyTarget() - private let password = LocalEnvManager.shared.backupAESKey static let backupFileName = "outblock_multi_backup" var deviceInfo: SyncInfo.DeviceInfo? var backupType: BackupType = .undefined var backupList: [MultiBackupType] = [] - @Published var mnemonic: String? + @Published + var mnemonic: String? - init() { - NotificationCenter.default.addObserver(self, selector: #selector(onTransactionManagerChanged), name: .transactionManagerDidChanged, object: nil) - } + // MARK: Private + + private let gdTarget = MultiBackupGoogleDriveTarget() + private let iCloudTarget = MultiBackupiCloudTarget() + private let phraseTarget = MultiBackupPhraseTarget() + private let passkeyTarget = MultiBackupPasskeyTarget() + private let password = LocalEnvManager.shared.backupAESKey } -// MARK: - Data +// MARK: MultiBackupManager.StoreItem extension MultiBackupManager { struct StoreItem: Codable { @@ -63,6 +80,7 @@ extension MultiBackupManager { var updatedTime: Double? = Date.now.timeIntervalSince1970 let deviceInfo: DeviceInfoRequest? var code: String? + func showDate() -> String { guard let updatedTime = updatedTime else { return "" } let date = Date(timeIntervalSince1970: updatedTime) @@ -102,7 +120,8 @@ extension MultiBackupManager { throw BackupError.missingMnemonic } - guard let hdWallet = WalletManager.shared.createHDWallet(), let mnemonicData = hdWallet.mnemonic.data(using: .utf8) else { + guard let hdWallet = WalletManager.shared.createHDWallet(), + let mnemonicData = hdWallet.mnemonic.data(using: .utf8) else { HUD.error(title: "empty_wallet_key".localized) throw BackupError.missingMnemonic } @@ -114,7 +133,10 @@ extension MultiBackupManager { throw BackupError.hexStringToDataFailed } - let dataHexString = try encryptMnemonic(mnemonicData, password: type.needPin ? pinCode : password) + let dataHexString = try encryptMnemonic( + mnemonicData, + password: type.needPin ? pinCode : password + ) let publicKey = hdWallet.flowAccountP256Key.publicKey.description let result = try await addKeyToFlow(key: publicKey) @@ -129,9 +151,22 @@ extension MultiBackupManager { } let flowPublicKey = Flow.PublicKey(hex: publicKey) - let flowKey = Flow.AccountKey(publicKey: flowPublicKey, signAlgo: .ECDSA_P256, hashAlgo: .SHA2_256, weight: 500) + let flowKey = Flow.AccountKey( + publicKey: flowPublicKey, + signAlgo: .ECDSA_P256, + hashAlgo: .SHA2_256, + weight: 500 + ) let backupName = type.showName() - let deviceInfo = SyncInfo.DeviceInfo(accountKey: flowKey.toCodableModel(), deviceInfo: IPManager.shared.toParams(), backupInfo: BackupInfoModel(createTime: nil, name: backupName, type: type.toBackupType().rawValue)) + let deviceInfo = SyncInfo.DeviceInfo( + accountKey: flowKey.toCodableModel(), + deviceInfo: IPManager.shared.toParams(), + backupInfo: BackupInfoModel( + createTime: nil, + name: backupName, + type: type.toBackupType().rawValue + ) + ) let item = MultiBackupManager.StoreItem( address: address, @@ -172,9 +207,13 @@ extension MultiBackupManager { return } do { - let response: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.User.syncDevice(model)) + let response: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.User.syncDevice(model)) if response.httpCode != 200 { - log.info("[MultiBackup] add device failed. publicKey: \(model.accountKey.publicKey)") + log + .info( + "[MultiBackup] add device failed. publicKey: \(model.accountKey.publicKey)" + ) } } catch { log.error("[sync account] error \(error.localizedDescription)") @@ -198,7 +237,11 @@ extension MultiBackupManager { } } - private func updateTarget(on type: MultiBackupType, item: MultiBackupManager.StoreItem, deviceInfo: SyncInfo.DeviceInfo) { + private func updateTarget( + on type: MultiBackupType, + item: MultiBackupManager.StoreItem, + deviceInfo: SyncInfo.DeviceInfo + ) { switch type { case .google: gdTarget.uploadedItem = item @@ -217,7 +260,8 @@ extension MultiBackupManager { } extension MultiBackupManager { - func getCloudDriveItems(from type: MultiBackupType) async throws -> [MultiBackupManager.StoreItem] { + func getCloudDriveItems(from type: MultiBackupType) async throws + -> [MultiBackupManager.StoreItem] { switch type { case .google: try await login(from: type) @@ -237,7 +281,6 @@ extension MultiBackupManager { try await gdTarget.loginCloud() case .passkey: log.info("not finished") - case .icloud: try await iCloudTarget.loginCloud() log.info("not finished") @@ -266,7 +309,11 @@ extension MultiBackupManager { extension MultiBackupManager { /// append current user mnemonic to list - func addNewMnemonic(on type: MultiBackupType, list: [MultiBackupManager.StoreItem], password _: String) async throws -> [MultiBackupManager.StoreItem] { + func addNewMnemonic( + on type: MultiBackupType, + list: [MultiBackupManager.StoreItem], + password _: String + ) async throws -> [MultiBackupManager.StoreItem] { guard let uid = UserManager.shared.activatedUID, !uid.isEmpty else { throw BackupError.missingUid } @@ -282,7 +329,10 @@ extension MultiBackupManager { return newList } - func removeCurrent(_ list: [MultiBackupManager.StoreItem], password _: String) async throws -> [MultiBackupManager.StoreItem] { + func removeCurrent( + _ list: [MultiBackupManager.StoreItem], + password _: String + ) async throws -> [MultiBackupManager.StoreItem] { guard let username = UserManager.shared.userInfo?.username, !username.isEmpty else { throw BackupError.missingUserName } @@ -309,7 +359,11 @@ extension MultiBackupManager { func encryptList(_ list: [MultiBackupManager.StoreItem]) throws -> String { let jsonData = try JSONEncoder().encode(list) let iv = iv() - let encrypedData = try WalletManager.encryptionAES(key: LocalEnvManager.shared.backupAESKey, iv: iv, data: jsonData) + let encrypedData = try WalletManager.encryptionAES( + key: LocalEnvManager.shared.backupAESKey, + iv: iv, + data: jsonData + ) return encrypedData.hexString } @@ -324,7 +378,11 @@ extension MultiBackupManager { private func decryptData(_ data: Data) throws -> [MultiBackupManager.StoreItem] { let iv = iv() - let jsonData = try WalletManager.decryptionAES(key: LocalEnvManager.shared.backupAESKey, iv: iv, data: data) + let jsonData = try WalletManager.decryptionAES( + key: LocalEnvManager.shared.backupAESKey, + iv: iv, + data: data + ) let list = try JSONDecoder().decode([MultiBackupManager.StoreItem].self, from: jsonData) return list } @@ -334,7 +392,11 @@ extension MultiBackupManager { guard let iv = password.toPassword() else { throw BackupError.decryptMnemonicFailed } - let dataHexString = try WalletManager.encryptionAES(key: password, iv: iv, data: mnemonicData).hexString + let dataHexString = try WalletManager.encryptionAES( + key: password, + iv: iv, + data: mnemonicData + ).hexString return dataHexString } @@ -346,7 +408,11 @@ extension MultiBackupManager { guard let iv = password.toPassword() else { throw BackupError.decryptMnemonicFailed } - let decryptedData = try WalletManager.decryptionAES(key: password, iv: iv, data: encryptData) + let decryptedData = try WalletManager.decryptionAES( + key: password, + iv: iv, + data: encryptData + ) guard let mm = String(data: decryptedData, encoding: .utf8), !mm.isEmpty else { throw BackupError.decryptMnemonicFailed } @@ -356,7 +422,8 @@ extension MultiBackupManager { } extension MultiBackupManager { - @objc private func onTransactionManagerChanged() { + @objc + private func onTransactionManagerChanged() { if TransactionManager.shared.holders.isEmpty { return } @@ -364,8 +431,17 @@ extension MultiBackupManager { private func addKeyToFlow(key: String) async throws -> Bool { let address = WalletManager.shared.address - let accountKey = Flow.AccountKey(publicKey: Flow.PublicKey(hex: key), signAlgo: .ECDSA_P256, hashAlgo: .SHA2_256, weight: 500) - let flowId = try await FlowNetwork.addKeyToAccount(address: address, accountKey: accountKey, signers: WalletManager.shared.defaultSigners) + let accountKey = Flow.AccountKey( + publicKey: Flow.PublicKey(hex: key), + signAlgo: .ECDSA_P256, + hashAlgo: .SHA2_256, + weight: 500 + ) + let flowId = try await FlowNetwork.addKeyToAccount( + address: address, + accountKey: accountKey, + signers: WalletManager.shared.defaultSigners + ) guard let data = try? JSONEncoder().encode(key) else { return false } @@ -401,7 +477,7 @@ extension MultiBackupManager { let account = try await FlowNetwork.getAccountAtLatestBlock(address: addressDes) var sequenNum: Int64 = 0 - account.keys.forEach { accountKey in + for accountKey in account.keys { let publicKey = accountKey.publicKey.description if publicKey == firstItem.publicKey { sequenNum = accountKey.sequenceNumber @@ -424,7 +500,13 @@ extension MultiBackupManager { let key = try sec.accountKey() do { HUD.loading() - let tx = try await FlowNetwork.addKeyWithMulti(address: address, keyIndex: firstItem.keyIndex, sequenceNum: sequenNum, accountKey: key, signers: [firstSigner, secondSigner, RemoteConfigManager.shared]) + let tx = try await FlowNetwork.addKeyWithMulti( + address: address, + keyIndex: firstItem.keyIndex, + sequenceNum: sequenNum, + accountKey: key, + signers: [firstSigner, secondSigner, RemoteConfigManager.shared] + ) let result = try await tx.onceSealed() if result.isComplete { let userId = firstSigner.provider.userId @@ -449,18 +531,27 @@ extension MultiBackupManager { signature: secondSignature, weight: secondSigner.weight ) - let request = SignedRequest(accountKey: AccountKey(hashAlgo: key.hashAlgo.index, - publicKey: key.publicKey.description, - signAlgo: key.signAlgo.index, - weight: key.weight), - signatures: [firstKeySignature, secondKeySignature]) - let response: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.User.addSigned(request)) + let request = SignedRequest( + accountKey: AccountKey( + hashAlgo: key.hashAlgo.index, + publicKey: key.publicKey + .description, + signAlgo: key.signAlgo.index, + weight: key.weight + ), + signatures: [firstKeySignature, secondKeySignature] + ) + let response: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.User.addSigned(request)) if response.httpCode != 200 { log.info("[Multi-backup] sync failed") } else { print("") if let privateKey = sec.key.privateKey { - try WallectSecureEnclave.Store.store(key: firstItem.userId, value: privateKey.dataRepresentation) + try WallectSecureEnclave.Store.store( + key: firstItem.userId, + value: privateKey.dataRepresentation + ) } try await UserManager.shared.restoreLogin(userId: firstItem.userId) @@ -479,20 +570,20 @@ extension MultiBackupManager { } } -// MARK: - Signer +// MARK: MultiBackupManager.Signer extension MultiBackupManager { class Signer: FlowSigner { - let provider: MultiBackupManager.StoreItem - var signature: Data? - var hdWallet: HDWallet? + // MARK: Lifecycle init(provider: MultiBackupManager.StoreItem) { self.provider = provider } + // MARK: Public + public var address: Flow.Address { - return Flow.Address(hex: provider.address) + Flow.Address(hex: provider.address) } public var hashAlgo: Flow.HashAlgorithm { @@ -517,34 +608,18 @@ extension MultiBackupManager { return provider.weight ?? 500 } - private func createHDWallet() async throws { - if hdWallet != nil { - return - } - var key = LocalEnvManager.shared.backupAESKey - if let code = provider.code { - guard let pinCode = code.toPassword() else { - throw BackupError.hexStringToDataFailed - } - key = pinCode - } - - let mnemonic = try MultiBackupManager.shared.decryptMnemonic(provider.data, password: key) - - guard let hdWallet = WalletManager.shared.createHDWallet(mnemonic: mnemonic) else { - throw BackupError.missingMnemonic - } - self.hdWallet = hdWallet - } - public func sign(transaction _: Flow.Transaction, signableData: Data) async throws -> Data { _ = try await createHDWallet() guard let hdWallet = hdWallet else { throw BackupError.missingMnemonic } - let curve: WalletCore.Curve = hdWallet.mnemonic.words.count == 15 ? .nist256p1 : .secp256k1 - var privateKey = hdWallet.getKeyByCurve(curve: curve, derivationPath: WalletManager.flowPath) + let curve: WalletCore.Curve = hdWallet.mnemonic.words + .count == 15 ? .nist256p1 : .secp256k1 + var privateKey = hdWallet.getKeyByCurve( + curve: curve, + derivationPath: WalletManager.flowPath + ) let hashedData = Hash.sha256(data: signableData) defer { @@ -559,6 +634,12 @@ extension MultiBackupManager { return signature } + // MARK: Internal + + let provider: MultiBackupManager.StoreItem + var signature: Data? + var hdWallet: HDWallet? + func sign(_ text: String) -> String? { guard let textData = text.data(using: .utf8) else { return nil @@ -572,8 +653,12 @@ extension MultiBackupManager { guard let hdWallet = hdWallet else { return nil } - let curve: WalletCore.Curve = hdWallet.mnemonic.words.count == 15 ? .nist256p1 : .secp256k1 - var privateKey = hdWallet.getKeyByCurve(curve: curve, derivationPath: WalletManager.flowPath) + let curve: WalletCore.Curve = hdWallet.mnemonic.words + .count == 15 ? .nist256p1 : .secp256k1 + var privateKey = hdWallet.getKeyByCurve( + curve: curve, + derivationPath: WalletManager.flowPath + ) defer { privateKey = PrivateKey() @@ -587,6 +672,31 @@ extension MultiBackupManager { signature.removeLast() return signature.hexValue } + + // MARK: Private + + private func createHDWallet() async throws { + if hdWallet != nil { + return + } + var key = LocalEnvManager.shared.backupAESKey + if let code = provider.code { + guard let pinCode = code.toPassword() else { + throw BackupError.hexStringToDataFailed + } + key = pinCode + } + + let mnemonic = try MultiBackupManager.shared.decryptMnemonic( + provider.data, + password: key + ) + + guard let hdWallet = WalletManager.shared.createHDWallet(mnemonic: mnemonic) else { + throw BackupError.missingMnemonic + } + self.hdWallet = hdWallet + } } } diff --git a/FRW/Modules/MultiBackup/View/BackupListView.swift b/FRW/Modules/MultiBackup/View/BackupListView.swift index 8f466ac1..9f358d64 100644 --- a/FRW/Modules/MultiBackup/View/BackupListView.swift +++ b/FRW/Modules/MultiBackup/View/BackupListView.swift @@ -1,5 +1,5 @@ // -// BackupPatternView.swift +// BackupListView.swift // FRW // // Created by cat on 2023/12/8. @@ -7,13 +7,17 @@ import SwiftUI +// MARK: - BackupListView + struct BackupListView: RouteableView { - @StateObject var viewModel = BackupListViewModel() + @StateObject + var viewModel = BackupListViewModel() - @State var deletePhrase = false + @State + var deletePhrase = false var title: String { - return "backup".localized + "backup".localized } var body: some View { @@ -42,7 +46,10 @@ struct BackupListView: RouteableView { Divider() .foregroundStyle(.clear) .background(Color.Theme.Line.line) - .visibility((viewModel.hasDeviceBackup && viewModel.hasMultiBackup) ? .gone : .visible) + .visibility( + (viewModel.hasDeviceBackup && viewModel.hasMultiBackup) ? .gone : + .visible + ) deviceListView .visibility(viewModel.hasDeviceBackup ? .visible : .gone) @@ -59,9 +66,11 @@ struct BackupListView: RouteableView { .applyRouteable(self) .backgroundFill(Color.LL.Neutrals.background) .halfSheet(showSheet: $viewModel.showRemoveTipView) { - DangerousTipSheetView(title: "account_key_revoke_title".localized, - detail: "account_key_revoke_content".localized, - buttonTitle: "hold_to_revoke".localized) { + DangerousTipSheetView( + title: "account_key_revoke_title".localized, + detail: "account_key_revoke_content".localized, + buttonTitle: "hold_to_revoke".localized + ) { viewModel.removeBackup() } onCancel: { viewModel.onCancelTip() @@ -121,7 +130,7 @@ struct BackupListView: RouteableView { .visibility(viewModel.showAllUITag ? .visible : .gone) } - ForEach(0 ..< viewModel.showDevicesCount, id: \.self) { index in + ForEach(0.. [GridItem] { let width = (screenWidth - 64 * 2) / 2 - return [GridItem(.adaptive(minimum: width)), - GridItem(.adaptive(minimum: width))] + return [ + GridItem(.adaptive(minimum: width)), + GridItem(.adaptive(minimum: width)), + ] } func onClick(item: BackupMultiViewModel.MultiItem) { @@ -100,20 +112,27 @@ struct BackupMultiView: RouteableView { } } -// MARK: ItemView +// MARK: BackupMultiView.ItemView extension BackupMultiView { struct ItemView: View { - @Binding var item: BackupMultiViewModel.MultiItem - var onClick: (BackupMultiViewModel.MultiItem) -> Void - @Binding private var isSelected: Bool + // MARK: Lifecycle - init(item: Binding, onClick: @escaping (BackupMultiViewModel.MultiItem) -> Void) { + init( + item: Binding, + onClick: @escaping (BackupMultiViewModel.MultiItem) -> Void + ) { _item = item self.onClick = onClick _isSelected = item.isBackup } + // MARK: Internal + + @Binding + var item: BackupMultiViewModel.MultiItem + var onClick: (BackupMultiViewModel.MultiItem) -> Void + var body: some View { VStack(alignment: .center, spacing: 16) { ZStack(alignment: .topTrailing) { @@ -145,6 +164,11 @@ extension BackupMultiView { onClick(item) } } + + // MARK: Private + + @Binding + private var isSelected: Bool } } diff --git a/FRW/Modules/MultiBackup/View/RecoveryPhraseBackupResultView.swift b/FRW/Modules/MultiBackup/View/RecoveryPhraseBackupResultView.swift index 2542cebf..c53738ab 100644 --- a/FRW/Modules/MultiBackup/View/RecoveryPhraseBackupResultView.swift +++ b/FRW/Modules/MultiBackup/View/RecoveryPhraseBackupResultView.swift @@ -8,13 +8,13 @@ import SwiftUI struct RecoveryPhraseBackupResultView: RouteableView { - var title: String { - return "backup".localized - } - var mnemonic: String var deviceInfo: DeviceInfoRequest = IPManager.shared.toParams() + var title: String { + "backup".localized + } + var body: some View { VStack { ScrollView(showsIndicators: false) { @@ -31,17 +31,23 @@ struct RecoveryPhraseBackupResultView: RouteableView { .font(.inter(size: 20, weight: .bold)) .foregroundStyle(Color.Theme.Text.black) - BackupedItemView(backupType: .phrase, mnemonic: mnemonic, deviceInfo: deviceInfo) + BackupedItemView( + backupType: .phrase, + mnemonic: mnemonic, + deviceInfo: deviceInfo + ) } } .padding(.bottom, 16) - VPrimaryButton(model: ButtonStyle.primary, - action: { - onConfirm() - }, title: "done".localized) - - .padding(.bottom, 20) + VPrimaryButton( + model: ButtonStyle.primary, + action: { + onConfirm() + }, + title: "done".localized + ) + .padding(.bottom, 20) } .padding(.horizontal, 18) .backgroundFill(Color.LL.background) diff --git a/FRW/Modules/MultiBackup/View/ShowRecoveryPhraseBackup.swift b/FRW/Modules/MultiBackup/View/ShowRecoveryPhraseBackup.swift index 7654e943..2bdae5fb 100644 --- a/FRW/Modules/MultiBackup/View/ShowRecoveryPhraseBackup.swift +++ b/FRW/Modules/MultiBackup/View/ShowRecoveryPhraseBackup.swift @@ -1,5 +1,5 @@ // -// CreateRecoveryPhraseBackup.swift +// ShowRecoveryPhraseBackup.swift // FRW // // Created by cat on 2024/9/19. @@ -8,14 +8,20 @@ import SwiftUI struct ShowRecoveryPhraseBackup: RouteableView { - @StateObject var viewModel: CreateRecoveryPhraseBackupViewModel + // MARK: Lifecycle - var title: String { - return "backup".localized + init(mnemonic: String) { + _viewModel = + StateObject(wrappedValue: CreateRecoveryPhraseBackupViewModel(mnemonic: mnemonic)) } - init(mnemonic: String) { - _viewModel = StateObject(wrappedValue: CreateRecoveryPhraseBackupViewModel(mnemonic: mnemonic)) + // MARK: Internal + + @StateObject + var viewModel: CreateRecoveryPhraseBackupViewModel + + var title: String { + "backup".localized } var body: some View { @@ -89,12 +95,14 @@ struct ShowRecoveryPhraseBackup: RouteableView { } Spacer() - VPrimaryButton(model: ButtonStyle.primary, - action: { - viewModel.onCreate() - }, title: "done".localized) - - .padding(.bottom, 20) + VPrimaryButton( + model: ButtonStyle.primary, + action: { + viewModel.onCreate() + }, + title: "done".localized + ) + .padding(.bottom, 20) } .frame(maxHeight: .infinity) .padding(.horizontal, 28) diff --git a/FRW/Modules/MultiBackup/View/ThingsNeedKnowView.swift b/FRW/Modules/MultiBackup/View/ThingsNeedKnowView.swift index 9be7db75..b180a794 100644 --- a/FRW/Modules/MultiBackup/View/ThingsNeedKnowView.swift +++ b/FRW/Modules/MultiBackup/View/ThingsNeedKnowView.swift @@ -8,15 +8,16 @@ import SwiftUI struct ThingsNeedKnowView: RouteableView { - @StateObject var viewModel = ThingsNeedKnowViewModel() + @StateObject + var viewModel = ThingsNeedKnowViewModel() + + @State + var allCheck = false var title: String { - return "" + "" } - @State var allCheck = false - - var body: some View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { @@ -49,12 +50,15 @@ struct ThingsNeedKnowView: RouteableView { } .padding(.top, 24) Spacer() - VPrimaryButton(model: ButtonStyle.primary, - state: allCheck ? .enabled : .disabled, - action: { - onConfirm() - }, title: "create_backup".localized) - .padding(.bottom) + VPrimaryButton( + model: ButtonStyle.primary, + state: allCheck ? .enabled : .disabled, + action: { + onConfirm() + }, + title: "create_backup".localized + ) + .padding(.bottom) } .padding(.horizontal, 28) .background(Color.LL.background, ignoresSafeAreaEdges: .all) diff --git a/FRW/Modules/MultiBackup/ViewModel/BackupMultiViewModel.swift b/FRW/Modules/MultiBackup/ViewModel/BackupMultiViewModel.swift index f00cd39b..18070835 100644 --- a/FRW/Modules/MultiBackup/ViewModel/BackupMultiViewModel.swift +++ b/FRW/Modules/MultiBackup/ViewModel/BackupMultiViewModel.swift @@ -1,5 +1,5 @@ // -// BackupSelectOptionsViewModel.swift +// BackupMultiViewModel.swift // FRW // // Created by cat on 2023/12/7. @@ -7,14 +7,14 @@ import Foundation +// MARK: - BackupMultiViewModel + class BackupMultiViewModel: ObservableObject { - @Published var list: [BackupMultiViewModel.MultiItem] = [] - @Published var nextable: Bool = false - let selectedList: [MultiBackupType] + // MARK: Lifecycle init(backups: [MultiBackupType]) { - list = [] - selectedList = backups + self.list = [] + self.selectedList = backups for type in MultiBackupType.allCases { if type != .passkey { list.append(MultiItem(type: type, isBackup: backups.contains(type))) @@ -22,6 +22,14 @@ class BackupMultiViewModel: ObservableObject { } } + // MARK: Internal + + @Published + var list: [BackupMultiViewModel.MultiItem] = [] + @Published + var nextable: Bool = false + let selectedList: [MultiBackupType] + func onClick(item: BackupMultiViewModel.MultiItem) { let existItem = selectedList.first { $0 == item.type } guard existItem == nil else { return } @@ -47,7 +55,7 @@ class BackupMultiViewModel: ObservableObject { let needPin = list.filter { $0.needPin } let hasPin = SecurityManager.shared.currentPinCode.count > 0 MultiBackupManager.shared.backupList = list - if needPin.count > 0 { + if !needPin.isEmpty { if hasPin { Router.route(to: RouteMap.Backup.verityPin(.backup) { allow, _ in if allow { @@ -70,7 +78,7 @@ class BackupMultiViewModel: ObservableObject { } } -// MARK: - MultiItem +// MARK: - MultiBackupType enum MultiBackupType: Int, CaseIterable { case google = 0 @@ -78,6 +86,8 @@ enum MultiBackupType: Int, CaseIterable { case icloud = 2 case phrase = 3 + // MARK: Internal + var title: String { switch self { case .google: @@ -91,19 +101,6 @@ enum MultiBackupType: Int, CaseIterable { } } - func iconName() -> String { - switch self { - case .google: - return "icon.google.drive" - case .passkey: - return "icon.passkey" - case .icloud: - return "Icloud" - case .phrase: - return "icon.recovery" - } - } - var noteDes: String { "backup_note_x".localized } @@ -142,8 +139,23 @@ enum MultiBackupType: Int, CaseIterable { return false } } + + func iconName() -> String { + switch self { + case .google: + return "icon.google.drive" + case .passkey: + return "icon.passkey" + case .icloud: + return "Icloud" + case .phrase: + return "icon.recovery" + } + } } +// MARK: - BackupMultiViewModel.MultiItem + extension BackupMultiViewModel { struct MultiItem: Hashable { let type: MultiBackupType @@ -154,7 +166,7 @@ extension BackupMultiViewModel { } var icon: String { - return type.iconName() + type.iconName() } } } diff --git a/FRW/Modules/MultiBackup/ViewModel/BackupUploadViewModel.swift b/FRW/Modules/MultiBackup/ViewModel/BackupUploadViewModel.swift index 0d182827..e937752a 100644 --- a/FRW/Modules/MultiBackup/ViewModel/BackupUploadViewModel.swift +++ b/FRW/Modules/MultiBackup/ViewModel/BackupUploadViewModel.swift @@ -8,9 +8,13 @@ import Foundation import SwiftUI +// MARK: - BackupProcess + enum BackupProcess { case idle, upload, regist, finish, end + // MARK: Internal + var title: String { switch self { case .idle: @@ -54,9 +58,36 @@ enum BackupProcess { // MARK: - BackupUploadViewModel class BackupUploadViewModel: ObservableObject { + // MARK: Lifecycle + + init(items: [MultiBackupType]) { + self.items = items + self.currentIndex = 0 + if !self.items.isEmpty { + self.currentType = self.items[0] + } + if currentType == .phrase { + self.buttonState = .disabled + } + } + + // MARK: Internal + let items: [MultiBackupType] - @Published var currentIndex: Int = 0 { + @Published + var process: BackupProcess = .idle + @Published + var hasError: Bool = false + @Published + var mnemonicBlur: Bool = true + + var currentType: MultiBackupType = .google + @Published + var buttonState: VPrimaryButtonState = .enabled + + @Published + var currentIndex: Int = 0 { didSet { if currentIndex < items.count { currentType = items[currentIndex] @@ -64,12 +95,9 @@ class BackupUploadViewModel: ObservableObject { } } - @Published var process: BackupProcess = .idle - @Published var hasError: Bool = false - @Published var mnemonicBlur: Bool = true - // TODO: - @Published var checkAllPhrase: Bool = true { + @Published + var checkAllPhrase: Bool = true { didSet { if checkAllPhrase { buttonState = .enabled @@ -77,23 +105,6 @@ class BackupUploadViewModel: ObservableObject { } } - var currentType: MultiBackupType = .google - init(items: [MultiBackupType]) { - self.items = items - currentIndex = 0 - if !self.items.isEmpty { - currentType = self.items[0] - } - if currentType == .phrase { - buttonState = .disabled - } - } - - func reset() { - hasError = false - process = .idle - } - // MARK: UI element var currentIcon: String { @@ -103,7 +114,8 @@ class BackupUploadViewModel: ObservableObject { var currentTitle: String { switch process { case .idle: - return "backup".localized + " \(currentIndex + 1):\(currentType.title) " + "backup".localized + return "backup".localized + " \(currentIndex + 1):\(currentType.title) " + "backup" + .localized case .upload: return "backup.status.upload".localized case .regist: @@ -115,8 +127,6 @@ class BackupUploadViewModel: ObservableObject { } } - @Published var buttonState: VPrimaryButtonState = .enabled - var currentNote: String { currentType.noteDes } @@ -128,8 +138,13 @@ class BackupUploadViewModel: ObservableObject { return process.title } + func reset() { + hasError = false + process = .idle + } + func showTimeline() -> Bool { - return process == .upload || process == .regist + process == .upload || process == .regist } func learnMore() { @@ -147,7 +162,8 @@ class BackupUploadViewModel: ObservableObject { self.mnemonicBlur = true } try await MultiBackupManager.shared.preLogin(with: currentType) - let result = try await MultiBackupManager.shared.registerKeyToChain(on: currentType) + let result = try await MultiBackupManager.shared + .registerKeyToChain(on: currentType) if result { toggleProcess(process: .upload) onClickButton() @@ -165,7 +181,7 @@ class BackupUploadViewModel: ObservableObject { DispatchQueue.main.async { self.buttonState = .loading } - + try await MultiBackupManager.shared.backupKey(on: currentType) toggleProcess(process: .regist) onClickButton() @@ -181,7 +197,7 @@ class BackupUploadViewModel: ObservableObject { DispatchQueue.main.async { self.buttonState = .loading } - + try await MultiBackupManager.shared.syncKeyToServer(on: currentType) DispatchQueue.main.async { self.mnemonicBlur = false @@ -189,7 +205,7 @@ class BackupUploadViewModel: ObservableObject { } toggleProcess(process: .finish) // onClickButton() - + } catch { buttonState = .enabled HUD.dismissLoading() diff --git a/FRW/Modules/MultiRestore/ViewModel/RestoreMultiAccountViewModel.swift b/FRW/Modules/MultiRestore/ViewModel/RestoreMultiAccountViewModel.swift index 6c50c4cf..007339a3 100644 --- a/FRW/Modules/MultiRestore/ViewModel/RestoreMultiAccountViewModel.swift +++ b/FRW/Modules/MultiRestore/ViewModel/RestoreMultiAccountViewModel.swift @@ -5,49 +5,55 @@ // Created by cat on 2024/1/7. // -import Foundation import FlowWalletCore +import Foundation class RestoreMultiAccountViewModel: ObservableObject { - var items: [[MultiBackupManager.StoreItem]] + // MARK: Lifecycle init(items: [[MultiBackupManager.StoreItem]]) { self.items = items } + // MARK: Internal + + var items: [[MultiBackupManager.StoreItem]] + func onClickUser(at index: Int) { guard index < items.count else { return } let selectedUser = items[index] - + guard let selectedUserId = selectedUser.first?.userId else { log.error("[restore] invaid user id") return } - + // If it is the current user, do nothing and return directly. if let userId = UserManager.shared.activatedUID, userId == selectedUserId { if (try? WallectSecureEnclave.Store.fetchModel(by: selectedUserId)) != nil { Router.popToRoot() return } - if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: selectedUserId), !mnemonic.isEmpty { + if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: selectedUserId), + !mnemonic.isEmpty { Router.popToRoot() return } } - + // If it is in the login list, switch user if UserManager.shared.loginUIDList.contains(selectedUserId) { var isValidKey = false if (try? WallectSecureEnclave.Store.fetchModel(by: selectedUserId)) != nil { isValidKey = true } - if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: selectedUserId), !mnemonic.isEmpty { + if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: selectedUserId), + !mnemonic.isEmpty { isValidKey = true } - + if isValidKey { Task { do { diff --git a/FRW/Modules/MultiRestore/ViewModel/RestoreMultiConnectViewModel.swift b/FRW/Modules/MultiRestore/ViewModel/RestoreMultiConnectViewModel.swift index 8cea19ba..6ce72598 100644 --- a/FRW/Modules/MultiRestore/ViewModel/RestoreMultiConnectViewModel.swift +++ b/FRW/Modules/MultiRestore/ViewModel/RestoreMultiConnectViewModel.swift @@ -8,33 +8,45 @@ import Flow import Foundation +// MARK: - RestoreMultiConnectViewModel + class RestoreMultiConnectViewModel: ObservableObject { - let items: [MultiBackupType] - @Published var enable: Bool = true - @Published var currentIndex: Int = 0 { - didSet { - if currentIndex < items.count { - currentType = items[currentIndex] - } + // MARK: Lifecycle + + init(items: [MultiBackupType]) { + self.items = items + self.currentIndex = 0 + if !self.items.isEmpty { + self.currentType = self.items[0] } } - @Published var process: BackupProcess = .idle - @Published var isEnd: Bool = false + // MARK: Internal + + let items: [MultiBackupType] + @Published + var enable: Bool = true + @Published + var process: BackupProcess = .idle + @Published + var isEnd: Bool = false var currentType: MultiBackupType = .google var storeItems: [[MultiBackupManager.StoreItem]] = [] var phraseItem: MultiBackupManager.StoreItem? - private var validationErrorsOccurred: Bool = false - - init(items: [MultiBackupType]) { - self.items = items - currentIndex = 0 - if !self.items.isEmpty { - currentType = self.items[0] + @Published + var currentIndex: Int = 0 { + didSet { + if currentIndex < items.count { + currentType = items[currentIndex] + } } } + + // MARK: Private + + private var validationErrorsOccurred: Bool = false } // MARK: Action @@ -43,7 +55,7 @@ extension RestoreMultiConnectViewModel { func onClickButton() { if isEnd { let list = checkValidUser() - if list.count == 0 { + if list.isEmpty { enable = true Router.route(to: RouteMap.RestoreLogin.restoreErrorView(.noAccountFound)) return @@ -61,7 +73,7 @@ extension RestoreMultiConnectViewModel { Task { do { let list = try await MultiBackupManager.shared.getCloudDriveItems(from: currentType) - if list.count == 0 { + if list.isEmpty { self.enable = true Router.route(to: RouteMap.RestoreLogin.restoreErrorView(.notfound)) return @@ -70,10 +82,14 @@ extension RestoreMultiConnectViewModel { Router.route(to: RouteMap.Backup.verityPin(.restore) { allow, pin in if allow { let verifyList = self.verify(list: list, with: pin) - if list.count > 0 && self.validationErrorsOccurred { + if !list.isEmpty, self.validationErrorsOccurred { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.enable = true - Router.route(to: RouteMap.RestoreLogin.restoreErrorView(.decryption)) + Router + .route( + to: RouteMap.RestoreLogin + .restoreErrorView(.decryption) + ) } return } @@ -106,12 +122,16 @@ extension RestoreMultiConnectViewModel { } } - private func verify(list: [MultiBackupManager.StoreItem], with pin: String) -> [MultiBackupManager.StoreItem] { + private func verify( + list: [MultiBackupManager.StoreItem], + with pin: String + ) -> [MultiBackupManager.StoreItem] { let pinCode = pin.toPassword() ?? pin var result: [MultiBackupManager.StoreItem] = [] validationErrorsOccurred = false for item in list { - if let _ = try? MultiBackupManager.shared.decryptMnemonic(item.data, password: pinCode) { + if let _ = try? MultiBackupManager.shared + .decryptMnemonic(item.data, password: pinCode) { var newItem = item newItem.code = pin result.append(newItem) @@ -122,20 +142,26 @@ extension RestoreMultiConnectViewModel { } private func createStoreItem(with mnemonic: String) { - guard let hdWallet = WalletManager.shared.createHDWallet(mnemonic: mnemonic), let mnemonicData = hdWallet.mnemonic.data(using: .utf8) else { + guard let hdWallet = WalletManager.shared.createHDWallet(mnemonic: mnemonic), + let mnemonicData = hdWallet.mnemonic.data(using: .utf8) else { HUD.error(title: "empty_wallet_key".localized) return } let key = LocalEnvManager.shared.backupAESKey do { - let dataHexString = try MultiBackupManager.shared.encryptMnemonic(mnemonicData, password: key) + let dataHexString = try MultiBackupManager.shared.encryptMnemonic( + mnemonicData, + password: key + ) let isOldAccount = hdWallet.mnemonic.words.count == 12 - let publicKey = isOldAccount ? hdWallet.getPublicKey() : hdWallet.flowAccountP256Key.publicKey.description + let publicKey = isOldAccount ? hdWallet.getPublicKey() : hdWallet.flowAccountP256Key + .publicKey.description let item = MultiBackupManager.StoreItem( address: "", userId: "", userName: "", publicKey: publicKey, data: dataHexString, keyIndex: 0, - signAlgo: isOldAccount ? Flow.SignatureAlgorithm.ECDSA_SECP256k1.index : Flow.SignatureAlgorithm.ECDSA_P256.index, + signAlgo: isOldAccount ? Flow.SignatureAlgorithm.ECDSA_SECP256k1.index : Flow + .SignatureAlgorithm.ECDSA_P256.index, hashAlgo: Flow.HashAlgorithm.SHA2_256.index, weight: isOldAccount ? 1000 : 500, deviceInfo: IPManager.shared.toParams() @@ -154,8 +180,8 @@ extension RestoreMultiConnectViewModel { func checkValidUser() -> [[MultiBackupManager.StoreItem]] { var items: [String: [MultiBackupManager.StoreItem]] = [:] - storeItems.forEach { list in - list.forEach { storeItem in + for list in storeItems { + for storeItem in list { if var exitList = items[storeItem.userId] { exitList.append(storeItem) items[storeItem.userId] = exitList diff --git a/FRW/Modules/NFT/NFTDetailPage.swift b/FRW/Modules/NFT/NFTDetailPage.swift index 6e7b52ba..9c84ed9f 100644 --- a/FRW/Modules/NFT/NFTDetailPage.swift +++ b/FRW/Modules/NFT/NFTDetailPage.swift @@ -11,16 +11,22 @@ import SPIndicator import SwiftUI import SwiftUIX +// MARK: - NFTDetailPage + struct NFTDetailPage: RouteableView { - static var ShareNFTView: NFTShareView? + // MARK: Lifecycle - var title: String { - "" + init(viewModel: NFTTabViewModel, nft: NFTModel, from childAccount: ChildAccount? = nil) { + _viewModel = StateObject(wrappedValue: viewModel) + _vm = StateObject(wrappedValue: NFTDetailPageViewModel(nft: nft)) + self.fromChildAccount = childAccount } - var isNavigationBarHidden: Bool { - return true - } + // MARK: Internal + + static var ShareNFTView: NFTShareView? + + static var retryCount: Int = 0 @StateObject var viewModel: NFTTabViewModel @@ -28,43 +34,42 @@ struct NFTDetailPage: RouteableView { @StateObject var vm: NFTDetailPageViewModel - @State var opacity: Double = 0 - - var theColor: Color { -// if let color = viewModel.state.colorsMap[vm.nft.imageURL.absoluteString]?[safe: 1] { -// return color.adjustbyTheme() -// } - return Color.LL.Primary.salmonPrimary - } - @State - private var isSharePresented: Bool = false + var opacity: Double = 0 @State - private var isFavorited: Bool = false - + var image: Image? @State - private var items: [UIImage] = [] - - @State var image: Image? - @State var rect: CGRect = .zero + var rect: CGRect = .zero - @State var viewState = CGSize.zero - @State var isDragging = false + @State + var viewState = CGSize.zero + @State + var isDragging = false @State var showImageViewer = false - @Namespace var heroAnimation: Namespace.ID + @Namespace + var heroAnimation: Namespace.ID var player = AVPlayer() var fromChildAccount: ChildAccount? - init(viewModel: NFTTabViewModel, nft: NFTModel, from childAccount: ChildAccount? = nil) { - _viewModel = StateObject(wrappedValue: viewModel) - _vm = StateObject(wrappedValue: NFTDetailPageViewModel(nft: nft)) - fromChildAccount = childAccount + var title: String { + "" + } + + var isNavigationBarHidden: Bool { + true + } + + var theColor: Color { +// if let color = viewModel.state.colorsMap[vm.nft.imageURL.absoluteString]?[safe: 1] { +// return color.adjustbyTheme() +// } + Color.LL.Primary.salmonPrimary } var body: some View { @@ -72,10 +77,17 @@ struct NFTDetailPage: RouteableView { OffsetScrollViewWithAppBar(title: "") { Spacer() .frame(height: 64) - CalloutView(type: .warning, corners: [.topLeading, .topTrailing, .bottomTrailing, .bottomLeading], content: calloutTitle()) - .padding(.horizontal, 18) - .padding(.bottom, 12) - .visibility(WalletManager.shared.accessibleManager.isAccessible(vm.nft) ? .gone : .visible) + CalloutView( + type: .warning, + corners: [.topLeading, .topTrailing, .bottomTrailing, .bottomLeading], + content: calloutTitle() + ) + .padding(.horizontal, 18) + .padding(.bottom, 12) + .visibility( + WalletManager.shared.accessibleManager + .isAccessible(vm.nft) ? .gone : .visible + ) VStack(alignment: .leading) { VStack(spacing: 0) { if vm.nft.isSVG { @@ -100,7 +112,10 @@ struct NFTDetailPage: RouteableView { player.play() } } - .frame(width: UIScreen.screenWidth - 16 * 2, height: UIScreen.screenWidth - 16 * 2) + .frame( + width: UIScreen.screenWidth - 16 * 2, + height: UIScreen.screenWidth - 16 * 2 + ) } // else if let base64ImageData = vm.nft.imageData { // let provider = Base64ImageDataProvider.init(base64String: base64ImageData, cacheKey: vm.nft.id) @@ -156,9 +171,14 @@ struct NFTDetailPage: RouteableView { .padding(.horizontal, 18) .clipped() .scaleEffect(isDragging ? 0.9 : 1) - .animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.8), value: isDragging) - - .rotation3DEffect(Angle(degrees: 5), axis: (x: viewState.width, y: viewState.height, z: 0)) + .animation( + .timingCurve(0.2, 0.8, 0.2, 1, duration: 0.8), + value: isDragging + ) + .rotation3DEffect( + Angle(degrees: 5), + axis: (x: viewState.width, y: viewState.height, z: 0) + ) .modifier(DragGestureViewModifier(onStart: nil, onUpdate: { value in self.viewState = value.translation self.isDragging = true @@ -186,7 +206,10 @@ struct NFTDetailPage: RouteableView { .frame(height: 28) Button { - NotificationCenter.default.post(name: .openNFTCollectionList, object: vm.nft.collection?.id) + NotificationCenter.default.post( + name: .openNFTCollectionList, + object: vm.nft.collection?.id + ) } label: { HStack(alignment: .center, spacing: 6) { KFImage @@ -247,17 +270,29 @@ struct NFTDetailPage: RouteableView { } label: { ZStack(alignment: .center) { Circle() - .strokeBorder(isFavorited ? theColor : Color.LL.outline, lineWidth: 2) - .background(Circle().fill(isFavorited ? theColor.opacity(0.2) : .clear)) + .strokeBorder( + isFavorited ? theColor : Color.LL.outline, + lineWidth: 2 + ) + .background( + Circle() + .fill(isFavorited ? theColor.opacity(0.2) : .clear) + ) .frame(width: 44, height: 44) - DOFavoriteButtonView(isSelected: isFavorited, imageColor: UIColor(theColor)) + DOFavoriteButtonView( + isSelected: isFavorited, + imageColor: UIColor(theColor) + ) } .frame(width: 44, height: 44) .foregroundColor(theColor) } .padding(.horizontal, 6) - .visibility(WalletManager.shared.isSelectedChildAccount ? .gone : .visible) + .visibility( + WalletManager.shared + .isSelectedChildAccount ? .gone : .visible + ) } .padding(.top, 16) .padding(.horizontal, 26) @@ -289,10 +324,20 @@ struct NFTDetailPage: RouteableView { Task { UIImpactFeedbackGenerator(style: .soft).impactOccurred() let image = await vm.image() - let itemSource = ShareActivityItemSource(shareText: vm.nft.title, shareImage: image) - let activityController = UIActivityViewController(activityItems: [image, vm.nft.title, itemSource], applicationActivities: nil) + let itemSource = ShareActivityItemSource( + shareText: vm.nft.title, + shareImage: image + ) + let activityController = UIActivityViewController( + activityItems: [image, vm.nft.title, itemSource], + applicationActivities: nil + ) activityController.isModalInPresentation = true - UIApplication.shared.windows.first?.rootViewController?.present(activityController, animated: true, completion: nil) + UIApplication.shared.windows.first?.rootViewController?.present( + activityController, + animated: true, + completion: nil + ) } } } @@ -309,9 +354,12 @@ struct NFTDetailPage: RouteableView { } } .background( - NFTBlurImageView(colors: viewModel.state.colorsMap[vm.nft.imageURL.absoluteString] ?? []) - .ignoresSafeArea() - .offset(y: -4) + NFTBlurImageView( + colors: viewModel.state + .colorsMap[vm.nft.imageURL.absoluteString] ?? [] + ) + .ignoresSafeArea() + .offset(y: -4) ) .safeAreaInset(edge: .bottom, content: { HStack(spacing: 8) { @@ -372,8 +420,7 @@ struct NFTDetailPage: RouteableView { } if let urlString = vm.nft.response.externalURL, - let url = URL(string: urlString) - { + let url = URL(string: urlString) { Button { Router.route(to: RouteMap.Explore.browser(url)) } label: { @@ -407,12 +454,15 @@ struct NFTDetailPage: RouteableView { vm.animationView.play() } .overlay( - ImageViewer(imageURL: vm.nft.imageURL.absoluteString, - viewerShown: self.$showImageViewer, - backgroundColor: viewModel.state.colorsMap[vm.nft.imageURL.absoluteString]?.first ?? .LL.background, - heroAnimation: heroAnimation) + ImageViewer( + imageURL: vm.nft.imageURL.absoluteString, + viewerShown: $showImageViewer, + backgroundColor: viewModel.state.colorsMap[vm.nft.imageURL.absoluteString]? + .first ?? .LL.background, + heroAnimation: heroAnimation + ) ) - .animation(.spring(), value: self.showImageViewer) + .animation(.spring(), value: showImageViewer) .applyRouteable(self) } @@ -462,7 +512,6 @@ struct NFTDetailPage: RouteableView { viewModel.trigger(.fetchColors(vm.nft.imageURL.absoluteString)) } - static var retryCount: Int = 0 func share() { if let colors = viewModel.state.colorsMap[vm.nft.imageURL.absoluteString] { NFTDetailPage.ShareNFTView = NFTShareView(nft: vm.nft, colors: colors) @@ -482,6 +531,17 @@ struct NFTDetailPage: RouteableView { } } + // MARK: Private + + @State + private var isSharePresented: Bool = false + + @State + private var isFavorited: Bool = false + + @State + private var items: [UIImage] = [] + private func calloutTitle() -> String { let token = vm.nft.title let account = WalletManager.shared.selectedAccountWalletName @@ -490,8 +550,11 @@ struct NFTDetailPage: RouteableView { } } +// MARK: - NFTDetailPage_Previews + struct NFTDetailPage_Previews: PreviewProvider { static var nft = NFTTabViewModel.testNFT() + static var previews: some View { NFTDetailPage(viewModel: NFTTabViewModel(), nft: nft) } diff --git a/FRW/Modules/NFT/NFTEmptyView_dev.swift b/FRW/Modules/NFT/NFTEmptyView_dev.swift index be838dff..f925089c 100644 --- a/FRW/Modules/NFT/NFTEmptyView_dev.swift +++ b/FRW/Modules/NFT/NFTEmptyView_dev.swift @@ -1,11 +1,12 @@ // -// NFTEmptyView.swift +// NFTEmptyView_dev.swift // Flow Wallet // // Created by cat on 2022/5/13. // import SwiftUI + // import RiveRuntime struct NFTEmptyView: View { diff --git a/FRW/Modules/NFT/NFTModel/NFTModel.swift b/FRW/Modules/NFT/NFTModel/NFTModel.swift index 43608a7f..3b26c6b8 100644 --- a/FRW/Modules/NFT/NFTModel/NFTModel.swift +++ b/FRW/Modules/NFT/NFTModel/NFTModel.swift @@ -13,6 +13,8 @@ let placeholder: String = AppPlaceholder.image // TODO: which filter? let filterMetadata = ["uri", "img", "description"] +// MARK: - NFTCollection + struct NFTCollection: Codable { let collection: NFTCollectionInfo var count: Int @@ -25,12 +27,16 @@ struct NFTCollection: Codable { } } +// MARK: - EVMNFTCollectionResponse + struct EVMNFTCollectionResponse: Codable { let tokens: [NFTCollectionInfo]? let chainId: Int? let network: String? } +// MARK: - NFTCollectionInfo + struct NFTCollectionInfo: Codable, Hashable, Mockable { let id: String let name: String? @@ -68,7 +74,19 @@ struct NFTCollectionInfo: Codable, Hashable, Mockable { } static func mock() -> NFTCollectionInfo { - return NFTCollectionInfo(id: randomString(), name: randomString(), contractName: randomString(), address: randomString(), logo: randomString(), banner: randomString(), officialWebsite: randomString(), description: randomString(), path: ContractPath.mock(), evmAddress: nil, flowIdentifier: nil) + NFTCollectionInfo( + id: randomString(), + name: randomString(), + contractName: randomString(), + address: randomString(), + logo: randomString(), + banner: randomString(), + officialWebsite: randomString(), + description: randomString(), + path: ContractPath.mock(), + evmAddress: nil, + flowIdentifier: nil + ) } } @@ -82,6 +100,8 @@ extension NFTCollectionInfo { } } +// MARK: - ContractPath + struct ContractPath: Codable, Hashable, Mockable { let storagePath: String let publicPath: String @@ -91,23 +111,74 @@ struct ContractPath: Codable, Hashable, Mockable { let privateType: String? static func mock() -> ContractPath { - return ContractPath(storagePath: randomString(), publicPath: randomString(), privatePath: "", publicCollectionName: randomString(), publicType: randomString(), privateType: randomString()) + ContractPath( + storagePath: randomString(), + publicPath: randomString(), + privatePath: "", + publicCollectionName: randomString(), + publicType: randomString(), + privateType: randomString() + ) } - + func storagePathId() -> String { let list = storagePath.components(separatedBy: "/") - if list.count > 0 { + if !list.isEmpty { return list.last ?? storagePath } return storagePath } } +// MARK: - NFTModel + struct NFTModel: Codable, Hashable, Identifiable { - var id: String { - return response.uniqueId + // MARK: Lifecycle + + init( + _ response: NFTResponse, + in collection: NFTCollectionInfo?, + from _: FlowModel.CollectionInfo? = nil + ) { + if let imgUrl = response.postMedia?.image, let url = URL(string: imgUrl) { + if response.postMedia?.isSvg == true { + self.image = URL(string: imgUrl) ?? URL(string: placeholder)! + self.isSVG = true + } else if let svgStr = imgUrl.parseBase64ToSVG() { + self.imageSVGStr = svgStr.decodeBase64WithFixed() + self.isSVG = true + self.image = URL(string: placeholder)! + } else { + if imgUrl.hasPrefix("https://lilico.infura-ipfs.io/ipfs/") { + let newImgURL = imgUrl + .replace( + by: [ + "https://lilico.infura-ipfs.io/ipfs/": "https://lilico.app/api/ipfs/", + ] + ) + self.image = URL(string: newImgURL)! + } else { + self.image = url + } + + self.isSVG = false + } + } else { + self.image = URL(string: placeholder)! + } + + if let videoUrl = response.postMedia?.video { + self.video = URL(string: videoUrl) + } + + self.subtitle = response.postMedia?.description ?? "" + self.title = response.postMedia?.title ?? response.collectionName ?? "" + self.collection = collection + self.response = response } + // MARK: Internal + let image: URL var video: URL? let title: String @@ -118,6 +189,10 @@ struct NFTModel: Codable, Hashable, Identifiable { var imageSVGStr: String? = nil + var id: String { + response.uniqueId + } + var imageURL: URL { if isSVG { return image.absoluteString.convertedSVGURL() ?? URL(string: placeholder)! @@ -130,39 +205,6 @@ struct NFTModel: Codable, Hashable, Identifiable { collection?.contractName?.trim() == "TopShot" } - init(_ response: NFTResponse, in collection: NFTCollectionInfo?, from _: FlowModel.CollectionInfo? = nil) { - if let imgUrl = response.postMedia?.image, let url = URL(string: imgUrl) { - if response.postMedia?.isSvg == true { - image = URL(string: imgUrl) ?? URL(string: placeholder)! - isSVG = true - } else if let svgStr = imgUrl.parseBase64ToSVG() { - imageSVGStr = svgStr.decodeBase64WithFixed() - isSVG = true - image = URL(string: placeholder)! - } else { - if imgUrl.hasPrefix("https://lilico.infura-ipfs.io/ipfs/") { - let newImgURL = imgUrl.replace(by: ["https://lilico.infura-ipfs.io/ipfs/": "https://lilico.app/api/ipfs/"]) - image = URL(string: newImgURL)! - } else { - image = url - } - - isSVG = false - } - } else { - image = URL(string: placeholder)! - } - - if let videoUrl = response.postMedia?.video { - video = URL(string: videoUrl) - } - - subtitle = response.postMedia?.description ?? "" - title = response.postMedia?.title ?? response.collectionName ?? "" - self.collection = collection - self.response = response - } - var declare: String { if let dec = response.postMedia?.description { return dec @@ -199,7 +241,8 @@ struct NFTModel: Codable, Hashable, Identifiable { } return traits.filter { trait in - !filterMetadata.contains(trait.name?.lowercased() ?? "") && !(trait.value ?? "").isEmpty && !(trait.value ?? "").hasPrefix("https://") + !filterMetadata.contains(trait.name?.lowercased() ?? "") && !(trait.value ?? "") + .isEmpty && !(trait.value ?? "").hasPrefix("https://") } } @@ -215,22 +258,18 @@ struct NFTModel: Codable, Hashable, Identifiable { } var publicIdentifier: String { - guard let path = collection?.path?.privatePath, let identifier = path.split(separator: "/").last else { + guard let path = collection?.path?.privatePath, + let identifier = path.split(separator: "/").last else { return "" } return String(identifier) } } +// MARK: - CollectionItem + class CollectionItem: Identifiable, ObservableObject { - static func mock() -> CollectionItem { - let item = CollectionItem() - item.isEnd = true - item.nfts = [0, 1, 2, 3].map { _ in - NFTModel(NFTResponse(id: "", name: "", description: "", thumbnail: "", externalURL: "", contractAddress: "", evmAddress: "", address: "", collectionID: "", collectionName: "", collectionDescription: "", collectionSquareImage: "", collectionExternalURL: "", collectionContractName: "", collectionBannerImage: "", traits: [], postMedia: NFTPostMedia(title: "", description: "", video: "", isSvg: false)), in: nil) - } - return item - } + // MARK: Internal var address: String = "" var id = UUID() @@ -238,7 +277,8 @@ class CollectionItem: Identifiable, ObservableObject { var collectionId: String = "" var count: Int = 0 var collection: NFTCollectionInfo? - @Published var nfts: [NFTModel] = [] + @Published + var nfts: [NFTModel] = [] var loadCallback: ((Bool) -> Void)? var loadCallback2: ((Bool) -> Void)? @@ -246,7 +286,7 @@ class CollectionItem: Identifiable, ObservableObject { var isRequesting: Bool = false var showName: String { - return collection?.name ?? "" + collection?.name ?? "" } var iconURL: URL { @@ -261,6 +301,36 @@ class CollectionItem: Identifiable, ObservableObject { return URL(string: placeholder)! } + static func mock() -> CollectionItem { + let item = CollectionItem() + item.isEnd = true + item.nfts = [0, 1, 2, 3].map { _ in + NFTModel( + NFTResponse( + id: "", + name: "", + description: "", + thumbnail: "", + externalURL: "", + contractAddress: "", + evmAddress: "", + address: "", + collectionID: "", + collectionName: "", + collectionDescription: "", + collectionSquareImage: "", + collectionExternalURL: "", + collectionContractName: "", + collectionBannerImage: "", + traits: [], + postMedia: NFTPostMedia(title: "", description: "", video: "", isSvg: false) + ), + in: nil + ) + } + return item + } + func loadFromCache() { if let cachedNFTs = NFTUIKitCache.cache.getNFTs(collectionId: collectionId) { let models = cachedNFTs.map { NFTModel($0, in: self.collection) } @@ -268,7 +338,7 @@ class CollectionItem: Identifiable, ObservableObject { } } - func load(address: String? = nil) { + func load(address _: String? = nil) { if isRequesting || isEnd { return } @@ -278,7 +348,10 @@ class CollectionItem: Identifiable, ObservableObject { let limit = 24 Task { do { - let response = try await requestCollectionListDetail(offset: nfts.count, limit: limit) + let response = try await requestCollectionListDetail( + offset: nfts.count, + limit: limit + ) DispatchQueue.main.async { self.isRequesting = false @@ -312,6 +385,8 @@ class CollectionItem: Identifiable, ObservableObject { } } + // MARK: Private + private func appendNFTsNoDuplicated(_ newNFTs: [NFTModel]) { for nft in newNFTs { let exist = nfts.first { $0.id == nft.id } @@ -322,11 +397,23 @@ class CollectionItem: Identifiable, ObservableObject { } } - private func requestCollectionListDetail(offset: Int, limit: Int = 24, fromAddress: String? = nil) async throws -> NFTListResponse { + private func requestCollectionListDetail( + offset: Int, + limit: Int = 24, + fromAddress: String? = nil + ) async throws -> NFTListResponse { let addr = fromAddress ?? WalletManager.shared.selectedAccountAddress - let request = NFTCollectionDetailListRequest(address: addr, collectionIdentifier: collection?.id ?? "", offset: offset, limit: limit) + let request = NFTCollectionDetailListRequest( + address: addr, + collectionIdentifier: collection?.id ?? "", + offset: offset, + limit: limit + ) let from: FRWAPI.From = EVMAccountManager.shared.selectedAccount == nil ? .main : .evm - let response: NFTListResponse = try await Network.request(FRWAPI.NFT.collectionDetailList(request, from)) + let response: NFTListResponse = try await Network.request(FRWAPI.NFT.collectionDetailList( + request, + from + )) return response } diff --git a/FRW/Modules/NFT/NFTTransferView.swift b/FRW/Modules/NFT/NFTTransferView.swift index ef68a0bc..120f6550 100644 --- a/FRW/Modules/NFT/NFTTransferView.swift +++ b/FRW/Modules/NFT/NFTTransferView.swift @@ -12,38 +12,76 @@ import SwiftUI import Web3Core import web3swift -class NFTTransferViewModel: ObservableObject { - @Published var nft: NFTModel - @Published var targetContact: Contact - @Published var isValidNFT = true - @Published var isEmptyTransation = true - - private var isRequesting: Bool = false +// MARK: - NFTTransferViewModel - var fromChildAccount: ChildAccount? +class NFTTransferViewModel: ObservableObject { + // MARK: Lifecycle init(nft: NFTModel, targetContact: Contact, fromChildAccount: ChildAccount? = nil) { self.nft = nft self.targetContact = targetContact self.fromChildAccount = fromChildAccount checkNFTReachable() - NotificationCenter.default.addObserver(self, selector: #selector(onHolderChanged(noti:)), name: .transactionStatusDidChanged, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(onHolderChanged(noti:)), + name: .transactionStatusDidChanged, + object: nil + ) } deinit { NotificationCenter.default.removeObserver(self) } + // MARK: Internal + + @Published + var nft: NFTModel + @Published + var targetContact: Contact + @Published + var isValidNFT = true + @Published + var isEmptyTransation = true + + var fromChildAccount: ChildAccount? + var fromTargetContent: Contact { if let account = fromChildAccount { - let contact = Contact(address: account.showAddress, avatar: account.icon, contactName: account.aName, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName) + let contact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: account.aName, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName + ) return contact } else if let account = EVMAccountManager.shared.selectedAccount { let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - let contact = Contact(address: account.showAddress, avatar: nil, contactName: user.name, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user) + let contact = Contact( + address: account.showAddress, + avatar: nil, + contactName: user.name, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user + ) return contact } else if let account = ChildAccountManager.shared.selectedChildAccount { - let contact = Contact(address: account.showAddress, avatar: account.icon, contactName: account.aName, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName) + let contact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: account.aName, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName + ) return contact } else { return UserManager.shared.userInfo!.toContactWithCurrentUserAddress() @@ -58,7 +96,8 @@ class NFTTransferViewModel: ObservableObject { isValidNFT = true return } - if EVMAccountManager.shared.selectedAccount != nil, let identifier = nft.response.flowIdentifier { + if EVMAccountManager.shared.selectedAccount != nil, + let identifier = nft.response.flowIdentifier { isValidNFT = true return } @@ -92,7 +131,10 @@ class NFTTransferViewModel: ObservableObject { return } - guard let toAddress = targetContact.address, let primaryAddress = WalletManager.shared.getPrimaryWalletAddress(), let currentAddress = WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress() else { + guard let toAddress = targetContact.address, + let primaryAddress = WalletManager.shared.getPrimaryWalletAddress(), + let currentAddress = WalletManager.shared + .getWatchAddressOrChildAccountAddressOrPrimaryAddress() else { return } @@ -108,39 +150,54 @@ class NFTTransferViewModel: ObservableObject { Task { do { var fromAccountType = WalletManager.shared.isSelectedEVMAccount ? AccountType.coa - : (ChildAccountManager.shared.selectedChildAccount == nil ? AccountType.flow : AccountType.linked) + : + ( + ChildAccountManager.shared.selectedChildAccount == nil ? AccountType + .flow : AccountType.linked + ) if fromChildAccount != nil { fromAccountType = .linked } var toAccountType = toAddress.isEVMAddress ? AccountType.coa : AccountType.flow if toAccountType == .flow { - let isChild = ChildAccountManager.shared.childAccounts.contains { $0.addr == toAddress } + let isChild = ChildAccountManager.shared.childAccounts + .contains { $0.addr == toAddress } if isChild { toAccountType = .linked } } - if toAccountType == .coa, toAddress != EVMAccountManager.shared.accounts.first?.showAddress { + if toAccountType == .coa, + toAddress != EVMAccountManager.shared.accounts.first?.showAddress { toAccountType = .eoa } var tid: Flow.ID? switch (fromAccountType, toAccountType) { case (.flow, .flow): - tid = try await FlowNetwork.transferNFT(to: Flow.Address(hex: toAddress), nft: nft) + tid = try await FlowNetwork.transferNFT( + to: Flow.Address(hex: toAddress), + nft: nft + ) case (.flow, .coa): let nftId = nft.response.id - let identifier = self.nft.collection?.flowIdentifier ?? nft.response.flowIdentifier + let identifier = self.nft.collection?.flowIdentifier ?? nft.response + .flowIdentifier guard let identifier, let IdInt = UInt64(nftId) else { throw NFTError.sendInvalidAddress } - tid = try await FlowNetwork.bridgeNFTToEVM(identifier: identifier, ids: [IdInt], fromEvm: false) + tid = try await FlowNetwork.bridgeNFTToEVM( + identifier: identifier, + ids: [IdInt], + fromEvm: false + ) case (.flow, .eoa): log.debug("[NFT] flow to eoa send") let nftId = nft.response.id guard let nftAddress = self.nft.collection?.address, - let identifier = nft.collection?.flowIdentifier ?? nft.response.flowIdentifier, + let identifier = nft.collection?.flowIdentifier ?? nft.response + .flowIdentifier, let toAddress = targetContact.address?.stripHexPrefix() else { throw NFTError.sendInvalidAddress @@ -151,10 +208,10 @@ class NFTTransferViewModel: ObservableObject { id: nftId, toAddress: toAddress ) - case (.coa, .flow): let nftId = nft.response.id - guard let identifier = nft.collection?.flowIdentifier ?? nft.response.flowIdentifier else { + guard let identifier = nft.collection?.flowIdentifier ?? nft.response + .flowIdentifier else { throw NFTError.noCollectionInfo } if primaryAddress.lowercased() == toAddress.lowercased() { @@ -162,11 +219,18 @@ class NFTTransferViewModel: ObservableObject { throw NFTError.sendInvalidAddress } - tid = try await FlowNetwork.bridgeNFTToEVM(identifier: identifier, ids: [IdInt], fromEvm: true) + tid = try await FlowNetwork.bridgeNFTToEVM( + identifier: identifier, + ids: [IdInt], + fromEvm: true + ) } else { - tid = try await FlowNetwork.bridgeNFTFromEVMToAnyFlow(identifier: identifier, id: nftId, receiver: toAddress) + tid = try await FlowNetwork.bridgeNFTFromEVMToAnyFlow( + identifier: identifier, + id: nftId, + receiver: toAddress + ) } - case (.coa, .eoa): // sendTransaction @@ -177,12 +241,21 @@ class NFTTransferViewModel: ObservableObject { else { throw NFTError.sendInvalidAddress } - guard let data = erc721?.contract.method("safeTransferFrom", parameters: [coaAddress, toAddress, nftId], extraData: nil) else { + guard let data = erc721?.contract.method( + "safeTransferFrom", + parameters: [coaAddress, toAddress, nftId], + extraData: nil + ) else { throw NFTError.sendInvalidAddress } log.debug("[NFT] nftID: \(nftId)") log.debug("[NFT] data:\(data.hexString)") - tid = try await FlowNetwork.sendTransaction(amount: "0", data: data, toAddress: evmContractAddress.stripHexPrefix(), gas: WalletManager.defaultGas) + tid = try await FlowNetwork.sendTransaction( + amount: "0", + data: data, + toAddress: evmContractAddress.stripHexPrefix(), + gas: WalletManager.defaultGas + ) log.debug("[NFT] tix:\(String(describing: tid))") case (.flow, .linked): // parent to child user move 'transferNFTToChild' @@ -190,7 +263,12 @@ class NFTTransferViewModel: ObservableObject { let collection = nft.collection else { throw NFTError.sendInvalidAddress } let identifier = nft.publicIdentifier - tid = try await FlowNetwork.moveNFTToChild(nftId: nftId, childAddress: toAddress, identifier: identifier, collection: collection) + tid = try await FlowNetwork.moveNFTToChild( + nftId: nftId, + childAddress: toAddress, + identifier: identifier, + collection: collection + ) case (.linked, .flow): guard let nftId = UInt64(nft.response.id), let collection = nft.collection @@ -198,9 +276,20 @@ class NFTTransferViewModel: ObservableObject { let identifier = nft.publicIdentifier let childAddr = fromChildAccount?.addr ?? currentAddress if toAddress.lowercased() == primaryAddress.lowercased() { - tid = try await FlowNetwork.moveNFTToParent(nftId: nftId, childAddress: childAddr, identifier: identifier, collection: collection) + tid = try await FlowNetwork.moveNFTToParent( + nftId: nftId, + childAddress: childAddr, + identifier: identifier, + collection: collection + ) } else { - tid = try await FlowNetwork.sendChildNFT(nftId: nftId, childAddress: childAddr, toAddress: toAddress, identifier: identifier, collection: collection) + tid = try await FlowNetwork.sendChildNFT( + nftId: nftId, + childAddress: childAddr, + toAddress: toAddress, + identifier: identifier, + collection: collection + ) } case (.linked, .linked): guard let nftId = UInt64(nft.response.id), @@ -208,9 +297,16 @@ class NFTTransferViewModel: ObservableObject { else { throw NFTError.sendInvalidAddress } let identifier = nft.publicIdentifier let childAddr = fromChildAccount?.addr ?? currentAddress - tid = try await FlowNetwork.sendChildNFTToChild(nftId: nftId, childAddress: childAddr, toAddress: toAddress, identifier: identifier, collection: collection) + tid = try await FlowNetwork.sendChildNFTToChild( + nftId: nftId, + childAddress: childAddr, + toAddress: toAddress, + identifier: identifier, + collection: collection + ) case (.linked, .coa): - guard let nftIdentifier = nft.response.flowIdentifier,let nftId = UInt64(nft.response.id) else { + guard let nftIdentifier = nft.response.flowIdentifier, + let nftId = UInt64(nft.response.id) else { return } let childAddr = fromChildAccount?.addr ?? currentAddress @@ -218,10 +314,12 @@ class NFTTransferViewModel: ObservableObject { .bridgeChildNFTToEvm( nft: nftIdentifier, id: nftId, - child: childAddr) + child: childAddr + ) case (.coa, .linked): guard let nftIdentifier = nft.response.flowIdentifier, - let nftId = UInt64(nft.response.id) else { + let nftId = UInt64(nft.response.id) + else { return } let childAddr = fromChildAccount?.addr ?? toAddress @@ -229,20 +327,29 @@ class NFTTransferViewModel: ObservableObject { .bridgeChildNFTFromEvm( nft: nftIdentifier, id: nftId, - child: childAddr) + child: childAddr + ) default: failedBlock() return } - let model = NFTTransferModel(nft: nft, target: self.targetContact, from: primaryAddress) + let model = NFTTransferModel( + nft: nft, + target: self.targetContact, + from: primaryAddress + ) guard let data = try? JSONEncoder().encode(model), let tid = tid else { failedBlock() return } DispatchQueue.main.async { - let holder = TransactionManager.TransactionHolder(id: tid, type: .transferNFT, data: data) + let holder = TransactionManager.TransactionHolder( + id: tid, + type: .transferNFT, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) HUD.dismissLoading() Router.dismiss() @@ -260,18 +367,85 @@ class NFTTransferViewModel: ObservableObject { isEmptyTransation = TransactionManager.shared.holders.count == 0 } - @objc private func onHolderChanged(noti _: Notification) { + // MARK: Private + + private var isRequesting: Bool = false + + @objc + private func onHolderChanged(noti _: Notification) { checkTransaction() } } +// MARK: - NFTTransferView + struct NFTTransferView: View { - @StateObject var vm: NFTTransferViewModel + // MARK: Lifecycle init(nft: NFTModel, target: Contact, fromChildAccount: ChildAccount? = nil) { - _vm = StateObject(wrappedValue: NFTTransferViewModel(nft: nft, targetContact: target, fromChildAccount: fromChildAccount)) + _vm = StateObject(wrappedValue: NFTTransferViewModel( + nft: nft, + targetContact: target, + fromChildAccount: fromChildAccount + )) } + // MARK: Internal + + struct SendConfirmProgressView: View { + // MARK: Internal + + var body: some View { + HStack(spacing: 12) { + ForEach(0.. some View { - VStack(spacing: 5) { - // avatar - ZStack { - if contact.user?.emoji != nil { - contact.user?.emoji.icon(size: 44) - } else if let avatar = contact.avatar?.convertedAvatarString(), avatar.isEmpty == false { - KFImage.url(URL(string: avatar)) - .placeholder { - Image("placeholder") - .resizable() - } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 44, height: 44) - } else if contact.needShowLocalAvatar { - Image(contact.localAvatar ?? "") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 44, height: 44) - } else if let user = contact.user { - user.emoji.icon(size: 44) - } else { - Text(String((contact.contactName?.first ?? "A").uppercased())) - .foregroundColor(.Theme.Accent.grey) - .font(.inter(size: 24, weight: .semibold)) - } - } - .frame(width: 44, height: 44) - .background(.Theme.Accent.grey.opacity(0.16)) - .clipShape(Circle()) - - // contact name - Text(contact.user?.name ?? contact.contactName ?? contact.displayName) - .foregroundColor(.LL.Neutrals.neutrals1) - .font(.inter(size: 14, weight: .semibold)) - .lineLimit(1) - - // address - Text(contact.address ?? "0x") - .foregroundColor(.LL.Neutrals.note) - .font(.inter(size: 12, weight: .regular)) - .lineLimit(1) - } - .frame(maxWidth: .infinity) - } - var detailView: some View { HStack(alignment: .center, spacing: 13) { KFImage.url(vm.nft.imageURL) @@ -411,49 +541,51 @@ struct NFTTransferView: View { } } - struct SendConfirmProgressView: View { - private let totalNum: Int = 7 - @State private var step: Int = 0 - private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - - var body: some View { - HStack(spacing: 12) { - ForEach(0 ..< totalNum, id: \.self) { index in - if step == index { - Image("icon-right-arrow-1") - .renderingMode(.template) - .foregroundColor(.Theme.Accent.green) - } else { - switch index { - case 0: - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.Theme.Accent.green) - case 1: - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.Theme.Accent.green) - case 2: - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.Theme.Accent.green) - default: - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.Theme.Accent.green) + func contactView(contact: Contact) -> some View { + VStack(spacing: 5) { + // avatar + ZStack { + if contact.user?.emoji != nil { + contact.user?.emoji.icon(size: 44) + } else if let avatar = contact.avatar?.convertedAvatarString(), + avatar.isEmpty == false { + KFImage.url(URL(string: avatar)) + .placeholder { + Image("placeholder") + .resizable() } - } - } - } - .onReceive(timer) { _ in - DispatchQueue.main.async { - if step < totalNum - 1 { - step += 1 - } else { - step = 0 - } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 44, height: 44) + } else if contact.needShowLocalAvatar { + Image(contact.localAvatar ?? "") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 44, height: 44) + } else if let user = contact.user { + user.emoji.icon(size: 44) + } else { + Text(String((contact.contactName?.first ?? "A").uppercased())) + .foregroundColor(.Theme.Accent.grey) + .font(.inter(size: 24, weight: .semibold)) } } + .frame(width: 44, height: 44) + .background(.Theme.Accent.grey.opacity(0.16)) + .clipShape(Circle()) + + // contact name + Text(contact.user?.name ?? contact.contactName ?? contact.displayName) + .foregroundColor(.LL.Neutrals.neutrals1) + .font(.inter(size: 14, weight: .semibold)) + .lineLimit(1) + + // address + Text(contact.address ?? "0x") + .foregroundColor(.LL.Neutrals.note) + .font(.inter(size: 12, weight: .regular)) + .lineLimit(1) } + .frame(maxWidth: .infinity) } } diff --git a/FRW/Modules/NFT/ViewModel/AddCollectionViewModel.swift b/FRW/Modules/NFT/ViewModel/AddCollectionViewModel.swift index c4fe150c..3729c48e 100644 --- a/FRW/Modules/NFT/ViewModel/AddCollectionViewModel.swift +++ b/FRW/Modules/NFT/ViewModel/AddCollectionViewModel.swift @@ -9,13 +9,37 @@ import Combine import Flow import Foundation +// MARK: - AddCollectionViewModel + class AddCollectionViewModel: ObservableObject { - private var cancelSets = Set() + // MARK: Lifecycle - @Published var searchQuery = "" - @Published var isAddingCollection: Bool = false - @Published var isConfirmSheetPresented: Bool = false - @Published var isMock: Bool = false + init() { + Task { + await load() + } + + NotificationCenter.default.publisher(for: .nftCollectionsDidChanged).sink { [weak self] _ in + guard let self = self else { + return + } + + Task { + await self.load() + } + }.store(in: &cancelSets) + } + + // MARK: Internal + + @Published + var searchQuery = "" + @Published + var isAddingCollection: Bool = false + @Published + var isConfirmSheetPresented: Bool = false + @Published + var isMock: Bool = false var liveList: [NFTCollectionItem] { if isMock { @@ -28,11 +52,12 @@ class AddCollectionViewModel: ObservableObject { var list: [NFTCollectionItem] = [] list = collectionList.filter { item in let name = item.collection.name ?? "" - + if name.localizedCaseInsensitiveContains(searchQuery) { return true } - if let des = item.collection.description, des.localizedCaseInsensitiveContains(searchQuery) { + if let des = item.collection.description, + des.localizedCaseInsensitiveContains(searchQuery) { return true } return false @@ -40,24 +65,6 @@ class AddCollectionViewModel: ObservableObject { return list } - private var collectionList: [NFTCollectionItem] = [] - - init() { - Task { - await load() - } - - NotificationCenter.default.publisher(for: .nftCollectionsDidChanged).sink { [weak self] _ in - guard let self = self else { - return - } - - Task { - await self.load() - } - }.store(in: &cancelSets) - } - func load() async { DispatchQueue.main.async { self.isMock = true @@ -87,12 +94,18 @@ class AddCollectionViewModel: ObservableObject { self.isMock = false } } + + // MARK: Private + + private var cancelSets = Set() + + private var collectionList: [NFTCollectionItem] = [] } extension AddCollectionViewModel { func hasTrending() -> Bool { // TODO: - return false + false } func addCollectionAction(item: NFTCollectionItem) { @@ -123,7 +136,10 @@ extension AddCollectionViewModel { Task { do { - let transactionId = try await FlowNetwork.addCollection(at: Flow.Address(hex: address), collection: item.collection) + let transactionId = try await FlowNetwork.addCollection( + at: Flow.Address(hex: address), + collection: item.collection + ) guard let data = try? JSONEncoder().encode(item.collection) else { failedBlock() @@ -134,7 +150,11 @@ extension AddCollectionViewModel { self.isAddingCollection = false self.isConfirmSheetPresented = false - let holder = TransactionManager.TransactionHolder(id: transactionId, type: .addCollection, data: data) + let holder = TransactionManager.TransactionHolder( + id: transactionId, + type: .addCollection, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) } } catch { @@ -145,6 +165,8 @@ extension AddCollectionViewModel { } } +// MARK: - NFTCollectionItem + struct NFTCollectionItem: Hashable, Mockable, Codable { enum ItemStatus: Codable { case idle @@ -156,6 +178,10 @@ struct NFTCollectionItem: Hashable, Mockable, Codable { var collection: NFTCollectionInfo var status: ItemStatus = .idle + static func mock() -> NFTCollectionItem { + NFTCollectionItem(collection: NFTCollectionInfo.mock(), status: .idle) + } + func processName() -> String { switch status { case .idle: @@ -168,8 +194,4 @@ struct NFTCollectionItem: Hashable, Mockable, Codable { return "nft_collection_add_failed".localized } } - - static func mock() -> NFTCollectionItem { - return NFTCollectionItem(collection: NFTCollectionInfo.mock(), status: .idle) - } } diff --git a/FRW/Modules/NFT/Views/NFTCollectionListView.swift b/FRW/Modules/NFT/Views/NFTCollectionListView.swift index b604f569..1bec9c06 100644 --- a/FRW/Modules/NFT/Views/NFTCollectionListView.swift +++ b/FRW/Modules/NFT/Views/NFTCollectionListView.swift @@ -8,34 +8,22 @@ import Kingfisher import SwiftUI -class NFTCollectionListViewViewModel: ObservableObject { - @Published var collection: CollectionItem - @Published var nfts: [NFTModel] = [] - - var address: String? - var collectionPath: String? - @Published var isLoading = false +// MARK: - NFTCollectionListViewViewModel - private var proxy: ScrollViewProxy? +class NFTCollectionListViewViewModel: ObservableObject { + // MARK: Lifecycle convenience init(address: String, path: String) { let item = CollectionItem.mock() self.init(collection: item) self.address = address - collectionPath = path - isLoading = true - } - - func load(address: String, path: String) { - self.address = address - collectionPath = path - isLoading = true - fetch() + self.collectionPath = path + self.isLoading = true } init(collection: CollectionItem) { self.collection = collection - nfts = collection.nfts + self.nfts = collection.nfts collection.loadCallback2 = { [weak self] result in guard let self = self else { @@ -54,10 +42,29 @@ class NFTCollectionListViewViewModel: ObservableObject { } if collection.nfts.isEmpty { - collection.load(address: self.address) + collection.load(address: address) } } + // MARK: Internal + + @Published + var collection: CollectionItem + @Published + var nfts: [NFTModel] = [] + + var address: String? + var collectionPath: String? + @Published + var isLoading = false + + func load(address: String, path: String) { + self.address = address + collectionPath = path + isLoading = true + fetch() + } + func load(collection: CollectionItem) { self.collection = collection nfts = collection.nfts @@ -91,9 +98,19 @@ class NFTCollectionListViewViewModel: ObservableObject { do { let address = address ?? WalletManager.shared.selectedAccountAddress - let from: FRWAPI.From = EVMAccountManager.shared.selectedAccount != nil ? .evm : .main - let request = NFTCollectionDetailListRequest(address: address, collectionIdentifier: path, offset: 0, limit: 24) - let response: NFTListResponse = try await Network.request(FRWAPI.NFT.collectionDetailList(request, from)) + let from: FRWAPI.From = EVMAccountManager.shared + .selectedAccount != nil ? .evm : .main + let request = NFTCollectionDetailListRequest( + address: address, + collectionIdentifier: path, + offset: 0, + limit: 24 + ) + let response: NFTListResponse = try await Network + .request(FRWAPI.NFT.collectionDetailList( + request, + from + )) DispatchQueue.main.async { self.collection = response.toCollectionItem() @@ -111,24 +128,16 @@ class NFTCollectionListViewViewModel: ObservableObject { self.proxy = proxy collection.load() } -} - -struct NFTCollectionListView: RouteableView { - @StateObject var viewModel: NFTTabViewModel - @StateObject var vm: NFTCollectionListViewViewModel - @State var opacity: Double = 0 - @Namespace var imageEffect + // MARK: Private - var childAccount: ChildAccount? + private var proxy: ScrollViewProxy? +} - var title: String { - return "" - } +// MARK: - NFTCollectionListView - var isNavigationBarHidden: Bool { - return true - } +struct NFTCollectionListView: RouteableView { + // MARK: Lifecycle init(viewModel: NFTTabViewModel, collection: CollectionItem) { _viewModel = StateObject(wrappedValue: viewModel) @@ -137,36 +146,77 @@ struct NFTCollectionListView: RouteableView { init(address: String, path: String, from linkedAccount: ChildAccount?) { _viewModel = StateObject(wrappedValue: NFTTabViewModel()) - _vm = StateObject(wrappedValue: NFTCollectionListViewViewModel(address: address, path: path)) - childAccount = linkedAccount + _vm = StateObject(wrappedValue: NFTCollectionListViewViewModel( + address: address, + path: path + )) + self.childAccount = linkedAccount + } + + // MARK: Internal + + @StateObject + var viewModel: NFTTabViewModel + @StateObject + var vm: NFTCollectionListViewViewModel + + @State + var opacity: Double = 0 + @Namespace + var imageEffect + + var childAccount: ChildAccount? + + var title: String { + "" + } + + var isNavigationBarHidden: Bool { + true } var body: some View { ZStack { ScrollViewReader { proxy in - OffsetScrollViewWithAppBar(title: vm.collection.showName, loadMoreEnabled: true, loadMoreCallback: { - if vm.collection.isRequesting || vm.collection.isEnd { - return - } + OffsetScrollViewWithAppBar( + title: vm.collection.showName, + loadMoreEnabled: true, + loadMoreCallback: { + if vm.collection.isRequesting || vm.collection.isEnd { + return + } - vm.loadMoreAction(proxy: proxy) - }, isNoData: vm.collection.isEnd) { + vm.loadMoreAction(proxy: proxy) + }, + isNoData: vm.collection.isEnd + ) { Spacer() .frame(height: 64) if let collection = vm.collection.collection { - CalloutView(type: .warning, corners: [.topLeading, .topTrailing, .bottomTrailing, .bottomLeading], content: calloutTitle()) - .padding(.bottom, 20) - .padding(.horizontal, 18) - .visibility(WalletManager.shared.accessibleManager.isAccessible(collection) ? .gone : .visible) + CalloutView( + type: .warning, + corners: [.topLeading, .topTrailing, .bottomTrailing, .bottomLeading], + content: calloutTitle() + ) + .padding(.bottom, 20) + .padding(.horizontal, 18) + .visibility( + WalletManager.shared.accessibleManager + .isAccessible(collection) ? .gone : .visible + ) } InfoView(collection: vm.collection) .padding(.bottom, 24) .mockPlaceholder(vm.isLoading) - NFTListView(list: vm.nfts, imageEffect: imageEffect, fromChildAccount: childAccount) - .id(999) - .mockPlaceholder(vm.isLoading) + NFTListView( + list: vm.nfts, + imageEffect: imageEffect, + fromChildAccount: childAccount + ) + .id(999) + .mockPlaceholder(vm.isLoading) } appBar: { BackAppBar { viewModel.trigger(.back) @@ -175,9 +225,12 @@ struct NFTCollectionListView: RouteableView { } } .background( - NFTBlurImageView(colors: viewModel.state.colorsMap[vm.collection.iconURL.absoluteString] ?? []) - .ignoresSafeArea() - .offset(y: -4) + NFTBlurImageView( + colors: viewModel.state + .colorsMap[vm.collection.iconURL.absoluteString] ?? [] + ) + .ignoresSafeArea() + .offset(y: -4) ) .applyRouteable(self) .environmentObject(viewModel) @@ -186,6 +239,8 @@ struct NFTCollectionListView: RouteableView { } } + // MARK: Private + private func calloutTitle() -> String { let token = vm.collection.name let account = WalletManager.shared.selectedAccountWalletName @@ -194,9 +249,12 @@ struct NFTCollectionListView: RouteableView { } } +// MARK: NFTCollectionListView.InfoView + extension NFTCollectionListView { struct InfoView: View { - @EnvironmentObject private var viewModel: NFTTabViewModel + @EnvironmentObject + private var viewModel: NFTTabViewModel var collection: CollectionItem @@ -273,6 +331,8 @@ extension NFTCollectionListView { } } +// MARK: - NFTCollectionListView_Previews + struct NFTCollectionListView_Previews: PreviewProvider { static var item = NFTTabViewModel.testCollection() diff --git a/FRW/Modules/NFT/Views/OffsetScrollViewWithAppBar.swift b/FRW/Modules/NFT/Views/OffsetScrollViewWithAppBar.swift index da276df8..91705747 100644 --- a/FRW/Modules/NFT/Views/OffsetScrollViewWithAppBar.swift +++ b/FRW/Modules/NFT/Views/OffsetScrollViewWithAppBar.swift @@ -1,5 +1,5 @@ // -// OffsetScrollWithAppBar.swift +// OffsetScrollViewWithAppBar.swift // Flow Wallet // // Created by cat on 2022/6/17. @@ -7,7 +7,29 @@ import SwiftUI +// MARK: - OffsetScrollViewWithAppBar + struct OffsetScrollViewWithAppBar: View { + // MARK: Lifecycle + + init( + title: String = "", + loadMoreEnabled: Bool = false, + loadMoreCallback: (() -> Void)? = nil, + isNoData: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder appBar: @escaping () -> Nav + ) { + self.content = content() + self.navBar = appBar() + self.title = title + self.loadMoreCallback = loadMoreCallback + self.isNoData = isNoData + self.loadMoreEnabled = loadMoreEnabled + } + + // MARK: Internal + var title: String let content: Content @@ -18,20 +40,13 @@ struct OffsetScrollViewWithAppBar: View { let isNoData: Bool let loadMoreEnabled: Bool - @State private var offset: CGFloat = 0 - @State private var opacity: CGFloat = 0 - - init(title: String = "", loadMoreEnabled: Bool = false, loadMoreCallback: (() -> Void)? = nil, isNoData: Bool = false, @ViewBuilder content: @escaping () -> Content, @ViewBuilder appBar: @escaping () -> Nav) { - self.content = content() - navBar = appBar() - self.title = title - self.loadMoreCallback = loadMoreCallback - self.isNoData = isNoData - self.loadMoreEnabled = loadMoreEnabled - } - var body: some View { - OffsetScrollView(offset: $offset, loadMoreEnabled: loadMoreEnabled, loadMoreCallback: loadMoreCallback, isNoData: isNoData) { + OffsetScrollView( + offset: $offset, + loadMoreEnabled: loadMoreEnabled, + loadMoreCallback: loadMoreCallback, + isNoData: isNoData + ) { content } .onChange(of: offset, perform: { value in @@ -48,7 +63,7 @@ struct OffsetScrollViewWithAppBar: View { .opacity(opacity) navBar .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - if title.count > 0 { + if !title.isEmpty { Text(title) .font(.title2) .foregroundColor(.LL.Neutrals.text) @@ -60,8 +75,17 @@ struct OffsetScrollViewWithAppBar: View { .frame(maxHeight: .infinity, alignment: .top) ) } + + // MARK: Private + + @State + private var offset: CGFloat = 0 + @State + private var opacity: CGFloat = 0 } +// MARK: - OffsetScrollWithAppBar_Previews + struct OffsetScrollWithAppBar_Previews: PreviewProvider { static var previews: some View { OffsetScrollViewWithAppBar {} appBar: {} diff --git a/FRW/Modules/Profile/About/AboutView.swift b/FRW/Modules/Profile/About/AboutView.swift index fef77ed7..b56175f8 100644 --- a/FRW/Modules/Profile/About/AboutView.swift +++ b/FRW/Modules/Profile/About/AboutView.swift @@ -7,7 +7,48 @@ import SwiftUI +// MARK: - AboutView + struct AboutView: RouteableView { + struct SocialButton: View { + let imageName: String + let text: String + var showDivider: Bool = true + let action: () -> Void + + var body: some View { + VStack(spacing: 0) { + Button { + action() + } label: { + HStack { + Image(imageName) + .resizable() + .frame(width: 35, height: 35) + + Text(text) + .font(.LL.body) + .foregroundColor(.LL.text) + + Spacer() + + Image(systemName: "arrow.up.right") + .font(.LL.body) + .foregroundColor(.LL.note) + } + .padding(18) + } + + if showDivider { + Divider() + .background(.LL.bgForIcon) + .padding(.horizontal, 12) + } + } + .background(.LL.bgForIcon) + } + } + let version = Bundle.main.infoDictionary?["CFBundleVersion"] as? String let buildVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String @@ -40,15 +81,21 @@ struct AboutView: RouteableView { Section { VStack(spacing: 0) { - SocialButton(imageName: "discord", - text: "Discord") { - UIApplication.shared.open(URL(string: "https://discord.com/invite/J6fFnh2xx6")!) + SocialButton( + imageName: "discord", + text: "Discord" + ) { + UIApplication.shared + .open(URL(string: "https://discord.com/invite/J6fFnh2xx6")!) } - SocialButton(imageName: "twitter", - text: "X", - showDivider: false) { - UIApplication.shared.open(URL(string: "https://twitter.com/flow_blockchain")!) + SocialButton( + imageName: "twitter", + text: "X", + showDivider: false + ) { + UIApplication.shared + .open(URL(string: "https://twitter.com/flow_blockchain")!) } // // SocialButton(imageName: "email", @@ -102,47 +149,10 @@ struct AboutView: RouteableView { .backgroundFill(.LL.background) .applyRouteable(self) } - - struct SocialButton: View { - let imageName: String - let text: String - var showDivider: Bool = true - let action: () -> Void - - var body: some View { - VStack(spacing: 0) { - Button { - action() - } label: { - HStack { - Image(imageName) - .resizable() - .frame(width: 35, height: 35) - - Text(text) - .font(.LL.body) - .foregroundColor(.LL.text) - - Spacer() - - Image(systemName: "arrow.up.right") - .font(.LL.body) - .foregroundColor(.LL.note) - } - .padding(18) - } - - if showDivider { - Divider() - .background(.LL.bgForIcon) - .padding(.horizontal, 12) - } - } - .background(.LL.bgForIcon) - } - } } +// MARK: - AboutView_Previews + struct AboutView_Previews: PreviewProvider { static var previews: some View { AboutView() diff --git a/FRW/Modules/Profile/AccountSetting/ChildAccountDetailView.swift b/FRW/Modules/Profile/AccountSetting/ChildAccountDetailView.swift index 3625cffe..0f13bc2c 100644 --- a/FRW/Modules/Profile/AccountSetting/ChildAccountDetailView.swift +++ b/FRW/Modules/Profile/AccountSetting/ChildAccountDetailView.swift @@ -11,20 +11,30 @@ import SwiftUI import SwiftUIX import UIKit +// MARK: - ChildAccountDetailViewModel + class ChildAccountDetailViewModel: ObservableObject { - @Published var childAccount: ChildAccount - @Published var isPresent: Bool = false - @Published var accessibleItems: [ChildAccountAccessible] = [] + // MARK: Lifecycle - @Published var isLoading: Bool = true + init(childAccount: ChildAccount) { + self.childAccount = childAccount + fetchCollections() + } - @Published var showEmptyCollection: Bool = true + // MARK: Internal - private var isUnlinking: Bool = false + @Published + var childAccount: ChildAccount + @Published + var isPresent: Bool = false + @Published + var accessibleItems: [ChildAccountAccessible] = [] - private var tabIndex: Int = 0 - private var collections: [ChildAccountAccessible]? - private var coins: [ChildAccountAccessible]? + @Published + var isLoading: Bool = true + + @Published + var showEmptyCollection: Bool = true var accessibleEmptyTitle: String { let title = "None Accessible " @@ -34,11 +44,6 @@ class ChildAccountDetailViewModel: ObservableObject { return title + "coins_cap".localized } - init(childAccount: ChildAccount) { - self.childAccount = childAccount - fetchCollections() - } - func copyAction() { UIPasteboard.general.string = childAccount.addr HUD.success(title: "copied".localized) @@ -61,29 +66,12 @@ class ChildAccountDetailViewModel: ObservableObject { isPresent = true } - private func checkChildAcountExist() -> Bool { - return ChildAccountManager.shared.childAccounts.contains(where: { $0.addr == childAccount.addr }) - } - - private func checkUnlinkingTransactionIsProcessing() -> Bool { - for holder in TransactionManager.shared.holders { - if holder.type == .unlinkAccount, holder.internalStatus == .pending, - let holderModel = try? JSONDecoder().decode(ChildAccount.self, from: holder.data), - holderModel.addr == self.childAccount.addr - { - return true - } - } - - return false - } - func switchTab(index: Int) { tabIndex = index if index == 0 { if var list = collections { if !showEmptyCollection { - list = list.filter { $0.count > 0 } + list = list.filter { !$0.isEmpty } } accessibleItems = list } else { @@ -98,12 +86,85 @@ class ChildAccountDetailViewModel: ObservableObject { } } + func doUnlinkAction() { + if isUnlinking { + return + } + + isUnlinking = true + + Task { + do { + let txId = try await FlowNetwork.unlinkChildAccount(childAccount.addr ?? "") + let data = try JSONEncoder().encode(self.childAccount) + let holder = TransactionManager.TransactionHolder( + id: txId, + type: .unlinkAccount, + data: data + ) + + DispatchQueue.main.async { + TransactionManager.shared.newTransaction(holder: holder) + self.isUnlinking = false + self.isPresent = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + Router.pop() + } + } + } catch { + log.error("unlink failed", context: error) + DispatchQueue.main.async { + self.isUnlinking = false + self.isPresent = false + } + + HUD.error(title: "request_failed".localized) + } + } + } + + @objc + func editChildAccountAction() { + Router.route(to: RouteMap.Profile.editChildAccount(childAccount)) + } + + func switchEmptyCollection() { + showEmptyCollection.toggle() + switchTab(index: tabIndex) + } + + // MARK: Private + + private var isUnlinking: Bool = false + + private var tabIndex: Int = 0 + private var collections: [ChildAccountAccessible]? + private var coins: [ChildAccountAccessible]? + + private func checkChildAcountExist() -> Bool { + ChildAccountManager.shared.childAccounts.contains(where: { $0.addr == childAccount.addr }) + } + + private func checkUnlinkingTransactionIsProcessing() -> Bool { + for holder in TransactionManager.shared.holders { + if holder.type == .unlinkAccount, holder.internalStatus == .pending, + let holderModel = try? JSONDecoder().decode(ChildAccount.self, from: holder.data), + holderModel.addr == self.childAccount.addr { + return true + } + } + + return false + } + private func fetchCollections() { accessibleItems = [FlowModel.NFTCollection].mock(1) isLoading = true Task { - guard let parent = WalletManager.shared.getPrimaryWalletAddress(), let child = self.childAccount.addr else { + guard let parent = WalletManager.shared.getPrimaryWalletAddress(), + let child = self.childAccount.addr else { DispatchQueue.main.async { self.collections = [] self.accessibleItems = [] @@ -112,17 +173,24 @@ class ChildAccountDetailViewModel: ObservableObject { } do { - let result = try await FlowNetwork.fetchAccessibleCollection(parent: parent, child: child) + let result = try await FlowNetwork.fetchAccessibleCollection( + parent: parent, + child: child + ) let offset = FRWAPI.Offset(start: 0, length: 100) - let response: Network.Response<[NFTCollection]> = try await Network.requestWithRawModel(FRWAPI.NFT.userCollection(child, offset, .main)) + let response: Network.Response<[NFTCollection]> = try await Network + .requestWithRawModel(FRWAPI.NFT.userCollection( + child, + offset, + .main + )) let collectionList = response.data let resultList: [NFTCollection] = result.compactMap { item in if let contractName = item.split(separator: ".")[safe: 2] { if let model = NFTCatalogCache.cache.find(by: String(contractName)) { - return NFTCollection(collection: model.collection, count: 0) - + // return FlowModel.NFTCollection( // id: model.collection.id, // path: model.collection.path?.storagePath, @@ -141,7 +209,8 @@ class ChildAccountDetailViewModel: ObservableObject { let tmpList = resultList.map { model in var model = model let collectionItem = collectionList?.first(where: { item in - item.maskContractName == model.maskContractName && item.maskAddress == model.maskAddress + item.maskContractName == model.maskContractName && item.maskAddress == model + .maskAddress }) if let item = collectionItem { model.ids = item.ids @@ -168,7 +237,8 @@ class ChildAccountDetailViewModel: ObservableObject { isLoading = true Task { - guard let parent = WalletManager.shared.getPrimaryWalletAddress(), let child = childAccount.addr else { + guard let parent = WalletManager.shared.getPrimaryWalletAddress(), + let child = childAccount.addr else { DispatchQueue.main.async { self.coins = [] self.accessibleItems = [] @@ -184,68 +254,26 @@ class ChildAccountDetailViewModel: ObservableObject { } } } - - func doUnlinkAction() { - if isUnlinking { - return - } - - isUnlinking = true - - Task { - do { - let txId = try await FlowNetwork.unlinkChildAccount(childAccount.addr ?? "") - let data = try JSONEncoder().encode(self.childAccount) - let holder = TransactionManager.TransactionHolder(id: txId, type: .unlinkAccount, data: data) - - DispatchQueue.main.async { - TransactionManager.shared.newTransaction(holder: holder) - self.isUnlinking = false - self.isPresent = false - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - Router.pop() - } - } - } catch { - log.error("unlink failed", context: error) - DispatchQueue.main.async { - self.isUnlinking = false - self.isPresent = false - } - - HUD.error(title: "request_failed".localized) - } - } - } - - @objc func editChildAccountAction() { - Router.route(to: RouteMap.Profile.editChildAccount(childAccount)) - } - - func switchEmptyCollection() { - showEmptyCollection.toggle() - switchTab(index: tabIndex) - } } +// MARK: - ChildAccountDetailView + struct ChildAccountDetailView: RouteableView { - @StateObject var vm: ChildAccountDetailViewModel + // MARK: Lifecycle init(vm: ChildAccountDetailViewModel) { _vm = StateObject(wrappedValue: vm) } + // MARK: Internal + + @StateObject + var vm: ChildAccountDetailViewModel + var title: String { "linked_account".localized } - func configNavigationItem(_ navigationItem: UINavigationItem) { - let editButton = UIBarButtonItem(image: UIImage(named: "icon-edit-child-account"), style: .plain, target: vm, action: #selector(ChildAccountDetailViewModel.editChildAccountAction)) - editButton.tintColor = UIColor(named: "button.color") - navigationItem.rightBarButtonItem = editButton - } - var body: some View { VStack(spacing: 20) { contentView @@ -308,10 +336,6 @@ struct ChildAccountDetailView: RouteableView { .clipped() } - private var bottomPadding: CGFloat { - return 24 - } - var addressContentView: some View { VStack(alignment: .leading, spacing: 8) { Text("address".localized) @@ -389,13 +413,19 @@ struct ChildAccountDetailView: RouteableView { LLSegmenControl(titles: ["collections".localized, "coins_cap".localized]) { idx in vm.switchTab(index: idx) } - if vm.accessibleItems.count == 0, !vm.isLoading { + if vm.accessibleItems.isEmpty, !vm.isLoading { emptyAccessibleView } ForEach(vm.accessibleItems.indices, id: \.self) { idx in AccessibleItemView(item: vm.accessibleItems[idx]) { item in - if let collectionInfo = item as? NFTCollection, let addr = vm.childAccount.addr, let pathId = collectionInfo.collection.path?.storagePathId() ,collectionInfo.count > 0 { - Router.route(to: RouteMap.NFT.collectionDetail(addr, pathId, vm.childAccount)) + if let collectionInfo = item as? NFTCollection, let addr = vm.childAccount.addr, + let pathId = collectionInfo.collection.path?.storagePathId(), + !collectionInfo.isEmpty { + Router.route(to: RouteMap.NFT.collectionDetail( + addr, + pathId, + vm.childAccount + )) } } } @@ -415,16 +445,33 @@ struct ChildAccountDetailView: RouteableView { .background(.LL.Neutrals.neutrals6) .cornerRadius(16, style: .continuous) } + + func configNavigationItem(_ navigationItem: UINavigationItem) { + let editButton = UIBarButtonItem( + image: UIImage(named: "icon-edit-child-account"), + style: .plain, + target: vm, + action: #selector(ChildAccountDetailViewModel.editChildAccountAction) + ) + editButton.tintColor = UIColor(named: "button.color") + navigationItem.rightBarButtonItem = editButton + } + + // MARK: Private + + private var bottomPadding: CGFloat { + 24 + } } extension ChildAccountDetailView { struct Indicator: View { var styleColor: Color { - return Color(hex: "#CCCCCC") + Color(hex: "#CCCCCC") } var barColors: [Color] { - return [.clear, styleColor] + [.clear, styleColor] } var body: some View { @@ -452,23 +499,26 @@ extension ChildAccountDetailView { .foregroundColor(styleColor) } - func lineView(start: UnitPoint, end: UnitPoint) -> some View { - LinearGradient(colors: barColors, startPoint: start, endPoint: end) - .frame(height: 2) - .frame(maxWidth: .infinity) - } - var dotView: some View { Circle() .frame(width: 8, height: 8) .foregroundColor(styleColor) } + + func lineView(start: UnitPoint, end: UnitPoint) -> some View { + LinearGradient(colors: barColors, startPoint: start, endPoint: end) + .frame(height: 2) + .frame(maxWidth: .infinity) + } } struct ChildAccountTargetView: View { - @State var iconURL: String - @State var name: String - @State var address: String + @State + var iconURL: String + @State + var name: String + @State + var address: String var body: some View { VStack(spacing: 8) { @@ -497,7 +547,7 @@ extension ChildAccountDetailView { } struct UnlinkConfirmView: View { - @EnvironmentObject private var vm: ChildAccountDetailViewModel + // MARK: Internal var body: some View { VStack { @@ -533,11 +583,19 @@ extension ChildAccountDetailView { var fromToView: some View { HStack { - ChildAccountTargetView(iconURL: vm.childAccount.icon, name: vm.childAccount.aName, address: vm.childAccount.addr ?? "") + ChildAccountTargetView( + iconURL: vm.childAccount.icon, + name: vm.childAccount.aName, + address: vm.childAccount.addr ?? "" + ) Spacer() - ChildAccountTargetView(iconURL: UserManager.shared.userInfo?.avatar.convertedAvatarString() ?? "", name: UserManager.shared.userInfo?.meowDomain ?? "", address: WalletManager.shared.getPrimaryWalletAddress() ?? "0x") + ChildAccountTargetView( + iconURL: UserManager.shared.userInfo?.avatar.convertedAvatarString() ?? "", + name: UserManager.shared.userInfo?.meowDomain ?? "", + address: WalletManager.shared.getPrimaryWalletAddress() ?? "0x" + ) } .padding(.vertical, 20) .padding(.horizontal, 24) @@ -562,17 +620,27 @@ extension ChildAccountDetailView { } var confirmButton: some View { - WalletSendButtonView(allowEnable: .constant(true), buttonText: "hold_to_unlink".localized) { + WalletSendButtonView( + allowEnable: .constant(true), + buttonText: "hold_to_unlink".localized + ) { vm.doUnlinkAction() } } + + // MARK: Private + + @EnvironmentObject + private var vm: ChildAccountDetailViewModel } } extension ChildAccountManager {} -private extension ChildAccountDetailView { - struct AccessibleItemView: View { +// MARK: - ChildAccountDetailView.AccessibleItemView + +extension ChildAccountDetailView { + fileprivate struct AccessibleItemView: View { var item: ChildAccountAccessible var onClick: ((_ item: ChildAccountAccessible) -> Void)? @@ -617,6 +685,8 @@ private extension ChildAccountDetailView { } } +// MARK: - ChildAccountAccessible + protocol ChildAccountAccessible { var img: String { get } var title: String { get } @@ -628,40 +698,42 @@ protocol ChildAccountAccessible { extension ChildAccountAccessible { var count: Int { - return 0 + 0 } } +// MARK: - NFTCollection + ChildAccountAccessible + extension NFTCollection: ChildAccountAccessible { - var img: String { collection.logoURL.absoluteString } - + var title: String { collection.name ?? "" } - + var subtitle: String { guard let count = ids?.count else { - return "" + return "" } return "\(count) Collectible" } - + var isShowNext: Bool { (ids?.count ?? 0) > 0 } - + var id: String { collection.id } - } +// MARK: - FlowModel.NFTCollection + ChildAccountAccessible + extension FlowModel.NFTCollection: ChildAccountAccessible { var img: String { - return display?.squareImage ?? AppPlaceholder.image + display?.squareImage ?? AppPlaceholder.image } var title: String { @@ -676,11 +748,11 @@ extension FlowModel.NFTCollection: ChildAccountAccessible { } var subtitle: String { - return "\(idList.count) Collectible" + "\(idList.count) Collectible" } var isShowNext: Bool { - return idList.count > 0 + !idList.isEmpty } var fromPath: String { @@ -689,19 +761,40 @@ extension FlowModel.NFTCollection: ChildAccountAccessible { } var count: Int { - return idList.count + idList.count } func toCollectionModel() -> CollectionItem { let item = CollectionItem() item.name = title item.count = idList.count - item.collection = NFTCollectionInfo(id: id, name: title, contractName: title, address: "", logo: img, banner: "", officialWebsite: "", description: "", path: ContractPath(storagePath: "", publicPath: "", privatePath: nil, publicCollectionName: "", publicType: "", privateType: ""), evmAddress: nil, flowIdentifier: nil) + item.collection = NFTCollectionInfo( + id: id, + name: title, + contractName: title, + address: "", + logo: img, + banner: "", + officialWebsite: "", + description: "", + path: ContractPath( + storagePath: "", + publicPath: "", + privatePath: nil, + publicCollectionName: "", + publicType: "", + privateType: "" + ), + evmAddress: nil, + flowIdentifier: nil + ) item.isEnd = true return item } } +// MARK: - FlowModel.TokenInfo + ChildAccountAccessible + extension FlowModel.TokenInfo: ChildAccountAccessible { var img: String { if let model = theToken, let url = model.icon?.absoluteString { @@ -716,7 +809,7 @@ extension FlowModel.TokenInfo: ChildAccountAccessible { return String(title) } - if model.name.count > 0 { + if !model.name.isEmpty { return model.name } return model.contractName @@ -731,12 +824,13 @@ extension FlowModel.TokenInfo: ChildAccountAccessible { } var isShowNext: Bool { - return false + false } private var theToken: TokenModel? { let contractName = id.split(separator: ".")[safe: 2] ?? "empty" let address = id.split(separator: ".")[safe: 1] ?? "empty_error" - return WalletManager.shared.supportedCoins?.filter { $0.contractName == contractName && ($0.getAddress() ?? "") == address }.first + return WalletManager.shared.supportedCoins? + .filter { $0.contractName == contractName && ($0.getAddress() ?? "") == address }.first } } diff --git a/FRW/Modules/Profile/DeveloperMode/DeveloperModeView.swift b/FRW/Modules/Profile/DeveloperMode/DeveloperModeView.swift index c615167e..a296609d 100644 --- a/FRW/Modules/Profile/DeveloperMode/DeveloperModeView.swift +++ b/FRW/Modules/Profile/DeveloperMode/DeveloperModeView.swift @@ -7,24 +7,21 @@ import SwiftUI +// MARK: - DeveloperModeView_Previews + struct DeveloperModeView_Previews: PreviewProvider { static var previews: some View { DeveloperModeView() } } -struct DeveloperModeView: RouteableView { - @StateObject private var lud = LocalUserDefaults.shared - @StateObject private var vm: DeveloperModeViewModel = .init() - @StateObject private var walletManager = WalletManager.shared - - @State private var showTool = false - @AppStorage("isDeveloperMode") private var isDeveloperMode = false +// MARK: - DeveloperModeView - @State private var openLogWindow = LocalUserDefaults.shared.openLogWindow +struct DeveloperModeView: RouteableView { + // MARK: Internal var title: String { - return "developer_mode".localized + "developer_mode".localized } var body: some View { @@ -59,16 +56,33 @@ struct DeveloperModeView: RouteableView { let isTestnet = lud.flowNetwork == .testnet let isPreviewnet = lud.flowNetwork == .previewnet - Cell(sysImageTuple: (isMainnet ? .checkmarkSelected : .checkmarkUnselected, isMainnet ? .LL.Primary.salmonPrimary : .LL.Neutrals.neutrals1), title: "Mainnet", desc: isMainnet ? "Selected::message".localized : "") - .onTapGestureOnBackground { - walletManager.changeNetwork(.mainnet) - } + Cell( + sysImageTuple: ( + isMainnet ? .checkmarkSelected : + .checkmarkUnselected, + isMainnet ? .LL.Primary.salmonPrimary : .LL.Neutrals.neutrals1 + ), + title: "Mainnet", + desc: isMainnet ? "Selected::message".localized : "" + ) + .onTapGestureOnBackground { + walletManager.changeNetwork(.mainnet) + } Divider() - Cell(sysImageTuple: (isTestnet ? .checkmarkSelected : .checkmarkUnselected, isTestnet ? LocalUserDefaults.FlowNetworkType.testnet.color : .LL.Neutrals.neutrals1), title: "Testnet", desc: isTestnet ? "Selected" : "") - .onTapGestureOnBackground { - walletManager.changeNetwork(.testnet) - } + Cell( + sysImageTuple: ( + isTestnet ? .checkmarkSelected : + .checkmarkUnselected, + isTestnet ? LocalUserDefaults.FlowNetworkType.testnet + .color : .LL.Neutrals.neutrals1 + ), + title: "Testnet", + desc: isTestnet ? "Selected" : "" + ) + .onTapGestureOnBackground { + walletManager.changeNetwork(.testnet) + } } .background(.LL.bgForIcon) } @@ -81,30 +95,65 @@ struct DeveloperModeView: RouteableView { .padding(.vertical, 8) VStack(spacing: 0) { Section { - Cell(sysImageTuple: (vm.isCustomAddress ? .checkmarkUnselected : .checkmarkSelected, vm.isCustomAddress ? .LL.Neutrals.neutrals1 : .LL.Primary.salmonPrimary), title: "my_own_address".localized, desc: "") - .onTapGestureOnBackground { - vm.changeCustomAddressAction("") - } + Cell( + sysImageTuple: ( + vm + .isCustomAddress ? .checkmarkUnselected : + .checkmarkSelected, + vm.isCustomAddress ? .LL.Neutrals.neutrals1 : .LL.Primary + .salmonPrimary + ), + title: "my_own_address".localized, + desc: "" + ) + .onTapGestureOnBackground { + vm.changeCustomAddressAction("") + } Divider() - Cell(sysImageTuple: (vm.isDemoAddress ? .checkmarkSelected : .checkmarkUnselected, vm.isDemoAddress ? .LL.Primary.salmonPrimary : .LL.Neutrals.neutrals1), title: vm.demoAddress, desc: "") - .onTapGestureOnBackground { - vm.changeCustomAddressAction(vm.demoAddress) - } + Cell( + sysImageTuple: ( + vm + .isDemoAddress ? .checkmarkSelected : .checkmarkUnselected, + vm.isDemoAddress ? .LL.Primary.salmonPrimary : .LL.Neutrals + .neutrals1 + ), + title: vm.demoAddress, + desc: "" + ) + .onTapGestureOnBackground { + vm.changeCustomAddressAction(vm.demoAddress) + } Divider() - Cell(sysImageTuple: (vm.isSVGDemoAddress ? .checkmarkSelected : .checkmarkUnselected, vm.isSVGDemoAddress ? .LL.Primary.salmonPrimary : .LL.Neutrals.neutrals1), title: vm.svgDemoAddress, desc: "") - .onTapGestureOnBackground { - vm.changeCustomAddressAction(vm.svgDemoAddress) - } + Cell( + sysImageTuple: ( + vm + .isSVGDemoAddress ? .checkmarkSelected : + .checkmarkUnselected, + vm.isSVGDemoAddress ? .LL.Primary.salmonPrimary : .LL.Neutrals + .neutrals1 + ), + title: vm.svgDemoAddress, + desc: "" + ) + .onTapGestureOnBackground { + vm.changeCustomAddressAction(vm.svgDemoAddress) + } Divider() HStack { - Image(systemName: vm.isCustomAddress ? .checkmarkSelected : .checkmarkUnselected) - .foregroundColor(vm.isCustomAddress ? .LL.Primary.salmonPrimary : .LL.Neutrals.neutrals1) + Image( + systemName: vm + .isCustomAddress ? .checkmarkSelected : .checkmarkUnselected + ) + .foregroundColor( + vm.isCustomAddress ? .LL.Primary + .salmonPrimary : .LL.Neutrals.neutrals1 + ) Text("custom_address".localized) .font(.inter()) @@ -122,7 +171,11 @@ struct DeveloperModeView: RouteableView { } DispatchQueue.main.async { - vm.changeCustomAddressAction(vm.customAddressText.trim()) + vm + .changeCustomAddressAction( + vm.customAddressText + .trim() + ) } } } @@ -186,14 +239,16 @@ struct DeveloperModeView: RouteableView { Section { VStack { HStack { - Toggle(openLogWindow ? "Hide Log View" : "Open Log View", - isOn: $openLogWindow) - .toggleStyle(SwitchToggleStyle(tint: .LL.Primary.salmonPrimary)) - .onChange(of: isDeveloperMode) { value in - if !value { - walletManager.changeNetwork(.mainnet) - } + Toggle( + openLogWindow ? "Hide Log View" : "Open Log View", + isOn: $openLogWindow + ) + .toggleStyle(SwitchToggleStyle(tint: .LL.Primary.salmonPrimary)) + .onChange(of: isDeveloperMode) { value in + if !value { + walletManager.changeNetwork(.mainnet) } + } } .frame(height: 64) .padding(.horizontal, 16) @@ -218,9 +273,16 @@ struct DeveloperModeView: RouteableView { .padding(.horizontal, 16) .onTapGesture { if let path = log.path { - let activityController = UIActivityViewController(activityItems: [path], applicationActivities: nil) + let activityController = UIActivityViewController( + activityItems: [path], + applicationActivities: nil + ) activityController.isModalInPresentation = true - UIApplication.shared.windows.first?.rootViewController?.present(activityController, animated: true, completion: nil) + UIApplication.shared.windows.first?.rootViewController?.present( + activityController, + animated: true, + completion: nil + ) } else { HUD.error(title: "Don't find log file.") } @@ -231,7 +293,7 @@ struct DeveloperModeView: RouteableView { } header: { headView(title: "Log") } - + Section { VStack { HStack { @@ -253,7 +315,7 @@ struct DeveloperModeView: RouteableView { headView(title: "Tools") } .visibility(showTool ? .visible : .gone) - + if isDevModel { Section { VStack { @@ -271,9 +333,11 @@ struct DeveloperModeView: RouteableView { .padding(.horizontal, 16) HStack { - Text("Reset the move asset configuration in the built-in browser") - .font(.inter(size: 14, weight: .medium)) - .foregroundStyle(Color.Theme.Text.black8) + Text( + "Reset the move asset configuration in the built-in browser" + ) + .font(.inter(size: 14, weight: .medium)) + .foregroundStyle(Color.Theme.Text.black8) Spacer() } .frame(height: 64) @@ -309,8 +373,7 @@ struct DeveloperModeView: RouteableView { LocalUserDefaults.shared.clickedWhatIsBack = false HUD.success(title: "done.") } - - + HStack { Text("Remove Custom token (click)") .font(.inter(size: 14, weight: .medium)) @@ -346,8 +409,25 @@ struct DeveloperModeView: RouteableView { .applyRouteable(self) } + // MARK: Private + + @StateObject + private var lud = LocalUserDefaults.shared + @StateObject + private var vm: DeveloperModeViewModel = .init() + @StateObject + private var walletManager = WalletManager.shared + + @State + private var showTool = false + @AppStorage("isDeveloperMode") + private var isDeveloperMode = false + + @State + private var openLogWindow = LocalUserDefaults.shared.openLogWindow + private func headView(title: String) -> some View { - return Text(title) + Text(title) .font(.LL.footnote) .foregroundColor(.LL.Neutrals.neutrals3) .frame(maxWidth: .infinity, alignment: .leading) @@ -355,6 +435,8 @@ struct DeveloperModeView: RouteableView { } } +// MARK: DeveloperModeView.Cell + extension DeveloperModeView { struct Cell: View { let sysImageTuple: (String, Color) @@ -367,7 +449,8 @@ extension DeveloperModeView { var body: some View { HStack { Image(systemName: sysImageTuple.0).foregroundColor(sysImageTuple.1) - Text(title).font(.inter()).frame(maxWidth: .infinity, alignment: .leading).opacity(titleAlpha) + Text(title).font(.inter()).frame(maxWidth: .infinity, alignment: .leading) + .opacity(titleAlpha) Text(desc).font(.inter()).foregroundColor(.LL.Neutrals.note) Button { diff --git a/FRW/Modules/Profile/DeveloperMode/KeychainListView.swift b/FRW/Modules/Profile/DeveloperMode/KeychainListView.swift index 047c574a..c758117a 100644 --- a/FRW/Modules/Profile/DeveloperMode/KeychainListView.swift +++ b/FRW/Modules/Profile/DeveloperMode/KeychainListView.swift @@ -8,19 +8,16 @@ import SwiftUI struct KeychainListView: RouteableView { - - @ObservedObject - private var viewModel = KeychainListViewModel() - + // MARK: Internal + var title: String { - return "All Keys on Local" + "All Keys on Local" } var body: some View { ScrollView { - Section { - ForEach(0 ..< viewModel.seList.count, id: \.self) { index in + ForEach(0.. 0 ? .visible : .gone) - + .visibility(!viewModel.localList.isEmpty ? .visible : .gone) + Section { - ForEach(0 ..< viewModel.localList.count, id: \.self) { index in + ForEach(0.. 0 ? .visible : .gone) + .visibility(!viewModel.localList.isEmpty ? .visible : .gone) Section { - ForEach(0 ..< viewModel.remoteList.count, id: \.self) { index in + ForEach(0.. 0 ? .visible : .gone) - - + .visibility(!viewModel.remoteList.isEmpty ? .visible : .gone) + Section { - ForEach(0 ..< $viewModel.multiICloudBackUpList.count, id: \.self) { index in + ForEach(0..<$viewModel.multiICloudBackUpList.count, id: \.self) { index in let item = viewModel.multiICloudBackUpList[index] seItemView(key: item.keys.first ?? "", value: item.values.first ?? "") } } header: { - HStack { Text("iCloud Multiple Backup(\($viewModel.multiICloudBackUpList.count))") Spacer() } .frame(height: 52) } - .visibility($viewModel.remoteList.count > 0 ? .visible : .gone) - - + .visibility(!$viewModel.remoteList.isEmpty ? .visible : .gone) } .applyRouteable(self) } func itemView(_ item: [String: Any]) -> some View { - return HStack { + HStack { VStack(alignment: .leading) { Text(viewModel.getKey(item: item)) .font(.inter(size: 16)) @@ -114,9 +104,9 @@ struct KeychainListView: RouteableView { .frame(height: 80) .background(Color.Theme.Background.silver) } - + func seItemView(key: String, value: String) -> some View { - return HStack { + HStack { VStack(alignment: .leading) { Text("userId:\(key)") .font(.inter(size: 16)) @@ -124,7 +114,10 @@ struct KeychainListView: RouteableView { .lineLimit(2) Text("publickKey: \(value)") .font(.inter(size: 16)) - .foregroundStyle( isCurrentKey(key: key) ? Color.Theme.Text.black8 : Color.Theme.evm) + .foregroundStyle( + isCurrentKey(key: key) ? Color.Theme.Text.black8 : Color.Theme + .evm + ) .lineLimit(2) } Spacer() @@ -140,10 +133,15 @@ struct KeychainListView: RouteableView { .frame(height: 80) .background(Color.Theme.Background.silver) } - + func isCurrentKey(key: String) -> Bool { UserManager.shared.activatedUID == key } + + // MARK: Private + + @ObservedObject + private var viewModel = KeychainListViewModel() } #Preview { diff --git a/FRW/Modules/Profile/DeveloperMode/KeychainListViewModel.swift b/FRW/Modules/Profile/DeveloperMode/KeychainListViewModel.swift index 2ad3ffee..a4c5de16 100644 --- a/FRW/Modules/Profile/DeveloperMode/KeychainListViewModel.swift +++ b/FRW/Modules/Profile/DeveloperMode/KeychainListViewModel.swift @@ -5,87 +5,66 @@ // Created by cat on 2024/4/26. // +import FlowWalletCore import Foundation import KeychainAccess import SwiftUI -import FlowWalletCore class KeychainListViewModel: ObservableObject { - @Published var localList: [[String: Any]] = [] - @Published var remoteList: [[String: Any]] = [] - @Published var seList: [[String: String]] = [] - @Published var multiICloudBackUpList: [[String: String]] = [] - - private let remoteKeychain: Keychain - private let localKeychain: Keychain - private let seKeychain: Keychain - private let mnemonicPrefix = "lilico.mnemonic." + // MARK: Lifecycle init() { let remoteService = (Bundle.main.bundleIdentifier ?? "com.flowfoundation.wallet") - remoteKeychain = Keychain(service: remoteService) + self.remoteKeychain = Keychain(service: remoteService) .label("Lilico app backup") let localService = remoteService + ".local" - localKeychain = Keychain(service: localService) + self.localKeychain = Keychain(service: localService) .label("Flow Wallet Backup") - - seKeychain = Keychain(service: "com.flowfoundation.wallet.securekey") - + + self.seKeychain = Keychain(service: "com.flowfoundation.wallet.securekey") + fecth() } - private func fecth() { - remoteList = remoteKeychain.allItems() - loadiCloudBackup() - if let item = remoteList.last { - log.info(item) - } - localList = localKeychain.allItems() - do { - guard let data = try seKeychain.getData("user.keystore") else { - return - } - let users = try? JSONDecoder().decode([WallectSecureEnclave.StoreInfo].self, from: data) - seList = users?.map({ info in - - if let sec = try? WallectSecureEnclave(privateKey: info.publicKey), let publicKey = sec.key.publickeyValue { - return [info.uniq: publicKey] - }else { - return [info.uniq: "undefined"] - } - - }) ?? [] - }catch { - log.error("[kc] fetch failed. \(error)") - } - } - + // MARK: Internal + + @Published + var localList: [[String: Any]] = [] + @Published + var remoteList: [[String: Any]] = [] + @Published + var seList: [[String: String]] = [] + @Published + var multiICloudBackUpList: [[String: String]] = [] + func loadiCloudBackup() { Task { if let list = try? await MultiBackupManager.shared.getCloudDriveItems(from: .icloud) { DispatchQueue.main.async { - self.multiICloudBackUpList = list.map{ [$0.userId : $0.publicKey] } + self.multiICloudBackUpList = list.map { [$0.userId: $0.publicKey] } } } } } - + func radomUpdatePrivateKey(index: Int) { if isDevModel { let model = seList[index] - if let key = model.keys.first, let model = try? WallectSecureEnclave.Store.fetchModel(by: key) { + if let key = model.keys.first, + let model = try? WallectSecureEnclave.Store.fetchModel(by: key) { do { - let toValue = model.publicKey + ("999".data(using: .utf8) ?? Data()) - try WallectSecureEnclave.Store.dangerUpdate(key: model.uniq, fromValue: model.publicKey, toValue: toValue) + let toValue = model.publicKey + ("999".data(using: .utf8) ?? Data()) + try WallectSecureEnclave.Store.dangerUpdate( + key: model.uniq, + fromValue: model.publicKey, + toValue: toValue + ) HUD.success(title: "修改成功") - }catch{} - + } catch {} } - } } - func getKey(item: [String: Any]) -> String { guard let key = item["key"] as? String else { @@ -101,7 +80,11 @@ class KeychainListViewModel: ObservableObject { if key.contains(mnemonicPrefix), let data = value as? Data { let uid = key.removePrefix(mnemonicPrefix) - if let decryptedData = try? WalletManager.decryptionChaChaPoly(key: uid, data: data), let mnemonic = String(data: decryptedData, encoding: .utf8) { + if let decryptedData = try? WalletManager.decryptionChaChaPoly(key: uid, data: data), + let mnemonic = String( + data: decryptedData, + encoding: .utf8 + ) { return mnemonic } return "decrypted failed" @@ -109,4 +92,38 @@ class KeychainListViewModel: ObservableObject { return "not mnemonic" } + + // MARK: Private + + private let remoteKeychain: Keychain + private let localKeychain: Keychain + private let seKeychain: Keychain + private let mnemonicPrefix = "lilico.mnemonic." + + private func fecth() { + remoteList = remoteKeychain.allItems() + loadiCloudBackup() + if let item = remoteList.last { + log.info(item) + } + localList = localKeychain.allItems() + do { + guard let data = try seKeychain.getData("user.keystore") else { + return + } + let users = try? JSONDecoder().decode([WallectSecureEnclave.StoreInfo].self, from: data) + seList = users?.map { info in + + if let sec = try? WallectSecureEnclave(privateKey: info.publicKey), + let publicKey = sec.key.publickeyValue { + return [info.uniq: publicKey] + } else { + return [info.uniq: "undefined"] + } + + } ?? [] + } catch { + log.error("[kc] fetch failed. \(error)") + } + } } diff --git a/FRW/Modules/Profile/Devices/DevicesInfoView.swift b/FRW/Modules/Profile/Devices/DevicesInfoView.swift index f2c294af..e91e3847 100644 --- a/FRW/Modules/Profile/Devices/DevicesInfoView.swift +++ b/FRW/Modules/Profile/Devices/DevicesInfoView.swift @@ -8,25 +8,34 @@ import MapKit import SwiftUI +// MARK: - CLLocationCoordinate2D + Identifiable + extension CLLocationCoordinate2D: Identifiable { public var id: String { "\(latitude)-\(longitude)" } } -struct DevicesInfoView: RouteableView { - var info: DeviceInfoModel - @StateObject var viewModel: DevicesInfoViewModel +// MARK: - DevicesInfoView - var title: String { - return "device_info".localized - } +struct DevicesInfoView: RouteableView { + // MARK: Lifecycle init(info: DeviceInfoModel) { self.info = info _viewModel = StateObject(wrappedValue: DevicesInfoViewModel(model: info)) } + // MARK: Internal + + var info: DeviceInfoModel + @StateObject + var viewModel: DevicesInfoViewModel + + var title: String { + "device_info".localized + } + var body: some View { VStack { ScrollView { @@ -61,7 +70,10 @@ struct DevicesInfoView: RouteableView { } VStack { - DeviceInfoItem(title: "application_tag".localized, detail: info.showApp()) + DeviceInfoItem( + title: "application_tag".localized, + detail: info.showApp() + ) Divider() .background(Color.Theme.Line.line) .padding(.vertical, 16) @@ -73,7 +85,10 @@ struct DevicesInfoView: RouteableView { Divider() .background(Color.Theme.Line.line) .padding(.vertical, 16) - DeviceInfoItem(title: "entry_date_tag".localized, detail: info.showDate()) + DeviceInfoItem( + title: "entry_date_tag".localized, + detail: info.showDate() + ) } .padding(.all, 16) .background(.Theme.Background.grey) @@ -105,9 +120,11 @@ struct DevicesInfoView: RouteableView { } .applyRouteable(self) .halfSheet(showSheet: $viewModel.showRemoveTipView) { - DangerousTipSheetView(title: "account_key_revoke_title".localized, - detail: "account_key_revoke_content".localized, - buttonTitle: "hold_to_revoke".localized) { + DangerousTipSheetView( + title: "account_key_revoke_title".localized, + detail: "account_key_revoke_content".localized, + buttonTitle: "hold_to_revoke".localized + ) { viewModel.revokeAction() } onCancel: { viewModel.onCancel() @@ -166,12 +183,15 @@ struct DevicesInfoView: RouteableView { } func region() -> MKCoordinateRegion { - let region = MKCoordinateRegion(center: info.coordinate(), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)) + let region = MKCoordinateRegion( + center: info.coordinate(), + span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) + ) return region } func annotations() -> [CLLocationCoordinate2D] { - return [ + [ info.coordinate(), ] } diff --git a/FRW/Modules/Profile/ProfileEdit/EditAvatar/EditAvatarViewModel.swift b/FRW/Modules/Profile/ProfileEdit/EditAvatar/EditAvatarViewModel.swift index 478abada..388f98bd 100644 --- a/FRW/Modules/Profile/ProfileEdit/EditAvatar/EditAvatarViewModel.swift +++ b/FRW/Modules/Profile/ProfileEdit/EditAvatar/EditAvatarViewModel.swift @@ -16,6 +16,16 @@ extension EditAvatarView { } struct AvatarItemModel: Identifiable { + // MARK: Lifecycle + + init(type: ItemType, avatarString: String? = nil, nft: NFTModel? = nil) { + self.type = type + self.avatarString = avatarString + self.nft = nft + } + + // MARK: Internal + enum ItemType { case string case nft @@ -25,12 +35,6 @@ extension EditAvatarView { var avatarString: String? var nft: NFTModel? - init(type: ItemType, avatarString: String? = nil, nft: NFTModel? = nil) { - self.type = type - self.avatarString = avatarString - self.nft = nft - } - var id: String { if let avatarString = avatarString { return avatarString @@ -39,10 +43,10 @@ extension EditAvatarView { if let tokenID = nft?.id { return tokenID } else { - assert(false, "tokenID should not be nil") + assertionFailure("tokenID should not be nil") } - assert(false, "AvatarItemModel id should not be nil") + assertionFailure("AvatarItemModel id should not be nil") return "" } @@ -68,41 +72,52 @@ extension EditAvatarView { } } +// MARK: - EditAvatarView.EditAvatarViewModel + extension EditAvatarView { class EditAvatarViewModel: ObservableObject { - @Published var mode: Mode = .preview - @Published var items: [AvatarItemModel] = [] - @Published var selectedItemId: String? - - private var oldAvatarItem: AvatarItemModel? - - private var isEnd: Bool = false - private var isRequesting: Bool = false + // MARK: Lifecycle init() { var cachedItems = [AvatarItemModel]() if let currentAvatar = UserManager.shared.userInfo?.avatar { - cachedItems.append(EditAvatarView.AvatarItemModel(type: .string, avatarString: currentAvatar)) + cachedItems.append(EditAvatarView.AvatarItemModel( + type: .string, + avatarString: currentAvatar + )) } if let cachedNFTs = NFTUIKitCache.cache.getGridNFTs() { for nft in cachedNFTs { let model = NFTModel(nft, in: nil) - cachedItems.append(EditAvatarView.AvatarItemModel(type: .nft, avatarString: nil, nft: model)) + cachedItems.append(EditAvatarView.AvatarItemModel( + type: .nft, + avatarString: nil, + nft: model + )) } } - items = cachedItems + self.items = cachedItems if let first = items.first, first.type == .string { - selectedItemId = first.id - oldAvatarItem = first + self.selectedItemId = first.id + self.oldAvatarItem = first } loadMoreAvatarIfNeededAction() } + // MARK: Internal + + @Published + var mode: Mode = .preview + @Published + var items: [AvatarItemModel] = [] + @Published + var selectedItemId: String? + func currentSelectModel() -> AvatarItemModel? { for item in items { if item.id == selectedItemId { @@ -160,7 +175,9 @@ extension EditAvatarView { KingfisherManager.shared.retrieveImage(with: url) { result in switch result { case let .success(r): - debugPrint("EditAvatarViewModel -> save action, did get image from: \(r.cacheType)") + debugPrint( + "EditAvatarViewModel -> save action, did get image from: \(r.cacheType)" + ) success(r.image) case let .failure(e): debugPrint("EditAvatarViewModel -> save action, did failed get image: \(e)") @@ -169,6 +186,13 @@ extension EditAvatarView { } } + // MARK: Private + + private var oldAvatarItem: AvatarItemModel? + + private var isEnd: Bool = false + private var isRequesting: Bool = false + private func uploadAvatarURL(_ url: String) async -> Bool { guard let nickname = UserManager.shared.userInfo?.nickname else { return false @@ -176,7 +200,8 @@ extension EditAvatarView { let request = UserInfoUpdateRequest(nickname: nickname, avatar: url) do { - let response: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.Profile.updateInfo(request)) + let response: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.Profile.updateInfo(request)) if response.httpCode != 200 { return false } @@ -191,7 +216,8 @@ extension EditAvatarView { extension EditAvatarView.EditAvatarViewModel { func loadMoreAvatarIfNeededAction() { - if let lastItem = items.last, let selectId = selectedItemId, lastItem.id == selectId, isRequesting == false, isEnd == false { + if let lastItem = items.last, let selectId = selectedItemId, lastItem.id == selectId, + isRequesting == false, isEnd == false { isRequesting = true Task { @@ -206,7 +232,9 @@ extension EditAvatarView.EditAvatarViewModel { self.isRequesting = false } } catch { - debugPrint("EditAvatarViewModel -> loadMoreAvatarIfNeededAction request failed: \(error)") + debugPrint( + "EditAvatarViewModel -> loadMoreAvatarIfNeededAction request failed: \(error)" + ) DispatchQueue.main.async { self.isRequesting = false } @@ -226,10 +254,15 @@ extension EditAvatarView.EditAvatarViewModel { } private func requestGrid(offset: Int, limit: Int = 24) async throws -> [NFTModel] { - let address = WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "" + let address = WalletManager.shared + .getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "" let request = NFTGridDetailListRequest(address: address, offset: offset, limit: limit) let from: FRWAPI.From = EVMAccountManager.shared.selectedAccount != nil ? .evm : .main - let response: Network.Response = try await Network.requestWithRawModel(FRWAPI.NFT.gridDetailList(request, from)) + let response: Network.Response = try await Network + .requestWithRawModel(FRWAPI.NFT.gridDetailList( + request, + from + )) guard let nfts = response.data?.nfts else { return [] @@ -244,7 +277,11 @@ extension EditAvatarView.EditAvatarViewModel { let exist = items.first { $0.type == .nft && $0.nft?.id == nft.id } if exist == nil { - items.append(EditAvatarView.AvatarItemModel(type: .nft, avatarString: nil, nft: nft)) + items.append(EditAvatarView.AvatarItemModel( + type: .nft, + avatarString: nil, + nft: nft + )) } } } diff --git a/FRW/Modules/Profile/ProfileView.swift b/FRW/Modules/Profile/ProfileView.swift index f4456082..d9702d51 100644 --- a/FRW/Modules/Profile/ProfileView.swift +++ b/FRW/Modules/Profile/ProfileView.swift @@ -1,5 +1,5 @@ // -// SettingView.swift +// ProfileView.swift // Flow Wallet-lite // // Created by Hao Fu on 30/11/21. @@ -9,9 +9,11 @@ import Instabug import Kingfisher import SwiftUI +// MARK: - ProfileView + AppTabBarPageProtocol + extension ProfileView: AppTabBarPageProtocol { static func tabTag() -> AppTabType { - return .profile + .profile } static func iconName() -> String { @@ -23,21 +25,21 @@ extension ProfileView: AppTabBarPageProtocol { // Hence, we manually set the color here // return .LL.Secondary.navy3 // UIScreen.main.traitCollection.userInterfaceStyle == .dark ? Color(hex: "#0B59BF") : - return Color(hex: "#579AF2") + Color(hex: "#579AF2") } } +// MARK: - ProfileView + struct ProfileView: RouteableView { - @StateObject private var vm = ProfileViewModel() - @StateObject private var lud = LocalUserDefaults.shared - @StateObject private var userManager = UserManager.shared + // MARK: Internal var title: String { - return "" + "" } var isNavigationBarHidden: Bool { - return true + true } var body: some View { @@ -80,8 +82,19 @@ struct ProfileView: RouteableView { .environmentObject(userManager) .applyRouteable(self) } + + // MARK: Private + + @StateObject + private var vm = ProfileViewModel() + @StateObject + private var lud = LocalUserDefaults.shared + @StateObject + private var userManager = UserManager.shared } +// MARK: ProfileView.NoLoginTipsView + // struct ProfileView_Previews: PreviewProvider { // static var previews: some View { //// ProfileView.NoLoginTipsView() @@ -93,12 +106,9 @@ struct ProfileView: RouteableView { // } // } -// MARK: - Section login tips - extension ProfileView { struct NoLoginTipsView: View { - private let title = "welcome_to_lilico".localized - private let desc = "welcome_desc".localized + // MARK: Internal var body: some View { Section { @@ -121,12 +131,21 @@ extension ProfileView { } .padding(.horizontal, 12) .padding(.vertical, 16) - .roundedBg(cornerRadius: 12, strokeColor: .LL.Primary.salmonPrimary, strokeLineWidth: 1) + .roundedBg( + cornerRadius: 12, + strokeColor: .LL.Primary.salmonPrimary, + strokeLineWidth: 1 + ) } .listRowInsets(.zero) .listRowBackground(Color.clear) .background(.clear) } + + // MARK: Private + + private let title = "welcome_to_lilico".localized + private let desc = "welcome_desc".localized } } @@ -134,7 +153,7 @@ extension ProfileView { extension ProfileView { struct InfoContainerView: View { - @EnvironmentObject private var vm: ProfileViewModel + // MARK: Internal var jailbreakTipsView: some View { Button { @@ -179,10 +198,15 @@ extension ProfileView { } .background(.LL.Neutrals.background) } + + // MARK: Private + + @EnvironmentObject + private var vm: ProfileViewModel } struct InfoView: View { - @EnvironmentObject private var userManager: UserManager + // MARK: Internal var body: some View { HStack(spacing: 16) { @@ -219,12 +243,20 @@ extension ProfileView { .roundedButtonStyle(bgColor: .clear) } } + + // MARK: Private + + @EnvironmentObject + private var userManager: UserManager } struct InfoActionView: View { var body: some View { HStack(alignment: .center, spacing: 0) { - ProfileView.InfoActionButton(iconName: "icon-address", title: "addresses".localized) { + ProfileView.InfoActionButton( + iconName: "icon-address", + title: "addresses".localized + ) { Router.route(to: RouteMap.Profile.addressBook) } @@ -239,8 +271,10 @@ extension ProfileView { // } } .padding(.vertical, 20) - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } } @@ -253,7 +287,10 @@ extension ProfileView { Button(action: action) { VStack { Image(iconName) - Text(title).foregroundColor(.LL.Neutrals.note).font(.inter(size: 12, weight: .medium)) + Text(title).foregroundColor(.LL.Neutrals.note).font(.inter( + size: 12, + weight: .medium + )) } } .buttonStyle(.plain) @@ -262,12 +299,12 @@ extension ProfileView { } } -// MARK: - Section action setting +// MARK: ProfileView.ActionSectionView extension ProfileView { struct ActionSectionView: View { - @EnvironmentObject private var vm: ProfileViewModel - @State private var showAlert = false + // MARK: Internal + enum Row { case backup(ProfileViewModel) case security @@ -281,7 +318,12 @@ extension ProfileView { Button { vm.linkedAccountAction() } label: { - ProfileView.SettingItemCell(iconName: Row.linkedAccount.iconName, title: Row.linkedAccount.title, style: Row.linkedAccount.style, desc: Row.linkedAccount.desc) + ProfileView.SettingItemCell( + iconName: Row.linkedAccount.iconName, + title: Row.linkedAccount.title, + style: Row.linkedAccount.style, + desc: Row.linkedAccount.desc + ) } Divider().background(Color.LL.Neutrals.background).padding(.horizontal, 8) @@ -293,7 +335,14 @@ extension ProfileView { } } label: { - ProfileView.SettingItemCell(iconName: Row.backup(vm).iconName, title: Row.backup(vm).title, style: Row.backup(vm).style, desc: Row.backup(vm).desc, imageName: Row.backup(vm).imageName, sysImageColor: Row.backup(vm).sysImageColor) + ProfileView.SettingItemCell( + iconName: Row.backup(vm).iconName, + title: Row.backup(vm).title, + style: Row.backup(vm).style, + desc: Row.backup(vm).desc, + imageName: Row.backup(vm).imageName, + sysImageColor: Row.backup(vm).sysImageColor + ) } .alert("wrong_network_title".localized, isPresented: $showAlert) { Button("switch_to_mainnet".localized) { @@ -310,19 +359,35 @@ extension ProfileView { Button { vm.securityAction() } label: { - ProfileView.SettingItemCell(iconName: Row.security.iconName, title: Row.security.title, style: Row.security.style, desc: Row.security.desc) + ProfileView.SettingItemCell( + iconName: Row.security.iconName, + title: Row.security.title, + style: Row.security.style, + desc: Row.security.desc + ) } } } - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } + + // MARK: Private + + @EnvironmentObject + private var vm: ProfileViewModel + @State + private var showAlert = false } } +// MARK: ProfileView.WalletConnectView + extension ProfileView { struct WalletConnectView: View { - @EnvironmentObject private var vm: ProfileViewModel + // MARK: Internal enum Row { case walletConnect @@ -364,9 +429,16 @@ extension ProfileView { } } } - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } + + // MARK: Private + + @EnvironmentObject + private var vm: ProfileViewModel } } @@ -483,11 +555,11 @@ extension ProfileView.ActionSectionView.Row { } } -// MARK: - Section general setting +// MARK: - ProfileView.GeneralSectionView extension ProfileView { struct GeneralSectionView: View { - @EnvironmentObject private var vm: ProfileViewModel + // MARK: Internal enum Row: Hashable { case notification @@ -504,7 +576,8 @@ extension ProfileView { if row == Row.notification { HStack { Image("icon-notification") - Text("notifications".localized).font(.inter()).frame(maxWidth: .infinity, alignment: .leading) + Text("notifications".localized).font(.inter()) + .frame(maxWidth: .infinity, alignment: .leading) Spacer() @@ -527,19 +600,35 @@ extension ProfileView { break } } label: { - ProfileView.SettingItemCell(iconName: row.iconName, title: row.title, style: row.style, desc: row.desc(with: vm), toggle: row.toggle) + ProfileView.SettingItemCell( + iconName: row.iconName, + title: row.title, + style: row.style, + desc: row.desc(with: vm), + toggle: row.toggle + ) } } if row != .theme { - Divider().background(Color.LL.Neutrals.background).padding(.horizontal, 8) + Divider().background(Color.LL.Neutrals.background).padding( + .horizontal, + 8 + ) } } } } - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } + + // MARK: Private + + @EnvironmentObject + private var vm: ProfileViewModel } } @@ -600,6 +689,8 @@ extension ProfileView.GeneralSectionView.Row { } } +// MARK: - ProfileView.FeedbackView + extension ProfileView { struct FeedbackView: View { enum Row { @@ -612,12 +703,18 @@ extension ProfileView { Button { Instabug.show() } label: { - ProfileView.SettingItemCell(iconName: Row.instabug.iconName, title: Row.instabug.title, style: Row.instabug.style) + ProfileView.SettingItemCell( + iconName: Row.instabug.iconName, + title: Row.instabug.title, + style: Row.instabug.style + ) } } } - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } } } @@ -645,18 +742,19 @@ extension ProfileView.FeedbackView.Row { } } -// MARK: - About setting +// MARK: - ProfileView.AboutSectionView extension ProfileView { struct AboutSectionView: View { - @EnvironmentObject var lud: LocalUserDefaults - enum Row { case developerMode(LocalUserDefaults) case plugin case about } + @EnvironmentObject + var lud: LocalUserDefaults + var body: some View { VStack { Section { @@ -665,15 +763,34 @@ extension ProfileView { Button { Router.route(to: RouteMap.Profile.developer) } label: { - ProfileView.SettingItemCell(iconName: dm.iconName, title: dm.title, style: dm.style, desc: dm.desc, toggle: dm.toggle) + ProfileView.SettingItemCell( + iconName: dm.iconName, + title: dm.title, + style: dm.style, + desc: dm.desc, + toggle: dm.toggle + ) } Divider().background(Color.LL.Neutrals.background).padding(.horizontal, 8) Button { - UIApplication.shared.open(URL(string: "https://chrome.google.com/webstore/detail/lilico/hpclkefagolihohboafpheddmmgdffjm")!) + UIApplication.shared + .open( + URL( + string: "https://chrome.google.com/webstore/detail/lilico/hpclkefagolihohboafpheddmmgdffjm" + )! + ) } label: { - ProfileView.SettingItemCell(iconName: Row.plugin.iconName, title: Row.plugin.title, style: Row.plugin.style, desc: Row.plugin.desc, toggle: Row.plugin.toggle, imageName: Row.plugin.imageName, sysImageColor: Row.plugin.sysImageColor) + ProfileView.SettingItemCell( + iconName: Row.plugin.iconName, + title: Row.plugin.title, + style: Row.plugin.style, + desc: Row.plugin.desc, + toggle: Row.plugin.toggle, + imageName: Row.plugin.imageName, + sysImageColor: Row.plugin.sysImageColor + ) } Divider().background(Color.LL.Neutrals.background).padding(.horizontal, 8) @@ -681,12 +798,20 @@ extension ProfileView { Button { Router.route(to: RouteMap.Profile.about) } label: { - ProfileView.SettingItemCell(iconName: Row.about.iconName, title: Row.about.title, style: Row.about.style, desc: Row.about.desc, toggle: Row.about.toggle) + ProfileView.SettingItemCell( + iconName: Row.about.iconName, + title: Row.about.title, + style: Row.about.style, + desc: Row.about.desc, + toggle: Row.about.toggle + ) } } } - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } } } @@ -770,7 +895,7 @@ extension ProfileView.AboutSectionView.Row { } } -// MARK: - Section more setting +// MARK: - ProfileView.MoreSectionView extension ProfileView { struct MoreSectionView: View { @@ -782,12 +907,20 @@ extension ProfileView { VStack { Section { ForEach(Row.allCases, id: \.self) { - ProfileView.SettingItemCell(iconName: $0.iconName, title: $0.title, style: $0.style, desc: $0.desc, toggle: $0.toggle) + ProfileView.SettingItemCell( + iconName: $0.iconName, + title: $0.title, + style: $0.style, + desc: $0.desc, + toggle: $0.toggle + ) } } } - .background(RoundedRectangle(cornerRadius: 16) - .fill(Color.LL.bgForIcon)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.LL.bgForIcon) + ) } } } @@ -848,7 +981,8 @@ extension ProfileView { let style: Style var desc: String? = "" - @State var toggle: Bool = false + @State + var toggle: Bool = false var imageName: String? = "" var toggleAction: ((Bool) -> Void)? = nil var sysImageColor: Color? = nil @@ -858,7 +992,8 @@ extension ProfileView { Image(iconName) Text(title).font(.inter()).frame(maxWidth: .infinity, alignment: .leading) - Text(desc ?? "").font(.inter()).foregroundColor(.LL.Neutrals.note).visibility(style == .desc ? .visible : .gone) + Text(desc ?? "").font(.inter()).foregroundColor(.LL.Neutrals.note) + .visibility(style == .desc ? .visible : .gone) Image("icon-black-right-arrow") .renderingMode(.template) .foregroundColor(Color.LL.Button.color) @@ -876,7 +1011,8 @@ extension ProfileView { Image(imageName) } - if let imageName = imageName, let sysImageColor = sysImageColor, style == .sysImage { + if let imageName = imageName, let sysImageColor = sysImageColor, + style == .sysImage { Image(systemName: imageName) .foregroundColor(sysImageColor) } diff --git a/FRW/Modules/Profile/ProfileViewModel.swift b/FRW/Modules/Profile/ProfileViewModel.swift index 8c2bd355..eb3cbda3 100644 --- a/FRW/Modules/Profile/ProfileViewModel.swift +++ b/FRW/Modules/Profile/ProfileViewModel.swift @@ -29,14 +29,7 @@ extension ProfileView { enum ProfileInput {} class ProfileViewModel: ViewModel { - @Published var state = ProfileState() - - private var cancelSets = Set() - - let version = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - let buildVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - - @Published var isLinkedAccount = false + // MARK: Lifecycle init() { state.colorScheme = ThemeManager.shared.style @@ -80,8 +73,23 @@ extension ProfileView { }.store(in: &cancelSets) } + // MARK: Internal + + @Published + var state = ProfileState() + + let version = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + let buildVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + + @Published + var isLinkedAccount = false + func trigger(_: ProfileInput) {} + // MARK: Private + + private var cancelSets = Set() + private func refreshBackupState() { guard let uid = UserManager.shared.activatedUID else { state.backupFetchingState = .none @@ -124,7 +132,6 @@ extension ProfileView { extension ProfileView.ProfileViewModel { func securityAction() { - Task { let result = await SecurityManager.shared.SecurityVerify() if result { diff --git a/FRW/Modules/Profile/WalletConnect/WalletConnectView.swift b/FRW/Modules/Profile/WalletConnect/WalletConnectView.swift index b82ea054..a1ba2369 100644 --- a/FRW/Modules/Profile/WalletConnect/WalletConnectView.swift +++ b/FRW/Modules/Profile/WalletConnect/WalletConnectView.swift @@ -1,5 +1,5 @@ // -// ProfileBackupView.swift +// WalletConnectView.swift // Flow Wallet // // Created by Selina on 2/8/2022. @@ -10,6 +10,8 @@ import Lottie import SwiftUI import WalletConnectSign +// MARK: - WalletConnectView + struct WalletConnectView: RouteableView { @StateObject private var vm = WalletConnectViewModel() @@ -18,7 +20,7 @@ struct WalletConnectView: RouteableView { var manager = WalletConnectManager.shared var title: String { - return "walletconnect".localized + "walletconnect".localized } var connectedViews: some View { @@ -46,14 +48,19 @@ struct WalletConnectView: RouteableView { .foregroundColor(.LL.warning2) } } label: { - ItemCell(title: session.peer.name, - url: session.peer.url, - network: String(session.namespaces.values.first?.accounts.first?.reference ?? ""), - icon: session.peer.icons.first ?? AppPlaceholder.image) - .buttonStyle(ScaleButtonStyle()) - .padding(.horizontal, 16) - .roundedBg() - .padding(.bottom, 12) + ItemCell( + title: session.peer.name, + url: session.peer.url, + network: String( + session.namespaces.values.first?.accounts.first? + .reference ?? "" + ), + icon: session.peer.icons.first ?? AppPlaceholder.image + ) + .buttonStyle(ScaleButtonStyle()) + .padding(.horizontal, 16) + .roundedBg() + .padding(.bottom, 12) } } } @@ -111,42 +118,20 @@ struct WalletConnectView: RouteableView { } var body: some View { - if manager.activeSessions.count > 0 || manager.pendingRequests.count > 0 { + if !manager.activeSessions.isEmpty || !manager.pendingRequests.isEmpty { ScrollView { VStack(spacing: 0) { pendingViews - .visibility(manager.pendingRequests.count > 0 ? .visible : .gone) + .visibility(!manager.pendingRequests.isEmpty ? .visible : .gone) connectedViews - .visibility(manager.activeSessions.count > 0 ? .visible : .gone) + .visibility(!manager.activeSessions.isEmpty ? .visible : .gone) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding(.horizontal, 18) } - .navigationBarItems(center: HStack { - Image("walletconnect") - .frame(width: 24, height: 24) - Text("walletconnect".localized) - .font(.LL.body) - .fontWeight(.semibold) - }, - trailing: - Button { - ScanHandler.scan() - } label: { - Image("icon-wallet-scan") - .renderingMode(.template) - .foregroundColor(.primary) - }) - .backgroundFill(Color.LL.Neutrals.background) - .applyRouteable(self) - } else { - WalletConnectView.EmptyView() - .backgroundFill(Color.LL.Neutrals.background) - .navigationBarBackButtonHidden(true) - .navigationBarTitleDisplayMode(navigationBarTitleDisplayMode) - .navigationBarHidden(isNavigationBarHidden) - .navigationBarItems(center: HStack { + .navigationBarItems( + center: HStack { Image("walletconnect") .frame(width: 24, height: 24) Text("walletconnect".localized) @@ -160,11 +145,39 @@ struct WalletConnectView: RouteableView { Image("icon-wallet-scan") .renderingMode(.template) .foregroundColor(.primary) - }) + } + ) + .backgroundFill(Color.LL.Neutrals.background) + .applyRouteable(self) + } else { + WalletConnectView.EmptyView() + .backgroundFill(Color.LL.Neutrals.background) + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(navigationBarTitleDisplayMode) + .navigationBarHidden(isNavigationBarHidden) + .navigationBarItems( + center: HStack { + Image("walletconnect") + .frame(width: 24, height: 24) + Text("walletconnect".localized) + .font(.LL.body) + .fontWeight(.semibold) + }, + trailing: + Button { + ScanHandler.scan() + } label: { + Image("icon-wallet-scan") + .renderingMode(.template) + .foregroundColor(.primary) + } + ) } } } +// MARK: WalletConnectView.EmptyView + extension WalletConnectView { struct EmptyView: View { let animationView = AnimationView(name: "QRScan", bundle: .main) @@ -172,12 +185,14 @@ extension WalletConnectView { var body: some View { VStack(alignment: .center, spacing: 18) { Spacer() - ResizableLottieView(lottieView: animationView, - color: Color(hex: "#3B99FC")) - .aspectRatio(contentMode: .fit) - .frame(width: 120, height: 120) - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) + ResizableLottieView( + lottieView: animationView, + color: Color(hex: "#3B99FC") + ) + .aspectRatio(contentMode: .fit) + .frame(width: 120, height: 120) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) Text("Scan to Connect") .foregroundColor(.LL.text) @@ -214,27 +229,34 @@ extension WalletConnectView { } } +// MARK: WalletConnectView.ItemCell + extension WalletConnectView { struct ItemCell: View { + // MARK: Lifecycle + + init(title: String, url: String, network: String, icon: String) { + self.title = title + self.url = url + self.network = network + self.icon = icon + fetchSVG() + } + + // MARK: Internal + let title: String let url: String let network: String let icon: String - @State var svgString: String? = nil + @State + var svgString: String? = nil var color: SwiftUI.Color { network == "testnet" ? Color.LL.flow : Color.LL.Primary.salmonPrimary } - init(title: String, url: String, network: String, icon: String) { - self.title = title - self.url = url - self.network = network - self.icon = icon - fetchSVG() - } - var body: some View { HStack(spacing: 0) { if let svg = svgString { diff --git a/FRW/Modules/Profile/WalletConnect/WalletConnectViewModel.swift b/FRW/Modules/Profile/WalletConnect/WalletConnectViewModel.swift index d7079271..76847ab4 100644 --- a/FRW/Modules/Profile/WalletConnect/WalletConnectViewModel.swift +++ b/FRW/Modules/Profile/WalletConnect/WalletConnectViewModel.swift @@ -1,5 +1,5 @@ // -// ProfileBackupViewModel.swift +// WalletConnectViewModel.swift // Flow Wallet // // Created by Selina on 2/8/2022. @@ -9,17 +9,24 @@ import Combine import SwiftUI import WalletConnectSign +// MARK: - WalletConnectViewModel + class WalletConnectViewModel: ObservableObject { - private var cancelSets = Set() + // MARK: Lifecycle init() { WalletConnectManager.shared.reloadPendingRequests() } + + // MARK: Private + + private var cancelSets = Set() } extension WalletConnectSign.Request { var logoURL: URL? { - if let session = WalletConnectManager.shared.activeSessions.first(where: { $0.topic == self.topic }), let logoString = session.peer.icons.first { + if let session = WalletConnectManager.shared.activeSessions + .first(where: { $0.topic == self.topic }), let logoString = session.peer.icons.first { return URL(string: logoString) } @@ -27,7 +34,8 @@ extension WalletConnectSign.Request { } var name: String? { - if let session = WalletConnectManager.shared.activeSessions.first(where: { $0.topic == self.topic }) { + if let session = WalletConnectManager.shared.activeSessions + .first(where: { $0.topic == self.topic }) { return session.peer.name } @@ -35,7 +43,8 @@ extension WalletConnectSign.Request { } var dappURL: URL? { - if let session = WalletConnectManager.shared.activeSessions.first(where: { $0.topic == self.topic }) { + if let session = WalletConnectManager.shared.activeSessions + .first(where: { $0.topic == self.topic }) { return URL(string: session.peer.url) } diff --git a/FRW/Modules/Profile/WalletSetting/PrivateKeyView.swift b/FRW/Modules/Profile/WalletSetting/PrivateKeyView.swift index fe1996d5..5316bf88 100644 --- a/FRW/Modules/Profile/WalletSetting/PrivateKeyView.swift +++ b/FRW/Modules/Profile/WalletSetting/PrivateKeyView.swift @@ -1,5 +1,5 @@ // -// PrivateKeyScreen.swift +// PrivateKeyView.swift // Flow Wallet // // Created by Hao Fu on 7/9/2022. @@ -7,14 +7,16 @@ import SwiftUI +// MARK: - PrivateKeyView + struct PrivateKeyView: RouteableView { + @State + var isBlur: Bool = true + var title: String { "Private Key".localized.capitalized } - @State - var isBlur: Bool = true - var body: some View { ScrollView { VStack(spacing: 16) { @@ -45,7 +47,8 @@ struct PrivateKeyView: RouteableView { Spacer() Button { - UIPasteboard.general.string = WalletManager.shared.getCurrentPrivateKey() ?? "" + UIPasteboard.general.string = WalletManager.shared + .getCurrentPrivateKey() ?? "" HUD.success(title: "copied".localized) } label: { Label(LocalizedStringKey("Copy".localized), colorImage: "Copy") @@ -69,7 +72,8 @@ struct PrivateKeyView: RouteableView { Spacer() Button { - UIPasteboard.general.string = WalletManager.shared.getCurrentPublicKey() ?? "" + UIPasteboard.general.string = WalletManager.shared + .getCurrentPublicKey() ?? "" HUD.success(title: "copied".localized) } label: { @@ -132,6 +136,8 @@ struct PrivateKeyView: RouteableView { } } +// MARK: - PrivateKeyScreen_Previews + struct PrivateKeyScreen_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/FRW/Modules/Profile/WalletSetting/WalletListView.swift b/FRW/Modules/Profile/WalletSetting/WalletListView.swift index 7f6d111f..fe13bc06 100644 --- a/FRW/Modules/Profile/WalletSetting/WalletListView.swift +++ b/FRW/Modules/Profile/WalletSetting/WalletListView.swift @@ -7,6 +7,8 @@ import SwiftUI +// MARK: - WalletListViewModel.Item + extension WalletListViewModel { struct Item { var user: WalletAccount.User @@ -16,9 +18,13 @@ extension WalletListViewModel { } } +// MARK: - WalletListViewModel + class WalletListViewModel: ObservableObject { - @Published var mainWallets: [WalletListViewModel.Item] = [] - @Published var multiVMWallets: [WalletListViewModel.Item] = [] + @Published + var mainWallets: [WalletListViewModel.Item] = [] + @Published + var multiVMWallets: [WalletListViewModel.Item] = [] func reload() { mainWallets = [] @@ -28,17 +34,28 @@ class WalletListViewModel: ObservableObject { if !balance.isEmpty { balance += " Flow" } - let mainWallet = WalletListViewModel.Item(user: user, address: mainAddress, balance: balance, isEvm: false) + let mainWallet = WalletListViewModel.Item( + user: user, + address: mainAddress, + balance: balance, + isEvm: false + ) mainWallets.append(mainWallet) } multiVMWallets = [] - EVMAccountManager.shared.accounts.forEach { account in + for account in EVMAccountManager.shared.accounts { let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - var balance = WalletManager.shared.balanceProvider.balanceValue(at: account.showAddress) ?? "" + var balance = WalletManager.shared.balanceProvider + .balanceValue(at: account.showAddress) ?? "" if !balance.isEmpty { balance += " Flow" } - let model = WalletListViewModel.Item(user: user, address: account.showAddress, balance: balance, isEvm: true) + let model = WalletListViewModel.Item( + user: user, + address: account.showAddress, + balance: balance, + isEvm: true + ) multiVMWallets.append(model) } } @@ -48,13 +65,16 @@ class WalletListViewModel: ObservableObject { } } +// MARK: - WalletListView + struct WalletListView: RouteableView { + @StateObject + var viewModel = WalletListViewModel() + var title: String { - return "wallet_list".localized + "wallet_list".localized } - @StateObject var viewModel = WalletListViewModel() - var body: some View { VStack { ScrollView { @@ -91,7 +111,7 @@ struct WalletListView: RouteableView { Spacer() } } - .visibility(viewModel.multiVMWallets.count > 0 ? .visible : .gone) + .visibility(!viewModel.multiVMWallets.isEmpty ? .visible : .gone) } } .padding(.horizontal, 18) @@ -112,6 +132,8 @@ struct WalletListView: RouteableView { } } +// MARK: WalletListView.Cell + extension WalletListView { struct Cell: View { let item: WalletListViewModel.Item diff --git a/FRW/Modules/Register/TermsAndPolicy.swift b/FRW/Modules/Register/TermsAndPolicy.swift index 41455d1a..ec76700f 100644 --- a/FRW/Modules/Register/TermsAndPolicy.swift +++ b/FRW/Modules/Register/TermsAndPolicy.swift @@ -7,11 +7,13 @@ import SwiftUI +// MARK: - TermsAndPolicy + struct TermsAndPolicy: RouteableView { let mnemonic: String? var title: String { - return "" + "" } var body: some View { @@ -57,19 +59,23 @@ struct TermsAndPolicy: RouteableView { }.padding() } .foregroundColor(Color.LL.text) - .overlay( RoundedRectangle(cornerRadius: 16) - .stroke(Color.LL.outline, - lineWidth: 1) + .stroke( + Color.LL.outline, + lineWidth: 1 + ) ) .padding(.bottom, 40) - VPrimaryButton(model: ButtonStyle.primary, - action: { - Router.route(to: RouteMap.Register.username(mnemonic)) - }, title: "i_accept".localized) - .padding(.bottom, 20) + VPrimaryButton( + model: ButtonStyle.primary, + action: { + Router.route(to: RouteMap.Register.username(mnemonic)) + }, + title: "i_accept".localized + ) + .padding(.bottom, 20) } .padding(.horizontal, 28) .background(Color.LL.background, ignoresSafeAreaEdges: .all) @@ -77,6 +83,8 @@ struct TermsAndPolicy: RouteableView { } } +// MARK: - TermsAndPolicy_Previews + struct TermsAndPolicy_Previews: PreviewProvider { static var previews: some View { TermsAndPolicy(mnemonic: nil) diff --git a/FRW/Modules/Review/AppConfig.swift b/FRW/Modules/Review/AppConfig.swift index e3070d76..9541fcd5 100644 --- a/FRW/Modules/Review/AppConfig.swift +++ b/FRW/Modules/Review/AppConfig.swift @@ -7,7 +7,9 @@ import Foundation -struct AppHide { +// MARK: - AppHide + +enum AppHide { static var FTAdd: ViewVisibility { if isChildAccount { return .gone @@ -46,6 +48,6 @@ struct AppHide { extension AppHide { private static var isChildAccount: Bool { - return ChildAccountManager.shared.selectedChildAccount != nil + ChildAccountManager.shared.selectedChildAccount != nil } } diff --git a/FRW/Modules/Secure/BionicErrorHandler.swift b/FRW/Modules/Secure/BionicErrorHandler.swift index 05516fb7..dd6212f7 100644 --- a/FRW/Modules/Secure/BionicErrorHandler.swift +++ b/FRW/Modules/Secure/BionicErrorHandler.swift @@ -8,7 +8,7 @@ import BiometricAuthentication import SwiftUI -struct BionicErrorHandler { +enum BionicErrorHandler { static func handleError(_ error: AuthenticationError) { switch error { case .biometryNotAvailable: diff --git a/FRW/Modules/Secure/CreatePinCodeView.swift b/FRW/Modules/Secure/CreatePinCodeView.swift index df5d7e5c..c81617f9 100644 --- a/FRW/Modules/Secure/CreatePinCodeView.swift +++ b/FRW/Modules/Secure/CreatePinCodeView.swift @@ -1,5 +1,5 @@ // -// CreatePinCode.swift +// CreatePinCodeView.swift // Flow Wallet // // Created by Hao Fu on 6/1/22. @@ -17,14 +17,20 @@ extension CreatePinCodeView { } } +// MARK: - CreatePinCodeView + struct CreatePinCodeView: RouteableView { - @StateObject var viewModel = CreatePinCodeViewModel() - @State var text: String = "" - @State var focuse: Bool = false - @FocusState private var pinCodeViewIsFocus: Bool + // MARK: Internal + + @StateObject + var viewModel = CreatePinCodeViewModel() + @State + var text: String = "" + @State + var focuse: Bool = false var title: String { - return "" + "" } var body: some View { @@ -69,8 +75,15 @@ struct CreatePinCodeView: RouteableView { .backgroundFill(.LL.background) .applyRouteable(self) } + + // MARK: Private + + @FocusState + private var pinCodeViewIsFocus: Bool } +// MARK: - CreatePinCodeView_Previews + struct CreatePinCodeView_Previews: PreviewProvider { static var previews: some View { CreatePinCodeView() diff --git a/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift b/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift index 14c2895e..be97882b 100644 --- a/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift +++ b/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift @@ -17,6 +17,8 @@ import web3swift import Web3Wallet import WebKit +// MARK: - TrustJSMessageHandler + class TrustJSMessageHandler: NSObject { weak var webVC: BrowserViewController? @@ -31,8 +33,7 @@ class TrustJSMessageHandler: NSObject { extension TrustJSMessageHandler { private func extractMethod(json: [String: Any]) -> TrustAppMethod? { - guard - let name = json["name"] as? String + guard let name = json["name"] as? String else { return nil } @@ -40,8 +41,7 @@ extension TrustJSMessageHandler { } private func extractNetwork(json: [String: Any]) -> ProviderNetwork? { - guard - let network = json["network"] as? String + guard let network = json["network"] as? String else { return nil } @@ -49,20 +49,18 @@ extension TrustJSMessageHandler { } private func extractMessage(json: [String: Any]) -> Data? { - guard - let params = json["object"] as? [String: Any], - let string = params["data"] as? String, - let data = Data(hexString: string) + guard let params = json["object"] as? [String: Any], + let string = params["data"] as? String, + let data = Data(hexString: string) else { return nil } return data } - + private func extractRaw(json: [String: Any]) -> String? { - guard - let params = json["object"] as? [String: Any], - let raw = params["raw"] as? String + guard let params = json["object"] as? [String: Any], + let raw = params["raw"] as? String else { return nil } @@ -77,11 +75,10 @@ extension TrustJSMessageHandler { } private func extractEthereumChainId(json: [String: Any]) -> Int? { - guard - let params = json["object"] as? [String: Any], - let string = params["chainId"] as? String, - let chainId = Int(String(string.dropFirst(2)), radix: 16), - chainId > 0 + guard let params = json["object"] as? [String: Any], + let string = params["chainId"] as? String, + let chainId = Int(String(string.dropFirst(2)), radix: 16), + chainId > 0 else { return nil } @@ -89,9 +86,13 @@ extension TrustJSMessageHandler { } } +// MARK: WKScriptMessageHandler + extension TrustJSMessageHandler: WKScriptMessageHandler { func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { let json = message.json + let url = message.frameInfo.request.url ?? webVC?.webView.url + guard let method = extractMethod(json: json), let id = json["id"] as? Int64, let network = extractNetwork(json: json) @@ -103,7 +104,7 @@ extension TrustJSMessageHandler: WKScriptMessageHandler { switch method { case .requestAccounts: log.info("[Trust] requestAccounts") - handleRequestAccounts(network: network, id: id) + handleRequestAccounts(url: url, network: network, id: id) case .signRawTransaction: log.info("[Trust] signRawTransaction") case .signTransaction: @@ -113,44 +114,42 @@ extension TrustJSMessageHandler: WKScriptMessageHandler { log.info("[Trust] data is missing") return } - handleSendTransaction(network: network, id: id, info: obj) + handleSendTransaction(url: url, network: network, id: id, info: obj) case .signMessage: log.info("[Trust] signMessage") - case .signTypedMessage: guard let data = extractMessage(json: json), - let raw = extractRaw(json: json) else { + let raw = extractRaw(json: json) + else { print("data is missing") return } - handleSignTypedMessage(id: id, data: data, raw: raw) + handleSignTypedMessage(url: url, id: id, data: data, raw: raw) case .signPersonalMessage: guard let data = extractMessage(json: json) else { log.info("[Trust] data is missing") return } - handleSignPersonal(network: network, id: id, data: data, addPrefix: true) + handleSignPersonal(url: url, network: network, id: id, data: data, addPrefix: true) case .sendTransaction: log.info("[Trust] sendTransaction") - case .ecRecover: log.info("[Trust] ecRecover") - case .watchAsset: print("[Trust] watchAsset") - guard let data = extractMessage(json: json) else { - log.info("[Trust] data is missing") + guard let obj = extractObject(json: json) + else { + log.info("[Trust] data is missing\(method)") return } - handleWatchAsset(network: network, id: id, data: Data()) + handleWatchAsset(network: network, id: id, json: obj) case .addEthereumChain: log.info("[Trust] addEthereumChain") case .switchEthereumChain: log.info("[Trust] switchEthereumChain") switch network { case .ethereum: - guard - let chainId = extractEthereumChainId(json: json) + guard let chainId = extractEthereumChainId(json: json) else { print("chain id is invalid") return @@ -166,22 +165,23 @@ extension TrustJSMessageHandler: WKScriptMessageHandler { } extension TrustJSMessageHandler { - private func handleRequestAccounts(network: ProviderNetwork, id: Int64) { + private func handleRequestAccounts(url: URL?, network: ProviderNetwork, id: Int64) { let callback = { [weak self] in guard let self = self else { return } - + let address = webVC?.trustProvider?.config.ethereum.address ?? "" let title = webVC?.webView.title ?? "unknown" let chainID = LocalUserDefaults.shared.flowNetwork.toFlowType() - let url = webVC?.webView.url - let vm = BrowserAuthnViewModel(title: title, - url: url?.host ?? "unknown", - logo: url?.absoluteString.toFavIcon()?.absoluteString, - walletAddress: address, - network: chainID) { [weak self] result in + let vm = BrowserAuthnViewModel( + title: title, + url: url?.host ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + walletAddress: address, + network: chainID + ) { [weak self] result in guard let self = self else { return } @@ -203,10 +203,19 @@ extension TrustJSMessageHandler { Router.route(to: RouteMap.Explore.authn(vm)) } - MoveAssetsAction.shared.startBrowserWithMoveAssets(appName: webVC?.webView.title, callback: callback) + MoveAssetsAction.shared.startBrowserWithMoveAssets( + appName: webVC?.webView.title, + callback: callback + ) } - - private func handleSignPersonal(network: ProviderNetwork, id: Int64, data: Data, addPrefix _: Bool) { + + private func handleSignPersonal( + url: URL?, + network: ProviderNetwork, + id: Int64, + data: Data, + addPrefix _: Bool + ) { Task { await TrustJSMessageHandler.checkCoa() } @@ -214,11 +223,13 @@ extension TrustJSMessageHandler { if title.isEmpty { title = "unknown" } - let url = webVC?.webView.url - let vm = BrowserSignMessageViewModel(title: title, - url: url?.absoluteString ?? "unknown", - logo: url?.absoluteString.toFavIcon()?.absoluteString, - cadence: data.hexString) { [weak self] result in + + let vm = BrowserSignMessageViewModel( + title: title, + url: url?.absoluteString ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + cadence: data.hexString + ) { [weak self] result in guard let self = self else { return } @@ -237,11 +248,20 @@ extension TrustJSMessageHandler { return } let keyIndex = BigUInt(WalletManager.shared.keyIndex) - let proof = COAOwnershipProof(keyIninces: [keyIndex], address: address.data, capabilityPath: "evm", signatures: [sig]) + let proof = COAOwnershipProof( + keyIninces: [keyIndex], + address: address.data, + capabilityPath: "evm", + signatures: [sig] + ) guard let encoded = RLP.encode(proof.rlpList) else { return } - webVC?.webView.tw.send(network: .ethereum, result: encoded.hexString.addHexPrefix(), to: id) + webVC?.webView.tw.send( + network: .ethereum, + result: encoded.hexString.addHexPrefix(), + to: id + ) } else { webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) } @@ -249,8 +269,8 @@ extension TrustJSMessageHandler { Router.route(to: RouteMap.Explore.signMessage(vm)) } - - func handleSignTypedMessage(id: Int64, data: Data, raw: String) { + + func handleSignTypedMessage(url: URL?, id: Int64, data: Data, raw: String) { Task { await TrustJSMessageHandler.checkCoa() } @@ -258,8 +278,13 @@ extension TrustJSMessageHandler { if title.isEmpty { title = "unknown" } - let url = webVC?.webView.url - let vm = BrowserSignTypedMessageViewModel(title: title, urlString: url?.absoluteString ?? "unknown", logo: url?.absoluteString.toFavIcon()?.absoluteString, rawString: raw) { [weak self] result in + + let vm = BrowserSignTypedMessageViewModel( + title: title, + urlString: url?.absoluteString ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + rawString: raw + ) { [weak self] result in guard let self = self else { return } @@ -276,11 +301,20 @@ extension TrustJSMessageHandler { return } let keyIndex = BigUInt(WalletManager.shared.keyIndex) - let proof = COAOwnershipProof(keyIninces: [keyIndex], address: address.data, capabilityPath: "evm", signatures: [sig]) + let proof = COAOwnershipProof( + keyIninces: [keyIndex], + address: address.data, + capabilityPath: "evm", + signatures: [sig] + ) guard let encoded = RLP.encode(proof.rlpList) else { return } - webVC?.webView.tw.send(network: .ethereum, result: encoded.hexString.addHexPrefix(), to: id) + webVC?.webView.tw.send( + network: .ethereum, + result: encoded.hexString.addHexPrefix(), + to: id + ) } else { webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) } @@ -289,12 +323,16 @@ extension TrustJSMessageHandler { Router.route(to: RouteMap.Explore.signTypedMessage(vm)) } - private func handleSendTransaction(network _: ProviderNetwork, id: Int64, info: [String: Any]) { + private func handleSendTransaction( + url: URL?, + network _: ProviderNetwork, + id: Int64, + info: [String: Any] + ) { var title = webVC?.webView.title ?? "unknown" if title.isEmpty { title = "unknown" } - let url = webVC?.webView.url let originCadence = CadenceManager.shared.current.evm?.callContract?.toFunc() ?? "" @@ -313,11 +351,13 @@ extension TrustJSMessageHandler { .uint64(receiveModel.gasValue), ] - let vm = BrowserAuthzViewModel(title: title, - url: url?.absoluteString ?? "unknown", - logo: url?.absoluteString.toFavIcon()?.absoluteString, - cadence: originCadence, - arguments: args.toArguments()) { [weak self] result in + let vm = BrowserAuthzViewModel( + title: title, + url: url?.absoluteString ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + cadence: originCadence, + arguments: args.toArguments() + ) { [weak self] result in guard let self = self else { self?.webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) @@ -331,7 +371,12 @@ extension TrustJSMessageHandler { Task { do { - let txid = try await FlowNetwork.sendTransaction(amount: receiveModel.amount, data: receiveModel.dataValue, toAddress: toAddr, gas: receiveModel.gasValue) + let txid = try await FlowNetwork.sendTransaction( + amount: receiveModel.amount, + data: receiveModel.dataValue, + toAddress: toAddr, + gas: receiveModel.gasValue + ) let holder = TransactionManager.TransactionHolder(id: txid, type: .transferCoin) TransactionManager.shared.newTransaction(holder: holder) let result = try await txid.onceSealed() @@ -385,7 +430,11 @@ extension TrustJSMessageHandler { self?.webVC?.webView.tw.sendNull(network: .ethereum, id: id) } else { log.error("Unknown chain id: \(chainId)") - self?.webVC?.webView.tw.send(network: .ethereum, error: "Unknown chain id", to: id) + self?.webVC?.webView.tw.send( + network: .ethereum, + error: "Unknown chain id", + to: id + ) } } Router.route(to: RouteMap.Explore.switchNetwork(fromId, toId, callback)) @@ -393,7 +442,7 @@ extension TrustJSMessageHandler { } private func signWithMessage(data: Data) -> Data? { - return WalletManager.shared.signSync(signableData: data) + WalletManager.shared.signSync(signableData: data) } private func cancel(id: Int64) { @@ -401,10 +450,32 @@ extension TrustJSMessageHandler { self.webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) } } - - private func handleWatchAsset(network: ProviderNetwork, id: Int64, data: Data) { + + private func handleWatchAsset(network: ProviderNetwork, id: Int64, json: [String: Any]) { let manager = WalletManager.shared.customTokenManager - + guard let contract = json["contract"] as? String else { + cancel(id: id) + return + } + Task { + HUD.loading() + guard let token = try await manager.findToken(evmAddress: contract) else { + HUD.dismissLoading() + DispatchQueue.main.async { + self.webVC?.webView.tw + .send(network: .ethereum, result: "false", to: id) + } + return + } + HUD.dismissLoading() + let callback: BoolClosure = { result in + DispatchQueue.main.async { + self.webVC?.webView.tw + .send(network: .ethereum, result: result ? "true" : "false", to: id) + } + } + Router.route(to: RouteMap.Wallet.addTokenSheet(token, callback)) + } } } @@ -420,18 +491,18 @@ extension TrustJSMessageHandler { do { HUD.loading() let result = try await FlowNetwork.checkCoaLink(address: addrStr) - if result != nil && result == false { + if result != nil, result == false { let txid = try await FlowNetwork.coaLink() let result = try await txid.onceSealed() if !result.isFailed { list.append(addrStr) } - }else { + } else { list.append(addrStr) } LocalUserDefaults.shared.checkCoa = list HUD.dismissLoading() - }catch { + } catch { HUD.dismissLoading() } } diff --git a/FRW/Modules/Wallet/AddToken/AddTokenViewModel.swift b/FRW/Modules/Wallet/AddToken/AddTokenViewModel.swift index 77765b7f..a4b6c4fe 100644 --- a/FRW/Modules/Wallet/AddToken/AddTokenViewModel.swift +++ b/FRW/Modules/Wallet/AddToken/AddTokenViewModel.swift @@ -11,15 +11,17 @@ import SwiftUI extension AddTokenViewModel { class Section: ObservableObject, Identifiable, Indexable { - @Published var sectionName: String = "#" - @Published var tokenList: [TokenModel] = [] + @Published + var sectionName: String = "#" + @Published + var tokenList: [TokenModel] = [] var id: String { - return sectionName + sectionName } var index: Index? { - return Index(sectionName, contentID: id) + Index(sectionName, contentID: id) } } @@ -29,29 +31,22 @@ extension AddTokenViewModel { } } -class AddTokenViewModel: ObservableObject { - @Published var sections: [Section] = [] - @Published var searchText: String = "" - - @Published var confirmSheetIsPresented = false - var pendingActiveToken: TokenModel? - - var mode: AddTokenViewModel.Mode = .addToken - var selectedToken: TokenModel? - var disableTokens: [TokenModel] = [] - var selectCallback: ((TokenModel) -> Void)? - - @Published var isRequesting: Bool = false +// MARK: - AddTokenViewModel - private var cancelSets = Set() +class AddTokenViewModel: ObservableObject { + // MARK: Lifecycle - init(selectedToken: TokenModel? = nil, disableTokens: [TokenModel] = [], selectCallback: ((TokenModel) -> Void)? = nil) { + init( + selectedToken: TokenModel? = nil, + disableTokens: [TokenModel] = [], + selectCallback: ((TokenModel) -> Void)? = nil + ) { self.selectedToken = selectedToken self.disableTokens = disableTokens self.selectCallback = selectCallback if selectCallback != nil { - mode = .selectToken + self.mode = .selectToken } WalletManager.shared.$activatedCoins.sink { _ in @@ -61,6 +56,29 @@ class AddTokenViewModel: ObservableObject { }.store(in: &cancelSets) } + // MARK: Internal + + @Published + var sections: [Section] = [] + @Published + var searchText: String = "" + + @Published + var confirmSheetIsPresented = false + var pendingActiveToken: TokenModel? + + var mode: AddTokenViewModel.Mode = .addToken + var selectedToken: TokenModel? + var disableTokens: [TokenModel] = [] + var selectCallback: ((TokenModel) -> Void)? + + @Published + var isRequesting: Bool = false + + // MARK: Private + + private var cancelSets = Set() + private func reloadData() { guard let supportedTokenList = WalletManager.shared.supportedCoins else { sections = [] @@ -82,24 +100,28 @@ class AddTokenViewModel: ObservableObject { private func regroup(_ tokens: [TokenModel]) { BMChineseSort.share.compareTpye = .fullPinyin - BMChineseSort.sortAndGroup(objectArray: tokens, key: "name") { success, _, sectionTitleArr, sortedObjArr in - if !success { - assert(false, "can not be here") - return - } + BMChineseSort + .sortAndGroup( + objectArray: tokens, + key: "name" + ) { success, _, sectionTitleArr, sortedObjArr in + if !success { + assertionFailure("can not be here") + return + } - var sections = [AddTokenViewModel.Section]() - for (index, title) in sectionTitleArr.enumerated() { - let section = AddTokenViewModel.Section() - section.sectionName = title - section.tokenList = sortedObjArr[index] - sections.append(section) - } + var sections = [AddTokenViewModel.Section]() + for (index, title) in sectionTitleArr.enumerated() { + let section = AddTokenViewModel.Section() + section.sectionName = title + section.tokenList = sortedObjArr[index] + sections.append(section) + } - DispatchQueue.main.async { - self.sections = sections + DispatchQueue.main.async { + self.sections = sections + } } - } } } @@ -131,7 +153,7 @@ extension AddTokenViewModel { } } - if list.count > 0 { + if !list.isEmpty { let newSection = AddTokenViewModel.Section() newSection.sectionName = section.sectionName newSection.tokenList = list @@ -211,7 +233,10 @@ extension AddTokenViewModel { Task { do { - let transactionId = try await FlowNetwork.enableToken(at: Flow.Address(hex: address), token: token) + let transactionId = try await FlowNetwork.enableToken( + at: Flow.Address(hex: address), + token: token + ) guard let data = try? JSONEncoder().encode(token) else { failedBlock() @@ -221,7 +246,11 @@ extension AddTokenViewModel { DispatchQueue.main.async { self.isRequesting = false self.confirmSheetIsPresented = false - let holder = TransactionManager.TransactionHolder(id: transactionId, type: .addToken, data: data) + let holder = TransactionManager.TransactionHolder( + id: transactionId, + type: .addToken, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) } } catch { diff --git a/FRW/Modules/Wallet/Card/CardBackground.swift b/FRW/Modules/Wallet/Card/CardBackground.swift index 02ae20c1..c3944a0f 100644 --- a/FRW/Modules/Wallet/Card/CardBackground.swift +++ b/FRW/Modules/Wallet/Card/CardBackground.swift @@ -9,16 +9,69 @@ import Foundation import SwiftUI enum CardBackground: CaseIterable { + case color(color: UIColor) + case image(imageIndex: Int) + case fade(imageIndex: Int) + case fluid + case matrix + case fluidGradient + + // MARK: Lifecycle + + init(value: String) { + let list = value.split(separator: ":", omittingEmptySubsequences: true) + switch list[0] { + case CardBackground.fluid.identify: + self = .fluid + case CardBackground.matrix.identify: + self = .matrix + case CardBackground.color(color: .clear).identify: + guard let hex = list[safe: 1] else { + self = .color(color: UIColor.LL.bgForIcon) + return + } + self = .color(color: UIColor(hex: String(hex))) + case CardBackground.fade(imageIndex: 0).identify: + guard let imageString = list[safe: 1], let index = Int(imageString) else { + self = .fade(imageIndex: 0) + return + } + self = .fade(imageIndex: index) + case CardBackground.image(imageIndex: 0).identify: + guard let imageString = list[safe: 1], let index = Int(imageString) else { + self = .image(imageIndex: 0) + return + } + self = .image(imageIndex: index) + case CardBackground.fluidGradient.identify: + self = .fluidGradient + default: + self = .fade(imageIndex: 0) + } + } + + // MARK: Internal + enum Style { case flow case evm } + struct Key: PreferenceKey { + public typealias Value = CardBackground + + public static var defaultValue = CardBackground.fluid + + public static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() + } + } + static var dynamicCases: [CardBackground] = [ .fluid, .matrix, .fade(imageIndex: 0), - .fluidGradient + .fluidGradient, ] static var imageCases: [CardBackground] = [ @@ -38,44 +91,15 @@ enum CardBackground: CaseIterable { .fade(imageIndex: 0), ] - case color(color: UIColor) - case image(imageIndex: Int) - case fade(imageIndex: Int) - case fluid - case matrix - case fluidGradient - - @ViewBuilder - func renderView() -> some View { - switch self { - case .fluid: - FluidView() - case .matrix: - MatrixRainView() - case let .color(color): - ZStack { - Color(color) - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Image("bg-circles") - .resizable() - .aspectRatio(contentMode: .fill) - } - case let .fade(imageIndex): - FadeAnimationBackground(image: fadeList[safe: imageIndex] ?? fadeList[0], color: fadeColor) - case let .image(imageIndex): - (imageList[safe: imageIndex] ?? imageList[0]) - .resizable() - .aspectRatio(contentMode: .fill) - case .fluidGradient: - FluidGradient(blobs: [.green, .blue, .red, .pink, .purple, .indigo], - highlights: [.green, .yellow, .orange, .blue, .pink, .indigo], - speed: 0.8) - } - } - var imageList: [Image] { - return [Image("wallpaper_0"), Image("wallpaper_1"), Image("wallpaper_2"), Image("wallpaper_3"), Image("wallpaper_4"), Image("wallpaper_5")] + [ + Image("wallpaper_0"), + Image("wallpaper_1"), + Image("wallpaper_2"), + Image("wallpaper_3"), + Image("wallpaper_4"), + Image("wallpaper_5"), + ] } var fadeList: [Image] { @@ -106,10 +130,6 @@ enum CardBackground: CaseIterable { } } - private var cardStyle: CardBackground.Style { - WalletManager.shared.isSelectedEVMAccount ? CardBackground.Style.evm : CardBackground.Style.flow - } - var rawValue: String { switch self { case let .color(color): @@ -157,45 +177,44 @@ enum CardBackground: CaseIterable { } } - init(value: String) { - let list = value.split(separator: ":", omittingEmptySubsequences: true) - switch list[0] { - case CardBackground.fluid.identify: - self = .fluid - case CardBackground.matrix.identify: - self = .matrix - case CardBackground.color(color: .clear).identify: - guard let hex = list[safe: 1] else { - self = .color(color: UIColor.LL.bgForIcon) - return - } - self = .color(color: UIColor(hex: String(hex))) - - case CardBackground.fade(imageIndex: 0).identify: - guard let imageString = list[safe: 1], let index = Int(imageString) else { - self = .fade(imageIndex: 0) - return - } - self = .fade(imageIndex: index) + @ViewBuilder + func renderView() -> some View { + switch self { + case .fluid: + FluidView() + case .matrix: + MatrixRainView() + case let .color(color): + ZStack { + Color(color) + .frame(maxWidth: .infinity, maxHeight: .infinity) - case CardBackground.image(imageIndex: 0).identify: - guard let imageString = list[safe: 1], let index = Int(imageString) else { - self = .image(imageIndex: 0) - return + Image("bg-circles") + .resizable() + .aspectRatio(contentMode: .fill) } - self = .image(imageIndex: index) - case CardBackground.fluidGradient.identify: - self = .fluidGradient - default: - self = .fade(imageIndex: 0) + case let .fade(imageIndex): + FadeAnimationBackground( + image: fadeList[safe: imageIndex] ?? fadeList[0], + color: fadeColor + ) + case let .image(imageIndex): + (imageList[safe: imageIndex] ?? imageList[0]) + .resizable() + .aspectRatio(contentMode: .fill) + case .fluidGradient: + FluidGradient( + blobs: [.green, .blue, .red, .pink, .purple, .indigo], + highlights: [.green, .yellow, .orange, .blue, .pink, .indigo], + speed: 0.8 + ) } } - struct Key: PreferenceKey { - public typealias Value = CardBackground - public static var defaultValue = CardBackground.fluid - public static func reduce(value: inout Value, nextValue: () -> Value) { - value = nextValue() - } + // MARK: Private + + private var cardStyle: CardBackground.Style { + WalletManager.shared.isSelectedEVMAccount ? CardBackground.Style.evm : CardBackground.Style + .flow } } diff --git a/FRW/Modules/Wallet/Card/StyleView/FluidView.swift b/FRW/Modules/Wallet/Card/StyleView/FluidView.swift index 22ab2982..509808a7 100644 --- a/FRW/Modules/Wallet/Card/StyleView/FluidView.swift +++ b/FRW/Modules/Wallet/Card/StyleView/FluidView.swift @@ -1,5 +1,5 @@ // -// WebView.swift +// FluidView.swift // Flow Wallet // // Created by Hao Fu on 21/8/2022. @@ -18,9 +18,10 @@ struct FluidView: UIViewRepresentable { } func updateUIView(_ webView: WKWebView, context _: Context) { - if let indexURL = Bundle.main.url(forResource: "index", - withExtension: "html") - { + if let indexURL = Bundle.main.url( + forResource: "index", + withExtension: "html" + ) { webView.loadFileURL(indexURL, allowingReadAccessTo: indexURL) } } diff --git a/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift b/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift index 07730862..3d49f5ce 100644 --- a/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift +++ b/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift @@ -7,6 +7,8 @@ import SwiftUI +// MARK: - MatrixRainView + struct MatrixRainView: View { var body: some View { ZStack { @@ -19,7 +21,7 @@ struct MatrixRainView: View { // Repeating the effects until it occupied the full screen // With the help of ForEach // For Count since our font size is 25 so width/fontSize will the give the count - ForEach(1 ... Int(size.width / 25), id: \.self) { _ in + ForEach(1...Int(size.width / 25), id: \.self) { _ in MatrixRainCharacters(size: size) } } @@ -29,23 +31,27 @@ struct MatrixRainView: View { } } +// MARK: - MatrixRainCharacters + struct MatrixRainCharacters: View { var size: CGSize // MARK: Animation Properties - @State var startAnimation: Bool = false + @State + var startAnimation: Bool = false - @State var random: Int = 0 + @State + var random: Int = 0 var body: some View { // Random Height - let randomHeight: CGFloat = .random(in: (size.height / 2) ... size.height) + let randomHeight: CGFloat = .random(in: (size.height / 2)...size.height) VStack { // MARK: Iterating String - ForEach(0 ..< constant.count, id: \.self) { index in + ForEach(0.. Bool { let result = address.lowercased().hasPrefix("0x") return result @@ -54,4 +58,3 @@ extension AddCustomTokenViewModel { Router.route(to: RouteMap.Wallet.showCustomToken(token)) } } - diff --git a/FRW/Modules/Wallet/CustomToken/AddTokenSheetView.swift b/FRW/Modules/Wallet/CustomToken/AddTokenSheetView.swift new file mode 100644 index 00000000..96db4c35 --- /dev/null +++ b/FRW/Modules/Wallet/CustomToken/AddTokenSheetView.swift @@ -0,0 +1,151 @@ +// +// AddTokenSheetView.swift +// FRW +// +// Created by cat on 11/5/24. +// + +import Kingfisher +import SwiftUI + +struct AddTokenSheetView: RouteableView & PresentActionDelegate { + var changeHeight: (() -> Void)? + + let customToken: CustomToken + let callback: (Bool) -> Void + + var title: String { + "" + } + + var isNavigationBarHidden: Bool { + true + } + + var body: some View { + GeometryReader { _ in + VStack(alignment: .leading, spacing: 0) { + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + HStack { + Text("Add Suggested Token".localized) + .font(.inter(size: 18, weight: .w700)) + .foregroundStyle(Color.LL.Neutrals.text) + .padding(.top, 6) + Spacer() + + Button { + onClose() + } label: { + Image("icon_close_circle_gray") + .resizable() + .frame(width: 24, height: 24) + } + } + .padding(.top, 8) + HStack { + Text("like_import_token".localized) + .font(.inter(size: 14)) + .foregroundStyle(Color.Theme.Text.black3) + .padding(.top, 12) + Spacer() + } + + Divider() + .foregroundStyle(Color.Theme.Line.line) + .padding(.top, 16) + .padding(.bottom, 16) + + tokenView() + .padding(.bottom, 16) + Spacer() + } + .padding(18) + } + } + .backgroundFill(Color.Theme.BG.bg1) + .cornerRadius([.topLeading, .topTrailing], 16) + .edgesIgnoringSafeArea(.bottom) + .overlay(alignment: .bottom) { + VPrimaryButton( + model: ButtonStyle.primary, + state: .enabled, + action: { + onAdd() + }, + title: "add_token".localized + ) + .padding(.horizontal, 18) + .padding(.bottom, 8) + } + } + .applyRouteable(self) + } + + func onClose() { + callback(false) + Router.dismiss() + } + + func customViewDidDismiss() { + callback(false) + } + + func onAdd() { + Task { + let manager = WalletManager.shared.customTokenManager + let isExist = manager.isExist(token: customToken) + if !isExist { + await manager.add(token: customToken) + } + DispatchQueue.main.async { + HUD.success(title: "successful".localized) + self.callback(true) + Router.dismiss() + } + } + } + + func tokenView() -> some View { + HStack { + KFImage.url(URL(string: customToken.icon ?? "")) + .placeholder { + Image("placeholder") + .resizable() + } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 40, height: 40) + .clipShape(Circle()) + + Text(customToken.name) + .font(.inter(size: 16, weight: .bold)) + .foregroundStyle(Color.Theme.Text.black) + + Spacer() + + Text(customToken.balanceValue + " " + "flow".localized.uppercased()) + .font(.inter(size: 16)) + .foregroundStyle(Color.Theme.Text.black) + } + .padding(16) + .background(Color.Theme.Background.pureWhite) + .cornerRadius(16) + } +} + +#Preview { + AddTokenSheetView( + customToken: CustomToken( + address: "", + decimals: 12, + name: "", + symbol: "", + flowIdentifier: nil, + belong: .evm, + balance: nil, + icon: nil + ) + ) { _ in + } +} diff --git a/FRW/Modules/Wallet/CustomToken/CustomTokenDetailView.swift b/FRW/Modules/Wallet/CustomToken/CustomTokenDetailView.swift index bb620481..c5ca7b39 100644 --- a/FRW/Modules/Wallet/CustomToken/CustomTokenDetailView.swift +++ b/FRW/Modules/Wallet/CustomToken/CustomTokenDetailView.swift @@ -7,15 +7,17 @@ import SwiftUI +// MARK: - CustomTokenDetailView + struct CustomTokenDetailView: RouteableView { let token: CustomToken - + var title: String { - return "Add Custom Token".localized + "Add Custom Token".localized } - + var body: some View { - VStack { + VStack(spacing: 12) { CustomTokenDetailView .Item( title: "Token Contract Address".localized, @@ -25,39 +27,43 @@ struct CustomTokenDetailView: RouteableView { .foregroundStyle(Color.Theme.Line.stroke) CustomTokenDetailView .Item(title: "Token Name".localized, content: token.name) - + CustomTokenDetailView .Item(title: "Token Symbol".localized, content: token.symbol) - + CustomTokenDetailView .Item(title: "Token Decimal".localized, content: String(token.decimals)) - + CustomTokenDetailView .Item( title: "Flow Identifier".localized, - content: token.flowIdentifier ?? "") + content: token.flowIdentifier ?? "" + ) .visibility(token.flowIdentifier == nil ? .gone : .visible) - + Spacer() - - VPrimaryButton(model: ButtonStyle.primary, - state: .enabled, - action: { - onClickImport() - }, title: "import_btn_text".localized) + + VPrimaryButton( + model: ButtonStyle.primary, + state: .enabled, + action: { + onClickImport() + }, + title: "import_btn_text".localized + ) } .padding(16) .background(.Theme.Background.bg2) .applyRouteable(self) } - + func onClickImport() { Task { let manager = WalletManager.shared.customTokenManager let inWhite = manager.isInWhite(token: token) if inWhite { HUD.success(title: "the token is added") - }else { + } else { HUD.loading() await manager.add(token: token) HUD.dismissLoading() @@ -67,13 +73,14 @@ struct CustomTokenDetailView: RouteableView { } } +// MARK: CustomTokenDetailView.Item extension CustomTokenDetailView { struct Item: View { var title: String var content: String var dark: Bool = false - + var body: some View { VStack(alignment: .leading) { TitleView(title: title, isStar: false) @@ -81,7 +88,7 @@ extension CustomTokenDetailView { Text(content) .font(.inter(size: 14)) .foregroundStyle( - dark ? Color.Theme.Text.black8 :Color.Theme.Text.black3 + dark ? Color.Theme.Text.black8 : Color.Theme.Text.black3 ) Spacer() } @@ -104,8 +111,7 @@ extension CustomTokenDetailView { decimals: 3, name: "Flow Test", symbol: "FLOW", - userId: "", - belongAddress: "" + belong: .evm ) ) } diff --git a/FRW/Modules/Wallet/CustomToken/CustomTokenManager.swift b/FRW/Modules/Wallet/CustomToken/CustomTokenManager.swift index 50a311f8..398b112c 100644 --- a/FRW/Modules/Wallet/CustomToken/CustomTokenManager.swift +++ b/FRW/Modules/Wallet/CustomToken/CustomTokenManager.swift @@ -5,21 +5,21 @@ // Created by cat on 11/1/24. // -import Foundation -import web3swift import BigInt +import Foundation import Web3Core +import web3swift +// MARK: - CustomTokenManager class CustomTokenManager: ObservableObject { - - @Published var list: [CustomToken] = [] - + // MARK: Internal + + @Published + var list: [CustomToken] = [] + var allTokens: [CustomToken] = LocalUserDefaults.shared.customToken - - private var queue = DispatchQueue(label: "CustomToken.add") - - + func refresh() { queue.sync { var result = findCurrent(list: allTokens) @@ -27,32 +27,7 @@ class CustomTokenManager: ObservableObject { list = result } } - - private func findCurrent(list: [CustomToken]) -> [CustomToken] { - guard let address = WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress(), let userId = UserManager.shared.activatedUID else { - return [] - } - let currentNetwork = LocalUserDefaults.shared.flowNetwork - let belong = EVMAccountManager.shared.selectedAccount != nil ? CustomToken.Belong.evm : .flow - - let result = list.filter { token in - token.belongAddress == address && token.network == currentNetwork && token.belong == belong && token.userId == userId - } - return result - } - - private func filterWhite(list: [CustomToken]) -> [CustomToken] { - - var result: [CustomToken] = [] - list.forEach { customToken in - if !isInWhite(token: customToken) { - result.append(customToken) - } - } - return result - } - - + func isInWhite(token: CustomToken) -> Bool { guard let support = WalletManager.shared.supportedCoins else { return true @@ -60,18 +35,26 @@ class CustomTokenManager: ObservableObject { let filterList = support.filter { model in model.evmAddress?.lowercased() == token.address.lowercased() } - return filterList.count > 0 + return !filterList.isEmpty } - + func isExist(token: CustomToken) -> Bool { queue.sync { let result = allTokens.filter { model in - token.belongAddress == model.belongAddress && token.network == model.network && token.belong == model.belong && token.userId == model.userId + token.network == model.network && model.address == token.address && token + .belong == model.belong } - return result.count > 0 + return !result.isEmpty + } + } + + func allowDelete(token: CustomToken) -> Bool { + guard !isInWhite(token: token) else { + return false } + return isExist(token: token) } - + func add(token: CustomToken) async { guard !isExist(token: token) else { return @@ -86,11 +69,56 @@ class CustomTokenManager: ObservableObject { refresh() WalletManager.shared.addCustomToken(token: tmpToken) } - + + func delete(token: CustomToken) { + queue.sync { + allTokens.removeAll { model in + token.network == model.network && token.belong == model.belong && model + .address == token.address + } + LocalUserDefaults.shared.customToken = allTokens + } + refresh() + WalletManager.shared.deleteCustomToken(token: token) + } + + // MARK: Private + + private var queue = DispatchQueue(label: "CustomToken.add") + + private func findCurrent(list: [CustomToken]) -> [CustomToken] { + guard let address = WalletManager.shared + .getWatchAddressOrChildAccountAddressOrPrimaryAddress(), + let userId = UserManager.shared.activatedUID else { + return [] + } + let currentNetwork = LocalUserDefaults.shared.flowNetwork + let belong = EVMAccountManager.shared.selectedAccount != nil ? CustomToken.Belong + .evm : .flow + + let result = list.filter { token in + token.network == currentNetwork && token.belong == belong + } + return result + } + + private func filterWhite(list: [CustomToken]) -> [CustomToken] { + var result: [CustomToken] = [] + for customToken in list { + if !isInWhite(token: customToken) { + result.append(customToken) + } + } + return result + } + private func update(token: CustomToken) { queue.sync { - let index = allTokens.firstIndex { $0.address == token.address } - guard let index else { + let index = allTokens.firstIndex { model in + token.belong == model.belong && model.address == token.address && token + .network == model.network + } + guard let index else { return } allTokens[index] = token @@ -102,55 +130,50 @@ class CustomTokenManager: ObservableObject { extension CustomTokenManager { func findToken(evmAddress: String) async throws -> CustomToken? { let evmAddress = evmAddress.addHexPrefix() - guard let uid = UserManager.shared.activatedUID, let belongAddress = WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress() else { - throw AddCustomTokenError.invalidProfile - } - - guard let web3 = try await FlowProvider.Web3.default() else{ + + guard let web3 = try await FlowProvider.Web3.default() else { throw AddCustomTokenError.providerFailed } let contratc = web3.contract(Web3Utils.erc20ABI, at: .init(evmAddress)) async let decimalsRequest = contratc?.createReadOperation("decimals")?.callContractMethod() - + let decimals = try await decimalsRequest?["0"] as? BigUInt let decimalInt = Int(decimals?.description ?? "6") ?? 0 - + async let symbolRequest = contratc?.createReadOperation("symbol")?.callContractMethod() async let nameRequest = contratc?.createReadOperation("name")?.callContractMethod() - let result: [String] = try await [symbolRequest, nameRequest].compactMap{ $0?["0"] as? String } + let result: [String] = try await [symbolRequest, nameRequest] + .compactMap { $0?["0"] as? String } guard result.count == 2 else { return nil } - + let name = result[1] let symbol = result[0] - + let flowIdentifier = try? await FlowNetwork.getAssociatedFlowIdentifier(address: evmAddress) - + let token = CustomToken( address: evmAddress, decimals: decimalInt, name: name, symbol: symbol, flowIdentifier: flowIdentifier, - userId: uid, - belongAddress: belongAddress, belong: .evm ) return token } - + func fetchAllEVMBalance() async { await withTaskGroup(of: Void.self) { group in - allTokens.forEach { token in + for token in allTokens { group.addTask { [weak self] in do { var model = token let balance = try await self?.fetchBalance(token: model) model.balance = balance self?.update(token: model) - } - catch { + } catch { log.info("[Custom Token] fetch balance failed.\(token.address)") } } @@ -158,12 +181,12 @@ extension CustomTokenManager { } refresh() } - + func fetchBalance(token: CustomToken) async throws -> BigUInt? { guard let coaAddresss = EVMAccountManager.shared.selectedAccount?.showAddress else { throw AddCustomTokenError.invalidProfile } - guard let web3 = try await FlowProvider.Web3.default() else{ + guard let web3 = try await FlowProvider.Web3.default() else { throw AddCustomTokenError.providerFailed } let contratc = web3.contract( @@ -171,7 +194,10 @@ extension CustomTokenManager { at: .init(token.address) ) // Parameters is user wallet address - let balanceRequest = try await contratc?.createReadOperation("balanceOf", parameters: [coaAddresss])?.callContractMethod() + let balanceRequest = try await contratc?.createReadOperation( + "balanceOf", + parameters: [coaAddresss] + )?.callContractMethod() if let balanceUInt = balanceRequest?["balance"] as? BigUInt { return balanceUInt } @@ -179,29 +205,66 @@ extension CustomTokenManager { } } -//MARK: - Model +// MARK: - CustomToken struct CustomToken: Codable { + // MARK: Lifecycle + + init( + address: String, + decimals: Int, + name: String, + symbol: String, + flowIdentifier: String? = nil, + belong: CustomToken.Belong = .evm, + balance: BigUInt? = nil, + icon _: String? = nil + ) { + self.address = address + self.decimals = decimals + self.name = name + self.symbol = symbol + self.belong = belong + self.balance = balance + self.flowIdentifier = flowIdentifier + + self.userId = UserManager.shared.activatedUID ?? "" + self.belongAddress = WalletManager.shared + .getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "" + self.network = LocalUserDefaults.shared.flowNetwork + } + + // MARK: Internal enum Belong: Codable { case flow case evm } - + var address: String var decimals: Int var name: String var symbol: String var flowIdentifier: String? - - + var userId: String var belongAddress: String var network: LocalUserDefaults.FlowNetworkType = .mainnet var belong: CustomToken.Belong = .flow // not store, var balance: BigUInt? - + var icon: String? = nil + + var balanceValue: String { + let balance = balance ?? BigUInt(0) + let result = Utilities.formatToPrecision( + balance, + units: .custom(decimals), + formattingDecimals: decimals + ) + return result + } + func toToken() -> TokenModel { TokenModel( name: name, @@ -221,10 +284,19 @@ struct CustomToken: Codable { } } +extension TokenModel { + func findCustomToken() -> CustomToken? { + let result = WalletManager.shared.customTokenManager.list + .filter { + $0.address == getAddress() && $0.name == name + }.first + return result + } +} + +// MARK: - AddCustomTokenError + enum AddCustomTokenError: Error { case invalidProfile case providerFailed - - } - diff --git a/FRW/Modules/Wallet/Message/WalletNewsHandler.swift b/FRW/Modules/Wallet/Message/WalletNewsHandler.swift index 469ac31e..56010f19 100644 --- a/FRW/Modules/Wallet/Message/WalletNewsHandler.swift +++ b/FRW/Modules/Wallet/Message/WalletNewsHandler.swift @@ -8,13 +8,22 @@ import Foundation import SwiftUI +// MARK: - WalletNewsHandler + class WalletNewsHandler: ObservableObject { - static let shared = WalletNewsHandler() + // MARK: Lifecycle + + private init() { + self.removeIds = LocalUserDefaults.shared.removedNewsIds + } + + // MARK: Internal - private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent) + static let shared = WalletNewsHandler() // TODO: Change it to Set - @Published var list: [RemoteConfigManager.News] = [] + @Published + var list: [RemoteConfigManager.News] = [] var removeIds: [String] = [] { didSet { @@ -22,10 +31,6 @@ class WalletNewsHandler: ObservableObject { } } - private init() { - removeIds = LocalUserDefaults.shared.removedNewsIds - } - /// Call only once when receive Firebase Config func addRemoteNews(_ news: [RemoteConfigManager.News]) { accessQueue.sync { [weak self] in @@ -78,9 +83,25 @@ class WalletNewsHandler: ObservableObject { } } + /// Call only once when view appear + func checkFirstNews() { + accessQueue.async(flags: .barrier) { [weak self] in + guard let self else { return } + if let item = list.first { + markItemIfNeed(item.id, displatyType: [.once]) + } + } + } + + // MARK: Private + + private let accessQueue = DispatchQueue( + label: "SynchronizedArrayAccess", + attributes: .concurrent + ) + private func removeExpiryNew() { accessQueue.sync { - let currentData = Date() list = list.filter { $0.expiryTime > currentData } log.debug("[NEWS] removeExpiryNew count:\(list.count)") @@ -111,7 +132,10 @@ class WalletNewsHandler: ObservableObject { } @discardableResult - private func markItemIfNeed(_ itemId: String, displatyType: [RemoteConfigManager.NewDisplayType] = [.once]) -> Bool { + private func markItemIfNeed( + _ itemId: String, + displatyType: [RemoteConfigManager.NewDisplayType] = [.once] + ) -> Bool { let item = list.first { $0.id == itemId } guard let type = item?.displayType, displatyType.contains(type) else { return false @@ -122,16 +146,6 @@ class WalletNewsHandler: ObservableObject { } return true } - - /// Call only once when view appear - func checkFirstNews() { - accessQueue.async(flags: .barrier) { [weak self] in - guard let self else { return } - if let item = list.first { - markItemIfNeed(item.id, displatyType: [.once]) - } - } - } } // MARK: User Action @@ -161,7 +175,9 @@ extension WalletNewsHandler { } } - if item.flag == .walletconnect, let request = WalletConnectManager.shared.pendingRequests.first(where: { $0.topic == item.id }) { + if item.flag == .walletconnect, + let request = WalletConnectManager.shared.pendingRequests + .first(where: { $0.topic == item.id }) { WalletConnectManager.shared.handleRequest(request) } diff --git a/FRW/Modules/Wallet/MoveAsset/MoveAccountsView.swift b/FRW/Modules/Wallet/MoveAsset/MoveAccountsView.swift index 0e69086a..2bc99424 100644 --- a/FRW/Modules/Wallet/MoveAsset/MoveAccountsView.swift +++ b/FRW/Modules/Wallet/MoveAsset/MoveAccountsView.swift @@ -9,17 +9,24 @@ import Kingfisher import SwiftUI import SwiftUIX +// MARK: - MoveAccountsView + struct MoveAccountsView: RouteableView, PresentActionDelegate { - var changeHeight: (() -> Void)? + // MARK: Lifecycle - var title: String { - return "" + init(viewModel: MoveAccountsViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) } - @StateObject var viewModel: MoveAccountsViewModel + // MARK: Internal - init(viewModel: MoveAccountsViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) + var changeHeight: (() -> Void)? + + @StateObject + var viewModel: MoveAccountsViewModel + + var title: String { + "" } var body: some View { @@ -66,10 +73,13 @@ struct MoveAccountsView: RouteableView, PresentActionDelegate { } } +// MARK: MoveAccountsView.AccountCell + extension MoveAccountsView { struct AccountCell: View { var contact: Contact var isSelected: Bool + var name: String { contact.user?.name ?? contact.name } @@ -79,7 +89,8 @@ extension MoveAccountsView { } var isEVM: Bool { - guard let evmAdd = EVMAccountManager.shared.accounts.first?.showAddress else { return false } + guard let evmAdd = EVMAccountManager.shared.accounts.first?.showAddress + else { return false } return evmAdd == address } @@ -134,43 +145,79 @@ extension MoveAccountsView { } } +// MARK: - MoveAccountsViewModel + class MoveAccountsViewModel: ObservableObject { - @Published var list: [Contact] = [] - var selectedAddr: String - var callback: (Contact?) -> Void + // MARK: Lifecycle init(selected address: String, callback: @escaping (Contact?) -> Void) { - selectedAddr = address + self.selectedAddr = address self.callback = callback - let currentAddr = WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress() + let currentAddr = WalletManager.shared + .getWatchAddressOrChildAccountAddressOrPrimaryAddress() let isChild = ChildAccountManager.shared.selectedChildAccount != nil let isEVM = EVMAccountManager.shared.selectedAccount != nil - if let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress(), currentAddr != primaryAddr { + if let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress(), + currentAddr != primaryAddr { let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - let contact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) + let contact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) list.append(contact) } - EVMAccountManager.shared.accounts.forEach { account in - + for account in EVMAccountManager.shared.accounts { if currentAddr != account.showAddress { let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - let contact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .evm) + let contact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .evm + ) list.append(contact) } } - ChildAccountManager.shared.childAccounts.forEach { account in - + for account in ChildAccountManager.shared.childAccounts { if currentAddr != account.showAddress { - let contact = Contact(address: account.showAddress, avatar: account.showIcon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) + let contact = Contact( + address: account.showAddress, + avatar: account.showIcon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) list.append(contact) } } } + // MARK: Internal + + @Published + var list: [Contact] = [] + var selectedAddr: String + var callback: (Contact?) -> Void + func onSelect(contact: Contact) { callback(contact) closeAction() diff --git a/FRW/Modules/Wallet/MoveAsset/MoveComponentView.swift b/FRW/Modules/Wallet/MoveAsset/MoveComponentView.swift index 81777c5f..d3788d0a 100644 --- a/FRW/Modules/Wallet/MoveAsset/MoveComponentView.swift +++ b/FRW/Modules/Wallet/MoveAsset/MoveComponentView.swift @@ -5,51 +5,56 @@ // Created by cat on 2024/10/11. // -import SwiftUI import Kingfisher +import SwiftUI + +typealias ContactCallback = (Contact) -> Void -typealias ContactCallback = (Contact)->() +// MARK: - ContactRelationView struct ContactRelationView: View { - enum Clickable { case none case from case to case all } - + var fromContact: Contact var toContact: Contact var clickable: Clickable = .none - + var clickFrom: ContactCallback? var clickTo: ContactCallback? - + var body: some View { ZStack { HStack(spacing: 20) { - userCard(contact: fromContact, showArrow: (clickable == .from || clickable == .all)) { + userCard(contact: fromContact, showArrow: clickable == .from || clickable == .all) { clickFrom?(fromContact) } - .frame(maxWidth: .infinity) - - userCard(contact: toContact, showArrow: (clickable == .to || clickable == .all)) { + .frame(maxWidth: .infinity) + + userCard(contact: toContact, showArrow: clickable == .to || clickable == .all) { clickTo?(toContact) } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity) } arrow() } } - + @ViewBuilder - func userCard(contact: Contact, showArrow: Bool = false, onClick: (()->())? = nil) -> some View { + func userCard( + contact: Contact, + showArrow: Bool = false, + onClick: (() -> Void)? = nil + ) -> some View { VStack(alignment: .leading, spacing: 4) { HStack { if let user = contact.user { user.emoji.icon(size: 32) - }else { + } else { KFImage.url(URL(string: contact.avatar ?? "")) .placeholder { Image("placeholder") @@ -60,29 +65,29 @@ struct ContactRelationView: View { .frame(width: 32, height: 32) .cornerRadius(16) } - + TagView(type: contact.walletType ?? .flow) - + Spacer() - + Image("icon_arrow_bottom_16") .resizable() .frame(width: 16, height: 16) .visibility(showArrow ? .visible : .gone) } - + Text(contact.displayName) .font(.inter(size: 14, weight: .semibold)) .foregroundStyle(Color.Theme.Text.black) .frame(height: 18) .frame(maxWidth: .infinity, alignment: .leading) - + Text(contact.address ?? "") - .font(.inter(size: 12)) - .truncationMode(.middle) - .foregroundStyle(Color.Theme.Text.black8) - .frame(height: 16) - .frame(maxWidth: .infinity, alignment: .leading) + .font(.inter(size: 12)) + .truncationMode(.middle) + .foregroundStyle(Color.Theme.Text.black8) + .frame(height: 16) + .frame(maxWidth: .infinity, alignment: .leading) } .onTapGesture { if showArrow { @@ -94,7 +99,7 @@ struct ContactRelationView: View { .cornerRadius(16) .shadow(color: .black.opacity(0.04), radius: 6, y: 2) } - + @ViewBuilder func arrow() -> some View { Image("icon_assets_move_arrow") @@ -104,9 +109,11 @@ struct ContactRelationView: View { } } +// MARK: - MoveFeeView struct MoveFeeView: View { var isFree = false + var body: some View { VStack(alignment: .leading, spacing: 4) { HStack { @@ -125,24 +132,43 @@ struct MoveFeeView: View { .frame(height: 16) } } - + func feeBalance() -> String { - return isFree ? "0.00 FLOW" : ("move_fee_cost".localized + " FLOW") + isFree ? "0.00 FLOW" : ("move_fee_cost".localized + " FLOW") } func feeHint() -> String { - return isFree ? "move_fee_hint_free".localized : "move_fee_hint_cost".localized + isFree ? "move_fee_hint_free".localized : "move_fee_hint_cost".localized } } - #Preview { Group { - ContactRelationView(fromContact: Contact(address: "0x123", avatar: nil, contactName: "abc", contactType: .user, domain: nil, id: 1, username: "", walletType: .evm), toContact: Contact(address: "0xabc", avatar: nil, contactName: "123", contactType: .user, domain: nil, id: 1, username: "", walletType: .link)) - .background(Color.Theme.Accent.grey) + ContactRelationView( + fromContact: Contact( + address: "0x123", + avatar: nil, + contactName: "abc", + contactType: .user, + domain: nil, + id: 1, + username: "", + walletType: .evm + ), + toContact: Contact( + address: "0xabc", + avatar: nil, + contactName: "123", + contactType: .user, + domain: nil, + id: 1, + username: "", + walletType: .link + ) + ) + .background(Color.Theme.Accent.grey) MoveFeeView(isFree: true) Divider() MoveFeeView(isFree: false) } - } diff --git a/FRW/Modules/Wallet/MoveAsset/MoveNFTsView.swift b/FRW/Modules/Wallet/MoveAsset/MoveNFTsView.swift index 77d27b11..a4830cd1 100644 --- a/FRW/Modules/Wallet/MoveAsset/MoveNFTsView.swift +++ b/FRW/Modules/Wallet/MoveAsset/MoveNFTsView.swift @@ -8,28 +8,27 @@ import Kingfisher import SwiftUI +// MARK: - MoveNFTsView + struct MoveNFTsView: RouteableView, PresentActionDelegate { + // MARK: Internal + var changeHeight: (() -> Void)? + @StateObject + var viewModel = MoveNFTsViewModel() + var title: String { - return "" + "" } var isNavigationBarHidden: Bool { - return true + true } - func configNavigationItem(_: UINavigationItem) {} - var detents: [UISheetPresentationController.Detent] { - return [.large()] + [.large()] } - @StateObject var viewModel = MoveNFTsViewModel() - - private let columns = [ - GridItem(.adaptive(minimum: 110, maximum: 125), spacing: 4), - ] - var body: some View { VStack(spacing: 0) { TitleWithClosedView(title: "select_nfts".localized) { @@ -46,11 +45,14 @@ struct MoveNFTsView: RouteableView, PresentActionDelegate { NFTListView() - VPrimaryButton(model: ButtonStyle.green, - state: viewModel.buttonState, - action: { - viewModel.moveAction() - }, title: viewModel.moveButtonTitle) + VPrimaryButton( + model: ButtonStyle.green, + state: viewModel.buttonState, + action: { + viewModel.moveAction() + }, + title: viewModel.moveButtonTitle + ) } .padding(.horizontal, 18) .applyRouteable(self) @@ -58,6 +60,24 @@ struct MoveNFTsView: RouteableView, PresentActionDelegate { .background(Color.Theme.Background.grey) } + var hintView: some View { + HStack(spacing: 4) { + Image("icon_move_waring") + .resizable() + .frame(width: 20, height: 20) + Text("move_nft_limit_x".localized(String(viewModel.limitCount))) + .font(.inter(size: 14)) + .foregroundStyle(Color.Theme.Text.black) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color.Theme.Accent.orange) + .cornerRadius(24) + .offset(y: -8) + } + + func configNavigationItem(_: UINavigationItem) {} + @ViewBuilder func accountView() -> some View { VStack(spacing: 16) { @@ -65,16 +85,24 @@ struct MoveNFTsView: RouteableView, PresentActionDelegate { titleView(title: "account".localized) Spacer() } - - ContactRelationView(fromContact: viewModel.fromContact, toContact: viewModel.toContact,clickable: .to, clickTo: { contact in - let model = MoveAccountsViewModel(selected: viewModel.toContact.address ?? "") { contact in - if let contact = contact { - viewModel.updateToContact(contact) + + ContactRelationView( + fromContact: viewModel.fromContact, + toContact: viewModel.toContact, + clickable: .to, + clickTo: { contact in + let model = MoveAccountsViewModel( + selected: viewModel.toContact + .address ?? "" + ) { contact in + if let contact = contact { + viewModel.updateToContact(contact) + } } + Router.route(to: RouteMap.Wallet.chooseChild(model)) } - Router.route(to: RouteMap.Wallet.chooseChild(model)) - }) - + ) + MoveFeeView(isFree: viewModel.fromContact.walletType == viewModel.toContact.walletType) .visibility(viewModel.showFee ? .visible : .gone) } @@ -165,7 +193,7 @@ struct MoveNFTsView: RouteableView, PresentActionDelegate { } .padding(.bottom, 8) - if viewModel.nfts.count == 0 { + if viewModel.nfts.isEmpty { HStack { Spacer() Text("0 NFTs") @@ -179,7 +207,11 @@ struct MoveNFTsView: RouteableView, PresentActionDelegate { ScrollView { LazyVGrid(columns: columns, spacing: 4) { ForEach(viewModel.nfts) { nft in - NFTView(nft: nft, reachMax: viewModel.showHint, collection: self.viewModel.selectedCollection) { model in + NFTView( + nft: nft, + reachMax: viewModel.showHint, + collection: self.viewModel.selectedCollection + ) { model in viewModel.toggleSelection(of: model) } } @@ -195,29 +227,23 @@ struct MoveNFTsView: RouteableView, PresentActionDelegate { } } - var hintView: some View { - HStack(spacing: 4) { - Image("icon_move_waring") - .resizable() - .frame(width: 20, height: 20) - Text("move_nft_limit_x".localized(String(viewModel.limitCount))) - .font(.inter(size: 14)) - .foregroundStyle(Color.Theme.Text.black) - } - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(Color.Theme.Accent.orange) - .cornerRadius(24) - .offset(y: -8) - } - func customViewDidDismiss() { MoveAssetsAction.shared.endBrowser() } + + // MARK: Private + + private let columns = [ + GridItem(.adaptive(minimum: 110, maximum: 125), spacing: 4), + ] } +// MARK: MoveNFTsView.NFTView + extension MoveNFTsView { struct NFTView: View { + // MARK: Internal + var nft: MoveNFTsViewModel.NFT var reachMax: Bool var collection: CollectionMask? @@ -262,6 +288,8 @@ extension MoveNFTsView { } } + // MARK: Private + private func showMask() -> Bool { if nft.isSelected { return false diff --git a/FRW/Modules/Wallet/MoveAsset/MoveSingleNFTView.swift b/FRW/Modules/Wallet/MoveAsset/MoveSingleNFTView.swift index c09fa816..c8023371 100644 --- a/FRW/Modules/Wallet/MoveAsset/MoveSingleNFTView.swift +++ b/FRW/Modules/Wallet/MoveAsset/MoveSingleNFTView.swift @@ -10,24 +10,33 @@ import SwiftUI import SwiftUIX struct MoveSingleNFTView: RouteableView, PresentActionDelegate { - @StateObject var viewModel: MoveSingleNFTViewModel + // MARK: Lifecycle + + init(nft: NFTModel, fromChildAccount: ChildAccount? = nil, callback: @escaping () -> Void) { + _viewModel = StateObject(wrappedValue: MoveSingleNFTViewModel( + nft: nft, + fromChildAccount: fromChildAccount, + callback: callback + )) + } + + // MARK: Internal + + @StateObject + var viewModel: MoveSingleNFTViewModel - var changeHeight: (() -> Void)? + var title: String { "" } - + var isNavigationBarHidden: Bool { true } - - init(nft: NFTModel, fromChildAccount: ChildAccount? = nil, callback: @escaping () -> Void) { - _viewModel = StateObject(wrappedValue: MoveSingleNFTViewModel(nft: nft, fromChildAccount: fromChildAccount, callback: callback)) - } var body: some View { - GeometryReader { geometry in + GeometryReader { _ in VStack { HStack { Text("move_single_nft".localized) @@ -35,7 +44,7 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { .foregroundStyle(Color.LL.Neutrals.text) .frame(height: 28) Spacer() - + Button { viewModel.closeAction() } label: { @@ -47,20 +56,26 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { } } .padding(.top, 18) - + ScrollView(showsIndicators: false) { VStack(spacing: 8) { - - ContactRelationView(fromContact: viewModel.fromContact, toContact: viewModel.toContact,clickable: .to, clickTo: { contact in - let model = MoveAccountsViewModel(selected: viewModel.toContact.address ?? "") { contact in - if let contact = contact { - viewModel.updateToContact(contact) + ContactRelationView( + fromContact: viewModel.fromContact, + toContact: viewModel.toContact, + clickable: .to, + clickTo: { contact in + let model = MoveAccountsViewModel( + selected: viewModel.toContact + .address ?? "" + ) { contact in + if let contact = contact { + viewModel.updateToContact(contact) + } } + Router.route(to: RouteMap.Wallet.chooseChild(model)) } - Router.route(to: RouteMap.Wallet.chooseChild(model)) - }) - - + ) + VStack(spacing: 12) { HStack(spacing: 40) { KFImage.url(viewModel.nft.imageURL) @@ -73,13 +88,13 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { .frame(width: 80, height: 80) .cornerRadius(8) .clipped() - + VStack(alignment: .leading, spacing: 0) { Text(viewModel.nft.title) .font(.inter(size: 16, weight: .bold)) .foregroundColor(.Theme.Text.black) .frame(height: 26) - + HStack(alignment: .center, spacing: 4) { KFImage .url(viewModel.nft.logoUrl) @@ -101,10 +116,10 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { .frame(width: 12, height: 12) } } - + Spacer() } - + VStack(spacing: 12) { Divider() .foregroundStyle(Color.Theme.Line.stroke) @@ -115,11 +130,8 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { .padding(16) .background(.Theme.BG.bg3) .cornerRadius(16) - } - } - } .padding(.horizontal, 18) .hideKeyboardWhenTappedAround() @@ -127,13 +139,15 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { .cornerRadius([.topLeading, .topTrailing], 16) .edgesIgnoringSafeArea(.bottom) .overlay(alignment: .bottom) { - VPrimaryButton(model: ButtonStyle.primary, - state: viewModel.buttonState, - action: { - viewModel.moveAction() - }, title: "move".localized) + VPrimaryButton( + model: ButtonStyle.primary, + state: viewModel.buttonState, + action: { + viewModel.moveAction() + }, + title: "move".localized + ) .padding(.horizontal, 18) - } } .applyRouteable(self) @@ -141,5 +155,26 @@ struct MoveSingleNFTView: RouteableView, PresentActionDelegate { } #Preview { - MoveSingleNFTView(nft: NFTModel(NFTResponse(id: "", name: "", description: "", thumbnail: "", externalURL: "", contractAddress: "", evmAddress: "", address: "", collectionID: "", collectionName: "", collectionDescription: "", collectionSquareImage: "", collectionExternalURL: "", collectionContractName: "", collectionBannerImage: "", traits: [], postMedia: NFTPostMedia(title: "", description: "", video: "", isSvg: false)), in: nil), callback: {}) + MoveSingleNFTView( + nft: NFTModel(NFTResponse( + id: "", + name: "", + description: "", + thumbnail: "", + externalURL: "", + contractAddress: "", + evmAddress: "", + address: "", + collectionID: "", + collectionName: "", + collectionDescription: "", + collectionSquareImage: "", + collectionExternalURL: "", + collectionContractName: "", + collectionBannerImage: "", + traits: [], + postMedia: NFTPostMedia(title: "", description: "", video: "", isSvg: false) + ), in: nil), + callback: {} + ) } diff --git a/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveNFTsViewModel.swift b/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveNFTsViewModel.swift index a991a22a..c5b527e2 100644 --- a/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveNFTsViewModel.swift +++ b/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveNFTsViewModel.swift @@ -10,61 +10,71 @@ import Foundation import Kingfisher import SwiftUI +// MARK: - MoveNFTsViewModel + class MoveNFTsViewModel: ObservableObject { - @Published var selectedCollection: CollectionMask? - private var collectionList: [CollectionMask] = [] + // MARK: Lifecycle + + init() { + fetchNFTs(0) + loadUserInfo() + } + + // MARK: Internal + + @Published + var selectedCollection: CollectionMask? // NFTModel - @Published var nfts: [MoveNFTsViewModel.NFT] = [ + @Published + var nfts: [MoveNFTsViewModel.NFT] = [ MoveNFTsViewModel.NFT.mock(), MoveNFTsViewModel.NFT.mock(), MoveNFTsViewModel.NFT.mock(), ] - @Published var isMock = true - @Published var showHint = false - @Published var showFee = false - - @Published var buttonState: VPrimaryButtonState = .disabled - - @Published var fromContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) - @Published var toContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) + @Published + var isMock = true + @Published + var showHint = false + @Published + var showFee = false + + @Published + var buttonState: VPrimaryButtonState = .disabled + + @Published + var fromContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + @Published + var toContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) let limitCount = 10 - init() { - fetchNFTs(0) - loadUserInfo() + var selectedCount: Int { + nfts.filter { $0.isSelected }.count } - private func loadUserInfo() { - guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() else { - return - } - if let account = ChildAccountManager.shared.selectedChildAccount { - fromContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } else if let account = EVMAccountManager.shared.selectedAccount { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - fromContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - fromContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } - - if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared.selectedAccount != nil { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - toContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } else if let account = EVMAccountManager.shared.accounts.first { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - toContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else if let account = ChildAccountManager.shared.childAccounts.first { - toContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) + var moveButtonTitle: String { + if selectedCount > 0 { + return "move_nft_x".localized(String(selectedCount)) } - - updateFee() + return "move_nft".localized } - private func updateFee() { - showFee = !(fromContact.walletType == .link || toContact.walletType == .link) - } func updateToContact(_ contact: Contact) { toContact = contact updateFee() @@ -77,7 +87,8 @@ class MoveNFTsViewModel: ObservableObject { buttonState = .loading Task { do { - let identifier = collection.maskFlowIdentifier ?? nfts.first?.model.maskFlowIdentifier ?? nil + let identifier = collection.maskFlowIdentifier ?? nfts.first?.model + .maskFlowIdentifier ?? nil let ids: [UInt64] = nfts.compactMap { nft in if !nft.isSelected { return nil @@ -94,23 +105,47 @@ class MoveNFTsViewModel: ObservableObject { var tid: Flow.ID? switch (fromContact.walletType, toContact.walletType) { case (.flow, .evm): - tid = try await FlowNetwork.bridgeNFTToEVM(identifier: identifier, ids: ids, fromEvm: false) + tid = try await FlowNetwork.bridgeNFTToEVM( + identifier: identifier, + ids: ids, + fromEvm: false + ) case (.evm, .flow): - tid = try await FlowNetwork.bridgeNFTToEVM(identifier: identifier, ids: ids, fromEvm: true) + tid = try await FlowNetwork.bridgeNFTToEVM( + identifier: identifier, + ids: ids, + fromEvm: true + ) case (.flow, .link): if let coll = collection as? NFTCollection { let identifier = coll.collection.path?.privatePath ?? "" - tid = try await FlowNetwork.batchMoveNFTToChild(childAddr: toContact.address ?? "", identifier: identifier, ids: ids, collection: coll.collection) + tid = try await FlowNetwork.batchMoveNFTToChild( + childAddr: toContact.address ?? "", + identifier: identifier, + ids: ids, + collection: coll.collection + ) } case (.link, .flow): if let coll = collection as? NFTCollection { let identifier = coll.collection.path?.privatePath ?? "" - tid = try await FlowNetwork.batchMoveNFTToParent(childAddr: fromContact.address ?? "", identifier: identifier, ids: ids, collection: coll.collection) + tid = try await FlowNetwork.batchMoveNFTToParent( + childAddr: fromContact.address ?? "", + identifier: identifier, + ids: ids, + collection: coll.collection + ) } case (.link, .link): if let coll = collection as? NFTCollection { let identifier = coll.collection.path?.privatePath ?? "" - tid = try await FlowNetwork.batchSendChildNFTToChild(fromAddress: fromContact.address ?? "", toAddress: toContact.address ?? "", identifier: identifier, ids: ids, collection: coll.collection) + tid = try await FlowNetwork.batchSendChildNFTToChild( + fromAddress: fromContact.address ?? "", + toAddress: toContact.address ?? "", + identifier: identifier, + ids: ids, + collection: coll.collection + ) } case (.link, .evm): if let coll = collection as? NFTCollection { @@ -119,7 +154,8 @@ class MoveNFTsViewModel: ObservableObject { .batchBridgeChildNFTToCoa( nft: identifier, ids: ids, - child: fromContact.address ?? "") + child: fromContact.address ?? "" + ) } case (.evm, .link): if let coll = collection as? NFTCollection { @@ -148,7 +184,10 @@ class MoveNFTsViewModel: ObservableObject { } func selectCollectionAction() { - let vm = SelectCollectionViewModel(selectedItem: selectedCollection, list: collectionList) { [weak self] item in + let vm = SelectCollectionViewModel( + selectedItem: selectedCollection, + list: collectionList + ) { [weak self] item in DispatchQueue.main.async { self?.updateCollection(item: item) } @@ -156,16 +195,6 @@ class MoveNFTsViewModel: ObservableObject { Router.route(to: RouteMap.NFT.selectCollection(vm)) } - private func updateCollection(item: CollectionMask) { - if item.maskId == selectedCollection?.maskId, item.maskContractName == selectedCollection?.maskContractName { - return - } - selectedCollection = item - - nfts = [] - fetchNFTs() - } - func closeAction() { Router.dismiss { MoveAssetsAction.shared.endBrowser() @@ -183,50 +212,6 @@ class MoveNFTsViewModel: ObservableObject { resetButtonState() } - var selectedCount: Int { - nfts.filter { $0.isSelected }.count - } - - private func resetButtonState() { - buttonState = selectedCount > 0 ? .enabled : .disabled - showHint = selectedCount >= limitCount - } - - var moveButtonTitle: String { - if selectedCount > 0 { - return "move_nft_x".localized(String(selectedCount)) - } - return "move_nft".localized - } - - private func fetchCollection() { - Task { - do { - let address = WalletManager.shared.selectedAccountAddress - let offset = FRWAPI.Offset(start: 0, length: 100) - let from: FRWAPI.From = EVMAccountManager.shared.selectedAccount != nil ? .evm : .main - let response: Network.Response<[NFTCollection]> = try await Network.requestWithRawModel(FRWAPI.NFT.userCollection(address, offset, from)) - DispatchQueue.main.async { - self.collectionList = response.data?.sorted(by: { $0.count > $1.count }) ?? [] - if self.selectedCollection == nil { - self.selectedCollection = self.collectionList.first - } - if self.selectedCollection != nil { - self.fetchNFTs() - } else { - DispatchQueue.main.async { - self.nfts = [] - self.isMock = false - self.resetButtonState() - } - } - } - } catch { - log.error("[MoveAsset] fetch Collection failed:\(error)") - } - } - } - func fetchNFTs(_ offset: Int = 0) { buttonState = .loading guard let collection = selectedCollection else { @@ -237,8 +222,17 @@ class MoveNFTsViewModel: ObservableObject { do { let isEVM = EVMAccountManager.shared.selectedAccount != nil let address = WalletManager.shared.selectedAccountAddress - let request = NFTCollectionDetailListRequest(address: address, collectionIdentifier: collection.maskId, offset: offset, limit: 30) - let response: NFTListResponse = try await Network.request(FRWAPI.NFT.collectionDetailList(request, isEVM ? .evm : .main)) + let request = NFTCollectionDetailListRequest( + address: address, + collectionIdentifier: collection.maskId, + offset: offset, + limit: 30 + ) + let response: NFTListResponse = try await Network + .request(FRWAPI.NFT.collectionDetailList( + request, + isEVM ? .evm : .main + )) DispatchQueue.main.async { if let list = response.nfts { self.nfts = list.map { MoveNFTsViewModel.NFT(isSelected: false, model: $0) } @@ -258,6 +252,7 @@ class MoveNFTsViewModel: ObservableObject { } } } + /* private func fetchFlowNFTs(_ offset: Int = 0) { buttonState = .loading @@ -325,6 +320,151 @@ class MoveNFTsViewModel: ObservableObject { } } */ + + // MARK: Private + + private var collectionList: [CollectionMask] = [] + + private func loadUserInfo() { + guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() + else { + return + } + if let account = ChildAccountManager.shared.selectedChildAccount { + fromContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } else if let account = EVMAccountManager.shared.selectedAccount { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + fromContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + fromContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } + + if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared + .selectedAccount != nil { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + toContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } else if let account = EVMAccountManager.shared.accounts.first { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + toContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else if let account = ChildAccountManager.shared.childAccounts.first { + toContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } + + updateFee() + } + + private func updateFee() { + showFee = !(fromContact.walletType == .link || toContact.walletType == .link) + } + + private func updateCollection(item: CollectionMask) { + if item.maskId == selectedCollection?.maskId, + item.maskContractName == selectedCollection?.maskContractName { + return + } + selectedCollection = item + + nfts = [] + fetchNFTs() + } + + private func resetButtonState() { + buttonState = selectedCount > 0 ? .enabled : .disabled + showHint = selectedCount >= limitCount + } + + private func fetchCollection() { + Task { + do { + let address = WalletManager.shared.selectedAccountAddress + let offset = FRWAPI.Offset(start: 0, length: 100) + let from: FRWAPI.From = EVMAccountManager.shared + .selectedAccount != nil ? .evm : .main + let response: Network.Response<[NFTCollection]> = try await Network + .requestWithRawModel(FRWAPI.NFT.userCollection( + address, + offset, + from + )) + DispatchQueue.main.async { + self.collectionList = response.data?.sorted(by: { $0.count > $1.count }) ?? [] + if self.selectedCollection == nil { + self.selectedCollection = self.collectionList.first + } + if self.selectedCollection != nil { + self.fetchNFTs() + } else { + DispatchQueue.main.async { + self.nfts = [] + self.isMock = false + self.resetButtonState() + } + } + } + } catch { + log.error("[MoveAsset] fetch Collection failed:\(error)") + } + } + } } extension MoveNFTsViewModel { @@ -353,11 +493,11 @@ extension MoveNFTsViewModel { } func accountName(isFirst: Bool) -> String { - return isFirst ? fromContact.displayName : toContact.displayName + isFirst ? fromContact.displayName : toContact.displayName } func accountAddress(isFirst: Bool) -> String { - return (isFirst ? fromContact.address : toContact.address) ?? "" + (isFirst ? fromContact.address : toContact.address) ?? "" } func showEVMTag(isFirst: Bool) -> Bool { @@ -373,6 +513,8 @@ extension MoveNFTsViewModel { } } +// MARK: MoveNFTsViewModel.NFT + extension MoveNFTsViewModel { struct NFT: Identifiable { let id: UUID = .init() @@ -380,7 +522,7 @@ extension MoveNFTsViewModel { var model: NFTMask var imageUrl: String { - return model.maskLogo + model.maskLogo } static func mock() -> MoveNFTsViewModel.NFT { diff --git a/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveSingleNFTViewModel.swift b/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveSingleNFTViewModel.swift index a3b89af1..6b491f6c 100644 --- a/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveSingleNFTViewModel.swift +++ b/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveSingleNFTViewModel.swift @@ -9,16 +9,10 @@ import Flow import Foundation import SwiftUI -class MoveSingleNFTViewModel: ObservableObject { - var nft: NFTModel - var fromChildAccount: ChildAccount? - var callback: () -> Void +// MARK: - MoveSingleNFTViewModel - @Published var fromContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) - @Published var toContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) - @Published var buttonState: VPrimaryButtonState = .enabled - - var accountCount: Int = 0 +class MoveSingleNFTViewModel: ObservableObject { + // MARK: Lifecycle init(nft: NFTModel, fromChildAccount: ChildAccount? = nil, callback: @escaping () -> Void) { self.nft = nft @@ -27,35 +21,39 @@ class MoveSingleNFTViewModel: ObservableObject { loadUserInfo() let accountViewModel = MoveAccountsViewModel(selected: "") { _ in } - accountCount = accountViewModel.list.count + self.accountCount = accountViewModel.list.count } - private func loadUserInfo() { - guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() else { - return - } - if let account = fromChildAccount { - fromContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } else if let account = ChildAccountManager.shared.selectedChildAccount { - fromContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } else if let account = EVMAccountManager.shared.selectedAccount { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - fromContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - fromContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } + // MARK: Internal - if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared.selectedAccount != nil || fromChildAccount != nil { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - toContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } else if let account = EVMAccountManager.shared.accounts.first { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - toContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else if let account = ChildAccountManager.shared.childAccounts.first { - toContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } - } + var nft: NFTModel + var fromChildAccount: ChildAccount? + var callback: () -> Void + + @Published + var fromContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + @Published + var toContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + @Published + var buttonState: VPrimaryButtonState = .enabled + + var accountCount: Int = 0 func closeAction() { Router.dismiss() @@ -90,11 +88,117 @@ class MoveSingleNFTViewModel: ObservableObject { } } + func updateToContact(_ contact: Contact) { + toContact = contact + } + + // MARK: Private + + private func loadUserInfo() { + guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() + else { + return + } + if let account = fromChildAccount { + fromContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } else if let account = ChildAccountManager.shared.selectedChildAccount { + fromContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } else if let account = EVMAccountManager.shared.selectedAccount { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + fromContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + fromContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } + + if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared + .selectedAccount != nil || fromChildAccount != nil { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + toContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } else if let account = EVMAccountManager.shared.accounts.first { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + toContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else if let account = ChildAccountManager.shared.childAccounts.first { + toContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } + } + private func moveForEVM(identifier: String, nftId: UInt64) async { do { let ids: [UInt64] = [nftId] let fromEvm = EVMAccountManager.shared.selectedAccount != nil - let tid = try await FlowNetwork.bridgeNFTToEVM(identifier: identifier, ids: ids, fromEvm: fromEvm) + let tid = try await FlowNetwork.bridgeNFTToEVM( + identifier: identifier, + ids: ids, + fromEvm: fromEvm + ) let holder = TransactionManager.TransactionHolder(id: tid, type: .moveAsset) TransactionManager.shared.newTransaction(holder: holder) closeAction() @@ -119,11 +223,27 @@ class MoveSingleNFTViewModel: ObservableObject { var tid = Flow.ID(hex: "") switch (fromContact.walletType, toContact.walletType) { case (.flow, .link): - tid = try await FlowNetwork.moveNFTToChild(nftId: nftId, childAddress: toContact.address ?? "", identifier: identifier, collection: collection) + tid = try await FlowNetwork.moveNFTToChild( + nftId: nftId, + childAddress: toContact.address ?? "", + identifier: identifier, + collection: collection + ) case (.link, .flow): - tid = try await FlowNetwork.moveNFTToParent(nftId: nftId, childAddress: fromContact.address ?? "", identifier: identifier, collection: collection) + tid = try await FlowNetwork.moveNFTToParent( + nftId: nftId, + childAddress: fromContact.address ?? "", + identifier: identifier, + collection: collection + ) case (.link, .link): - tid = try await FlowNetwork.sendChildNFTToChild(nftId: nftId, childAddress: fromContact.address ?? "", toAddress: toContact.address ?? "", identifier: identifier, collection: collection) + tid = try await FlowNetwork.sendChildNFTToChild( + nftId: nftId, + childAddress: fromContact.address ?? "", + toAddress: toContact.address ?? "", + identifier: identifier, + collection: collection + ) case (.link, .evm): guard let nftIdentifier = nft.response.flowIdentifier else { return @@ -133,9 +253,10 @@ class MoveSingleNFTViewModel: ObservableObject { nft: nftIdentifier, id: nftId, child: fromContact - .address ?? "") + .address ?? "" + ) case (.evm, .link): - guard let nftIdentifier = nft.response.flowIdentifier else { + guard let nftIdentifier = nft.response.flowIdentifier else { return } tid = try await FlowNetwork @@ -143,7 +264,8 @@ class MoveSingleNFTViewModel: ObservableObject { nft: nftIdentifier, id: nftId, child: toContact - .address ?? "") + .address ?? "" + ) default: log.info("===") } @@ -155,10 +277,6 @@ class MoveSingleNFTViewModel: ObservableObject { log.error(error) } } - - func updateToContact(_ contact: Contact) { - toContact = contact - } } extension MoveSingleNFTViewModel { @@ -174,11 +292,11 @@ extension MoveSingleNFTViewModel { let isSelectedEVM = EVMAccountManager.shared.selectedAccount != nil return isSelectedEVM ? Image("icon_qr_evm") : Image("Flow") } - + var showFee: Bool { !(fromContact.walletType == .link || toContact.walletType == .link) } - + var isFeeFree: Bool { fromContact.walletType == toContact.walletType } diff --git a/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveTokenViewModel.swift b/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveTokenViewModel.swift index 82b749a0..0caad050 100644 --- a/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveTokenViewModel.swift +++ b/FRW/Modules/Wallet/MoveAsset/ViewModel/MoveTokenViewModel.swift @@ -5,30 +5,13 @@ // Created by cat on 2024/2/27. // -import SwiftUI import Flow +import SwiftUI -class MoveTokenViewModel: ObservableObject { - @Published var inputDollarNum: Double = 0 - - @Published var showBalance: String = "" - var actualBalance: Double = 0 - - @Published var inputTokenNum: Double = 0 - @Published var amountBalance: Double = 0 - @Published var coinRate: Double = 0 - @Published var errorType: WalletSendAmountView.ErrorType = .none - - @Published var buttonState: VPrimaryButtonState = .disabled - - @Published var fromContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) - @Published var toContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) +// MARK: - MoveTokenViewModel - private var minBalance: Double? = nil - private var maxButtonClickedOnce = false - - var token: TokenModel - @Binding var isPresent: Bool +class MoveTokenViewModel: ObservableObject { + // MARK: Lifecycle init(token: TokenModel, isPresent: Binding) { self.token = token @@ -40,49 +23,61 @@ class MoveTokenViewModel: ObservableObject { } } - private func loadUserInfo() { - guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() else { - return - } - if let account = ChildAccountManager.shared.selectedChildAccount { - fromContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } else if let account = EVMAccountManager.shared.selectedAccount { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - fromContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - fromContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } + // MARK: Internal - if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared.selectedAccount != nil { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - toContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } else if let account = EVMAccountManager.shared.accounts.first { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - toContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else if let account = ChildAccountManager.shared.childAccounts.first { - toContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } - } + @Published + var inputDollarNum: Double = 0 - private func fetchMinFlowBalance() async { - do { - self.minBalance = try await FlowNetwork.minFlowBalance() - log.debug("[Flow] min balance:\(self.minBalance ?? 0.001)") - } catch { - self.minBalance = 0.001 - } + @Published + var showBalance: String = "" + var actualBalance: Double = 0 + + @Published + var inputTokenNum: Double = 0 + @Published + var amountBalance: Double = 0 + @Published + var coinRate: Double = 0 + @Published + var errorType: WalletSendAmountView.ErrorType = .none + + @Published + var buttonState: VPrimaryButtonState = .disabled + + @Published + var fromContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + @Published + var toContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + + var token: TokenModel + @Binding + var isPresent: Bool + + var isReadyForSend: Bool { + errorType == .none && showBalance.isNumber && !showBalance.isEmpty } - - private func updateBalance(_ text: String) { - guard !text.isEmpty else { - showBalance = "" - actualBalance = 0 - return - } - + + var currentBalance: String { + let totalStr = amountBalance.formatCurrencyString() + return "Balance: \(totalStr)" } - + func changeTokenModelAction(token: TokenModel) { if token.contractId == self.token.contractId { return @@ -93,12 +88,7 @@ class MoveTokenViewModel: ObservableObject { refreshTokenData() } - private func refreshTokenData() { - amountBalance = WalletManager.shared.getBalance(bySymbol: token.symbol ?? "") - coinRate = CoinRateCache.cache.getSummary(for: token.symbol ?? "")?.getLastRate() ?? 0 - } - - func inputTextDidChangeAction(text: String) { + func inputTextDidChangeAction(text _: String) { if !maxButtonClickedOnce { actualBalance = showBalance.doubleValue } @@ -122,15 +112,15 @@ class MoveTokenViewModel: ObservableObject { errorType = .formatError return } - + inputTokenNum = token.balance?.description.doubleValue ?? actualBalance inputDollarNum = inputTokenNum * coinRate * CurrencyCache.cache.currentCurrencyRate - + if inputTokenNum > amountBalance { errorType = .insufficientBalance return } - + if !allowZero() && inputTokenNum == 0 { errorType = .insufficientBalance return @@ -150,23 +140,135 @@ class MoveTokenViewModel: ObservableObject { } } } - + + // MARK: Private + + private var minBalance: Double? = nil + private var maxButtonClickedOnce = false + + private func loadUserInfo() { + guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() + else { + return + } + if let account = ChildAccountManager.shared.selectedChildAccount { + fromContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } else if let account = EVMAccountManager.shared.selectedAccount { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + fromContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + fromContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } + + if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared + .selectedAccount != nil { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + toContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } else if let account = EVMAccountManager.shared.accounts.first { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + toContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else if let account = ChildAccountManager.shared.childAccounts.first { + toContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } + } + + private func fetchMinFlowBalance() async { + do { + minBalance = try await FlowNetwork.minFlowBalance() + log.debug("[Flow] min balance:\(minBalance ?? 0.001)") + } catch { + minBalance = 0.001 + } + } + + private func updateBalance(_ text: String) { + guard !text.isEmpty else { + showBalance = "" + actualBalance = 0 + return + } + } + + private func refreshTokenData() { + amountBalance = WalletManager.shared.getBalance(bySymbol: token.symbol ?? "") + coinRate = CoinRateCache.cache.getSummary(for: token.symbol ?? "")?.getLastRate() ?? 0 + } + private func isFromFlowToCoa() -> Bool { - return token.isFlowCoin && fromContact.walletType == .flow && toContact.walletType == .evm + token.isFlowCoin && fromContact.walletType == .flow && toContact.walletType == .evm } - + private func allowZero() -> Bool { guard isFromFlowToCoa() else { return true } return false } - + private func updateAmountIfNeed(inputAmount: Double) async -> Double { guard isFromFlowToCoa() else { return max(inputAmount, 0) } - if self.minBalance == nil { + if minBalance == nil { HUD.loading() await fetchMinFlowBalance() HUD.dismissLoading() @@ -181,36 +283,28 @@ class MoveTokenViewModel: ObservableObject { return num } - private func updateState() { DispatchQueue.main.async { self.buttonState = self.isReadyForSend ? .enabled : .disabled } } - - var isReadyForSend: Bool { - return errorType == .none && showBalance.isNumber && !showBalance.isEmpty - } - - var currentBalance: String { - let totalStr = amountBalance.formatCurrencyString() - return "Balance: \(totalStr)" - } } extension MoveTokenViewModel { var fromIsEVM: Bool { - EVMAccountManager.shared.accounts.contains { $0.showAddress.lowercased() == fromContact.address?.lowercased() } + EVMAccountManager.shared.accounts + .contains { $0.showAddress.lowercased() == fromContact.address?.lowercased() } } var toIsEVM: Bool { - EVMAccountManager.shared.accounts.contains { $0.showAddress.lowercased() == toContact.address?.lowercased() } + EVMAccountManager.shared.accounts + .contains { $0.showAddress.lowercased() == toContact.address?.lowercased() } } var balanceAsCurrentCurrencyString: String { - return inputDollarNum.formatCurrencyString(considerCustomCurrency: true) + inputDollarNum.formatCurrencyString(considerCustomCurrency: true) } - + var showFee: Bool { !(fromContact.walletType == .link || toContact.walletType == .link) } @@ -218,13 +312,15 @@ extension MoveTokenViewModel { extension MoveTokenViewModel { func onNext() { - if fromContact.walletType == .link || toContact.walletType == .link { Task { do { var tid: Flow.ID? let amount = self.inputTokenNum.decimalValue - let vaultIdentifier = (fromIsEVM ? (token.flowIdentifier ?? "") : token.contractId + ".Vault") + let vaultIdentifier = ( + fromIsEVM ? (token.flowIdentifier ?? "") : token + .contractId + ".Vault" + ) switch (fromContact.walletType, toContact.walletType) { case (.link, .evm): tid = try await FlowNetwork @@ -243,23 +339,27 @@ extension MoveTokenViewModel { default: break } - + if let txid = tid { - let holder = TransactionManager.TransactionHolder(id: txid, type: .moveAsset) + let holder = TransactionManager.TransactionHolder( + id: txid, + type: .moveAsset + ) TransactionManager.shared.newTransaction(holder: holder) } DispatchQueue.main.async { self.closeAction() self.buttonState = .enabled } - } - catch { - log.error(" Move Token: \(fromContact.walletType?.rawValue ?? "") to \(toContact.walletType?.rawValue ?? "") failed. \(error)") + } catch { + log + .error( + " Move Token: \(fromContact.walletType?.rawValue ?? "") to \(toContact.walletType?.rawValue ?? "") failed. \(error)" + ) log.error(error) buttonState = .enabled } } - } if token.isFlowCoin { if WalletManager.shared.isSelectedEVMAccount { @@ -347,8 +447,16 @@ extension MoveTokenViewModel { log.info("[EVM] bridge token \(fromIsEVM ? "FromEVM" : "ToEVM")") let amount = self.inputTokenNum.decimalValue - let vaultIdentifier = (fromIsEVM ? (token.flowIdentifier ?? "") : token.contractId + ".Vault") - let txid = try await FlowNetwork.bridgeToken(vaultIdentifier: vaultIdentifier, amount: amount, fromEvm: fromIsEVM, decimals: token.decimal) + let vaultIdentifier = ( + fromIsEVM ? (token.flowIdentifier ?? "") : token + .contractId + ".Vault" + ) + let txid = try await FlowNetwork.bridgeToken( + vaultIdentifier: vaultIdentifier, + amount: amount, + fromEvm: fromIsEVM, + decimals: token.decimal + ) let holder = TransactionManager.TransactionHolder(id: txid, type: .transferCoin) TransactionManager.shared.newTransaction(holder: holder) diff --git a/FRW/Modules/Wallet/Receive/WalletReceiveView.swift b/FRW/Modules/Wallet/Receive/WalletReceiveView.swift index d9714049..980f377a 100644 --- a/FRW/Modules/Wallet/Receive/WalletReceiveView.swift +++ b/FRW/Modules/Wallet/Receive/WalletReceiveView.swift @@ -9,6 +9,8 @@ import LinkPresentation import QRCode import SwiftUI +// MARK: - WalletReceiveView_Previews + struct WalletReceiveView_Previews: PreviewProvider { static var previews: some View { NavigationView { @@ -17,22 +19,25 @@ struct WalletReceiveView_Previews: PreviewProvider { } } +// MARK: - WalletReceiveView + struct WalletReceiveView: RouteableView { - @StateObject var vm = WalletReceiveViewModel() + @StateObject + var vm = WalletReceiveViewModel() - var title: String { - return "" - } + @State + var isDismissing: Bool = false + @State + var dragOffset: CGSize = .zero + @State + var dragOffsetPredicted: CGSize = .zero - func backButtonAction() { - Router.dismiss() - } - - @State var isDismissing: Bool = false - @State var dragOffset: CGSize = .zero - @State var dragOffsetPredicted: CGSize = .zero + @State + var isShowing: Bool = false - @State var isShowing: Bool = false + var title: String { + "" + } var body: some View { VStack(alignment: .center) { @@ -57,39 +62,41 @@ struct WalletReceiveView: RouteableView { Spacer() } .animation(.alertViewSpring, value: isShowing) - .offset(x: 0, y: self.dragOffset.height > 0 ? self.dragOffset.height : 0) - .gesture(DragGesture() - .onChanged { value in - self.dragOffset = value.translation - self.dragOffsetPredicted = value.predictedEndTranslation - } - .onEnded { _ in - if (self.dragOffset.height > 100) || (self.dragOffsetPredicted.height / (self.dragOffset.height)) > 2 { - withAnimation(.spring()) { - // self.dragOffset = self.dragOffsetPredicted - self.dragOffset = CGSize(width: 0, height: UIScreen.screenHeight / 2) - } + .offset(x: 0, y: dragOffset.height > 0 ? dragOffset.height : 0) + .gesture( + DragGesture() + .onChanged { value in + self.dragOffset = value.translation + self.dragOffsetPredicted = value.predictedEndTranslation + } + .onEnded { _ in + if (self.dragOffset.height > 100) || + (self.dragOffsetPredicted.height / (self.dragOffset.height)) > 2 { + withAnimation(.spring()) { + // self.dragOffset = self.dragOffsetPredicted + self.dragOffset = CGSize(width: 0, height: UIScreen.screenHeight / 2) + } - self.isDismissing = true - self.isShowing = false + self.isDismissing = true + self.isShowing = false - // Hacky way - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - Router.dismiss(animated: false) - } + // Hacky way + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + Router.dismiss(animated: false) + } - return - } else { - withAnimation(.interactiveSpring()) { - self.dragOffset = .zero + return + } else { + withAnimation(.interactiveSpring()) { + self.dragOffset = .zero + } } } - } ) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .background( Color(hex: "#333333") - .opacity(isShowing ? (1.0 - (Double(max(0, self.dragOffset.height)) / 1000)) : 0) + .opacity(isShowing ? (1.0 - (Double(max(0, dragOffset.height)) / 1000)) : 0) .edgesIgnoringSafeArea(.all) .animation(.alertViewSpring, value: isShowing) ) @@ -143,7 +150,11 @@ struct WalletReceiveView: RouteableView { .cornerRadius(25) .overlay( RoundedRectangle(cornerRadius: 25) - .stroke(currentNetwork.isMainnet ? Color.LL.Neutrals.background : currentNetwork.color, lineWidth: 5) + .stroke( + currentNetwork.isMainnet ? Color.LL.Neutrals.background : currentNetwork + .color, + lineWidth: 5 + ) .colorScheme(.light) ) .aspectRatio(1, contentMode: .fit) @@ -204,9 +215,17 @@ struct WalletReceiveView: RouteableView { let itemSource = ShareActivityItemSource(shareText: vm.address, shareImage: image) - let activityController = UIActivityViewController(activityItems: [image, vm.address, itemSource], applicationActivities: nil) + let activityController = UIActivityViewController( + activityItems: [image, vm.address, itemSource], + applicationActivities: nil + ) activityController.isModalInPresentation = true - UIApplication.shared.windows.first?.rootViewController?.presentedViewController?.present(activityController, animated: true, completion: nil) + UIApplication.shared.windows.first?.rootViewController?.presentedViewController? + .present( + activityController, + animated: true, + completion: nil + ) } label: { Label { @@ -226,6 +245,10 @@ struct WalletReceiveView: RouteableView { } .buttonStyle(ScaleButtonStyle()) } + + func backButtonAction() { + Router.dismiss() + } } extension WalletReceiveView { @@ -233,7 +256,10 @@ extension WalletReceiveView { let d = QRCode.Document(generator: QRCodeGenerator_External()) d.utf8String = text if let logo = UIImage(named: "lilico-app-icon")?.cgImage { - let path = CGPath(ellipseIn: CGRect(x: 0.38, y: 0.38, width: 0.24, height: 0.24), transform: nil) + let path = CGPath( + ellipseIn: CGRect(x: 0.38, y: 0.38, width: 0.24, height: 0.24), + transform: nil + ) d.logoTemplate = QRCode.LogoTemplate(image: logo, path: path, inset: 6) } @@ -251,7 +277,9 @@ extension WalletReceiveView { } var qrCodeView: some View { - QRCodeDocumentUIView(document: doc(text: vm.address, - eyeColor: eyeColor)) + QRCodeDocumentUIView(document: doc( + text: vm.address, + eyeColor: eyeColor + )) } } diff --git a/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift b/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift index eb505312..f7f054b7 100644 --- a/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift +++ b/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift @@ -26,6 +26,8 @@ extension WalletSendAmountView { case invalidAddress case belowMinimum + // MARK: Internal + var desc: String { switch self { case .none: @@ -43,35 +45,13 @@ extension WalletSendAmountView { } } -class WalletSendAmountViewModel: ObservableObject { - @Published var targetContact: Contact - @Published var token: TokenModel - @Published var amountBalance: Double = 0 - @Published var coinRate: Double = 0 - - @Published var inputText: String = "" - @Published var inputTokenNum: Double = 0 - @Published var inputDollarNum: Double = 0 - var actualBalance: String = "" - - @Published var exchangeType: WalletSendAmountView.ExchangeType = .token - @Published var errorType: WalletSendAmountView.ErrorType = .none - - @Published var showConfirmView: Bool = false - - @Published var isValidToken: Bool = true - - @Published var isEmptyTransation = true - - private var isSending = false - private var cancelSets = Set() +// MARK: - WalletSendAmountViewModel - private var addressIsValid: Bool? - - private var minBalance: Double = 0.001 +class WalletSendAmountViewModel: ObservableObject { + // MARK: Lifecycle init(target: Contact, token: TokenModel) { - targetContact = target + self.targetContact = target self.token = token WalletManager.shared.$coinBalances.sink { [weak self] _ in @@ -83,20 +63,67 @@ class WalletSendAmountViewModel: ObservableObject { checkAddress() checkTransaction() fetchMinFlowBalance() - NotificationCenter.default.addObserver(self, selector: #selector(onHolderChanged(noti:)), name: .transactionStatusDidChanged, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(onHolderChanged(noti:)), + name: .transactionStatusDidChanged, + object: nil + ) } deinit { NotificationCenter.default.removeObserver(self) } + // MARK: Internal + + @Published + var targetContact: Contact + @Published + var token: TokenModel + @Published + var amountBalance: Double = 0 + @Published + var coinRate: Double = 0 + + @Published + var inputText: String = "" + @Published + var inputTokenNum: Double = 0 + @Published + var inputDollarNum: Double = 0 + var actualBalance: String = "" + + @Published + var exchangeType: WalletSendAmountView.ExchangeType = .token + @Published + var errorType: WalletSendAmountView.ErrorType = .none + + @Published + var showConfirmView: Bool = false + + @Published + var isValidToken: Bool = true + + @Published + var isEmptyTransation = true + var amountBalanceAsDollar: Double { - return coinRate * amountBalance + coinRate * amountBalance } var isReadyForSend: Bool { - return errorType == .none && inputText.isNumber && addressIsValid == true + errorType == .none && inputText.isNumber && addressIsValid == true } + + // MARK: Private + + private var isSending = false + private var cancelSets = Set() + + private var addressIsValid: Bool? + + private var minBalance: Double = 0.001 } extension WalletSendAmountViewModel { @@ -129,7 +156,8 @@ extension WalletSendAmountViewModel { } return } - let list = try await FlowNetwork.checkTokensEnable(address: Flow.Address(hex: address)) + let list = try await FlowNetwork + .checkTokensEnable(address: Flow.Address(hex: address)) let model = list.first { $0.key.lowercased() == token.contractId.lowercased() } let isValid = model?.value DispatchQueue.main.async { @@ -160,7 +188,7 @@ extension WalletSendAmountViewModel { errorType = .formatError return } - + if exchangeType == .token { inputTokenNum = actualBalance.doubleValue inputDollarNum = inputTokenNum * coinRate * CurrencyCache.cache.currentCurrencyRate @@ -172,7 +200,7 @@ extension WalletSendAmountViewModel { inputTokenNum = inputDollarNum / CurrencyCache.cache.currentCurrencyRate / coinRate } } - + if inputTokenNum > amountBalance { errorType = .insufficientBalance return @@ -182,7 +210,7 @@ extension WalletSendAmountViewModel { let validBalance = ( Decimal(amountBalance) - Decimal(minBalance) ).doubleValue - if validBalance < inputTokenNum { + if validBalance < inputTokenNum { errorType = .belowMinimum return } @@ -215,8 +243,8 @@ extension WalletSendAmountViewModel { func maxAction() { exchangeType = .token - if token.isFlowCoin && WalletManager.shared - .isCoa(targetContact.address) && WalletManager.shared.isMain() { + if token.isFlowCoin, WalletManager.shared + .isCoa(targetContact.address), WalletManager.shared.isMain() { Task { do { let topAmount = try await FlowNetwork.minFlowBalance() @@ -227,7 +255,7 @@ extension WalletSendAmountViewModel { DispatchQueue.main.async { self.inputText = num.formatCurrencyString() } - + actualBalance = num.formatCurrencyString(digits: token.decimal) } catch { let num = max(amountBalance - minBalance, 0) @@ -299,7 +327,8 @@ extension WalletSendAmountViewModel { return } - guard let address = WalletManager.shared.getPrimaryWalletAddress(), let targetAddress = targetContact.address else { + guard let address = WalletManager.shared.getPrimaryWalletAddress(), + let targetAddress = targetContact.address else { return } @@ -320,44 +349,69 @@ extension WalletSendAmountViewModel { var txId: Flow.ID? let amount = inputTokenNum.decimalValue - let fromAccountType = WalletManager.shared.isSelectedEVMAccount ? AccountType.coa : AccountType.flow + let fromAccountType = WalletManager.shared.isSelectedEVMAccount ? AccountType + .coa : AccountType.flow var toAccountType = targetAddress.isEVMAddress ? AccountType.coa : AccountType.flow - if toAccountType == .coa, targetAddress != EVMAccountManager.shared.accounts.first?.address { + if toAccountType == .coa, + targetAddress != EVMAccountManager.shared.accounts.first?.address { toAccountType = .eoa } switch (fromAccountType, toAccountType) { case (.flow, .flow): - txId = try await FlowNetwork.transferToken(to: Flow.Address(hex: targetContact.address ?? "0x"), - amount: amount, - token: token) + txId = try await FlowNetwork.transferToken( + to: Flow.Address(hex: targetContact.address ?? "0x"), + amount: amount, + token: token + ) case (.flow, .coa): txId = try await FlowNetwork.fundCoa(amount: amount) case (.coa, .flow): if token.isFlowCoin { - txId = try await FlowNetwork.sendFlowTokenFromCoaToFlow(amount: amount, address: targetAddress) + txId = try await FlowNetwork.sendFlowTokenFromCoaToFlow( + amount: amount, + address: targetAddress + ) } else { - guard let bigUIntValue = Utilities.parseToBigUInt(amount.description, units: .ether), - let flowIdentifier = self.token.flowIdentifier + guard let bigUIntValue = Utilities.parseToBigUInt( + amount.description, + units: .ether + ), + let flowIdentifier = self.token.flowIdentifier else { failureBlock() return } - txId = try await FlowNetwork.bridgeTokensFromEvmToFlow(identifier: flowIdentifier, amount: bigUIntValue, receiver: targetAddress) + txId = try await FlowNetwork.bridgeTokensFromEvmToFlow( + identifier: flowIdentifier, + amount: bigUIntValue, + receiver: targetAddress + ) } - case (.coa, .coa): - txId = try await FlowNetwork.sendTransaction(amount: amount.description, data: nil, toAddress: targetAddress.stripHexPrefix(), gas: gas) + txId = try await FlowNetwork.sendTransaction( + amount: amount.description, + data: nil, + toAddress: targetAddress.stripHexPrefix(), + gas: gas + ) case (.flow, .eoa): if token.isFlowCoin { - txId = try await FlowNetwork.sendFlowToEvm(evmAddress: targetAddress.stripHexPrefix(), amount: amount, gas: gas) + txId = try await FlowNetwork.sendFlowToEvm( + evmAddress: targetAddress.stripHexPrefix(), + amount: amount, + gas: gas + ) } else { let flowIdentifier = "\(self.token.contractId).Vault" - txId = try await FlowNetwork.sendNoFlowTokenToEVM(vaultIdentifier: flowIdentifier, amount: amount, recipient: targetAddress) + txId = try await FlowNetwork.sendNoFlowTokenToEVM( + vaultIdentifier: flowIdentifier, + amount: amount, + recipient: targetAddress + ) } - case (.coa, .eoa): if token.isFlowCoin { txId = try await FlowNetwork @@ -369,11 +423,23 @@ extension WalletSendAmountViewModel { ) } else { let erc20Contract = try await FlowProvider.Web3.defaultContract() - let testData = erc20Contract?.contract.method("transfer", parameters: [targetAddress, Utilities.parseToBigUInt(amount.description, units: .ether)!], extraData: nil) + let testData = erc20Contract?.contract.method( + "transfer", + parameters: [ + targetAddress, + Utilities.parseToBigUInt(amount.description, units: .ether)!, + ], + extraData: nil + ) guard let toAddress = token.getAddress() else { throw LLError.invalidAddress } - txId = try await FlowNetwork.sendTransaction(amount: "0", data: testData, toAddress: toAddress.stripHexPrefix(), gas: gas) + txId = try await FlowNetwork.sendTransaction( + amount: "0", + data: testData, + toAddress: toAddress.stripHexPrefix(), + gas: gas + ) } default: failureBlock() @@ -386,7 +452,12 @@ extension WalletSendAmountViewModel { } DispatchQueue.main.async { - let obj = CoinTransferModel(amount: self.inputTokenNum, symbol: self.token.symbol ?? "", target: self.targetContact, from: address) + let obj = CoinTransferModel( + amount: self.inputTokenNum, + symbol: self.token.symbol ?? "", + target: self.targetContact, + from: address + ) guard let data = try? JSONEncoder().encode(obj) else { debugPrint("WalletSendAmountViewModel -> obj encode failed") failureBlock() @@ -403,7 +474,11 @@ extension WalletSendAmountViewModel { let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) - let holder = TransactionManager.TransactionHolder(id: id, type: .transferCoin, data: data) + let holder = TransactionManager.TransactionHolder( + id: id, + type: .transferCoin, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) } } catch { @@ -428,7 +503,8 @@ extension WalletSendAmountViewModel { isEmptyTransation = TransactionManager.shared.holders.count == 0 } - @objc private func onHolderChanged(noti _: Notification) { + @objc + private func onHolderChanged(noti _: Notification) { checkTransaction() } } diff --git a/FRW/Modules/Wallet/Send/WalletSendViewModel.swift b/FRW/Modules/Wallet/Send/WalletSendViewModel.swift index b0d17f9f..c56bdd9a 100644 --- a/FRW/Modules/Wallet/Send/WalletSendViewModel.swift +++ b/FRW/Modules/Wallet/Send/WalletSendViewModel.swift @@ -33,6 +33,8 @@ extension WalletSendView { case notFound case failed + // MARK: Internal + var desc: String { switch self { case .none: @@ -52,28 +54,10 @@ extension WalletSendView { } } -class WalletSendViewModel: ObservableObject { - @Published var status: WalletSendView.ViewStatus = .normal - @Published var errorType: WalletSendView.ErrorType = .none - - @Published var tabType: WalletSendView.TabType = .recent - @Published var searchText: String = "" - @Published var page: Page = .first() - - @Published var recentList: [Contact] = [] - @Published var linkedWalletList: [Contact] = [] - @Published var ownAccountList: [Contact] = [] - let addressBookVM = AddressBookView.AddressBookViewModel() +// MARK: - WalletSendViewModel - @Published var localSearchResults: [WalletSendView.SearchSection] = [] - @Published var serverSearchList: [Contact]? - @Published var findSearchList: [Contact]? - @Published var flownsSearchList: [Contact]? - @Published var meowSearchList: [Contact]? - - private var cancelSets = Set() - - private var selectCallback: WalletSendView.WalletSendViewSelectTargetCallback? +class WalletSendViewModel: ObservableObject { + // MARK: Lifecycle init(selectCallback: WalletSendView.WalletSendViewSelectTargetCallback? = nil) { self.selectCallback = selectCallback @@ -97,48 +81,125 @@ class WalletSendViewModel: ObservableObject { addresList.append(primaryAddr) } - addresList.forEach { address in + for address in addresList { let user = WalletManager.shared.walletAccount.readInfo(at: address) - let contract = Contact(address: address, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: nil, user: user) - self.ownAccountList.append(contract) + let contract = Contact( + address: address, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: nil, + user: user + ) + ownAccountList.append(contract) } if WalletManager.shared.isSelectedEVMAccount == false, let emvAddr = EVMAccountManager.shared.accounts.first?.showAddress {} - EVMAccountManager.shared.accounts.forEach { account in + for account in EVMAccountManager.shared.accounts { let evmAddr = account.showAddress let user = WalletManager.shared.walletAccount.readInfo(at: evmAddr) - let contract = Contact(address: evmAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: nil, user: user) + let contract = Contact( + address: evmAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: nil, + user: user + ) linkedWalletList.append(contract) } - ChildAccountManager.shared.childAccounts.forEach { account in - let contact = Contact(address: account.showAddress, avatar: account.showIcon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.aName) + for account in ChildAccountManager.shared.childAccounts { + let contact = Contact( + address: account.showAddress, + avatar: account.showIcon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.aName + ) linkedWalletList.append(contact) } } + // MARK: Internal + + @Published + var status: WalletSendView.ViewStatus = .normal + @Published + var errorType: WalletSendView.ErrorType = .none + + @Published + var tabType: WalletSendView.TabType = .recent + @Published + var searchText: String = "" + @Published + var page: Page = .first() + + @Published + var recentList: [Contact] = [] + @Published + var linkedWalletList: [Contact] = [] + @Published + var ownAccountList: [Contact] = [] + let addressBookVM = AddressBookView.AddressBookViewModel() + + @Published + var localSearchResults: [WalletSendView.SearchSection] = [] + @Published + var serverSearchList: [Contact]? + @Published + var findSearchList: [Contact]? + @Published + var flownsSearchList: [Contact]? + @Published + var meowSearchList: [Contact]? + var remoteSearchResults: [WalletSendView.SearchSection] { var sections = [WalletSendView.SearchSection]() if let serverSearchList = serverSearchList, !serverSearchList.isEmpty { - sections.append(WalletSendView.SearchSection(title: "lilico_user".localized, rows: serverSearchList)) + sections.append(WalletSendView.SearchSection( + title: "lilico_user".localized, + rows: serverSearchList + )) } if let findSearchList = findSearchList, !findSearchList.isEmpty { - sections.append(WalletSendView.SearchSection(title: ".find".localized, rows: findSearchList)) + sections.append(WalletSendView.SearchSection( + title: ".find".localized, + rows: findSearchList + )) } if let flownsSearchList = flownsSearchList, !flownsSearchList.isEmpty { - sections.append(WalletSendView.SearchSection(title: ".flowns".localized, rows: flownsSearchList)) + sections.append(WalletSendView.SearchSection( + title: ".flowns".localized, + rows: flownsSearchList + )) } if let meowSearchList = meowSearchList, !meowSearchList.isEmpty { - sections.append(WalletSendView.SearchSection(title: ".meow".localized, rows: meowSearchList)) + sections.append(WalletSendView.SearchSection( + title: ".meow".localized, + rows: meowSearchList + )) } return sections } + + // MARK: Private + + private var cancelSets = Set() + + private var selectCallback: WalletSendView.WalletSendViewSelectTargetCallback? } // MARK: - Search @@ -171,7 +232,8 @@ extension WalletSendViewModel { Task { do { - let response: UserSearchResponse = try await Network.request(FRWAPI.User.search(trimedText)) + let response: UserSearchResponse = try await Network + .request(FRWAPI.User.search(trimedText)) if trimedText != self.searchText.trim() { return } @@ -208,19 +270,25 @@ extension WalletSendViewModel { Task { do { - let address = try await FlowNetwork.queryAddressByDomainFind(domain: trimedText.lowercased().removeSuffix(".find")) + let address = try await FlowNetwork + .queryAddressByDomainFind(domain: trimedText.lowercased().removeSuffix(".find")) if trimedText != self.searchText.trim() { return } DispatchQueue.main.async { - let contact = Contact(address: address, - avatar: nil, - contactName: trimedText, - contactType: .domain, - domain: Contact.Domain(domainType: .find, value: trimedText), - id: UUID().hashValue, - username: nil) + let contact = Contact( + address: address, + avatar: nil, + contactName: trimedText, + contactType: .domain, + domain: Contact.Domain( + domainType: .find, + value: trimedText + ), + id: UUID().hashValue, + username: nil + ) self.findSearchList = [contact] self.refreshCurrentSearchingStatusAfterPerSearchComplete() @@ -245,19 +313,25 @@ extension WalletSendViewModel { Task { do { - let address = try await FlowNetwork.queryAddressByDomainFlowns(domain: trimedText.removeSuffix(".fn")) + let address = try await FlowNetwork + .queryAddressByDomainFlowns(domain: trimedText.removeSuffix(".fn")) if trimedText != self.searchText.trim() { return } DispatchQueue.main.async { - let contact = Contact(address: address, - avatar: nil, - contactName: trimedText, - contactType: .domain, - domain: Contact.Domain(domainType: .flowns, value: trimedText), - id: UUID().hashValue, - username: nil) + let contact = Contact( + address: address, + avatar: nil, + contactName: trimedText, + contactType: .domain, + domain: Contact.Domain( + domainType: .flowns, + value: trimedText + ), + id: UUID().hashValue, + username: nil + ) self.flownsSearchList = [contact] self.refreshCurrentSearchingStatusAfterPerSearchComplete() @@ -282,19 +356,27 @@ extension WalletSendViewModel { Task { do { - let address = try await FlowNetwork.queryAddressByDomainFlowns(domain: trimedText.removeSuffix(".meow"), root: Contact.DomainType.meow.domain) + let address = try await FlowNetwork.queryAddressByDomainFlowns( + domain: trimedText.removeSuffix(".meow"), + root: Contact.DomainType.meow.domain + ) if trimedText != self.searchText.trim() { return } DispatchQueue.main.async { - let contact = Contact(address: address, - avatar: nil, - contactName: trimedText, - contactType: .domain, - domain: Contact.Domain(domainType: .meow, value: trimedText), - id: UUID().hashValue, - username: nil) + let contact = Contact( + address: address, + avatar: nil, + contactName: trimedText, + contactType: .domain, + domain: Contact.Domain( + domainType: .meow, + value: trimedText + ), + id: UUID().hashValue, + username: nil + ) self.meowSearchList = [contact] self.refreshCurrentSearchingStatusAfterPerSearchComplete() @@ -324,8 +406,7 @@ extension WalletSendViewModel { private func refreshCurrentSearchingStatusAfterPerSearchComplete() { if let serverSearchList = serverSearchList, serverSearchList.isEmpty, let findSearchList = findSearchList, findSearchList.isEmpty, - let flownsSearchList = flownsSearchList, flownsSearchList.isEmpty - { + let flownsSearchList = flownsSearchList, flownsSearchList.isEmpty { errorType = .notFound status = .error return @@ -357,7 +438,15 @@ extension WalletSendViewModel { if trimedText.isFlowOrEVMAddress { if let callback = selectCallback { - let target = Contact(address: trimedText, avatar: nil, contactName: trimedText, contactType: .external, domain: nil, id: UUID().hashValue, username: nil) + let target = Contact( + address: trimedText, + avatar: nil, + contactName: trimedText, + contactType: .external, + domain: nil, + id: UUID().hashValue, + username: nil + ) callback(target) return } @@ -383,7 +472,15 @@ extension WalletSendViewModel { } private func sendToAddressAction(_ address: String) { - let contact = Contact(address: address, avatar: nil, contactName: address, contactType: .external, domain: nil, id: UUID().hashValue, username: nil) + let contact = Contact( + address: address, + avatar: nil, + contactName: address, + contactType: .external, + domain: nil, + id: UUID().hashValue, + username: nil + ) let symbol = LocalUserDefaults.shared.recentToken ?? "flow" guard let token = WalletManager.shared.getToken(bySymbol: symbol) else { return @@ -451,12 +548,16 @@ extension WalletSendViewModel { Task { do { - let request = AddressBookAddRequest(contactName: contactName, - address: address, - domain: contact.domain?.value ?? "", - domainType: contact.domain?.domainType ?? .unknown, - username: contact.username ?? "") - let response: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.AddressBook.addExternal(request)) + let request = AddressBookAddRequest( + contactName: contactName, + address: address, + domain: contact.domain?.value ?? "", + domainType: contact.domain? + .domainType ?? .unknown, + username: contact.username ?? "" + ) + let response: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.AddressBook.addExternal(request)) if response.httpCode != 200 { errorAction() diff --git a/FRW/Modules/Wallet/SideMenu/SideMenuView.swift b/FRW/Modules/Wallet/SideMenu/SideMenuView.swift index c700e75f..6c87ffd5 100644 --- a/FRW/Modules/Wallet/SideMenu/SideMenuView.swift +++ b/FRW/Modules/Wallet/SideMenu/SideMenuView.swift @@ -11,6 +11,8 @@ import SwiftUI private let SideOffset: CGFloat = 65 +// MARK: - SideMenuViewModel.AccountPlaceholder + extension SideMenuViewModel { struct AccountPlaceholder { let uid: String @@ -18,19 +20,10 @@ extension SideMenuViewModel { } } -class SideMenuViewModel: ObservableObject { - @Published var nftCount: Int = 0 - @Published var accountPlaceholders: [AccountPlaceholder] = [] - @Published var isSwitchOpen = false - @Published var userInfoBackgroudColor = Color.LL.Neutrals.neutrals6 - @Published var walletBalance: [String: String] = [:] - - var colorsMap: [String: Color] = [:] +// MARK: - SideMenuViewModel - private var cancelSets = Set() - var currentAddress: String { - WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "" - } +class SideMenuViewModel: ObservableObject { + // MARK: Lifecycle init() { UserManager.shared.$loginUIDList @@ -40,7 +33,8 @@ class SideMenuViewModel: ObservableObject { guard let self = self else { return } self.accountPlaceholders = Array(uidList.dropFirst().prefix(2)).map { uid in - let avatar = MultiAccountStorage.shared.getUserInfo(uid)?.avatar.convertedAvatarString() ?? "" + let avatar = MultiAccountStorage.shared.getUserInfo(uid)?.avatar + .convertedAvatarString() ?? "" return AccountPlaceholder(uid: uid, avatar: avatar) } }.store(in: &cancelSets) @@ -51,10 +45,35 @@ class SideMenuViewModel: ObservableObject { .sink { [weak self] balances in self?.walletBalance = balances }.store(in: &cancelSets) - NotificationCenter.default.addObserver(self, selector: #selector(onToggle), name: .toggleSideMenu, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(onToggle), + name: .toggleSideMenu, + object: nil + ) } - @objc func onToggle() { + // MARK: Internal + + @Published + var nftCount: Int = 0 + @Published + var accountPlaceholders: [AccountPlaceholder] = [] + @Published + var isSwitchOpen = false + @Published + var userInfoBackgroudColor = Color.LL.Neutrals.neutrals6 + @Published + var walletBalance: [String: String] = [:] + + var colorsMap: [String: Color] = [:] + + var currentAddress: String { + WalletManager.shared.getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "" + } + + @objc + func onToggle() { isSwitchOpen = false } @@ -116,18 +135,16 @@ class SideMenuViewModel: ObservableObject { func switchProfile() { LocalUserDefaults.shared.recentToken = nil } + + // MARK: Private + + private var cancelSets = Set() } -struct SideMenuView: View { - @StateObject private var vm = SideMenuViewModel() - @StateObject private var um = UserManager.shared - @StateObject private var wm = WalletManager.shared - @StateObject private var cm = ChildAccountManager.shared - @StateObject private var evmManager = EVMAccountManager.shared - @AppStorage("isDeveloperMode") private var isDeveloperMode = false - @State private var showSwitchUserAlert = false +// MARK: - SideMenuView - private let cPadding = 12.0 +struct SideMenuView: View { + // MARK: Internal var body: some View { GeometryReader { proxy in @@ -212,7 +229,7 @@ struct SideMenuView: View { } var enableEVMView: some View { - return VStack { + VStack { ZStack(alignment: .topLeading) { Image("icon_planet") .resizable() @@ -264,12 +281,19 @@ struct SideMenuView: View { VStack(spacing: 0) { Section { VStack(spacing: 0) { - AccountSideCell(address: WalletManager.shared.getPrimaryWalletAddress() ?? "", - currentAddress: vm.currentAddress, - detail: vm.balanceValue(at: WalletManager.shared.getPrimaryWalletAddress() ?? "")) { _, action in + AccountSideCell( + address: WalletManager.shared.getPrimaryWalletAddress() ?? "", + currentAddress: vm.currentAddress, + detail: vm + .balanceValue( + at: WalletManager.shared + .getPrimaryWalletAddress() ?? "" + ) + ) { _, action in if action == .card { vm.switchProfile() - WalletManager.shared.changeNetwork(LocalUserDefaults.shared.flowNetwork) + WalletManager.shared + .changeNetwork(LocalUserDefaults.shared.flowNetwork) } } } @@ -292,9 +316,11 @@ struct SideMenuView: View { VStack(spacing: 0) { ForEach(evmManager.accounts, id: \.address) { account in let address = account.showAddress - AccountSideCell(address: address, - currentAddress: vm.currentAddress, - detail: vm.balanceValue(at: address)) { _, action in + AccountSideCell( + address: address, + currentAddress: vm.currentAddress, + detail: vm.balanceValue(at: address) + ) { _, action in if action == .card { vm.switchProfile() ChildAccountManager.shared.select(nil) @@ -305,10 +331,12 @@ struct SideMenuView: View { ForEach(cm.childAccounts, id: \.addr) { childAccount in if let address = childAccount.addr { - AccountSideCell(address: address, - currentAddress: vm.currentAddress, - name: childAccount.aName, - logo: childAccount.icon) { _, action in + AccountSideCell( + address: address, + currentAddress: vm.currentAddress, + name: childAccount.aName, + logo: childAccount.icon + ) { _, action in if action == .card { vm.switchProfile() EVMAccountManager.shared.select(nil) @@ -326,7 +354,10 @@ struct SideMenuView: View { .padding(.vertical, 8) Spacer() } - .visibility(evmManager.accounts.count > 0 || cm.childAccounts.count > 0 ? .visible : .gone) + .visibility( + !evmManager.accounts.isEmpty || !cm.childAccounts + .isEmpty ? .visible : .gone + ) } } } @@ -359,7 +390,10 @@ struct SideMenuView: View { WalletManager.shared.changeNetwork(.mainnet) } label: { - NetworkMenuItem(network: .mainnet, currentNetwork: LocalUserDefaults.shared.flowNetwork) + NetworkMenuItem( + network: .mainnet, + currentNetwork: LocalUserDefaults.shared.flowNetwork + ) } Button { @@ -367,7 +401,10 @@ struct SideMenuView: View { WalletManager.shared.changeNetwork(.testnet) } label: { - NetworkMenuItem(network: .testnet, currentNetwork: LocalUserDefaults.shared.flowNetwork) + NetworkMenuItem( + network: .testnet, + currentNetwork: LocalUserDefaults.shared.flowNetwork + ) } } @@ -404,20 +441,47 @@ struct SideMenuView: View { } } } + + // MARK: Private + + @StateObject + private var vm = SideMenuViewModel() + @StateObject + private var um = UserManager.shared + @StateObject + private var wm = WalletManager.shared + @StateObject + private var cm = ChildAccountManager.shared + @StateObject + private var evmManager = EVMAccountManager.shared + @AppStorage("isDeveloperMode") + private var isDeveloperMode = false + @State + private var showSwitchUserAlert = false + + private let cPadding = 12.0 } -class SideContainerViewModel: ObservableObject { - @Published var isOpen: Bool = false - @Published var isLinkedAccount: Bool = false - @Published var hideBrowser: Bool = false +// MARK: - SideContainerViewModel - private var cancellableSet = Set() +class SideContainerViewModel: ObservableObject { + // MARK: Lifecycle init() { - NotificationCenter.default.addObserver(self, selector: #selector(onToggle), name: .toggleSideMenu, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(onRemoteConfigDidChange), name: .remoteConfigDidUpdate, object: nil) - - isLinkedAccount = ChildAccountManager.shared.selectedChildAccount != nil + NotificationCenter.default.addObserver( + self, + selector: #selector(onToggle), + name: .toggleSideMenu, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(onRemoteConfigDidChange), + name: .remoteConfigDidUpdate, + object: nil + ) + + self.isLinkedAccount = ChildAccountManager.shared.selectedChildAccount != nil ChildAccountManager.shared.$selectedChildAccount .receive(on: DispatchQueue.main) .map { $0 } @@ -426,23 +490,38 @@ class SideContainerViewModel: ObservableObject { }).store(in: &cancellableSet) } - @objc func onToggle() { + // MARK: Internal + + @Published + var isOpen: Bool = false + @Published + var isLinkedAccount: Bool = false + @Published + var hideBrowser: Bool = false + + @objc + func onToggle() { withAnimation { isOpen.toggle() } } - @objc func onRemoteConfigDidChange() { + @objc + func onRemoteConfigDidChange() { DispatchQueue.main.async { self.hideBrowser = RemoteConfigManager.shared.config?.features.hideBrowser ?? true } } + + // MARK: Private + + private var cancellableSet = Set() } +// MARK: - SideContainerView + struct SideContainerView: View { - @StateObject private var vm = SideContainerViewModel() - @State private var dragOffset: CGSize = .zero - @State private var isDragging: Bool = false + // MARK: Internal var drag: some Gesture { DragGesture() @@ -485,30 +564,68 @@ struct SideContainerView: View { } } - @ViewBuilder private func makeTabView() -> some View { - let wallet = TabBarPageModel(tag: WalletView.tabTag(), iconName: WalletView.iconName(), color: WalletView.color()) { + // MARK: Private + + @StateObject + private var vm = SideContainerViewModel() + @State + private var dragOffset: CGSize = .zero + @State + private var isDragging: Bool = false + + @ViewBuilder + private func makeTabView() -> some View { + let wallet = TabBarPageModel( + tag: WalletView.tabTag(), + iconName: WalletView.iconName(), + color: WalletView.color() + ) { AnyView(WalletHomeView()) } - let nft = TabBarPageModel(tag: NFTTabScreen.tabTag(), iconName: NFTTabScreen.iconName(), color: NFTTabScreen.color()) { + let nft = TabBarPageModel( + tag: NFTTabScreen.tabTag(), + iconName: NFTTabScreen.iconName(), + color: NFTTabScreen.color() + ) { AnyView(NFTTabScreen()) } - let explore = TabBarPageModel(tag: ExploreTabScreen.tabTag(), iconName: ExploreTabScreen.iconName(), color: ExploreTabScreen.color()) { + let explore = TabBarPageModel( + tag: ExploreTabScreen.tabTag(), + iconName: ExploreTabScreen.iconName(), + color: ExploreTabScreen.color() + ) { AnyView(ExploreTabScreen()) } - let profile = TabBarPageModel(tag: ProfileView.tabTag(), iconName: ProfileView.iconName(), color: ProfileView.color()) { + let profile = TabBarPageModel( + tag: ProfileView.tabTag(), + iconName: ProfileView.iconName(), + color: ProfileView.color() + ) { AnyView(ProfileView()) } if vm.isLinkedAccount { - TabBarView(current: .wallet, pages: [wallet, nft, profile], maxWidth: UIScreen.main.bounds.width) + TabBarView( + current: .wallet, + pages: [wallet, nft, profile], + maxWidth: UIScreen.main.bounds.width + ) } else { if vm.hideBrowser { - TabBarView(current: .wallet, pages: [wallet, nft, profile], maxWidth: UIScreen.main.bounds.width) + TabBarView( + current: .wallet, + pages: [wallet, nft, profile], + maxWidth: UIScreen.main.bounds.width + ) } else { - TabBarView(current: .wallet, pages: [wallet, nft, explore, profile], maxWidth: UIScreen.main.bounds.width) + TabBarView( + current: .wallet, + pages: [wallet, nft, explore, profile], + maxWidth: UIScreen.main.bounds.width + ) } } } diff --git a/FRW/Modules/Wallet/TokenDetail/TokenDetailView.swift b/FRW/Modules/Wallet/TokenDetail/TokenDetailView.swift index 66dbc0d9..4ea0a7bb 100644 --- a/FRW/Modules/Wallet/TokenDetail/TokenDetailView.swift +++ b/FRW/Modules/Wallet/TokenDetail/TokenDetailView.swift @@ -56,6 +56,15 @@ struct TokenDetailView: RouteableView { MoveTokenView(tokenModel: vm.token, isPresent: $vm.showSheet) } } + .navigationBarItems(trailing: HStack(spacing: 6) { + Menu(systemImage: "ellipsis") { + Button("Delete EFT", systemImage: "trash") { + vm.deleteCustomToken() + } + } + .foregroundStyle(Color.Theme.Background.icon) + .visibility(vm.showDeleteToken ? .visible : .gone) + }) } var summaryView: some View { diff --git a/FRW/Modules/Wallet/TokenDetail/TokenDetailViewModel.swift b/FRW/Modules/Wallet/TokenDetail/TokenDetailViewModel.swift index ade1308f..64b4702c 100644 --- a/FRW/Modules/Wallet/TokenDetail/TokenDetailViewModel.swift +++ b/FRW/Modules/Wallet/TokenDetail/TokenDetailViewModel.swift @@ -22,7 +22,10 @@ extension TokenDetailView { var generateChartPoint: LineChartDataPoint { let date = Date(timeIntervalSince1970: closeTime) - let price = Double(closePrice.formatCurrencyString(digits: 2, considerCustomCurrency: true)) ?? 0 + let price = Double(closePrice.formatCurrencyString( + digits: 2, + considerCustomCurrency: true + )) ?? 0 return LineChartDataPoint(value: price, description: date.ymdString, date: date) } } @@ -43,6 +46,8 @@ extension TokenDetailView { case y1 case all + // MARK: Internal + var title: String { switch self { case .d1: @@ -79,15 +84,30 @@ extension TokenDetailView { let oneDayInterval: TimeInterval = 24 * 60 * 60 switch self { case .d1: - return String(format: "%.0lf", Date(timeIntervalSinceNow: -oneDayInterval).timeIntervalSince1970) + return String( + format: "%.0lf", + Date(timeIntervalSinceNow: -oneDayInterval).timeIntervalSince1970 + ) case .w1: - return String(format: "%.0lf", Date(timeIntervalSinceNow: -oneDayInterval * 7).timeIntervalSince1970) + return String( + format: "%.0lf", + Date(timeIntervalSinceNow: -oneDayInterval * 7).timeIntervalSince1970 + ) case .m1: - return String(format: "%.0lf", Date(timeIntervalSinceNow: -oneDayInterval * 30).timeIntervalSince1970) + return String( + format: "%.0lf", + Date(timeIntervalSinceNow: -oneDayInterval * 30).timeIntervalSince1970 + ) case .m3: - return String(format: "%.0lf", Date(timeIntervalSinceNow: -oneDayInterval * 90).timeIntervalSince1970) + return String( + format: "%.0lf", + Date(timeIntervalSinceNow: -oneDayInterval * 90).timeIntervalSince1970 + ) case .y1: - return String(format: "%.0lf", Date(timeIntervalSinceNow: -oneDayInterval * 365).timeIntervalSince1970) + return String( + format: "%.0lf", + Date(timeIntervalSinceNow: -oneDayInterval * 365).timeIntervalSince1970 + ) case .all: return "" } @@ -95,6 +115,8 @@ extension TokenDetailView { } } +// MARK: - TokenDetailViewModel.Action + extension TokenDetailViewModel { enum Action { case none @@ -102,24 +124,10 @@ extension TokenDetailViewModel { } } -class TokenDetailViewModel: ObservableObject { - @Published var token: TokenModel - @Published var market: QuoteMarket = LocalUserDefaults.shared.market - @Published var selectedRangeType: TokenDetailView.ChartRangeType = .d1 - @Published var chartData: LineChartData? - @Published var balance: Double = 0 - @Published var balanceAsUSD: Double = 0 - @Published var changePercent: Double = 0 - @Published var rate: Double = 0 - @Published var recentTransfers: [FlowScanTransfer] = [] - - @Published var showSwapButton: Bool = true - @Published var showBuyButton: Bool = true - - @Published var showSheet: Bool = false - var buttonAction: TokenDetailViewModel.Action = .none +// MARK: - TokenDetailViewModel - private var cancelSets = Set() +class TokenDetailViewModel: ObservableObject { + // MARK: Lifecycle init(token: TokenModel) { self.token = token @@ -128,6 +136,42 @@ class TokenDetailViewModel: ObservableObject { refreshButtonState() } + // MARK: Internal + + @Published + var token: TokenModel + @Published + var market: QuoteMarket = LocalUserDefaults.shared.market + @Published + var selectedRangeType: TokenDetailView.ChartRangeType = .d1 + @Published + var chartData: LineChartData? + @Published + var balance: Double = 0 + @Published + var balanceAsUSD: Double = 0 + @Published + var changePercent: Double = 0 + @Published + var rate: Double = 0 + @Published + var recentTransfers: [FlowScanTransfer] = [] + + @Published + var showSwapButton: Bool = true + @Published + var showBuyButton: Bool = true + @Published + var showDeleteToken: Bool = false + + @Published + var showSheet: Bool = false + var buttonAction: TokenDetailViewModel.Action = .none + + // MARK: Private + + private var cancelSets = Set() + private func setupObserver() { NotificationCenter.default.publisher(for: .quoteMarketUpdated).sink { [weak self] _ in DispatchQueue.main.async { @@ -159,19 +203,19 @@ extension TokenDetailViewModel { } var balanceString: String { - return balance.formatCurrencyString() + balance.formatCurrencyString() } var balanceAsCurrentCurrencyString: String { - return balanceAsUSD.formatCurrencyString(considerCustomCurrency: true) + balanceAsUSD.formatCurrencyString(considerCustomCurrency: true) } var changeIsNegative: Bool { - return changePercent < 0 + changePercent < 0 } var changeColor: Color { - return changeIsNegative ? Color.LL.Warning.warning2 : Color.LL.Success.success2 + changeIsNegative ? Color.LL.Warning.warning2 : Color.LL.Success.success2 } var hasRateAndChartData: Bool { @@ -184,7 +228,9 @@ extension TokenDetailViewModel { } var movable: Bool { - EVMAccountManager.shared.hasAccount && (token.evmAddress != nil || token.flowIdentifier != nil || token.isFlowCoin) + EVMAccountManager.shared + .hasAccount && + (token.evmAddress != nil || token.flowIdentifier != nil || token.isFlowCoin) } } @@ -245,6 +291,14 @@ extension TokenDetailViewModel { showSheet = true } } + + func deleteCustomToken() { + guard let customToken = token.findCustomToken() else { + return + } + WalletManager.shared.customTokenManager.delete(token: customToken) + HUD.success(title: "") + } } // MARK: - Fetch & Refresh @@ -282,10 +336,16 @@ extension TokenDetailViewModel { let currentRangeType = selectedRangeType - let request = CryptoHistoryRequest(provider: market.rawValue, pair: pair, after: currentRangeType.after, period: "\(currentRangeType.frequency.rawValue)") + let request = CryptoHistoryRequest( + provider: market.rawValue, + pair: pair, + after: currentRangeType.after, + period: "\(currentRangeType.frequency.rawValue)" + ) do { - let response: CryptoHistoryResponse = try await Network.request(FRWAPI.Crypto.history(request)) + let response: CryptoHistoryResponse = try await Network + .request(FRWAPI.Crypto.history(request)) if currentRangeType != self.selectedRangeType { // selectedRangeType is changed, this is an outdated response @@ -304,17 +364,26 @@ extension TokenDetailViewModel { private func generateChartData(response: CryptoHistoryResponse) { let quotes = response.parseMarketQuoteData(rangeType: selectedRangeType) let linePoints = quotes.map { $0.generateChartPoint } - let chartLineStyle = LineStyle(lineColour: ColourStyle(colours: [Color.LL.Primary.salmonPrimary.opacity(0.24), Color.LL.Primary.salmonPrimary.opacity(0)], startPoint: .top, endPoint: .bottom)) + let chartLineStyle = LineStyle(lineColour: ColourStyle( + colours: [ + Color.LL.Primary.salmonPrimary.opacity(0.24), + Color.LL.Primary.salmonPrimary.opacity(0), + ], + startPoint: .top, + endPoint: .bottom + )) let set = LineDataSet(dataPoints: linePoints, style: chartLineStyle) - let chartStyle = LineChartStyle(infoBoxPlacement: .floating, - infoBoxBorderColour: .LL.Primary.salmonPrimary, - infoBoxBorderStyle: StrokeStyle(lineWidth: 1), - markerType: .vertical(attachment: .point), - yAxisLabelPosition: .trailing, - yAxisLabelFont: .inter(size: 12, weight: .regular), - yAxisLabelColour: Color.LL.Neutrals.neutrals8, - yAxisNumberOfLabels: 4) + let chartStyle = LineChartStyle( + infoBoxPlacement: .floating, + infoBoxBorderColour: .LL.Primary.salmonPrimary, + infoBoxBorderStyle: StrokeStyle(lineWidth: 1), + markerType: .vertical(attachment: .point), + yAxisLabelPosition: .trailing, + yAxisLabelFont: .inter(size: 12, weight: .regular), + yAxisLabelColour: Color.LL.Neutrals.neutrals8, + yAxisNumberOfLabels: 4 + ) let cd = LineChartData(dataSets: set, chartStyle: chartStyle) cd.legends = [] @@ -322,20 +391,29 @@ extension TokenDetailViewModel { } var transactionsCacheKey: String { - return "token_detail_transaction_cache_\(token.contractId)" + "token_detail_transaction_cache_\(token.contractId)" } private func fetchTransactionsData() { Task { - if let cachedTransactions = try? await PageCache.cache.get(forKey: self.transactionsCacheKey, type: [FlowScanTransfer].self), !cachedTransactions.isEmpty { + if let cachedTransactions = try? await PageCache.cache.get( + forKey: self.transactionsCacheKey, + type: [FlowScanTransfer].self + ), !cachedTransactions.isEmpty { DispatchQueue.main.async { self.recentTransfers = cachedTransactions } } do { - let request = TokenTransfersRequest(address: WalletManager.shared.getPrimaryWalletAddress() ?? "", limit: 3, after: "", token: self.token.contractId) - let response: TransfersResponse = try await Network.request(FRWAPI.Account.tokenTransfers(request)) + let request = TokenTransfersRequest( + address: WalletManager.shared.getPrimaryWalletAddress() ?? "", + limit: 3, + after: "", + token: self.token.contractId + ) + let response: TransfersResponse = try await Network + .request(FRWAPI.Account.tokenTransfers(request)) let list = response.transactions ?? [] PageCache.cache.set(value: list, forKey: self.transactionsCacheKey) @@ -355,7 +433,8 @@ extension TokenDetailViewModel { // Swap if (RemoteConfigManager.shared.config?.features.swap ?? false) == true { // don't show when current is Linked account - if ChildAccountManager.shared.selectedChildAccount != nil { + if ChildAccountManager.shared.selectedChildAccount != nil || ChildAccountManager.shared + .selectedChildAccount != nil { showSwapButton = false } else { showSwapButton = true @@ -365,7 +444,8 @@ extension TokenDetailViewModel { } // buy - if RemoteConfigManager.shared.config?.features.onRamp ?? false == true, flow.chainID == .mainnet { + if RemoteConfigManager.shared.config?.features.onRamp ?? false == true, + flow.chainID == .mainnet { if ChildAccountManager.shared.selectedChildAccount != nil { showBuyButton = false } else { @@ -375,5 +455,7 @@ extension TokenDetailViewModel { } else { showBuyButton = false } + // delete custom token + showDeleteToken = token.findCustomToken() != nil } } diff --git a/FRW/Modules/Wallet/WalletAccount/WalletAccount.swift b/FRW/Modules/Wallet/WalletAccount/WalletAccount.swift index ef5ff38e..66aea8bf 100644 --- a/FRW/Modules/Wallet/WalletAccount/WalletAccount.swift +++ b/FRW/Modules/Wallet/WalletAccount/WalletAccount.swift @@ -8,9 +8,21 @@ import Foundation import SwiftUI +// MARK: - WalletAccount + struct WalletAccount { + // MARK: Lifecycle + + init() { + self.storedAccount = LocalUserDefaults.shared.walletAccount ?? [:] + } + + // MARK: Internal + var storedAccount: [String: [WalletAccount.User]] + // MARK: Private + private var key: String { guard let userId = UserManager.shared.activatedUID else { return "empty" @@ -18,10 +30,6 @@ struct WalletAccount { return "\(userId)" } - init() { - storedAccount = LocalUserDefaults.shared.walletAccount ?? [:] - } - private func saveCache() { LocalUserDefaults.shared.walletAccount = storedAccount } @@ -58,7 +66,8 @@ extension WalletAccount { mutating func update(at address: String, emoji: WalletAccount.Emoji, name: String? = nil) { let currentNetwork = LocalUserDefaults.shared.flowNetwork if var list = storedAccount[key] { - if var index = list.lastIndex(where: { $0.network == currentNetwork && $0.address == address }) { + if var index = list + .lastIndex(where: { $0.network == currentNetwork && $0.address == address }) { var user = list[index] user.emoji = emoji user.name = name ?? emoji.name @@ -93,6 +102,8 @@ extension WalletAccount { case lemon = "🍋" case avocado = "🥑" + // MARK: Internal + var name: String { switch self { case .koala: return "Koala" @@ -100,7 +111,6 @@ extension WalletAccount { case .panda: return "Panda" case .butterfly: return "Butterfly" case .penguin: return "Penguin" - case .cherry: return "Cherry" case .chestnut: return "Chestnut" case .peach: return "Peach" @@ -141,7 +151,7 @@ extension WalletAccount { } func icon(size: CGFloat = 24) -> some View { - return VStack { + VStack { Text(self.rawValue) .font(.system(size: size / 2 + 2)) } @@ -152,30 +162,47 @@ extension WalletAccount { } struct User: Codable { - var emoji: WalletAccount.Emoji - var name: String - var address: String - var network: LocalUserDefaults.FlowNetworkType + // MARK: Lifecycle init(emoji: WalletAccount.Emoji, address: String) { self.emoji = emoji - name = emoji.name + self.name = emoji.name self.address = address - network = LocalUserDefaults.shared.flowNetwork + self.network = LocalUserDefaults.shared.flowNetwork } init(from decoder: any Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: WalletAccount.User.CodingKeys.self) + let container: KeyedDecodingContainer = try decoder + .container(keyedBy: WalletAccount.User.CodingKeys.self) do { - emoji = try container.decode(WalletAccount.Emoji.self, forKey: WalletAccount.User.CodingKeys.emoji) + self.emoji = try container.decode( + WalletAccount.Emoji.self, + forKey: WalletAccount.User.CodingKeys.emoji + ) } catch { - emoji = WalletAccount.Emoji.avocado + self.emoji = WalletAccount.Emoji.avocado } - name = try container.decode(String.self, forKey: WalletAccount.User.CodingKeys.name) - address = try container.decode(String.self, forKey: WalletAccount.User.CodingKeys.address) - network = try container.decode(LocalUserDefaults.FlowNetworkType.self, forKey: WalletAccount.User.CodingKeys.network) + self.name = try container.decode( + String.self, + forKey: WalletAccount.User.CodingKeys.name + ) + self.address = try container.decode( + String.self, + forKey: WalletAccount.User.CodingKeys.address + ) + self.network = try container.decode( + LocalUserDefaults.FlowNetworkType.self, + forKey: WalletAccount.User.CodingKeys.network + ) } + + // MARK: Internal + + var emoji: WalletAccount.Emoji + var name: String + var address: String + var network: LocalUserDefaults.FlowNetworkType } } @@ -188,7 +215,7 @@ extension Array where Element: Equatable { var selectedElements: [Element] = [] var num = count * 36 - for i in 0 ..< num { + for i in 0.. AppTabType { - return .wallet + .wallet } static func iconName() -> String { @@ -25,29 +27,42 @@ extension WalletHomeView: AppTabBarPageProtocol { } static func color() -> Color { - return .Flow.accessory + .Flow.accessory } } +// MARK: - WalletHomeView + struct WalletHomeView: View { - @State var safeArea: EdgeInsets = .zero - @State var size: CGSize = .zero - - @StateObject var um = UserManager.shared - @StateObject var wm = WalletManager.shared - @StateObject private var vm = WalletViewModel() - @StateObject var newsHandler = WalletNewsHandler.shared - @State var isRefreshing: Bool = false - @State private var showActionSheet = false + @State + var safeArea: EdgeInsets = .zero + @State + var size: CGSize = .zero + + @StateObject + var um = UserManager.shared + @StateObject + var wm = WalletManager.shared + @StateObject + private var vm = WalletViewModel() + @StateObject + var newsHandler = WalletNewsHandler.shared + @State + var isRefreshing: Bool = false + @State + private var showActionSheet = false @AppStorage("WalletCardBackrgound") private var walletCardBackrgound: String = "fade:0" - @State var selectedNewsId: String? = nil - @State var scrollNext: Bool = false + @State + var selectedNewsId: String? = nil + @State + var scrollNext: Bool = false private let scrollName: String = "WALLETSCROLL" - @State private var logViewPresented: Bool = false + @State + private var logViewPresented: Bool = false var body: some View { GeometryReader { proxy in @@ -87,18 +102,21 @@ struct WalletHomeView: View { isRefreshing = false } } progress: { state in - ImageAnimated(imageSize: CGSize(width: 60, height: 60), - imageNames: ImageAnimated.appRefreshImageNames(), - duration: 1.6, - isAnimating: state == .loading || state == .primed) - .frame(maxWidth: .infinity) - .frame(height: 60) - .transition( - AnyTransition.move(edge: .bottom).combined(with: .scale).combined(with: .opacity) - ) - .visibility(state == .waiting ? .gone : .visible) - .zIndex(2) - .offset(y: headerHeight + 20) + ImageAnimated( + imageSize: CGSize(width: 60, height: 60), + imageNames: ImageAnimated.appRefreshImageNames(), + duration: 1.6, + isAnimating: state == .loading || state == .primed + ) + .frame(maxWidth: .infinity) + .frame(height: 60) + .transition( + AnyTransition.move(edge: .bottom).combined(with: .scale) + .combined(with: .opacity) + ) + .visibility(state == .waiting ? .gone : .visible) + .zIndex(2) + .offset(y: headerHeight + 20) } content: { VStack(spacing: 0) { JailbreakTipsView() @@ -124,7 +142,8 @@ struct WalletHomeView: View { func TopMenuView() -> some View { GeometryReader { proxy in let minY = proxy.frame(in: .named(scrollName)).minY - let progress = minY / (headerHeight * (minY > 0 ? 0.5 : 0.8) + proxy.safeAreaInsets.top - 33) + let progress = minY / + (headerHeight * (minY > 0 ? 0.5 : 0.8) + proxy.safeAreaInsets.top - 33) HStack { Button { @@ -295,7 +314,11 @@ struct WalletHomeView: View { .opacity(-progress) } .offset(y: -minY) - .confirmationDialog("Select a color", isPresented: $showActionSheet, titleVisibility: .hidden) { + .confirmationDialog( + "Select a color", + isPresented: $showActionSheet, + titleVisibility: .hidden + ) { Button("change wallpaper") { showWallpaper() } @@ -324,7 +347,8 @@ struct WalletHomeView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .blur(radius: 6) - LinearGradient(colors: + LinearGradient( + colors: [ Color(hex: "#333333"), Color(hex: "#333333"), @@ -332,7 +356,8 @@ struct WalletHomeView: View { Color(hex: "#333333").opacity(0.32), ], startPoint: .leading, - endPoint: .trailing) + endPoint: .trailing + ) } } @@ -354,9 +379,13 @@ struct WalletHomeView: View { VStack(spacing: 0) { VStack(spacing: 4) { HStack { - Text(vm.isHidden ? "****" : "\(CurrencyCache.cache.currencySymbol) \(vm.balance.formatCurrencyString(considerCustomCurrency: true))") - .font(.Ukraine(size: 30, weight: .bold)) - .foregroundStyle(Color.Theme.Text.black) + Text( + vm + .isHidden ? "****" : + "\(CurrencyCache.cache.currencySymbol) \(vm.balance.formatCurrencyString(considerCustomCurrency: true))" + ) + .font(.Ukraine(size: 30, weight: .bold)) + .foregroundStyle(Color.Theme.Text.black) Spacer() @@ -420,13 +449,21 @@ struct WalletHomeView: View { } private func walletActionBar() -> some View { - return HStack { - WalletHomeView.ActionView(isH: vm.showHorLayout, action: .send, allowClick: !wm.isSelectedChildAccount) + HStack { + WalletHomeView.ActionView( + isH: vm.showHorLayout, + action: .send, + allowClick: !wm.isSelectedChildAccount + ) WalletHomeView.ActionView(isH: vm.showHorLayout, action: .receive, allowClick: true) WalletHomeView.ActionView(isH: vm.showHorLayout, action: .swap, allowClick: true) .visibility(vm.showSwapButton ? .visible : .gone) - WalletHomeView.ActionView(isH: vm.showHorLayout, action: .stake, allowClick: !wm.isSelectedChildAccount) - .visibility(vm.showStakeButton ? .visible : .gone) + WalletHomeView.ActionView( + isH: vm.showHorLayout, + action: .stake, + allowClick: !wm.isSelectedChildAccount + ) + .visibility(vm.showStakeButton ? .visible : .gone) } } @@ -434,7 +471,7 @@ struct WalletHomeView: View { func CoinListView() -> some View { VStack(spacing: 16) { HStack { - Text((vm.mCoinItems.count > 0 ? "\(vm.mCoinItems.count) " : "") + "tokens".localized) + Text((!vm.mCoinItems.isEmpty ? "\(vm.mCoinItems.count) " : "") + "tokens".localized) .font(.inter(size: 18, weight: .bold)) .foregroundStyle(Color.Theme.Text.black3) Spacer() @@ -477,7 +514,10 @@ struct WalletHomeView: View { VStack(spacing: 5) { ForEach(vm.mCoinItems, id: \.token.symbol) { coin in Button { - Router.route(to: RouteMap.Wallet.tokenDetail(coin.token, WalletManager.shared.accessibleManager.isAccessible(coin.token))) + Router.route(to: RouteMap.Wallet.tokenDetail( + coin.token, + WalletManager.shared.accessibleManager.isAccessible(coin.token) + )) } label: { WalletHomeView.CoinCell(coin: coin) .contentShape(Rectangle()) @@ -508,11 +548,15 @@ struct WalletHomeView: View { private let CoinIconHeight: CGFloat = 44 private let CoinCellHeight: CGFloat = 76 +// MARK: WalletHomeView.CoinCell + extension WalletHomeView { struct CoinCell: View { let coin: WalletViewModel.WalletCoinItemModel - @EnvironmentObject var vm: WalletViewModel - @StateObject var stakingManager = StakingManager.shared + @EnvironmentObject + var vm: WalletViewModel + @StateObject + var stakingManager = StakingManager.shared var body: some View { VStack(spacing: 0) { @@ -535,9 +579,11 @@ extension WalletHomeView { Spacer() - Text("\(vm.isHidden ? "****" : coin.balance.formatCurrencyString()) \(coin.token.symbol?.uppercased() ?? "?")") - .foregroundColor(.LL.Neutrals.text) - .font(.inter(size: 14, weight: .medium)) + Text( + "\(vm.isHidden ? "****" : coin.balance.formatCurrencyString()) \(coin.token.symbol?.uppercased() ?? "?")" + ) + .foregroundColor(.LL.Neutrals.text) + .font(.inter(size: 14, weight: .medium)) } HStack { @@ -554,7 +600,10 @@ extension WalletHomeView { .background(coin.changeBG) .cornerRadius(11, style: .continuous) } - .visibility(WalletManager.shared.accessibleManager.isAccessible(coin.token) ? .visible : .gone) + .visibility( + WalletManager.shared.accessibleManager + .isAccessible(coin.token) ? .visible : .gone + ) Text("Inaccessible".localized) .foregroundStyle(Color.Flow.Font.inaccessible) @@ -563,20 +612,28 @@ extension WalletHomeView { .padding(.vertical, 5) .background(.Flow.Font.inaccessible.opacity(0.16)) .cornerRadius(4, style: .continuous) - .visibility(WalletManager.shared.accessibleManager.isAccessible(coin.token) ? .gone : .visible) + .visibility( + WalletManager.shared.accessibleManager + .isAccessible(coin.token) ? .gone : .visible + ) Spacer() - Text(vm.isHidden ? "****" : "\(CurrencyCache.cache.currencySymbol)\(coin.balanceAsCurrentCurrency)") - .foregroundColor(.LL.Neutrals.neutrals7) - .font(.inter(size: 14, weight: .regular)) + Text( + vm + .isHidden ? "****" : + "\(CurrencyCache.cache.currencySymbol)\(coin.balanceAsCurrentCurrency)" + ) + .foregroundColor(.LL.Neutrals.neutrals7) + .font(.inter(size: 14, weight: .regular)) } } .frame(maxWidth: .infinity) } .frame(minHeight: CoinCellHeight) - if EVMAccountManager.shared.selectedAccount == nil && ChildAccountManager.shared.selectedChildAccount == nil { + if EVMAccountManager.shared.selectedAccount == nil && ChildAccountManager.shared + .selectedChildAccount == nil { HStack(spacing: 0) { Divider() .frame(width: 1, height: 10) @@ -603,9 +660,11 @@ extension WalletHomeView { Spacer() - Text("\(vm.isHidden ? "****" : stakingManager.stakingCount.formatCurrencyString()) FLOW") - .foregroundColor(.LL.Neutrals.text) - .font(.inter(size: 14, weight: .medium)) + Text( + "\(vm.isHidden ? "****" : stakingManager.stakingCount.formatCurrencyString()) FLOW" + ) + .foregroundColor(.LL.Neutrals.text) + .font(.inter(size: 14, weight: .medium)) } } .padding(.leading, 19) @@ -626,6 +685,8 @@ extension WalletHomeView { enum Action: String { case send, receive, swap, stake + // MARK: Internal + var icon: String { switch self { case .send: @@ -651,10 +712,10 @@ extension WalletHomeView { UIImpactFeedbackGenerator(style: .soft).impactOccurred() Router.route(to: RouteMap.Explore.browser(url)) } - // Router.route(to: RouteMap.Wallet.swap(nil)) case .stake: - if !LocalUserDefaults.shared.stakingGuideDisplayed && !StakingManager.shared.isStaked { + if !LocalUserDefaults.shared.stakingGuideDisplayed && !StakingManager.shared + .isStaked { Router.route(to: RouteMap.Wallet.stakeGuide) return } @@ -663,6 +724,8 @@ extension WalletHomeView { } } + // MARK: Private + private func incrementUrl() -> String { if LocalUserDefaults.shared.flowNetwork == .mainnet { return "https://app.increment.fi/swap" @@ -673,6 +736,29 @@ extension WalletHomeView { } struct ActionView: View { + // MARK: Public + + public func container(@ViewBuilder content: () -> Content) -> some View { + HStack(alignment: .center, spacing: 10) { + if isH { + HStack { + content() + } + } else { + VStack { + content() + } + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .frame(alignment: .center) + .background(Color.Theme.Background.grey) + .cornerRadius(16) + } + + // MARK: Internal + let isH: Bool let action: WalletHomeView.Action @@ -699,32 +785,19 @@ extension WalletHomeView { .buttonStyle(ScaleButtonStyle()) .disabled(!allowClick) } - - public func container(@ViewBuilder content: () -> Content) -> some View { - return HStack(alignment: .center, spacing: 10) { - if isH { - HStack { - content() - } - } else { - VStack { - content() - } - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 10) - .frame(alignment: .center) - .background(Color.Theme.Background.grey) - .cornerRadius(16) - } } } +// MARK: - VisualEffectView + struct VisualEffectView: UIViewRepresentable { var effect: UIVisualEffect? - func makeUIView(context _: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() } - func updateUIView(_ uiView: UIVisualEffectView, context _: UIViewRepresentableContext) { uiView.effect = effect } + + func makeUIView(context _: UIViewRepresentableContext) + -> UIVisualEffectView { UIVisualEffectView() } + func updateUIView(_ uiView: UIVisualEffectView, context _: UIViewRepresentableContext) { + uiView.effect = effect + } } #Preview { @@ -734,6 +807,8 @@ struct VisualEffectView: UIViewRepresentable { } } +// MARK: - VisualEffectBlur + struct VisualEffectBlur: UIViewRepresentable { var effect: UIBlurEffect.Style @@ -748,31 +823,29 @@ struct VisualEffectBlur: UIViewRepresentable { } extension StackTransformViewOptions { - static let flowStack: StackTransformViewOptions = { - StackTransformViewOptions( - scaleFactor: 0.06, - minScale: 0.00, - maxScale: 1.00, - maxStackSize: 4, - spacingFactor: 0.12, - maxSpacing: nil, - alphaFactor: 0.05, - bottomStackAlphaSpeedFactor: 10.00, - topStackAlphaSpeedFactor: 0.10, - perspectiveRatio: 0.00, - shadowEnabled: true, - shadowColor: .black, - shadowOpacity: 0.06, - shadowOffset: .zero, - shadowRadius: 10.00, - stackRotateAngel: 0.00, - popAngle: 0.00, - popOffsetRatio: .init(width: -0.50, height: 0.30), - stackPosition: .init(x: -0.08, y: 1.00), - reverse: false, - blurEffectEnabled: false, - maxBlurEffectRadius: 0.00, - blurEffectStyle: .light - ) - }() + static let flowStack: StackTransformViewOptions = .init( + scaleFactor: 0.06, + minScale: 0.00, + maxScale: 1.00, + maxStackSize: 4, + spacingFactor: 0.12, + maxSpacing: nil, + alphaFactor: 0.05, + bottomStackAlphaSpeedFactor: 10.00, + topStackAlphaSpeedFactor: 0.10, + perspectiveRatio: 0.00, + shadowEnabled: true, + shadowColor: .black, + shadowOpacity: 0.06, + shadowOffset: .zero, + shadowRadius: 10.00, + stackRotateAngel: 0.00, + popAngle: 0.00, + popOffsetRatio: .init(width: -0.50, height: 0.30), + stackPosition: .init(x: -0.08, y: 1.00), + reverse: false, + blurEffectEnabled: false, + maxBlurEffectRadius: 0.00, + blurEffectStyle: .light + ) } diff --git a/FRW/Modules/Wallet/WalletNotificationView.swift b/FRW/Modules/Wallet/WalletNotificationView.swift index 054ac09a..d8409ea9 100644 --- a/FRW/Modules/Wallet/WalletNotificationView.swift +++ b/FRW/Modules/Wallet/WalletNotificationView.swift @@ -1,5 +1,5 @@ // -// NotificationView.swift +// WalletNotificationView.swift // FRW // // Created by cat on 2024/6/27. @@ -8,13 +8,15 @@ import Kingfisher import SwiftUI +// MARK: - WalletNotificationView + struct WalletNotificationView: View { + // MARK: Internal + let item: RemoteConfigManager.News var onClose: (String) -> Void var onAction: (String) -> Void - @State private var opacity: Double = 1.0 - var body: some View { ZStack(alignment: .topTrailing) { HStack(spacing: 12) { @@ -83,14 +85,33 @@ struct WalletNotificationView: View { .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.32), radius: 2, x: 0, y: 4) .opacity(opacity) } + + // MARK: Private + + @State + private var opacity: Double = 1.0 } // MARK: - Demo extension RemoteConfigManager.News { - static let sample = RemoteConfigManager.News(id: "22722ad7-fd47-4167-a7e6-c4c69973bc5d", priority: .high, type: .image, title: "Missing USDC?", body: "Please upgrade USDC to USDCf", icon: "https://cdn.jsdelivr.net/gh/FlowFans/flow-token-list@main/token-registry/A.b19436aae4d94622.FiatToken/logo.svg", image: "https://w.wallhaven.cc/full/3l/wallhaven-3lv8j6.jpg", url: "https://port.flow.com/transaction?hash=a32bf0cabf37d52ca3c60daccc10b9ba79db5975d29e7a105d96983b918788e4", expiryTime: Calendar.current.date(byAdding: .day, value: 1, to: Date())!, displayType: .click, conditions: nil) + static let sample = RemoteConfigManager.News( + id: "22722ad7-fd47-4167-a7e6-c4c69973bc5d", + priority: .high, + type: .image, + title: "Missing USDC?", + body: "Please upgrade USDC to USDCf", + icon: "https://cdn.jsdelivr.net/gh/FlowFans/flow-token-list@main/token-registry/A.b19436aae4d94622.FiatToken/logo.svg", + image: "https://w.wallhaven.cc/full/3l/wallhaven-3lv8j6.jpg", + url: "https://port.flow.com/transaction?hash=a32bf0cabf37d52ca3c60daccc10b9ba79db5975d29e7a105d96983b918788e4", + expiryTime: Calendar.current.date(byAdding: .day, value: 1, to: Date())!, + displayType: .click, + conditions: nil + ) } +// MARK: - WalletNotificationView_Previews + struct WalletNotificationView_Previews: PreviewProvider { static var previews: some View { WalletNotificationView( diff --git a/FRW/Modules/Wallet/WalletViewModel.swift b/FRW/Modules/Wallet/WalletViewModel.swift index 0387d324..683fc25a 100644 --- a/FRW/Modules/Wallet/WalletViewModel.swift +++ b/FRW/Modules/Wallet/WalletViewModel.swift @@ -7,10 +7,10 @@ import Combine import Flow +import FlowWalletCore import Foundation import SwiftUI import SwiftUIPager -import FlowWalletCore extension WalletViewModel { enum WalletState { @@ -27,7 +27,7 @@ extension WalletViewModel { let changePercentage: Double var changeIsNegative: Bool { - return changePercentage < 0 + changePercentage < 0 } var priceValue: String { @@ -47,75 +47,36 @@ extension WalletViewModel { } var changeColor: Color { - return changeIsNegative ? Color.Flow.Font.descend : Color.Flow.Font.ascend + changeIsNegative ? Color.Flow.Font.descend : Color.Flow.Font.ascend } var changeBG: Color { if changePercentage == 0 { return Color.Theme.Background.grey.opacity(0.16) } - return changeIsNegative ? Color.Flow.Font.descend.opacity(0.16) : Color.Flow.Font.ascend.opacity(0.16) + return changeIsNegative ? Color.Flow.Font.descend.opacity(0.16) : Color.Flow.Font.ascend + .opacity(0.16) } var balanceAsCurrentCurrency: String { - return (balance * last).formatCurrencyString(considerCustomCurrency: true) + (balance * last).formatCurrencyString(considerCustomCurrency: true) } static func mock() -> WalletViewModel.WalletCoinItemModel { - return WalletCoinItemModel(token: TokenModel.mock(), balance: 999, last: 10, changePercentage: 50) + WalletCoinItemModel( + token: TokenModel.mock(), + balance: 999, + last: 10, + changePercentage: 50 + ) } } } -class WalletViewModel: ObservableObject { - @Published var isHidden: Bool = LocalUserDefaults.shared.walletHidden - @Published var balance: Double = 0 - @Published var coinItems: [WalletCoinItemModel] = [] - @Published var walletState: WalletState = .noAddress - @Published var transactionCount: Int = LocalUserDefaults.shared.transactionCount - @Published var pendingRequestCount: Int = 0 - @Published var backupTipsPresent: Bool = false - - @Published var isMock: Bool = false - - @Published var moveAssetsPresent: Bool = false - @Published var moveTokenPresent: Bool = false - - @Published var currentPage: Int = 0 - @Published var page: Page = .first() - - @Published var showHeaderMask = false - - @Published var showAddTokenButton: Bool = true - @Published var showSwapButton: Bool = true - @Published var showStakeButton: Bool = true - @Published var showHorLayout: Bool = false - - @Published var showBuyButton: Bool = true - - @Published var showMoveAsset: Bool = false - - var needShowPlaceholder: Bool { - return isMock || walletState == .noAddress - } - - var mCoinItems: [WalletCoinItemModel] { - if needShowPlaceholder { - return [WalletCoinItemModel].mock() - } else { - return coinItems - } - } +// MARK: - WalletViewModel - private var lastRefreshTS: TimeInterval = 0 - private let autoRefreshInterval: TimeInterval = 30 - - private var isReloading: Bool = false - - /// If the current account is not backed up, each time start app, backup tips will be displayed. - private var backupTipsShown: Bool = false - - private var cancelSets = Set() +class WalletViewModel: ObservableObject { + // MARK: Lifecycle init() { WalletManager.shared.$walletInfo @@ -174,14 +135,30 @@ class WalletViewModel: ObservableObject { return } - if abs(self.lastRefreshTS - Date().timeIntervalSince1970) > self.autoRefreshInterval { + if abs(self.lastRefreshTS - Date().timeIntervalSince1970) > self + .autoRefreshInterval { self.reloadWalletData() } }.store(in: &cancelSets) - NotificationCenter.default.addObserver(self, selector: #selector(transactionCountDidChanged), name: .transactionCountDidChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(willReset), name: .willResetWallet, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReset), name: .didResetWallet, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(transactionCountDidChanged), + name: .transactionCountDidChanged, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(willReset), + name: .willResetWallet, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(didReset), + name: .didResetWallet, + object: nil + ) refreshButtonState() @@ -199,6 +176,78 @@ class WalletViewModel: ObservableObject { }.store(in: &cancelSets) } + // MARK: Internal + + @Published + var isHidden: Bool = LocalUserDefaults.shared.walletHidden + @Published + var balance: Double = 0 + @Published + var coinItems: [WalletCoinItemModel] = [] + @Published + var walletState: WalletState = .noAddress + @Published + var transactionCount: Int = LocalUserDefaults.shared.transactionCount + @Published + var pendingRequestCount: Int = 0 + @Published + var backupTipsPresent: Bool = false + + @Published + var isMock: Bool = false + + @Published + var moveAssetsPresent: Bool = false + @Published + var moveTokenPresent: Bool = false + + @Published + var currentPage: Int = 0 + @Published + var page: Page = .first() + + @Published + var showHeaderMask = false + + @Published + var showAddTokenButton: Bool = true + @Published + var showSwapButton: Bool = true + @Published + var showStakeButton: Bool = true + @Published + var showHorLayout: Bool = false + + @Published + var showBuyButton: Bool = true + + @Published + var showMoveAsset: Bool = false + + var needShowPlaceholder: Bool { + isMock || walletState == .noAddress + } + + var mCoinItems: [WalletCoinItemModel] { + if needShowPlaceholder { + return [WalletCoinItemModel].mock() + } else { + return coinItems + } + } + + // MARK: Private + + private var lastRefreshTS: TimeInterval = 0 + private let autoRefreshInterval: TimeInterval = 30 + + private var isReloading: Bool = false + + /// If the current account is not backed up, each time start app, backup tips will be displayed. + private var backupTipsShown: Bool = false + + private var cancelSets = Set() + private func updateTheme() { showHeaderMask = false if ThemeManager.shared.style == .dark { @@ -220,10 +269,13 @@ class WalletViewModel: ObservableObject { } let summary = CoinRateCache.cache.getSummary(for: symbol) - let item = WalletCoinItemModel(token: token, - balance: WalletManager.shared.getBalance(bySymbol: symbol), - last: summary?.getLastRate() ?? 0, - changePercentage: summary?.getChangePercentage() ?? 0) + let item = WalletCoinItemModel( + token: token, + balance: WalletManager.shared + .getBalance(bySymbol: symbol), + last: summary?.getLastRate() ?? 0, + changePercentage: summary?.getChangePercentage() ?? 0 + ) list.append(item) } list.sort { first, second in @@ -287,7 +339,7 @@ class WalletViewModel: ObservableObject { return false } - if result.count == 0, LocalUserDefaults.shared.nftCount == 0 { + if result.isEmpty, LocalUserDefaults.shared.nftCount == 0 { return } @@ -314,30 +366,35 @@ class WalletViewModel: ObservableObject { LocalUserDefaults.shared.transactionCount = finalCount } } catch { - debugPrint("WalletViewModel -> reloadTransactionCount, fetch transaction count failed: \(error)") + debugPrint( + "WalletViewModel -> reloadTransactionCount, fetch transaction count failed: \(error)" + ) } } } - @objc private func transactionCountDidChanged() { + @objc + private func transactionCountDidChanged() { DispatchQueue.syncOnMain { self.transactionCount = LocalUserDefaults.shared.transactionCount } } - @objc private func willReset() { + @objc + private func willReset() { LocalUserDefaults.shared.transactionCount = 0 } - @objc private func didReset() { + @objc + private func didReset() { backupTipsShown = false } private func updateMoveAsset() { log.info("[Home] update move asset status") - showMoveAsset = EVMAccountManager.shared.accounts.count > 0 || ChildAccountManager.shared.childAccounts.count > 0 + showMoveAsset = EVMAccountManager.shared.accounts.count > 0 || !ChildAccountManager.shared + .childAccounts.isEmpty } - } // MARK: - Action @@ -404,7 +461,7 @@ extension WalletViewModel { } func stakingAction() { - if !LocalUserDefaults.shared.stakingGuideDisplayed && !StakingManager.shared.isStaked { + if !LocalUserDefaults.shared.stakingGuideDisplayed, !StakingManager.shared.isStaked { Router.route(to: RouteMap.Wallet.stakeGuide) return } @@ -421,9 +478,8 @@ extension WalletViewModel { } else { Router.route(to: RouteMap.Wallet.addToken) } - } - + func sideToggleAction() { NotificationCenter.default.post(name: .toggleSideMenu, object: nil) } @@ -440,14 +496,16 @@ extension WalletViewModel { extension WalletViewModel { func refreshButtonState() { - let canAddToken = ChildAccountManager.shared.selectedChildAccount == nil // && EVMAccountManager.shared.selectedAccount == nil + let canAddToken = ChildAccountManager.shared + .selectedChildAccount == nil // && EVMAccountManager.shared.selectedAccount == nil if canAddToken { showAddTokenButton = true } else { showAddTokenButton = false } - let isNotPrimary = ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared.selectedAccount != nil + let isNotPrimary = ChildAccountManager.shared + .selectedChildAccount != nil || EVMAccountManager.shared.selectedAccount != nil // Swap if (RemoteConfigManager.shared.config?.features.swap ?? false) == true { // don't show when current is Linked account @@ -474,7 +532,8 @@ extension WalletViewModel { showHorLayout = (showSwapButton == false && showStakeButton == false) // buy - if RemoteConfigManager.shared.config?.features.onRamp ?? false == true, flow.chainID == .mainnet { + if RemoteConfigManager.shared.config?.features.onRamp ?? false == true, + flow.chainID == .mainnet { if isNotPrimary { showBuyButton = false } else { diff --git a/FRW/Modules/Web3/Web3Provider.swift b/FRW/Modules/Web3/Web3Provider.swift index b143e786..d3295cf9 100644 --- a/FRW/Modules/Web3/Web3Provider.swift +++ b/FRW/Modules/Web3/Web3Provider.swift @@ -9,14 +9,17 @@ import BigInt import Foundation import web3swift -struct FlowProvider { +enum FlowProvider { struct Web3 { static func `default`() async throws -> web3swift.Web3? { let networkType = LocalUserDefaults.shared.flowNetwork guard let url = networkType.evmUrl else { return nil } - let provider = try await Web3HttpProvider(url: url, network: .Custom(networkID: BigUInt(networkType.networkID))) + let provider = try await Web3HttpProvider( + url: url, + network: .Custom(networkID: BigUInt(networkType.networkID)) + ) return web3swift.Web3(provider: provider) } diff --git a/FRW/Resource/MaterialMap/ImageName.swift b/FRW/Resource/MaterialMap/ImageName.swift index 1093e343..2509fca2 100644 --- a/FRW/Resource/MaterialMap/ImageName.swift +++ b/FRW/Resource/MaterialMap/ImageName.swift @@ -1,5 +1,5 @@ // -// ImageMap.swift +// ImageName.swift // Flow Wallet // // Created by cat on 2022/5/16. diff --git a/FRW/Resource/en.lproj/Localizable.strings b/FRW/Resource/en.lproj/Localizable.strings index f213b635..45cfde00 100644 --- a/FRW/Resource/en.lproj/Localizable.strings +++ b/FRW/Resource/en.lproj/Localizable.strings @@ -1043,3 +1043,7 @@ Please create backups for your wallet to ensure you retain access to your Flow a "Token Decimal" = "Token Decimal"; "Flow Identifier" = "Flow Identifier"; "Add Custom Token" = "Add Custom Token"; +"Seed Phrase Backup" = "Seed Phrase Backup"; +"Add Suggested Token" = "Add Suggested Token"; +"like_import_token" = "Would you like to import this token?"; +"invalid_erc20" = "Invalid ERC20 address"; diff --git a/FRW/SDK/WallectSecureEnclave.swift b/FRW/SDK/WallectSecureEnclave.swift index bd3b035c..087dfd74 100644 --- a/FRW/SDK/WallectSecureEnclave.swift +++ b/FRW/SDK/WallectSecureEnclave.swift @@ -1,5 +1,5 @@ // -// SecureEnclave.swift +// WallectSecureEnclave.swift // FRW // // Created by cat on 2023/11/6. @@ -8,11 +8,15 @@ import CryptoKit import Foundation +// MARK: - SignError + enum SignError: Error, LocalizedError { case unknown case privateKeyEmpty case emptySignature + // MARK: Internal + var errorDescription: String? { switch self { default: @@ -21,17 +25,23 @@ enum SignError: Error, LocalizedError { } } +// MARK: - WallectSecureEnclave + struct WallectSecureEnclave { - let key: WallectSecureEnclave.PrivateKey + // MARK: Lifecycle init(privateKey data: Data) { - key = PrivateKey(data: data) + self.key = PrivateKey(data: data) } init() { - key = PrivateKey() + self.key = PrivateKey() } + // MARK: Internal + + let key: WallectSecureEnclave.PrivateKey + func sign(data: Data) throws -> Data { guard let privateKey = key.privateKey else { throw SignError.privateKeyEmpty diff --git a/FRW/Services/Cache/CoinRateCache.swift b/FRW/Services/Cache/CoinRateCache.swift index 921905c8..283838b8 100644 --- a/FRW/Services/Cache/CoinRateCache.swift +++ b/FRW/Services/Cache/CoinRateCache.swift @@ -9,39 +9,33 @@ import Combine import Haneke import SwiftUI +// MARK: - CoinRateCache.CoinRateModel + extension CoinRateCache { struct CoinRateModel: Codable, Hashable { - static func == (lhs: CoinRateCache.CoinRateModel, rhs: CoinRateCache.CoinRateModel) -> Bool { - return lhs.symbol == rhs.symbol + let updateTime: TimeInterval + let symbol: String + let summary: CryptoSummaryResponse + + static func == ( + lhs: CoinRateCache.CoinRateModel, + rhs: CoinRateCache.CoinRateModel + ) -> Bool { + lhs.symbol == rhs.symbol } func hash(into hasher: inout Hasher) { hasher.combine(symbol) } - - let updateTime: TimeInterval - let symbol: String - let summary: CryptoSummaryResponse } } private let CacheUpdateInverval = TimeInterval(30) -class CoinRateCache { - static let cache = CoinRateCache() - - private var queue = DispatchQueue(label: "CoinRateCache.cache") - private var addPrices: [CryptoSummaryResponse.AddPrice] = [] - private var _summarys = Set() - var summarys: Set { - queue.sync { - _summarys - } - } - - private var isRefreshing = false +// MARK: - CoinRateCache - private var cancelSets = Set() +class CoinRateCache { + // MARK: Lifecycle init() { loadFromCache() @@ -65,15 +59,35 @@ class CoinRateCache { }.store(in: &cancelSets) } - @objc private func willReset() { + // MARK: Internal + + static let cache = CoinRateCache() + + var summarys: Set { queue.sync { - _summarys.removeAll() + _summarys } - saveToCache() } func getSummary(for symbol: String) -> CryptoSummaryResponse? { - return summarys.first { $0.symbol == symbol }?.summary + summarys.first { $0.symbol == symbol }?.summary + } + + // MARK: Private + + private var queue = DispatchQueue(label: "CoinRateCache.cache") + private var addPrices: [CryptoSummaryResponse.AddPrice] = [] + private var _summarys = Set() + private var isRefreshing = false + + private var cancelSets = Set() + + @objc + private func willReset() { + queue.sync { + _summarys.removeAll() + } + saveToCache() } } @@ -113,46 +127,51 @@ extension CoinRateCache { // flow token if EVMAccountManager.shared.selectedAccount == nil { await withTaskGroup(of: Void.self) { group in - supportedCoins.forEach { coin in + for coin in supportedCoins { group.addTask { [weak self] in do { try await self?.fetchCoinRate(coin) } catch { - debugPrint("CoinRateCache -> fetchCoinRate:\(coin.symbol ?? "") failed: \(error)") + debugPrint( + "CoinRateCache -> fetchCoinRate:\(coin.symbol ?? "") failed: \(error)" + ) } } } - } } // evm token if EVMAccountManager.shared.selectedAccount != nil { await withTaskGroup(of: Void.self) { group in - - supportedCoins.forEach { coin in + + for coin in supportedCoins { if coin.isFlowCoin { group.addTask { [weak self] in do { try await self?.fetchCoinRate(coin) } catch { - debugPrint("CoinRateCache -> fetchCoinRate:\(coin.symbol ?? "") failed: \(error)") + debugPrint( + "CoinRateCache -> fetchCoinRate:\(coin.symbol ?? "") failed: \(error)" + ) } } } } - + evmCoins?.forEach { coin in group.addTask { [weak self] in do { try await self?.fetchCoinRate(coin) } catch { - log.debug("CoinRateCache -> fetchCoinRate:\(coin.symbol ?? "") failed: \(error)") + log + .debug( + "CoinRateCache -> fetchCoinRate:\(coin.symbol ?? "") failed: \(error)" + ) } } } } } - isRefreshing = false log.debug("CoinRateCache -> end refreshing") @@ -180,10 +199,12 @@ extension CoinRateCache { case let .query(coinPair): let market = LocalUserDefaults.shared.market let request = CryptoSummaryRequest(provider: market.rawValue, pair: coinPair) - let response: CryptoSummaryResponse = try await Network.request(FRWAPI.Crypto.summary(request)) + let response: CryptoSummaryResponse = try await Network + .request(FRWAPI.Crypto.summary(request)) await set(summary: response, forSymbol: symbol) case let .mirror(token): - guard let mirrorTokenModel = WalletManager.shared.supportedCoins?.first(where: { $0.symbol == token.rawValue }) else { + guard let mirrorTokenModel = WalletManager.shared.supportedCoins? + .first(where: { $0.symbol == token.rawValue }) else { break } @@ -200,27 +221,38 @@ extension CoinRateCache { } } - private func createFixedRateResponse(fixedRate: Decimal, for token: TokenModel) -> CryptoSummaryResponse { + private func createFixedRateResponse( + fixedRate: Decimal, + for token: TokenModel + ) -> CryptoSummaryResponse { var model: CryptoSummaryResponse.AddPrice? if EVMAccountManager.shared.selectedAccount != nil { - model = addPrices.first { $0.evmAddress?.lowercased() == token.getAddress()?.lowercased() } + model = addPrices + .first { $0.evmAddress?.lowercased() == token.getAddress()?.lowercased() } } else { - model = addPrices.first { $0.contractName.uppercased() == token.contractName.uppercased() } + model = addPrices + .first { $0.contractName.uppercased() == token.contractName.uppercased() } } let change = CryptoSummaryResponse.Change(absolute: 0, percentage: 0) - let price = CryptoSummaryResponse.Price(last: model?.rateToUSD ?? fixedRate.doubleValue, - low: fixedRate.doubleValue, - high: fixedRate.doubleValue, - change: change) + let price = CryptoSummaryResponse.Price( + last: model?.rateToUSD ?? fixedRate.doubleValue, + low: fixedRate.doubleValue, + high: fixedRate.doubleValue, + change: change + ) let result = CryptoSummaryResponse.Result(price: price) return CryptoSummaryResponse(result: result) } @MainActor private func set(summary: CryptoSummaryResponse, forSymbol: String) { - let model = CoinRateModel(updateTime: Date().timeIntervalSince1970, symbol: forSymbol, summary: summary) + let model = CoinRateModel( + updateTime: Date().timeIntervalSince1970, + symbol: forSymbol, + summary: summary + ) _ = queue.sync { _summarys.update(with: model) } diff --git a/FRW/Services/Crypto/SymmetricEncryption.swift b/FRW/Services/Crypto/SymmetricEncryption.swift index 7c6b5c46..8255dabe 100644 --- a/FRW/Services/Crypto/SymmetricEncryption.swift +++ b/FRW/Services/Crypto/SymmetricEncryption.swift @@ -1,5 +1,5 @@ // -// BackupEncryption.swift +// SymmetricEncryption.swift // Dapper // // Created by Hao Fu on 7/10/2022. @@ -8,6 +8,8 @@ import CryptoKit import Foundation +// MARK: - SymmetricEncryption + protocol SymmetricEncryption { var key: SymmetricKey { get } var keySize: SymmetricKeySize { get } @@ -15,12 +17,29 @@ protocol SymmetricEncryption { func decrypt(combinedData: Data) throws -> Data } +// MARK: - EncryptionError + enum EncryptionError: Swift.Error { case encryptFailed case initFailed } +// MARK: - ChaChaPolyCipher + class ChaChaPolyCipher: SymmetricEncryption { + // MARK: Lifecycle + + init?(key: String) { + guard let keyData = key.data(using: .utf8) else { + return nil + } + let hashedKey = SHA256.hash(data: keyData) + let bitKey = Data(hashedKey.prefix(keySize.bitCount)) + self.key = SymmetricKey(data: bitKey) + } + + // MARK: Internal + var key: SymmetricKey var keySize: SymmetricKeySize = .bits256 @@ -34,6 +53,12 @@ class ChaChaPolyCipher: SymmetricEncryption { let decryptedData = try ChaChaPoly.open(sealedBox, using: key) return decryptedData } +} + +// MARK: - AESGCMCipher + +class AESGCMCipher: SymmetricEncryption { + // MARK: Lifecycle init?(key: String) { guard let keyData = key.data(using: .utf8) else { @@ -43,9 +68,9 @@ class ChaChaPolyCipher: SymmetricEncryption { let bitKey = Data(hashedKey.prefix(keySize.bitCount)) self.key = SymmetricKey(data: bitKey) } -} -class AESGCMCipher: SymmetricEncryption { + // MARK: Internal + var key: SymmetricKey var keySize: SymmetricKeySize = .bits256 @@ -62,13 +87,4 @@ class AESGCMCipher: SymmetricEncryption { let decryptedData = try AES.GCM.open(sealedBox, using: key) return decryptedData } - - init?(key: String) { - guard let keyData = key.data(using: .utf8) else { - return nil - } - let hashedKey = SHA256.hash(data: keyData) - let bitKey = Data(hashedKey.prefix(keySize.bitCount)) - self.key = SymmetricKey(data: bitKey) - } } diff --git a/FRW/Services/Firebase/FirebaseConfig.swift b/FRW/Services/Firebase/FirebaseConfig.swift index 6f366f3c..57cebfdc 100644 --- a/FRW/Services/Firebase/FirebaseConfig.swift +++ b/FRW/Services/Firebase/FirebaseConfig.swift @@ -1,5 +1,5 @@ // -// FlowCoins.swift +// FirebaseConfig.swift // Flow Wallet // // Created by cat on 2022/4/30. @@ -9,11 +9,15 @@ import FirebaseRemoteConfig import Foundation import Haneke +// MARK: - FirebaseConfigError + enum FirebaseConfigError: Error { case fetch case decode } +// MARK: - FirebaseConfig + enum FirebaseConfig: String { case all case flowCoins = "flow_coins" @@ -24,6 +28,8 @@ enum FirebaseConfig: String { case ENVConfig = "i_config" case news + // MARK: Internal + static func start() { Task { do { @@ -85,7 +91,7 @@ extension FirebaseConfig { setting.minimumFetchInterval = 3600 #if DEBUG - setting.minimumFetchInterval = 0 + setting.minimumFetchInterval = 0 #endif remoteConfig.configSettings = setting @@ -93,10 +99,14 @@ extension FirebaseConfig { remoteConfig.fetchAndActivate(completionHandler: { status, error in if status == .error { continuation.resume(throwing: FirebaseConfigError.fetch) - log.error("[Firebase] fetch Error: \(error?.localizedDescription ?? "No error available.")") + log + .error( + "[Firebase] fetch Error: \(error?.localizedDescription ?? "No error available.")" + ) } else { log.info("[Firebase] Config fetched!") - let configValues: RemoteConfigValue = remoteConfig.configValue(forKey: self.rawValue) + let configValues: RemoteConfigValue = remoteConfig + .configValue(forKey: self.rawValue) continuation.resume(returning: configValues) } }) diff --git a/FRW/Services/Firebase/FirebaseStorageUtils.swift b/FRW/Services/Firebase/FirebaseStorageUtils.swift index deca3fe4..d46ccc99 100644 --- a/FRW/Services/Firebase/FirebaseStorageUtils.swift +++ b/FRW/Services/Firebase/FirebaseStorageUtils.swift @@ -11,7 +11,7 @@ import FirebaseStorage import SwiftUI import UIKit -struct FirebaseStorageUtils { +enum FirebaseStorageUtils { static func upload(avatar: UIImage, removeQuery: Bool = true) async -> String? { await withCheckedContinuation { config in guard let username = UserManager.shared.userInfo?.username else { diff --git a/FRW/Services/FlowCoin/CadenceManager.swift b/FRW/Services/FlowCoin/CadenceManager.swift index 6ab8c336..16b19453 100644 --- a/FRW/Services/FlowCoin/CadenceManager.swift +++ b/FRW/Services/FlowCoin/CadenceManager.swift @@ -7,9 +7,21 @@ import SwiftUI +// MARK: - CadenceManager + class CadenceManager { + // MARK: Lifecycle + + private init() { + loadLocalCache() + fetchScript() + + log.info("[Cadence] current version is \(String(describing: version))") + } + + // MARK: Internal + static let shared = CadenceManager() - private let localVersion = "2.13" var version: String = "" var scripts: CadenceScript! @@ -25,12 +37,9 @@ class CadenceManager { } } - private init() { - loadLocalCache() - fetchScript() + // MARK: Private - log.info("[Cadence] current version is \(String(describing: version))") - } + private let localVersion = "2.13" private func loadLocalCache() { if let response = loadCache() { @@ -39,7 +48,8 @@ class CadenceManager { log.info("[Cadence] local cache version is \(String(describing: response.version))") } else { do { - guard let filePath = Bundle.main.path(forResource: "cloudfunctions", ofType: "json") else { + guard let filePath = Bundle.main + .path(forResource: "cloudfunctions", ofType: "json") else { log.error("CadenceManager -> loadFromLocalFile error: no local file") return } @@ -58,7 +68,8 @@ class CadenceManager { private func fetchScript() { Task { do { - let response: CadenceRemoteResponse = try await Network.requestWithRawModel(FRWAPI.Cadence.list) + let response: CadenceRemoteResponse = try await Network + .requestWithRawModel(FRWAPI.Cadence.list) DispatchQueue.main.async { // first call before self.saveCache(response: response.data) @@ -109,7 +120,8 @@ class CadenceManager { private func filePath() -> URL? { do { - let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("cadence") + let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! + .appendingPathComponent("cadence") if !FileManager.default.fileExists(atPath: root.relativePath) { try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) } @@ -122,21 +134,29 @@ class CadenceManager { } } +// MARK: - CadenceRemoteResponse + struct CadenceRemoteResponse: Codable { let data: CadenceResponse let status: Int } +// MARK: - CadenceResponse + struct CadenceResponse: Codable { let scripts: CadenceScript let version: String? } +// MARK: - CadenceScript + struct CadenceScript: Codable { let testnet: CadenceModel let mainnet: CadenceModel } +// MARK: - CadenceModel + struct CadenceModel: Codable { let version: String? let basic: CadenceModel.Basic? @@ -245,13 +265,13 @@ extension CadenceModel { let batchSendChildNFTToChild: String? /// send NFT from child to child let sendChildNFTToChild: String? - + let bridgeChildNFTToEvm: String? let bridgeChildNFTFromEvm: String? - + let batchBridgeChildNFTToEvm: String? let batchBridgeChildNFTFromEvm: String? - + let bridgeChildFTToEvm: String? let bridgeChildFTFromEvm: String? } @@ -368,19 +388,19 @@ extension CadenceModel { /// nft flow to any evm let bridgeNFTToEvmAddressV2: String? let bridgeNFTFromEvmToFlowV2: String? - + let getAssociatedEvmAddress: String? let getAssociatedFlowIdentifier: String? } } -public extension String { - func fromBase64() -> String? { +extension String { + public func fromBase64() -> String? { guard let data = Data(base64Encoded: self) else { return nil } return String(data: data, encoding: .utf8) } - func toFunc() -> String { + public func toFunc() -> String { guard let decodeStr = fromBase64() else { log.error("[Cadence] base decode failed") return "" @@ -391,7 +411,8 @@ public extension String { } private func platformInfo() -> String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + let version = Bundle.main + .infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" let buildVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" let model = isDevModel ? "(Dev)" : "" return "iOS-\(version)-\(buildVersion)\(model)" diff --git a/FRW/Services/FlowCoin/CadenceTemplate.swift b/FRW/Services/FlowCoin/CadenceTemplate.swift index 613d79df..f7d3ed49 100644 --- a/FRW/Services/FlowCoin/CadenceTemplate.swift +++ b/FRW/Services/FlowCoin/CadenceTemplate.swift @@ -1,9 +1,10 @@ // -// Cadence.swift +// CadenceTemplate.swift // Flow Wallet // // Created by Selina on 29/6/2022. // import Foundation + class CadenceTemplate {} diff --git a/FRW/Services/FlowCoin/FlowQuery.swift b/FRW/Services/FlowCoin/FlowQuery.swift index df2e6e83..c4bb4637 100644 --- a/FRW/Services/FlowCoin/FlowQuery.swift +++ b/FRW/Services/FlowCoin/FlowQuery.swift @@ -1,5 +1,5 @@ // -// LLCadence.swift +// FlowQuery.swift // Flow Wallet // // Created by cat on 2022/5/2. @@ -13,12 +13,16 @@ typealias TokenCadence = LLCadence typealias BalanceCadence = LLCadence typealias NFTCadence = LLCadence +// MARK: - LLCadenceAction + enum LLCadenceAction { enum token {} enum balance {} enum nft {} } +// MARK: - LLCadence + struct LLCadence {} // MARK: Check Token vault is enabled @@ -55,13 +59,17 @@ extension LLCadence { // MARK: NFT extension LLCadence where T == LLCadenceAction.nft { - static func collectionListCheckEnabled(with list: [NFTCollectionInfo], on _: Flow.ChainID) -> String { + static func collectionListCheckEnabled( + with list: [NFTCollectionInfo], + on _: Flow.ChainID + ) -> String { let tokenImports = list.map { $0.formatCadence(script: "import from ") }.joined(separator: "\r\n") let tokenFunctions = list.map { - $0.formatCadence(script: + $0.formatCadence( + script: """ pub fun checkVault(address: Address) : Bool { let account = getAccount(address) @@ -75,7 +83,8 @@ extension LLCadence where T == LLCadenceAction.nft { }.joined(separator: "\r\n") let tokenCalls = list.map { - $0.formatCadence(script: + $0.formatCadence( + script: """ checkVault(address: address) """ @@ -135,10 +144,16 @@ extension NFTCollectionInfo { if let path = path { newScript = newScript .replacingOccurrences(of: "", with: path.storagePath) - .replacingOccurrences(of: "", with: path.publicCollectionName ?? "") + .replacingOccurrences( + of: "", + with: path.publicCollectionName ?? "" + ) .replacingOccurrences(of: "", with: path.publicPath) .replacingOccurrences(of: "", with: path.storagePath) - .replacingOccurrences(of: "", with: path.publicCollectionName ?? "") + .replacingOccurrences( + of: "", + with: path.publicCollectionName ?? "" + ) .replacingOccurrences(of: "", with: path.publicPath) .replacingOccurrences(of: "", with: path.publicType ?? "") .replacingOccurrences(of: "", with: path.privateType ?? "") diff --git a/FRW/Services/Manager/Backup/Target/BackupGDTarget.swift b/FRW/Services/Manager/Backup/Target/BackupGDTarget.swift index 509ff612..a44642c8 100644 --- a/FRW/Services/Manager/Backup/Target/BackupGDTarget.swift +++ b/FRW/Services/Manager/Backup/Target/BackupGDTarget.swift @@ -12,13 +12,10 @@ import GTMSessionFetcherCore import SwiftUI import UIKit -class BackupGDTarget: BackupTarget { - private var clientID = "" - private lazy var config: GIDConfiguration = { - GIDConfiguration(clientID: clientID) - }() +// MARK: - BackupGDTarget - private var api: GoogleDriveAPI? +class BackupGDTarget: BackupTarget { + // MARK: Lifecycle init() { guard let filePath = Bundle.main.path(forResource: "GoogleOAuth2", ofType: "plist") else { @@ -31,8 +28,10 @@ class BackupGDTarget: BackupTarget { tryToRestoreLogin() } + // MARK: Internal + var isPrepared: Bool { - return api != nil + api != nil } func relogin() async throws { @@ -42,6 +41,13 @@ class BackupGDTarget: BackupTarget { createGoogleDriveService(user: user) } + // MARK: Private + + private var clientID = "" + private lazy var config: GIDConfiguration = .init(clientID: clientID) + + private var api: GoogleDriveAPI? + private func tryToRestoreLogin() { if !GIDSignIn.sharedInstance.hasPreviousSignIn() { return @@ -108,7 +114,7 @@ extension BackupGDTarget { } private func googleRestoreLogin() async throws -> GIDGoogleUser { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { continuation in DispatchQueue.main.async { GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in guard let signInUser = user else { @@ -123,17 +129,19 @@ extension BackupGDTarget { } private func googleUserLogin() async throws -> GIDGoogleUser { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { continuation in DispatchQueue.main.async { let topVC = Router.topPresentedController() - GIDSignIn.sharedInstance.signIn(with: self.config, presenting: topVC) { user, error in - guard let signInUser = user else { - continuation.resume(throwing: error ?? GoogleBackupError.missingLoginUser) - return + GIDSignIn.sharedInstance + .signIn(with: self.config, presenting: topVC) { user, error in + guard let signInUser = user else { + continuation + .resume(throwing: error ?? GoogleBackupError.missingLoginUser) + return + } + + continuation.resume(returning: signInUser) } - - continuation.resume(returning: signInUser) - } } } } @@ -160,19 +168,22 @@ extension BackupGDTarget { return try await withCheckedThrowingContinuation { continuation in DispatchQueue.main.async { - GIDSignIn.sharedInstance.addScopes([driveScope], presenting: topVC) { grantedUser, error in - guard let grantedUser = grantedUser else { - continuation.resume(throwing: error ?? GoogleBackupError.missingLoginUser) - return - } - - guard let scopes = grantedUser.grantedScopes, scopes.contains(driveScope) else { - continuation.resume(throwing: GoogleBackupError.noDriveScope) - return + GIDSignIn.sharedInstance + .addScopes([driveScope], presenting: topVC) { grantedUser, error in + guard let grantedUser = grantedUser else { + continuation + .resume(throwing: error ?? GoogleBackupError.missingLoginUser) + return + } + + guard let scopes = grantedUser.grantedScopes, + scopes.contains(driveScope) else { + continuation.resume(throwing: GoogleBackupError.noDriveScope) + return + } + + continuation.resume(returning: grantedUser) } - - continuation.resume(returning: grantedUser) - } } } } diff --git a/FRW/Services/Manager/ChildAccountAccessibleManager.swift b/FRW/Services/Manager/ChildAccountAccessibleManager.swift index b8bf2f1f..a145cb0e 100644 --- a/FRW/Services/Manager/ChildAccountAccessibleManager.swift +++ b/FRW/Services/Manager/ChildAccountAccessibleManager.swift @@ -9,13 +9,7 @@ import Foundation extension ChildAccountManager { struct AccessibleManager { - private var parentAddr: String? { - return WalletManager.shared.getPrimaryWalletAddress() - } - - private var childAddr: String? { - return WalletManager.shared.childAccount?.addr - } + // MARK: Internal var coins: [FlowModel.TokenInfo]? var collections: [String]? @@ -33,7 +27,10 @@ extension ChildAccountManager { collections = [] return } - collections = try await FlowNetwork.fetchAccessibleCollection(parent: parentAddr, child: childAddr) + collections = try await FlowNetwork.fetchAccessibleCollection( + parent: parentAddr, + child: childAddr + ) } func isChildAccount() -> Bool { @@ -69,10 +66,9 @@ extension ChildAccountManager { let result = collections?.filter { idStr in let list = idStr.split(separator: ".") if let contractName = list[safe: 2], - let address = list[safe: 1] - { + let address = list[safe: 1] { if let name = model.contractName, let modelAddress = model.address { - return (contractName == name && modelAddress.hasSuffix(address)) + return contractName == name && modelAddress.hasSuffix(address) } return false } else { @@ -82,20 +78,6 @@ extension ChildAccountManager { return result?.count == 1 } - private func isAccessible(contractName: String, address: String) -> Bool { - let result = collections?.filter { idStr in - let list = idStr.split(separator: ".") - if let contractName = list[safe: 2], - let addr = list[safe: 1] - { - return contractName == contractName && address.hasSuffix(addr) - } else { - return false - } - } - return result?.count == 1 - } - // check nft func isAccessible(_ model: NFTModel) -> Bool { guard isChildAccount() else { @@ -106,11 +88,35 @@ extension ChildAccountManager { return isAccessible(collection) } - if let address = model.response.contractAddress, let name = model.response.collectionContractName { + if let address = model.response.contractAddress, + let name = model.response.collectionContractName { return isAccessible(contractName: name, address: address) } return false } + + // MARK: Private + + private var parentAddr: String? { + WalletManager.shared.getPrimaryWalletAddress() + } + + private var childAddr: String? { + WalletManager.shared.childAccount?.addr + } + + private func isAccessible(contractName _: String, address: String) -> Bool { + let result = collections?.filter { idStr in + let list = idStr.split(separator: ".") + if let contractName = list[safe: 2], + let addr = list[safe: 1] { + return contractName == contractName && address.hasSuffix(addr) + } else { + return false + } + } + return result?.count == 1 + } } } diff --git a/FRW/Services/Manager/ChildAccountManager.swift b/FRW/Services/Manager/ChildAccountManager.swift index 68fdf489..7c6bba92 100644 --- a/FRW/Services/Manager/ChildAccountManager.swift +++ b/FRW/Services/Manager/ChildAccountManager.swift @@ -8,9 +8,31 @@ import Combine import SwiftUI +// MARK: - ChildAccount + struct ChildAccount: Codable { + // MARK: Lifecycle + + init(address: String, name: String?, desc: String?, icon: String?, pinTime: TimeInterval) { + self.addr = address + self.name = name + self.description = desc + self.thumbnail = Thumbnail(url: icon) + self.time = pinTime + } + + // MARK: Internal + + struct Thumbnail: Codable { + let url: String? + } + var addr: String? let name: String? + let description: String? + let thumbnail: Thumbnail? + var time: TimeInterval? + var aName: String { if let n = name?.trim, !n.isEmpty { return n @@ -18,8 +40,6 @@ struct ChildAccount: Codable { return "Linked Account" } - let description: String? - let thumbnail: Thumbnail? var icon: String { if let t = thumbnail?.url, !t.isEmpty { return t @@ -28,29 +48,17 @@ struct ChildAccount: Codable { return AppPlaceholder.image } - var time: TimeInterval? var pinTime: TimeInterval { time ?? 0 } var isPinned: Bool { - return pinTime > 0 - } - - struct Thumbnail: Codable { - let url: String? - } - - init(address: String, name: String?, desc: String?, icon: String?, pinTime: TimeInterval) { - addr = address - self.name = name - description = desc - thumbnail = Thumbnail(url: icon) - time = pinTime + pinTime > 0 } var isSelected: Bool { - if let selectedChildAccount = ChildAccountManager.shared.selectedChildAccount, selectedChildAccount.addr == addr, let addr = addr, !addr.isEmpty { + if let selectedChildAccount = ChildAccountManager.shared.selectedChildAccount, + selectedChildAccount.addr == addr, let addr = addr, !addr.isEmpty { return true } @@ -58,9 +66,11 @@ struct ChildAccount: Codable { } } +// MARK: ChildAccountSideCellItem + extension ChildAccount: ChildAccountSideCellItem { var showAddress: String { - return addr ?? "" + addr ?? "" } var showIcon: String { @@ -76,29 +86,10 @@ extension ChildAccount: ChildAccountSideCellItem { } } -class ChildAccountManager: ObservableObject { - static let shared = ChildAccountManager() - - @Published var isLoading: Bool = false - - @Published var childAccounts: [ChildAccount] = [] { - didSet { - validSelectedChildAccount() - } - } +// MARK: - ChildAccountManager - @Published var selectedChildAccount: ChildAccount? = LocalUserDefaults.shared.selectedChildAccount { - didSet { - LocalUserDefaults.shared.selectedChildAccount = selectedChildAccount - } - } - - var sortedChildAccounts: [ChildAccount] { - return childAccounts.sorted { $0.pinTime > $1.pinTime } - } - - private var cacheLoaded = false - private var cancelSets = Set() +class ChildAccountManager: ObservableObject { + // MARK: Lifecycle private init() { UserManager.shared.$activatedUID @@ -131,7 +122,12 @@ class ChildAccountManager: ObservableObject { self.clean() }.store(in: &cancelSets) - NotificationCenter.default.addObserver(self, selector: #selector(willReset), name: .willResetWallet, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(willReset), + name: .willResetWallet, + object: nil + ) NotificationCenter.default.publisher(for: .transactionStatusDidChanged) .receive(on: DispatchQueue.main) @@ -141,44 +137,35 @@ class ChildAccountManager: ObservableObject { }.store(in: &cancelSets) } - @objc private func onTransactionStatusChanged(_ noti: Notification) { - guard let obj = noti.object as? TransactionManager.TransactionHolder, obj.type == .editChildAccount else { - return - } + // MARK: Internal - switch obj.internalStatus { - case .success: - refresh() - default: - break - } - } + static let shared = ChildAccountManager() - @objc private func willReset() { - childAccounts = [] - } + @Published + var isLoading: Bool = false - private func loadCache() { - if cacheLoaded { - return + @Published + var childAccounts: [ChildAccount] = [] { + didSet { + validSelectedChildAccount() } - cacheLoaded = true + } - guard let uid = UserManager.shared.activatedUID, let address = WalletManager.shared.getPrimaryWalletAddress() else { - log.warning("uid or address is nil") - return + @Published + var selectedChildAccount: ChildAccount? = LocalUserDefaults.shared + .selectedChildAccount { + didSet { + LocalUserDefaults.shared.selectedChildAccount = selectedChildAccount } - - childAccounts = MultiAccountStorage.shared.getChildAccounts(uid: uid, address: address) ?? [] } - private func clean() { - log.debug("cleaned") - childAccounts = [] + var sortedChildAccounts: [ChildAccount] { + childAccounts.sorted { $0.pinTime > $1.pinTime } } func refresh() { - guard let uid = UserManager.shared.activatedUID, let address = WalletManager.shared.getPrimaryWalletAddress() else { + guard let uid = UserManager.shared.activatedUID, + let address = WalletManager.shared.getPrimaryWalletAddress() else { log.warning("uid or address is nil") clean() return @@ -190,7 +177,7 @@ class ChildAccountManager: ObservableObject { DispatchQueue.main.async { self.isLoading = true } - + Task { do { let list = try await FlowNetwork.queryChildAccountMeta(address) @@ -199,10 +186,19 @@ class ChildAccountManager: ObservableObject { if UserManager.shared.activatedUID != uid { return } if LocalUserDefaults.shared.flowNetwork != network { return } - let oldList = MultiAccountStorage.shared.getChildAccounts(uid: uid, address: address) ?? [] + let oldList = MultiAccountStorage.shared.getChildAccounts( + uid: uid, + address: address + ) ?? [] let finalList = list.map { newAccount in if let oldAccount = oldList.first(where: { $0.addr == newAccount.addr }) { - return ChildAccount(address: newAccount.addr ?? "", name: newAccount.name, desc: newAccount.description, icon: newAccount.icon, pinTime: oldAccount.pinTime) + return ChildAccount( + address: newAccount.addr ?? "", + name: newAccount.name, + desc: newAccount.description, + icon: newAccount.icon, + pinTime: oldAccount.pinTime + ) } else { return newAccount } @@ -222,9 +218,59 @@ class ChildAccountManager: ObservableObject { } } + // MARK: Private + + private var cacheLoaded = false + private var cancelSets = Set() + + @objc + private func onTransactionStatusChanged(_ noti: Notification) { + guard let obj = noti.object as? TransactionManager.TransactionHolder, + obj.type == .editChildAccount else { + return + } + + switch obj.internalStatus { + case .success: + refresh() + default: + break + } + } + + @objc + private func willReset() { + childAccounts = [] + } + + private func loadCache() { + if cacheLoaded { + return + } + cacheLoaded = true + + guard let uid = UserManager.shared.activatedUID, + let address = WalletManager.shared.getPrimaryWalletAddress() else { + log.warning("uid or address is nil") + return + } + + childAccounts = MultiAccountStorage.shared + .getChildAccounts(uid: uid, address: address) ?? [] + } + + private func clean() { + log.debug("cleaned") + childAccounts = [] + } + private func saveToCache(_ childAccounts: [ChildAccount], uid: String, address: String) { do { - try MultiAccountStorage.shared.saveChildAccounts(childAccounts, uid: uid, address: address) + try MultiAccountStorage.shared.saveChildAccounts( + childAccounts, + uid: uid, + address: address + ) } catch { log.error("save to cache failed", context: error) } @@ -251,12 +297,19 @@ extension ChildAccountManager { oldList.removeAll(where: { $0.addr == childAccount.addr }) - let newChildAccount = ChildAccount(address: oldChildAccount.addr ?? "", name: oldChildAccount.name, desc: oldChildAccount.description, icon: oldChildAccount.icon, pinTime: oldChildAccount.isPinned ? 0 : Date().timeIntervalSince1970) + let newChildAccount = ChildAccount( + address: oldChildAccount.addr ?? "", + name: oldChildAccount.name, + desc: oldChildAccount.description, + icon: oldChildAccount.icon, + pinTime: oldChildAccount.isPinned ? 0 : Date().timeIntervalSince1970 + ) oldList.append(newChildAccount) childAccounts = oldList - guard let uid = UserManager.shared.activatedUID, let address = WalletManager.shared.getPrimaryWalletAddress() else { + guard let uid = UserManager.shared.activatedUID, + let address = WalletManager.shared.getPrimaryWalletAddress() else { log.error("uid or address is nil") return } @@ -269,7 +322,8 @@ extension ChildAccountManager { oldList.removeAll(where: { $0.addr == childAccount.addr }) childAccounts = oldList - guard let uid = UserManager.shared.activatedUID, let address = WalletManager.shared.getPrimaryWalletAddress() else { + guard let uid = UserManager.shared.activatedUID, + let address = WalletManager.shared.getPrimaryWalletAddress() else { log.error("uid or address is nil") return } diff --git a/FRW/Services/Manager/Config/RemoteConfig.swift b/FRW/Services/Manager/Config/RemoteConfig.swift index 2ea13a4c..4fd8cd48 100644 --- a/FRW/Services/Manager/Config/RemoteConfig.swift +++ b/FRW/Services/Manager/Config/RemoteConfig.swift @@ -16,17 +16,17 @@ extension RemoteConfigManager { } struct ENVConfig: Codable { - let version: String - let versionProd: String - let prod: Config - let staging: Config - enum CodingKeys: String, CodingKey { case version case prod case staging case versionProd = "version_prod" } + + let version: String + let versionProd: String + let prod: Config + let staging: Config } struct Config: Codable { @@ -37,15 +37,6 @@ extension RemoteConfigManager { // MARK: - Features struct Features: Codable { - let freeGas: Bool - let walletConnect: Bool - let onRamp: Bool? - let appList: Bool? - let swap: Bool? - let browser: Bool? - let nftTransfer: Bool? - let hideBrowser: Bool? - enum CodingKeys: String, CodingKey { case freeGas = "free_gas" case walletConnect = "wallet_connect" @@ -56,6 +47,15 @@ extension RemoteConfigManager { case nftTransfer = "nft_transfer" case hideBrowser = "hide_browser" } + + let freeGas: Bool + let walletConnect: Bool + let onRamp: Bool? + let appList: Bool? + let swap: Bool? + let browser: Bool? + let nftTransfer: Bool? + let hideBrowser: Bool? } // MARK: - Payer @@ -70,13 +70,13 @@ extension RemoteConfigManager { // MARK: - Net struct PayerInfo: Codable { - let address: String - let keyID: Int - enum CodingKeys: String, CodingKey { case address case keyID = "keyId" } + + let address: String + let keyID: Int } // MARK: - News @@ -87,6 +87,8 @@ extension RemoteConfigManager { case undefined + // MARK: Lifecycle + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try? container.decode(String.self) @@ -100,6 +102,25 @@ extension RemoteConfigManager { case high case urgent + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try? container.decode(String.self) + self = NewsPriority(rawValue: rawValue ?? "") ?? .low + } + + // MARK: Internal + + static func < ( + lhs: RemoteConfigManager.NewsPriority, + rhs: RemoteConfigManager.NewsPriority + ) -> Bool { + lhs.level < rhs.level + } + + // MARK: Private + private var level: Int { switch self { case .low: @@ -112,16 +133,6 @@ extension RemoteConfigManager { return 1000 } } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try? container.decode(String.self) - self = NewsPriority(rawValue: rawValue ?? "") ?? .low - } - - static func < (lhs: RemoteConfigManager.NewsPriority, rhs: RemoteConfigManager.NewsPriority) -> Bool { - return lhs.level < rhs.level - } } enum NewDisplayType: String, Codable { @@ -129,6 +140,8 @@ extension RemoteConfigManager { case click // 用户点击,或者关闭后,不再显示 case expiry // 一直显示直到过期,用户关闭后,下次启动再显示 + // MARK: Lifecycle + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try? container.decode(String.self) @@ -140,16 +153,18 @@ extension RemoteConfigManager { case normal case walletconnect case upgrade - + + // MARK: Lifecycle + init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try? container.decode(String.self) self = NewsFlag(rawValue: rawValue ?? "") ?? .normal } } - - struct Condition: Codable,Hashable { - let type: ConditionType + + struct Condition: Codable, Hashable { + let type: ConditionType // let data: JsonObject? // can be ignored this time } @@ -157,28 +172,32 @@ extension RemoteConfigManager { case unknow case canUpgrade case isIOS - case isAndroid - case isWeb - case cadence // can be ignored this time - case noBackup // can be ignored this time - case noBiometric // can be ignored this time - + case isAndroid + case isWeb + case cadence // can be ignored this time + case noBackup // can be ignored this time + case noBiometric // can be ignored this time + + // MARK: Lifecycle + init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try? container.decode(String.self) self = ConditionType(rawValue: rawValue ?? "") ?? .unknow } - + + // MARK: Internal + func boolValue() -> Bool { - switch self { case .unknow: return false case .canUpgrade: if let remoteVersion = RemoteConfigManager.shared.remoteVersion, - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { - return remoteVersion.compareVersion(to: currentVersion) != .orderedAscending - }else { + let currentVersion = Bundle.main + .infoDictionary?["CFBundleShortVersionString"] as? String { + return remoteVersion.compareVersion(to: currentVersion) == .orderedDescending + } else { return false } case .isIOS: @@ -190,8 +209,6 @@ extension RemoteConfigManager { } struct News: Codable, Comparable, Identifiable, Hashable { - - let id: String let priority: NewsPriority let type: NewsType @@ -220,7 +237,7 @@ extension RemoteConfigManager { static func < (lhs: RemoteConfigManager.News, rhs: RemoteConfigManager.News) -> Bool { lhs.priority < rhs.priority } - + static func == (lhs: RemoteConfigManager.News, rhs: RemoteConfigManager.News) -> Bool { lhs.id == rhs.id } diff --git a/FRW/Services/Manager/Config/RemoteConfigManager.swift b/FRW/Services/Manager/Config/RemoteConfigManager.swift index 110b12ac..39707df9 100644 --- a/FRW/Services/Manager/Config/RemoteConfigManager.swift +++ b/FRW/Services/Manager/Config/RemoteConfigManager.swift @@ -1,5 +1,5 @@ // -// GasManager.swift +// RemoteConfigManager.swift // Flow Wallet // // Created by Selina on 5/9/2022. @@ -11,9 +11,29 @@ import SwiftUI import UIKit import WalletCore +// MARK: - RemoteConfigManager + class RemoteConfigManager { + // MARK: Lifecycle + + init() { + do { + try loadLocalConfig() + } catch { + log.error("[Firebase] load local file config failed:\(error)") + } + updateFromRemote() + } + + // MARK: Internal + static let shared = RemoteConfigManager() + var config: Config? + var contractAddress: ContractAddress? + + var isFailed: Bool = false + var emptyAddress: String { switch LocalUserDefaults.shared.flowNetwork.toFlowType() { case .mainnet: @@ -27,12 +47,6 @@ class RemoteConfigManager { } } - private var envConfig: ENVConfig? - var config: Config? - var contractAddress: ContractAddress? - - var isFailed: Bool = false - var freeGasEnabled: Bool { if !remoteGreeGas { return false @@ -40,7 +54,6 @@ class RemoteConfigManager { return localGreeGas } - @AppStorage(LocalUserDefaults.Keys.freeGas.rawValue) private var localGreeGas = true var remoteGreeGas: Bool { if let config = config { return config.features.freeGas @@ -95,31 +108,26 @@ class RemoteConfigManager { } } - init() { - do { - try loadLocalConfig() - } catch { - log.error("[Firebase] load local file config failed:\(error)") - } - updateFromRemote() - } - func updateFromRemote() { fetchNews() do { let data: String = try FirebaseConfig.ENVConfig.fetch() let key = LocalEnvManager.shared.backupAESKey if let keyData = key.data(using: .utf8), - let ivData = key.sha256().prefix(16).data(using: .utf8) - { - let decodeData = AES.decryptCBC(key: keyData, data: Data(hex: data), iv: ivData, mode: .pkcs7)! + let ivData = key.sha256().prefix(16).data(using: .utf8) { + let decodeData = AES.decryptCBC( + key: keyData, + data: Data(hex: data), + iv: ivData, + mode: .pkcs7 + )! let decoder = JSONDecoder() let config = try decoder.decode(ENVConfig.self, from: decodeData) envConfig = config self.config = nil - if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, - let version = envConfig?.version - { + if let currentVersion = Bundle.main + .infoDictionary?["CFBundleShortVersionString"] as? String, + let version = envConfig?.version { if version.compareVersion(to: currentVersion) == .orderedDescending { self.config = envConfig?.prod } @@ -149,7 +157,8 @@ class RemoteConfigManager { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .convertFromSnakeCase - let list: [RemoteConfigManager.News] = try FirebaseConfig.news.fetch(decoder: decoder) + let list: [RemoteConfigManager.News] = try FirebaseConfig.news + .fetch(decoder: decoder) DispatchQueue.main.async { WalletNewsHandler.shared.addRemoteNews(list) } @@ -159,14 +168,24 @@ class RemoteConfigManager { } } + // MARK: Private + + private var envConfig: ENVConfig? + @AppStorage(LocalUserDefaults.Keys.freeGas.rawValue) + private var localGreeGas = true + private func loadLocalConfig() throws { do { let data: String = try FirebaseConfig.ENVConfig.fetch() let key = LocalEnvManager.shared.backupAESKey if let keyData = key.data(using: .utf8), - let ivData = key.sha256().prefix(16).data(using: .utf8) - { - let decodeData = AES.decryptCBC(key: keyData, data: Data(hex: data), iv: ivData, mode: .pkcs7)! + let ivData = key.sha256().prefix(16).data(using: .utf8) { + let decodeData = AES.decryptCBC( + key: keyData, + data: Data(hex: data), + iv: ivData, + mode: .pkcs7 + )! let config = try? JSONDecoder().decode(ENVConfig.self, from: decodeData) self.config = config?.staging } @@ -178,6 +197,8 @@ class RemoteConfigManager { } } +// MARK: FlowSigner + extension RemoteConfigManager: FlowSigner { var address: Flow.Address { .init(hex: payer) @@ -196,20 +217,28 @@ extension RemoteConfigManager: FlowSigner { } func sign(transaction: Flow.Transaction, signableData: Data) async throws -> Data { - let request = SignPayerRequest(transaction: transaction.voucher, message: .init(envelopeMessage: signableData.hexValue)) - let signature: SignPayerResponse = try await Network.requestWithRawModel(FirebaseAPI.signAsPayer(request)) + let request = SignPayerRequest( + transaction: transaction.voucher, + message: .init(envelopeMessage: signableData.hexValue) + ) + let signature: SignPayerResponse = try await Network + .requestWithRawModel(FirebaseAPI.signAsPayer(request)) return Data(hex: signature.envelopeSigs.sig) } func sign(voucher: FCLVoucher, signableData: Data) async throws -> Data { - let request = SignPayerRequest(transaction: voucher, message: .init(envelopeMessage: signableData.hexValue)) - let signature: SignPayerResponse = try await Network.requestWithRawModel(FirebaseAPI.signAsPayer(request)) + let request = SignPayerRequest( + transaction: voucher, + message: .init(envelopeMessage: signableData.hexValue) + ) + let signature: SignPayerResponse = try await Network + .requestWithRawModel(FirebaseAPI.signAsPayer(request)) return Data(hex: signature.envelopeSigs.sig) } } extension RemoteConfigManager { var remoteVersion: String? { - return envConfig?.versionProd + envConfig?.versionProd } } diff --git a/FRW/Services/Manager/DBManager.swift b/FRW/Services/Manager/DBManager.swift index a844c2a1..6d9ee87c 100644 --- a/FRW/Services/Manager/DBManager.swift +++ b/FRW/Services/Manager/DBManager.swift @@ -9,16 +9,16 @@ import Combine import FMDB import Foundation +// MARK: - DBTable + private enum DBTable: String { case webBookmark = "web_bookmark" } -class DBManager { - static let shared = DBManager() - - private var db: FMDatabase? +// MARK: - DBManager - private var cancelSets = Set() +class DBManager { + // MARK: Lifecycle init() { prepare() @@ -33,15 +33,31 @@ class DBManager { self.prepare() }.store(in: &cancelSets) - NotificationCenter.default.addObserver(self, selector: #selector(willReset), name: .willResetWallet, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(willReset), + name: .willResetWallet, + object: nil + ) } + // MARK: Internal + + static let shared = DBManager() + + // MARK: Private + + private var db: FMDatabase? + + private var cancelSets = Set() + private func closeDB() { db?.close() db = nil } - @objc private func willReset() { + @objc + private func willReset() { closeDB() } } @@ -51,7 +67,8 @@ class DBManager { extension DBManager { var dbURL: URL { let uid = UserManager.shared.activatedUID ?? "0" - return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("app_database/\(uid)/database.db") + return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) + .first!.appendingPathComponent("app_database/\(uid)/database.db") } private func prepare() { @@ -106,7 +123,10 @@ extension DBManager { """ try db.executeUpdate(sql, values: nil) - try db.executeUpdate("CREATE INDEX web_bookmark_index ON \(DBTable.webBookmark.rawValue) (id,url)", values: nil) + try db.executeUpdate( + "CREATE INDEX web_bookmark_index ON \(DBTable.webBookmark.rawValue) (id,url)", + values: nil + ) } } catch { debugPrint("DBManager: table create failed: \(error)") @@ -118,12 +138,16 @@ extension DBManager { extension DBManager { func save(webBookmark bookmark: WebBookmark) { - insert(into: .webBookmark, columns: ["url", "title", "is_fav", "create_time", "update_time"], values: bookmark.dbValues) + insert( + into: .webBookmark, + columns: ["url", "title", "is_fav", "create_time", "update_time"], + values: bookmark.dbValues + ) NotificationCenter.default.post(name: .webBookmarkDidChanged) } func webBookmarkCount() -> Int { - return count(in: .webBookmark) + count(in: .webBookmark) } func webBookmarkIsExist(url: String) -> Bool { @@ -172,7 +196,14 @@ extension DBManager { extension DBManager { /// 查询 - private func query(_ column: String = "*", from: DBTable, where: String? = nil, limit: Int? = nil, orderBy: String? = nil, values: [Any]? = nil) -> FMResultSet? { + private func query( + _ column: String = "*", + from: DBTable, + where: String? = nil, + limit: Int? = nil, + orderBy: String? = nil, + values: [Any]? = nil + ) -> FMResultSet? { guard let db = db else { debugPrint("execute query but db is nil") return nil @@ -212,7 +243,7 @@ extension DBManager { let columnString = columns.joined(separator: ",") var valuesArray = [String]() - columns.forEach { _ in + for _ in columns { valuesArray.append("?") } let valuesString = valuesArray.joined(separator: ",") @@ -231,7 +262,12 @@ extension DBManager { /// 更新 @discardableResult - private func update(in table: DBTable, set: String, where: String? = nil, values: [Any]) -> Bool { + private func update( + in table: DBTable, + set: String, + where: String? = nil, + values: [Any] + ) -> Bool { guard let db = db else { debugPrint("execute update but db is nil") return false diff --git a/FRW/Services/Manager/LocalUserDefaults.swift b/FRW/Services/Manager/LocalUserDefaults.swift index b9ce8976..e16284d6 100644 --- a/FRW/Services/Manager/LocalUserDefaults.swift +++ b/FRW/Services/Manager/LocalUserDefaults.swift @@ -1,5 +1,5 @@ // -// LocalUserDefaultsManager.swift +// LocalUserDefaults.swift // Flow Wallet // // Created by Selina on 7/6/2022. @@ -49,7 +49,7 @@ extension LocalUserDefaults { case whatIsBack case backupSheetNotAsk case checkCoa - + case customToken } @@ -58,6 +58,23 @@ extension LocalUserDefaults { case mainnet case previewnet + // MARK: Lifecycle + + init?(chainId: Flow.ChainID) { + switch chainId { + case .testnet: + self = .testnet + case .mainnet: + self = .mainnet + case .previewnet: + self = .previewnet + default: + return nil + } + } + + // MARK: Internal + var color: Color { switch self { case .mainnet: @@ -83,19 +100,6 @@ extension LocalUserDefaults { return Flow.ChainID.previewnet } } - - init?(chainId: Flow.ChainID) { - switch chainId { - case .testnet: - self = .testnet - case .mainnet: - self = .mainnet - case .previewnet: - self = .previewnet - default: - return nil - } - } } } @@ -105,20 +109,76 @@ extension Flow.ChainID { } } +// MARK: - LocalUserDefaults + class LocalUserDefaults: ObservableObject { - static let shared = LocalUserDefaults() + // MARK: Lifecycle init() { - NotificationCenter.default.addObserver(self, selector: #selector(willReset), name: .willResetWallet, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(willReset), + name: .willResetWallet, + object: nil + ) } + // MARK: Internal + + static let shared = LocalUserDefaults() + #if DEBUG - @AppStorage(Keys.flowNetwork.rawValue) var flowNetwork: FlowNetworkType = .testnet + @AppStorage(Keys.flowNetwork.rawValue) + var flowNetwork: FlowNetworkType = .testnet #else - @AppStorage(Keys.flowNetwork.rawValue) var flowNetwork: FlowNetworkType = .mainnet + @AppStorage(Keys.flowNetwork.rawValue) + var flowNetwork: FlowNetworkType = .mainnet #endif - @AppStorage(Keys.activatedUID.rawValue) var activatedUID: String? + @AppStorage(Keys.activatedUID.rawValue) + var activatedUID: String? + + @AppStorage(Keys.recentSendByToken.rawValue) + var recentToken: String? + + @AppStorage(Keys.legacyBackupType.rawValue) + var legacyBackupType: BackupManager + .BackupType = .none + + @AppStorage(Keys.securityType.rawValue) + var securityType: SecurityManager.SecurityType = .none + @AppStorage(Keys.lockOnExit.rawValue) + var lockOnExit: Bool = false + + @AppStorage(Keys.tryToRestoreAccountFlag.rawValue) + var tryToRestoreAccountFlag: Bool = false + + @AppStorage(Keys.currentCurrency.rawValue) + var currentCurrency: Currency = .USD + @AppStorage(Keys.currentCurrencyRate.rawValue) + var currentCurrencyRate: Double = 1 + + @AppStorage(Keys.stakingGuideDisplayed.rawValue) + var stakingGuideDisplayed: Bool = false + + @AppStorage(Keys.onBoardingShown.rawValue) + var onBoardingShown: Bool = false + @AppStorage(Keys.multiAccountUpgradeFlag.rawValue) + var multiAccountUpgradeFlag: Bool = false + + @AppStorage(Keys.showMoveAssetOnBrowser.rawValue) + var showMoveAssetOnBrowser: Bool = true + + @AppStorage(Keys.switchProfileTipsFlag.rawValue) + var switchProfileTipsFlag: Bool = false + + var openLogWindow: Bool = false + + @AppStorage(Keys.whatIsBack.rawValue) + var clickedWhatIsBack: Bool = false + + @AppStorage(Keys.backupSheetNotAsk.rawValue) + var backupSheetNotAsk: Bool = false var legacyUserInfo: UserInfo? { set { @@ -129,7 +189,11 @@ class LocalUserDefaults: ObservableObject { } } get { - if let data = UserDefaults.standard.data(forKey: Keys.legacyUserInfo.rawValue), let info = try? FRWAPI.jsonDecoder.decode(UserInfo.self, from: data) { + if let data = UserDefaults.standard.data(forKey: Keys.legacyUserInfo.rawValue), + let info = try? FRWAPI.jsonDecoder.decode( + UserInfo.self, + from: data + ) { return info } else { return nil @@ -137,13 +201,15 @@ class LocalUserDefaults: ObservableObject { } } - @AppStorage(Keys.walletHidden.rawValue) var walletHidden: Bool = false { + @AppStorage(Keys.walletHidden.rawValue) + var walletHidden: Bool = false { didSet { NotificationCenter.default.post(name: .walletHiddenFlagUpdated, object: nil) } } - @AppStorage(Keys.quoteMarket.rawValue) var market: QuoteMarket = .binance { + @AppStorage(Keys.quoteMarket.rawValue) + var market: QuoteMarket = .binance { didSet { NotificationCenter.default.post(name: .quoteMarketUpdated, object: nil) } @@ -158,7 +224,11 @@ class LocalUserDefaults: ObservableObject { } } get { - if let data = UserDefaults.standard.data(forKey: Keys.coinSummary.rawValue), let info = try? FRWAPI.jsonDecoder.decode([CoinRateCache.CoinRateModel].self, from: data) { + if let data = UserDefaults.standard.data(forKey: Keys.coinSummary.rawValue), + let info = try? FRWAPI.jsonDecoder.decode( + [CoinRateCache.CoinRateModel].self, + from: data + ) { return info } else { return nil @@ -166,13 +236,6 @@ class LocalUserDefaults: ObservableObject { } } - @AppStorage(Keys.recentSendByToken.rawValue) var recentToken: String? - - @AppStorage(Keys.legacyBackupType.rawValue) var legacyBackupType: BackupManager.BackupType = .none - - @AppStorage(Keys.securityType.rawValue) var securityType: SecurityManager.SecurityType = .none - @AppStorage(Keys.lockOnExit.rawValue) var lockOnExit: Bool = false - var panelHolderFrame: CGRect? { set { if let value = newValue { @@ -191,42 +254,33 @@ class LocalUserDefaults: ObservableObject { } } - @AppStorage(Keys.transactionCount.rawValue) var transactionCount: Int = 0 { + @AppStorage(Keys.transactionCount.rawValue) + var transactionCount: Int = 0 { didSet { NotificationCenter.default.post(name: .transactionCountDidChanged, object: nil) } } - @AppStorage(Keys.customWatchAddress.rawValue) var customWatchAddress: String? { + @AppStorage(Keys.customWatchAddress.rawValue) + var customWatchAddress: String? { didSet { NotificationCenter.default.post(name: .watchAddressDidChanged, object: nil) } } - @AppStorage(Keys.tryToRestoreAccountFlag.rawValue) var tryToRestoreAccountFlag: Bool = false - - @AppStorage(Keys.currentCurrency.rawValue) var currentCurrency: Currency = .USD - @AppStorage(Keys.currentCurrencyRate.rawValue) var currentCurrencyRate: Double = 1 - - @AppStorage(Keys.stakingGuideDisplayed.rawValue) var stakingGuideDisplayed: Bool = false - - @AppStorage(Keys.nftCount.rawValue) var nftCount: Int = 0 { + @AppStorage(Keys.nftCount.rawValue) + var nftCount: Int = 0 { didSet { NotificationCenter.default.post(name: .nftCountChanged, object: nil) } } - @AppStorage(Keys.onBoardingShown.rawValue) var onBoardingShown: Bool = false - @AppStorage(Keys.multiAccountUpgradeFlag.rawValue) var multiAccountUpgradeFlag: Bool = false - - @AppStorage(Keys.showMoveAssetOnBrowser.rawValue) var showMoveAssetOnBrowser: Bool = true - var loginUIDList: [String] { set { UserDefaults.standard.setValue(newValue, forKey: Keys.loginUIDList.rawValue) } get { - return UserDefaults.standard.array(forKey: Keys.loginUIDList.rawValue) as? [String] ?? [] + UserDefaults.standard.array(forKey: Keys.loginUIDList.rawValue) as? [String] ?? [] } } @@ -235,7 +289,9 @@ class LocalUserDefaults: ObservableObject { UserDefaults.standard.setValue(newValue, forKey: Keys.userAddressOfDeletedApp.rawValue) } get { - return UserDefaults.standard.dictionary(forKey: Keys.userAddressOfDeletedApp.rawValue) as? [String: String] ?? [:] + UserDefaults.standard + .dictionary(forKey: Keys.userAddressOfDeletedApp.rawValue) as? [String: String] ?? + [:] } } @@ -248,7 +304,11 @@ class LocalUserDefaults: ObservableObject { } } get { - if let data = UserDefaults.standard.data(forKey: Keys.selectedChildAccount.rawValue), let model = try? JSONDecoder().decode(ChildAccount.self, from: data) { + if let data = UserDefaults.standard.data(forKey: Keys.selectedChildAccount.rawValue), + let model = try? JSONDecoder().decode( + ChildAccount.self, + from: data + ) { return model } else { return nil @@ -265,7 +325,11 @@ class LocalUserDefaults: ObservableObject { } } get { - if let data = UserDefaults.standard.data(forKey: Keys.selectedEVMAccount.rawValue), let model = try? JSONDecoder().decode(EVMAccountManager.Account.self, from: data) { + if let data = UserDefaults.standard.data(forKey: Keys.selectedEVMAccount.rawValue), + let model = try? JSONDecoder().decode( + EVMAccountManager.Account.self, + from: data + ) { return model } else { return nil @@ -273,8 +337,6 @@ class LocalUserDefaults: ObservableObject { } } - @AppStorage(Keys.switchProfileTipsFlag.rawValue) var switchProfileTipsFlag: Bool = false - var walletAccount: [String: [WalletAccount.User]]? { set { if let data = try? JSONEncoder().encode(newValue) { @@ -285,8 +347,10 @@ class LocalUserDefaults: ObservableObject { } get { if let data = UserDefaults.standard.data(forKey: Keys.walletAccountInfo.rawValue), - let model = try? JSONDecoder().decode([String: [WalletAccount.User]].self, from: data) - { + let model = try? JSONDecoder().decode( + [String: [WalletAccount.User]].self, + from: data + ) { return model } else { return nil @@ -299,35 +363,29 @@ class LocalUserDefaults: ObservableObject { UserDefaults.standard.setValue(newValue, forKey: Keys.EVMAddress.rawValue) } get { - return UserDefaults.standard.dictionary(forKey: Keys.EVMAddress.rawValue) as? [String: [String]] ?? [:] + UserDefaults.standard + .dictionary(forKey: Keys.EVMAddress.rawValue) as? [String: [String]] ?? [:] } } - var openLogWindow: Bool = false - var removedNewsIds: [String] { set { UserDefaults.standard.setValue(newValue, forKey: Keys.removedNewsIds.rawValue) } get { - return UserDefaults.standard.array(forKey: Keys.removedNewsIds.rawValue) as? [String] ?? [] + UserDefaults.standard.array(forKey: Keys.removedNewsIds.rawValue) as? [String] ?? [] } } - @AppStorage(Keys.whatIsBack.rawValue) var clickedWhatIsBack: Bool = false - - @AppStorage(Keys.backupSheetNotAsk.rawValue) var backupSheetNotAsk: Bool = false - var checkCoa: [String] { set { UserDefaults.standard.setValue(newValue, forKey: Keys.checkCoa.rawValue) } get { - return UserDefaults.standard.array(forKey: Keys.checkCoa.rawValue) as? [String] ?? [] + UserDefaults.standard.array(forKey: Keys.checkCoa.rawValue) as? [String] ?? [] } } - - + var customToken: [CustomToken] { set { if let data = try? JSONEncoder().encode(newValue) { @@ -340,8 +398,7 @@ class LocalUserDefaults: ObservableObject { } get { if let data = UserDefaults.standard.data(forKey: Keys.customToken.rawValue), - let list = try? JSONDecoder().decode([CustomToken].self, from: data) - { + let list = try? JSONDecoder().decode([CustomToken].self, from: data) { return list } else { return [] @@ -351,7 +408,8 @@ class LocalUserDefaults: ObservableObject { } extension LocalUserDefaults { - @objc private func willReset() { + @objc + private func willReset() { recentToken = nil WalletManager.shared.changeNetwork(.mainnet) } diff --git a/FRW/Services/Manager/SecurityManager.swift b/FRW/Services/Manager/SecurityManager.swift index b730fc60..a6e2f836 100644 --- a/FRW/Services/Manager/SecurityManager.swift +++ b/FRW/Services/Manager/SecurityManager.swift @@ -22,6 +22,8 @@ extension SecurityManager { case faceid case touchid + // MARK: Internal + var desc: String { switch self { case .none: @@ -35,25 +37,37 @@ extension SecurityManager { } } +// MARK: - SecurityManager + class SecurityManager { + // MARK: Lifecycle + + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(onEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + + // MARK: Internal + static let shared = SecurityManager() - private let PinCodeKey = "PinCodeKey" + var isLocked: Bool = false - - private var ignoreOnce: Bool = false var securityType: SecurityType { - return LocalUserDefaults.shared.securityType + LocalUserDefaults.shared.securityType } - init() { - NotificationCenter.default.addObserver(self, - selector: #selector(onEnterBackground), - name: UIApplication.didEnterBackgroundNotification, - object: nil) - } + // MARK: Private + + private let PinCodeKey = "PinCodeKey" + private var ignoreOnce: Bool = false - @objc private func onEnterBackground() { + @objc + private func onEnterBackground() { lockAppIfNeeded() } } @@ -62,7 +76,7 @@ class SecurityManager { extension SecurityManager { var isLockOnExitEnabled: Bool { - return LocalUserDefaults.shared.lockOnExit + LocalUserDefaults.shared.lockOnExit } func changeLockOnExistStatus(_ lock: Bool) { @@ -99,11 +113,11 @@ extension SecurityManager { }) } } - + func openIgnoreOnce() { ignoreOnce = true } - + func SecurityVerify() async -> Bool { guard !ignoreOnce else { ignoreOnce = false @@ -127,7 +141,7 @@ extension SecurityManager { extension SecurityManager { var isPinCodeEnabled: Bool { - return securityType == .pin || securityType == .both + securityType == .pin || securityType == .both } var currentPinCode: String { @@ -171,7 +185,7 @@ extension SecurityManager { } func authPinCode(_ code: String) -> Bool { - return currentPinCode == code + currentPinCode == code } } @@ -179,7 +193,7 @@ extension SecurityManager { extension SecurityManager { var isBionicEnabled: Bool { - return securityType == .bionic || securityType == .both + securityType == .bionic || securityType == .both } var supportedBionic: SecurityManager.BionicType { diff --git a/FRW/Services/Manager/UUIDManager.swift b/FRW/Services/Manager/UUIDManager.swift index 5cccfc23..dc274d13 100644 --- a/FRW/Services/Manager/UUIDManager.swift +++ b/FRW/Services/Manager/UUIDManager.swift @@ -8,7 +8,7 @@ import KeychainAccess import SwiftUI -struct UUIDManager { +enum UUIDManager { static func appUUID() -> String { var service = Bundle.main.bundleIdentifier ?? "com.flowfoundation.wallet" service += ".uuid" diff --git a/FRW/Services/Manager/UserManager.swift b/FRW/Services/Manager/UserManager.swift index 8565c090..33810711 100644 --- a/FRW/Services/Manager/UserManager.swift +++ b/FRW/Services/Manager/UserManager.swift @@ -13,6 +13,8 @@ import Flow import FlowWalletCore import Foundation +// MARK: - UserManager.UserType + extension UserManager { enum UserType: Codable { case phrase @@ -20,10 +22,37 @@ extension UserManager { } } +// MARK: - UserManager + class UserManager: ObservableObject { + // MARK: Lifecycle + + init() { + checkIfHasOldAccount() + + self.loginUIDList = LocalUserDefaults.shared.loginUIDList + + if let activatedUID = activatedUID { + self.userInfo = MultiAccountStorage.shared.getUserInfo(activatedUID) + uploadUserNameIfNeeded() + initRefreshUserInfo() + verifyUserType(by: activatedUID) + } + + loginAnonymousIfNeeded() + } + + // MARK: Internal + static let shared = UserManager() - @Published var activatedUID: String? = LocalUserDefaults.shared.activatedUID { + @Published + var isMeowDomainEnabled: Bool = false + + var userType: UserManager.UserType = .secure + + @Published + var activatedUID: String? = LocalUserDefaults.shared.activatedUID { didSet { LocalUserDefaults.shared.activatedUID = activatedUID if oldValue != activatedUID { @@ -32,7 +61,8 @@ class UserManager: ObservableObject { } } - @Published var userInfo: UserInfo? { + @Published + var userInfo: UserInfo? { didSet { do { guard let uid = activatedUID else { return } @@ -43,35 +73,32 @@ class UserManager: ObservableObject { } } - @Published var loginUIDList: [String] = [] { + @Published + var loginUIDList: [String] = [] { didSet { LocalUserDefaults.shared.loginUIDList = loginUIDList } } - @Published var isMeowDomainEnabled: Bool = false - - var userType: UserManager.UserType = .secure - var isLoggedIn: Bool { - return activatedUID != nil + activatedUID != nil } - init() { - checkIfHasOldAccount() - - loginUIDList = LocalUserDefaults.shared.loginUIDList - - if let activatedUID = activatedUID { - userInfo = MultiAccountStorage.shared.getUserInfo(activatedUID) - uploadUserNameIfNeeded() - initRefreshUserInfo() - verifyUserType(by: activatedUID) + func verifyUserType(by _: String) { + Task { + do { + userType = try await checkUserType() + if let uid = activatedUID { + WalletManager.shared.warningIfKeyIsInvalid(userId: uid) + } + } catch { + log.error("[User] check user type:\(error)") + } } - - loginAnonymousIfNeeded() } + // MARK: Private + private func initRefreshUserInfo() { if !isLoggedIn { return @@ -103,27 +130,17 @@ class UserManager: ObservableObject { } } - func verifyUserType(by _: String) { - Task { - do { - userType = try await checkUserType() - if let uid = activatedUID { - WalletManager.shared.warningIfKeyIsInvalid(userId: uid) - } - } catch { - log.error("[User] check user type:\(error)") - } - } - } - private func checkUserType() async throws -> UserManager.UserType { - guard let address = WalletManager.shared.getPrimaryWalletAddress(), let uid = activatedUID else { return .secure } + guard let address = WalletManager.shared.getPrimaryWalletAddress(), + let uid = activatedUID else { return .secure } let account = try await FlowNetwork.getAccountAtLatestBlock(address: address) - if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: uid), !mnemonic.isEmpty { + if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: uid), + !mnemonic.isEmpty { let hdWallet = WalletManager.shared.createHDWallet(mnemonic: mnemonic) - let accountKeys = account.keys.first { $0.publicKey.description == hdWallet?.getPublicKey() } + let accountKeys = account.keys + .first { $0.publicKey.description == hdWallet?.getPublicKey() } if accountKeys != nil { return .phrase } @@ -181,14 +198,21 @@ extension UserManager { if IPManager.shared.info == nil { await IPManager.shared.fetch() } - let request = RegisterRequest(username: username, accountKey: key.toCodableModel(), deviceInfo: IPManager.shared.toParams()) + let request = RegisterRequest( + username: username, + accountKey: key.toCodableModel(), + deviceInfo: IPManager.shared.toParams() + ) let model: RegisterResponse = try await Network.request(FRWAPI.User.register(request)) try await finishLogin(mnemonic: "", customToken: model.customToken, isRegiter: true) WalletManager.shared.asyncCreateWalletAddressFromServer() userType = .secure if let privateKey = sec.key.privateKey { - try WallectSecureEnclave.Store.store(key: model.id, value: privateKey.dataRepresentation) + try WallectSecureEnclave.Store.store( + key: model.id, + value: privateKey.dataRepresentation + ) } else { log.error("store public key on iPhone failed") } @@ -213,14 +237,16 @@ extension UserManager { Task { do { var list = try WallectSecureEnclave.Store.fetch() - list = list.filter({ $0.isShow ?? true }) + list = list.filter { $0.isShow ?? true } var addressList: [String: String] = [:] for item in list { do { let sec = try WallectSecureEnclave(privateKey: item.publicKey) guard let publicKey = sec.key.publickeyValue else { continue } - let response: AccountResponse = try await Network.requestWithRawModel(FRWAPI.Utils.flowAddress(publicKey)) - let account = response.accounts?.filter { ($0.weight ?? 0) >= 1000 && $0.address != nil }.first + let response: AccountResponse = try await Network + .requestWithRawModel(FRWAPI.Utils.flowAddress(publicKey)) + let account = response.accounts? + .filter { ($0.weight ?? 0) >= 1000 && $0.address != nil }.first if let model = account { addressList[item.uniq] = model.address ?? "0x" } @@ -228,7 +254,7 @@ extension UserManager { log.error("[Launch] first login check failed:\(item.uniq)", context: error) } } - + let uidList = addressList.map { $0.key } let userAddress = addressList DispatchQueue.main.async { @@ -250,7 +276,8 @@ extension UserManager { func restoreLogin(withMnemonic mnemonic: String, userId: String? = nil) async throws { if let uid = userId { - if let address = MultiAccountStorage.shared.getWalletInfo(uid)?.currentNetworkWalletModel?.getAddress { + if let address = MultiAccountStorage.shared.getWalletInfo(uid)? + .currentNetworkWalletModel?.getAddress { try? await WalletManager.shared.findFlowAccount(with: uid, at: address) } } @@ -283,12 +310,19 @@ extension UserManager { await IPManager.shared.fetch() let hashAlgo = Flow.HashAlgorithm.SHA2_256.index let signAlgo = Flow.SignatureAlgorithm.ECDSA_SECP256k1.index - let key = AccountKey(hashAlgo: hashAlgo, - publicKey: publicKey, - signAlgo: signAlgo) - - let request = LoginRequest(signature: signature, accountKey: key, deviceInfo: IPManager.shared.toParams()) - let response: Network.Response = try await Network.requestWithRawModel(FRWAPI.User.login(request)) + let key = AccountKey( + hashAlgo: hashAlgo, + publicKey: publicKey, + signAlgo: signAlgo + ) + + let request = LoginRequest( + signature: signature, + accountKey: key, + deviceInfo: IPManager.shared.toParams() + ) + let response: Network.Response = try await Network + .requestWithRawModel(FRWAPI.User.login(request)) if response.httpCode == 404 { throw LLError.accountNotFound } @@ -314,7 +348,8 @@ extension UserManager { throw LLError.restoreLoginFailed } - guard let publicData = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey, !publicData.isEmpty else { + guard let publicData = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey, + !publicData.isEmpty else { throw LLError.restoreLoginFailed } @@ -329,12 +364,19 @@ extension UserManager { let signature = try sec.sign(data: signData).hexValue await IPManager.shared.fetch() // TODO: hash & sign algo - let key = AccountKey(hashAlgo: Flow.HashAlgorithm.SHA2_256.index, - publicKey: publicKey, - signAlgo: Flow.SignatureAlgorithm.ECDSA_P256.index) - - let request = LoginRequest(signature: signature, accountKey: key, deviceInfo: IPManager.shared.toParams()) - let response: Network.Response = try await Network.requestWithRawModel(FRWAPI.User.login(request)) + let key = AccountKey( + hashAlgo: Flow.HashAlgorithm.SHA2_256.index, + publicKey: publicKey, + signAlgo: Flow.SignatureAlgorithm.ECDSA_P256.index + ) + + let request = LoginRequest( + signature: signature, + accountKey: key, + deviceInfo: IPManager.shared.toParams() + ) + let response: Network.Response = try await Network + .requestWithRawModel(FRWAPI.User.login(request)) if response.httpCode == 404 { throw LLError.accountNotFound } @@ -360,10 +402,12 @@ extension UserManager { return } - if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: uid), !mnemonic.isEmpty { + if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: uid), + !mnemonic.isEmpty { var addressStr = LocalUserDefaults.shared.userAddressOfDeletedApp[uid] if addressStr == nil { - addressStr = MultiAccountStorage.shared.getWalletInfo(uid)?.getNetworkWalletModel(network: .mainnet)?.getAddress + addressStr = MultiAccountStorage.shared.getWalletInfo(uid)? + .getNetworkWalletModel(network: .mainnet)?.getAddress } guard let address = addressStr else { throw LLError.invalidAddress @@ -371,7 +415,8 @@ extension UserManager { var accountKeys: Flow.AccountKey? let account = try? await FlowNetwork.getAccountAtLatestBlock(address: address) let hdWallet = WalletManager.shared.createHDWallet(mnemonic: mnemonic) - accountKeys = account?.keys.first { $0.publicKey.description == hdWallet?.getPublicKey() } + accountKeys = account?.keys + .first { $0.publicKey.description == hdWallet?.getPublicKey() } if accountKeys != nil { try await restoreLogin(withMnemonic: mnemonic, userId: uid) return @@ -379,12 +424,12 @@ extension UserManager { } let allModel = try WallectSecureEnclave.Store.fetchAllModel(by: uid) let model = try WallectSecureEnclave.Store.fetchModel(by: uid) - + if model != nil { try await restoreLogin(userId: uid) return } - if model == nil && allModel.count > 0 { + if model == nil, !allModel.isEmpty { WalletManager.shared.warningIfKeyIsInvalid(userId: uid, markHide: true) return } @@ -396,7 +441,11 @@ extension UserManager { // MARK: - Internal Login Logic extension UserManager { - private func finishLogin(mnemonic: String, customToken: String, isRegiter: Bool = false) async throws { + private func finishLogin( + mnemonic: String, + customToken: String, + isRegiter: Bool = false + ) async throws { try await firebaseLogin(customToken: customToken) var info = try await fetchUserInfo() info.type = userType @@ -410,7 +459,7 @@ extension UserManager { try WalletManager.shared.storeAndActiveMnemonicToKeychain(mnemonic, uid: uid) } - if !loginUIDList.contains(uid) && !isRegiter { + if !loginUIDList.contains(uid), !isRegiter { ConfettiManager.show() } DispatchQueue.main.async { @@ -445,7 +494,13 @@ extension UserManager { private func fetchUserInfo() async throws -> UserInfo { let response: UserInfoResponse = try await Network.request(FRWAPI.User.userInfo) - let info = UserInfo(avatar: response.avatar, nickname: response.nickname, username: response.username, private: response.private, address: nil) + let info = UserInfo( + avatar: response.avatar, + nickname: response.nickname, + username: response.username, + private: response.private, + address: nil + ) if info.username.isEmpty { throw LLError.fetchUserInfoFailed @@ -457,7 +512,10 @@ extension UserManager { private func fetchMeowDomainStatus(_ username: String) { Task { do { - _ = try await FlowNetwork.queryAddressByDomainFlowns(domain: username, root: Contact.DomainType.meow.domain) + _ = try await FlowNetwork.queryAddressByDomainFlowns( + domain: username, + root: Contact.DomainType.meow.domain + ) if userInfo?.username == username { DispatchQueue.main.async { self.isMeowDomainEnabled = true @@ -490,11 +548,11 @@ extension UserManager { } private func getFirebaseUID() -> String? { - return Auth.auth().currentUser?.uid + Auth.auth().currentUser?.uid } func getIDToken() async throws -> String? { - return try await Auth.auth().currentUser?.getIDToken() + try await Auth.auth().currentUser?.getIDToken() } } @@ -534,7 +592,13 @@ extension UserManager { return } - let newUserInfo = UserInfo(avatar: current.avatar, nickname: name, username: current.username, private: current.private, address: nil) + let newUserInfo = UserInfo( + avatar: current.avatar, + nickname: name, + username: current.username, + private: current.private, + address: nil + ) userInfo = newUserInfo } @@ -543,7 +607,13 @@ extension UserManager { return } - let newUserInfo = UserInfo(avatar: current.avatar, nickname: current.nickname, username: current.username, private: isPrivate ? 2 : 1, address: nil) + let newUserInfo = UserInfo( + avatar: current.avatar, + nickname: current.nickname, + username: current.username, + private: isPrivate ? 2 : 1, + address: nil + ) userInfo = newUserInfo } @@ -552,7 +622,13 @@ extension UserManager { return } - let newUserInfo = UserInfo(avatar: avatar, nickname: current.nickname, username: current.username, private: current.private, address: nil) + let newUserInfo = UserInfo( + avatar: avatar, + nickname: current.nickname, + username: current.username, + private: current.private, + address: nil + ) userInfo = newUserInfo } } diff --git a/FRW/Services/Manager/WalletConnect/Model/SessionItem.swift b/FRW/Services/Manager/WalletConnect/Model/SessionItem.swift index 5670546c..b6ba9d0a 100644 --- a/FRW/Services/Manager/WalletConnect/Model/SessionItem.swift +++ b/FRW/Services/Manager/WalletConnect/Model/SessionItem.swift @@ -1,5 +1,5 @@ // -// Model.swift +// SessionItem.swift // Flow Wallet // // Created by Hao Fu on 30/7/2022. diff --git a/FRW/Services/Manager/WalletConnect/Model/Signable.swift b/FRW/Services/Manager/WalletConnect/Model/Signable.swift index 9ee87b3f..84a4b844 100644 --- a/FRW/Services/Manager/WalletConnect/Model/Signable.swift +++ b/FRW/Services/Manager/WalletConnect/Model/Signable.swift @@ -10,6 +10,8 @@ import Combine import Flow import Foundation +// MARK: - FCLError + public enum FCLError: String, Error, LocalizedError { case generic case invaildURL @@ -26,18 +28,50 @@ public enum FCLError: String, Error, LocalizedError { case invaildProposer case fetchAccountFailure + // MARK: Public + public var errorDescription: String? { - return rawValue + rawValue } } +// MARK: - SignableMessage + struct SignableMessage: Codable { let addr: String // let data: [String: String]? let message: String } +// MARK: - Signable + struct Signable: Codable { + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.fType = try container.decode(String.self, forKey: .fType) + self.fVsn = try container.decode(String.self, forKey: .fVsn) + self.data = try? container.decode([String: String].self, forKey: .data) + self.message = try container.decode(String.self, forKey: .message) + self.keyId = try? container.decode(Int.self, forKey: .keyId) + self.addr = try? container.decode(String.self, forKey: .addr) + self.roles = try container.decode(Role.self, forKey: .roles) + self.cadence = try? container.decode(String.self, forKey: .cadence) + self.args = try container.decode([Flow.Argument].self, forKey: .args) + +// voucher = try container.decode(Voucher.self, forKey: .voucher) +// interaction = try container.decode(Interaction.self, forKey: .interaction) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case fType = "f_type" + case fVsn = "f_vsn" + case roles, data, message, keyId, addr, cadence, args + } + var fType: String = "Signable" var fVsn: String = "1.0.1" var data: [String: String]? @@ -49,56 +83,40 @@ struct Signable: Codable { let args: [Flow.Argument] var interaction = Interaction() - enum CodingKeys: String, CodingKey { - case fType = "f_type" - case fVsn = "f_vsn" - case roles, data, message, keyId, addr, cadence, args - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - fType = try container.decode(String.self, forKey: .fType) - fVsn = try container.decode(String.self, forKey: .fVsn) - data = try? container.decode([String: String].self, forKey: .data) - message = try container.decode(String.self, forKey: .message) - keyId = try? container.decode(Int.self, forKey: .keyId) - addr = try? container.decode(String.self, forKey: .addr) - roles = try container.decode(Role.self, forKey: .roles) - cadence = try? container.decode(String.self, forKey: .cadence) - args = try container.decode([Flow.Argument].self, forKey: .args) - -// voucher = try container.decode(Voucher.self, forKey: .voucher) -// interaction = try container.decode(Interaction.self, forKey: .interaction) - } - var voucher: Voucher { let insideSigners: [Singature] = interaction.findInsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr?.sansPrefix(), - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr?.sansPrefix(), + keyId: account.keyID, + sig: account.signature + ) } let outsideSigners: [Singature] = interaction.findOutsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr?.sansPrefix(), - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr?.sansPrefix(), + keyId: account.keyID, + sig: account.signature + ) } - return Voucher(cadence: interaction.message.cadence, - refBlock: interaction.message.refBlock, - computeLimit: interaction.message.computeLimit, - arguments: interaction.message.arguments.compactMap { tempId in - interaction.arguments[tempId]?.asArgument - }, - proposalKey: interaction.createProposalKey(), - payer: interaction.accounts[interaction.payer ?? ""]?.addr?.sansPrefix(), - authorizers: interaction.authorizations - .compactMap { cid in interaction.accounts[cid]?.addr?.sansPrefix() } - .uniqued(), - payloadSigs: insideSigners, - envelopeSigs: outsideSigners) + return Voucher( + cadence: interaction.message.cadence, + refBlock: interaction.message.refBlock, + computeLimit: interaction.message.computeLimit, + arguments: interaction.message.arguments.compactMap { tempId in + interaction.arguments[tempId]?.asArgument + }, + proposalKey: interaction.createProposalKey(), + payer: interaction.accounts[interaction.payer ?? ""]?.addr?.sansPrefix(), + authorizers: interaction.authorizations + .compactMap { cid in interaction.accounts[cid]?.addr?.sansPrefix() } + .uniqued(), + payloadSigs: insideSigners, + envelopeSigs: outsideSigners + ) } func encode(to encoder: Encoder) throws { @@ -117,7 +135,16 @@ struct Signable: Codable { } } +// MARK: - PreSignable + struct PreSignable: Encodable { + enum CodingKeys: String, CodingKey { + case fType = "f_type" + case fVsn = "f_vsn" + case roles, cadence, args, interaction + case voucher + } + let fType: String = "PreSignable" let fVsn: String = "1.0.1" let roles: Role @@ -129,38 +156,37 @@ struct PreSignable: Encodable { var voucher: Voucher { let insideSigners: [Singature] = interaction.findInsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr, - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr, + keyId: account.keyID, + sig: account.signature + ) } let outsideSigners: [Singature] = interaction.findOutsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr, - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr, + keyId: account.keyID, + sig: account.signature + ) } - return Voucher(cadence: interaction.message.cadence, - refBlock: interaction.message.refBlock, - computeLimit: interaction.message.computeLimit, - arguments: interaction.message.arguments.compactMap { tempId in - interaction.arguments[tempId]?.asArgument - }, - proposalKey: interaction.createProposalKey(), - payer: interaction.payer, - authorizers: interaction.authorizations - .compactMap { cid in interaction.accounts[cid]?.addr } - .uniqued(), - payloadSigs: insideSigners, - envelopeSigs: outsideSigners) - } - - enum CodingKeys: String, CodingKey { - case fType = "f_type" - case fVsn = "f_vsn" - case roles, cadence, args, interaction - case voucher + return Voucher( + cadence: interaction.message.cadence, + refBlock: interaction.message.refBlock, + computeLimit: interaction.message.computeLimit, + arguments: interaction.message.arguments.compactMap { tempId in + interaction.arguments[tempId]?.asArgument + }, + proposalKey: interaction.createProposalKey(), + payer: interaction.payer, + authorizers: interaction.authorizations + .compactMap { cid in interaction.accounts[cid]?.addr } + .uniqued(), + payloadSigs: insideSigners, + envelopeSigs: outsideSigners + ) } func encode(to encoder: Encoder) throws { @@ -175,6 +201,8 @@ struct PreSignable: Encodable { } } +// MARK: - Argument + struct Argument: Codable { var kind: String var tempId: String @@ -183,6 +211,8 @@ struct Argument: Codable { var xform: Xform } +// MARK: - Xform + struct Xform: Codable { var label: String } @@ -191,35 +221,22 @@ extension Flow.Argument { func toFCLArgument() -> Argument { func randomString(length: Int) -> String { let letters = "abcdefghijklmnopqrstuvwxyz0123456789" - return String((0 ..< length).map { _ in letters.randomElement()! }) + return String((0.. Bool { - self.tag == tag - } - - @discardableResult - mutating func setTag(_ tag: Tag) -> Self { - self.tag = tag - return self - } - var findInsideSigners: [String] { // Inside Signers Are: (authorizers + proposer) - payer var inside = Set(authorizations) @@ -277,6 +301,16 @@ struct Interaction: Codable { return Array(outside) } + func `is`(_ tag: Tag) -> Bool { + self.tag == tag + } + + @discardableResult + mutating func setTag(_ tag: Tag) -> Self { + self.tag = tag + return self + } + func createProposalKey() -> ProposalKey { guard let proposer = proposer, let account = accounts[proposer] @@ -284,9 +318,11 @@ struct Interaction: Codable { return ProposalKey() } - return ProposalKey(address: account.addr?.sansPrefix(), - keyID: account.keyID, - sequenceNum: account.sequenceNum) + return ProposalKey( + address: account.addr?.sansPrefix(), + keyID: account.keyID, + sequenceNum: account.sequenceNum + ) } func createFlowProposalKey() async throws -> Flow.TransactionProposalKey { @@ -303,21 +339,28 @@ struct Interaction: Codable { if account.sequenceNum == nil { let accountData = try await flow.accessAPI.getAccountAtLatestBlock(address: flowAddress) account.sequenceNum = Int(accountData.keys[keyID].sequenceNumber) - return Flow.TransactionProposalKey(address: Flow.Address(hex: address), - keyIndex: keyID, - sequenceNumber: Int64(account.sequenceNum ?? 0)) + return Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: keyID, + sequenceNumber: Int64(account.sequenceNum ?? 0) + ) } - return Flow.TransactionProposalKey(address: Flow.Address(hex: address), - keyIndex: keyID, - sequenceNumber: Int64(account.sequenceNum ?? 0)) + return Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: keyID, + sequenceNumber: Int64(account.sequenceNum ?? 0) + ) } func buildPreSignable(role: Role) -> PreSignable { - return PreSignable(roles: role, - cadence: message.cadence ?? "", - args: message.arguments.compactMap { tempId in arguments[tempId]?.asArgument }, - interaction: self) + PreSignable( + roles: role, + cadence: message.cadence ?? "", + args: message.arguments + .compactMap { tempId in arguments[tempId]?.asArgument }, + interaction: self + ) } } @@ -331,73 +374,88 @@ extension Interaction { throw FCLError.missingPayer } - var tx = Flow.Transaction(script: Flow.Script(text: message.cadence ?? ""), - arguments: message.arguments.compactMap { tempId in arguments[tempId]?.asArgument }, - referenceBlockId: Flow.ID(hex: message.refBlock ?? ""), - gasLimit: BigUInt(message.computeLimit ?? 100), - proposalKey: proposalKey, - payer: Flow.Address(hex: payerAddress), - authorizers: authorizations - .compactMap { cid in accounts[cid]?.addr } - .uniqued() - .compactMap { Flow.Address(hex: $0) }) + var tx = Flow.Transaction( + script: Flow.Script(text: message.cadence ?? ""), + arguments: message.arguments + .compactMap { tempId in arguments[tempId]?.asArgument }, + referenceBlockId: Flow.ID(hex: message.refBlock ?? ""), + gasLimit: BigUInt(message.computeLimit ?? 100), + proposalKey: proposalKey, + payer: Flow.Address(hex: payerAddress), + authorizers: authorizations + .compactMap { cid in accounts[cid]?.addr } + .uniqued() + .compactMap { Flow.Address(hex: $0) } + ) let insideSigners = findInsideSigners - insideSigners.forEach { address in + for address in insideSigners { if let account = accounts[address], let address = account.addr, let keyId = account.keyID, - let signature = account.signature - { - tx.addPayloadSignature(address: Flow.Address(hex: address), - keyIndex: keyId, - signature: Data(signature.hexValue)) + let signature = account.signature { + tx.addPayloadSignature( + address: Flow.Address(hex: address), + keyIndex: keyId, + signature: Data(signature.hexValue) + ) } } let outsideSigners = findOutsideSigners - outsideSigners.forEach { address in + for address in outsideSigners { if let account = accounts[address], let address = account.addr, let keyId = account.keyID, - let signature = account.signature - { - tx.addEnvelopeSignature(address: Flow.Address(hex: address), - keyIndex: keyId, - signature: Data(signature.hexValue)) + let signature = account.signature { + tx.addEnvelopeSignature( + address: Flow.Address(hex: address), + keyIndex: keyId, + signature: Data(signature.hexValue) + ) } } return tx } } +// MARK: - Block + struct Block: Codable { var id: String? var height: Int64? var isSealed: Bool? } +// MARK: - Account + struct Account: Codable { var addr: String? } +// MARK: - Id + struct Id: Codable { var id: String? } -struct Events: Codable { - var eventType: String? - var start: String? - var end: String? - var blockIDS: [String] = [] +// MARK: - Events +struct Events: Codable { enum CodingKeys: String, CodingKey { case eventType, start, end case blockIDS = "blockIds" } + + var eventType: String? + var start: String? + var end: String? + var blockIDS: [String] = [] } +// MARK: - Message + struct Message: Codable { var cadence: String? var refBlock: String? @@ -409,6 +467,8 @@ struct Message: Codable { var arguments: [String] = [] } +// MARK: - Voucher + struct Voucher: Codable { let cadence: String? let refBlock: String? @@ -421,39 +481,67 @@ struct Voucher: Codable { let envelopeSigs: [Singature]? func toFCLVoucher() -> FCLVoucher { - let pkey = FCLVoucher.ProposalKey(address: Flow.Address(hex: proposalKey.address ?? ""), keyId: proposalKey.keyID ?? 0, sequenceNum: UInt64(proposalKey.sequenceNum ?? 0)) + let pkey = FCLVoucher.ProposalKey( + address: Flow.Address(hex: proposalKey.address ?? ""), + keyId: proposalKey.keyID ?? 0, + sequenceNum: UInt64(proposalKey.sequenceNum ?? 0) + ) let authorArray = authorizers?.map { Flow.Address(hex: $0) } ?? [Flow.Address]() - let payloadSigsArray = payloadSigs?.map { FCLVoucher.Signature(address: Flow.Address(hex: $0.address ?? ""), keyId: $0.keyId ?? 0, sig: $0.sig ?? "") } ?? [FCLVoucher.Signature]() - - let v = FCLVoucher(cadence: Flow.Script(text: cadence ?? ""), payer: Flow.Address(hex: payer ?? ""), refBlock: Flow.ID(hex: refBlock ?? ""), arguments: arguments, proposalKey: pkey, computeLimit: UInt64(computeLimit ?? 0), authorizers: authorArray, payloadSigs: payloadSigsArray) + let payloadSigsArray = payloadSigs?.map { FCLVoucher.Signature( + address: Flow.Address(hex: $0.address ?? ""), + keyId: $0.keyId ?? 0, + sig: $0.sig ?? "" + ) } ?? [FCLVoucher.Signature]() + + let v = FCLVoucher( + cadence: Flow.Script(text: cadence ?? ""), + payer: Flow.Address(hex: payer ?? ""), + refBlock: Flow.ID(hex: refBlock ?? ""), + arguments: arguments, + proposalKey: pkey, + computeLimit: UInt64(computeLimit ?? 0), + authorizers: authorArray, + payloadSigs: payloadSigsArray + ) return v } } -struct Accounts: Codable { - let currentUser: SignableUser +// MARK: - Accounts +struct Accounts: Codable { enum CodingKeys: String, CodingKey { case currentUser = "CURRENT_USER" } + + let currentUser: SignableUser } +// MARK: - Singature + struct Singature: Codable { let address: String? let keyId: Int? let sig: String? } -// MARK: - CurrentUser +// MARK: - SignableUser struct SignableUser: Codable { - var kind: String? - var tempID: String? - var addr: String? - var signature: String? - var keyID: Int? - var sequenceNum: Int? - var role: Role + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.kind = try? container.decode(String.self, forKey: .kind) + self.tempID = try? container.decode(String.self, forKey: .tempID) + self.addr = try? container.decode(String.self, forKey: .addr) + self.signature = try? container.decode(String.self, forKey: .signature) + self.keyID = try? container.decode(Int.self, forKey: .keyID) + self.sequenceNum = try? container.decode(Int.self, forKey: .sequenceNum) + self.role = try container.decode(Role.self, forKey: .role) + } + + // MARK: Internal enum CodingKeys: String, CodingKey { case kind @@ -463,16 +551,13 @@ struct SignableUser: Codable { case sequenceNum, signature, role } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - kind = try? container.decode(String.self, forKey: .kind) - tempID = try? container.decode(String.self, forKey: .tempID) - addr = try? container.decode(String.self, forKey: .addr) - signature = try? container.decode(String.self, forKey: .signature) - keyID = try? container.decode(Int.self, forKey: .keyID) - sequenceNum = try? container.decode(Int.self, forKey: .sequenceNum) - role = try container.decode(Role.self, forKey: .role) - } + var kind: String? + var tempID: String? + var addr: String? + var signature: String? + var keyID: Int? + var sequenceNum: Int? + var role: Role func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -486,18 +571,22 @@ struct SignableUser: Codable { } } -struct ProposalKey: Codable { - var address: String? - var keyID: Int? - var sequenceNum: Int? +// MARK: - ProposalKey +struct ProposalKey: Codable { enum CodingKeys: String, CodingKey { case address case keyID = "keyId" case sequenceNum } + + var address: String? + var keyID: Int? + var sequenceNum: Int? } +// MARK: - Role + struct Role: Codable { var proposer: Bool = false var authorizer: Bool = false @@ -511,9 +600,11 @@ struct Role: Codable { } } +// MARK: - NullDecodable + @propertyWrapper struct NullDecodable: Decodable where T: Decodable { - var wrappedValue: T? + // MARK: Lifecycle init(wrappedValue: T?) { self.wrappedValue = wrappedValue @@ -526,6 +617,10 @@ struct NullDecodable: Decodable where T: Decodable { // case .none: try container.encodeNil() // } // } + + // MARK: Internal + + var wrappedValue: T? } extension String { @@ -537,7 +632,7 @@ extension String { } func withPrefix() -> String { - return "0x" + sansPrefix() + "0x" + sansPrefix() } } @@ -548,6 +643,8 @@ extension Array where Element: Hashable { } } +// MARK: - BaseConfigRequest + public struct BaseConfigRequest: Codable { var app: [String: String]? var service: [String: String]? @@ -558,6 +655,8 @@ public struct BaseConfigRequest: Codable { var nonce: String? } +// MARK: - ClientInfo + public struct ClientInfo: Codable { var fclVersion: String? var fclLibrary: URL? diff --git a/FRW/Services/Manager/WalletConnect/Model/SyncInfo.swift b/FRW/Services/Manager/WalletConnect/Model/SyncInfo.swift index b0e8344a..296aa26c 100644 --- a/FRW/Services/Manager/WalletConnect/Model/SyncInfo.swift +++ b/FRW/Services/Manager/WalletConnect/Model/SyncInfo.swift @@ -7,7 +7,7 @@ import Foundation -struct SyncInfo { +enum SyncInfo { struct SyncResponse: Codable { var method: String? var status: String? diff --git a/FRW/Services/Manager/WalletConnect/Model/serviceDefinition.swift b/FRW/Services/Manager/WalletConnect/Model/serviceDefinition.swift index 55a3b750..4dd2d690 100644 --- a/FRW/Services/Manager/WalletConnect/Model/serviceDefinition.swift +++ b/FRW/Services/Manager/WalletConnect/Model/serviceDefinition.swift @@ -1,5 +1,5 @@ // -// ServiceDef.swift +// serviceDefinition.swift // Flow Wallet // // Created by Hao Fu on 30/7/2022. @@ -8,27 +8,33 @@ import Foundation func serviceDefinition(address: String, keyId: Int, type: FCLServiceType) -> Service { - var service = Service(fType: "Service", - fVsn: "1.0.0", - type: type, - method: .walletConnect, - endpoint: nil, - uid: "https://frw-link.lilico.app/wc", - id: nil, - identity: Identity(address: address, keyId: keyId), - provider: nil, params: nil, data: nil) + var service = Service( + fType: "Service", + fVsn: "1.0.0", + type: type, + method: .walletConnect, + endpoint: nil, + uid: "https://frw-link.lilico.app/wc", + id: nil, + identity: Identity(address: address, keyId: keyId), + provider: nil, + params: nil, + data: nil + ) if type == .authn { service.id = address - service.provider = Provider(fType: "ServiceProvider", - fVsn: "1.0.0", - address: address, - name: "Flow Wallet", - description: "Flow Wallet is built from the ground up for Flow Blockchain!", - color: "#41CC5D", - supportEmail: "wallet@flow.com", - website: "https://frw-link.lilico.app/wc", - icon: "https://lilico.app/logo_mobile.png") + service.provider = Provider( + fType: "ServiceProvider", + fVsn: "1.0.0", + address: address, + name: "Flow Wallet", + description: "Flow Wallet is built from the ground up for Flow Blockchain!", + color: "#41CC5D", + supportEmail: "wallet@flow.com", + website: "https://frw-link.lilico.app/wc", + icon: "https://lilico.app/logo_mobile.png" + ) } service.endpoint = FCLWalletConnectMethod(type: type)?.rawValue return service @@ -58,28 +64,39 @@ func serviceDefinition(address: String, keyId: Int, type: FCLServiceType) -> Ser // } // }, -func accountProofServiceDefinition(address: String, keyId: Int, nonce: String, signature: String) -> Service { - var service = Service(fType: "Service", - fVsn: "1.0.0", - type: FCLServiceType.accountProof, - method: .walletConnect, - endpoint: nil, - uid: "https://frw-link.lilico.app/wc", - id: nil, - identity: nil, - provider: nil, - params: nil, - data: nil) +func accountProofServiceDefinition( + address: String, + keyId: Int, + nonce: String, + signature: String +) -> Service { + var service = Service( + fType: "Service", + fVsn: "1.0.0", + type: FCLServiceType.accountProof, + method: .walletConnect, + endpoint: nil, + uid: "https://frw-link.lilico.app/wc", + id: nil, + identity: nil, + provider: nil, + params: nil, + data: nil + ) - service.data = AccountProof(fType: FCLServiceType.accountProof.rawValue, - fVsn: "2.0.0", - address: address, - nonce: nonce, - signatures: [AccountProofSignature(fType: "CompositeSignature", - fVsn: "1.0.0", - addr: address, - keyID: keyId, - signature: signature)]) + service.data = AccountProof( + fType: FCLServiceType.accountProof.rawValue, + fVsn: "2.0.0", + address: address, + nonce: nonce, + signatures: [AccountProofSignature( + fType: "CompositeSignature", + fVsn: "1.0.0", + addr: address, + keyID: keyId, + signature: signature + )] + ) service.endpoint = FCLWalletConnectMethod(type: FCLServiceType.accountProof)?.rawValue return service diff --git a/FRW/Services/Manager/WalletConnect/WalletConnectChildHandlerProtocol.swift b/FRW/Services/Manager/WalletConnect/WalletConnectChildHandlerProtocol.swift index 98ea1c35..e985cbb7 100644 --- a/FRW/Services/Manager/WalletConnect/WalletConnectChildHandlerProtocol.swift +++ b/FRW/Services/Manager/WalletConnect/WalletConnectChildHandlerProtocol.swift @@ -22,6 +22,7 @@ protocol WalletConnectChildHandlerProtocol { func handlePersonalSignRequest(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) func handleSendTransactionRequest(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) func handleSignTypedDataV4(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) + func handleWatchAsset(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) } extension WalletConnectChildHandlerProtocol { diff --git a/FRW/Services/Manager/WalletConnect/WalletConnectEVMHandler.swift b/FRW/Services/Manager/WalletConnect/WalletConnectEVMHandler.swift index 4660d448..ef5021d6 100644 --- a/FRW/Services/Manager/WalletConnect/WalletConnectEVMHandler.swift +++ b/FRW/Services/Manager/WalletConnect/WalletConnectEVMHandler.swift @@ -14,6 +14,8 @@ import Web3Core import web3swift import Web3Wallet +// MARK: - WalletConnectEVMMethod + enum WalletConnectEVMMethod: String, Codable, CaseIterable { case personalSign = "personal_sign" case sendTransaction = "eth_sendTransaction" @@ -22,6 +24,7 @@ enum WalletConnectEVMMethod: String, Codable, CaseIterable { case signTypedDataV3 = "eth_signTypedData_v3" case signTypedDataV4 = "eth_signTypedData_v4" case switchEthereumChain = "wallet_switchEthereumChain" + case watchAsset = "wallet_watchAsset" } extension Flow.ChainID { @@ -43,17 +46,19 @@ extension Flow.ChainID { } } +// MARK: - WalletConnectEVMHandler + struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { + let supportNetwork: [Flow.ChainID] = [.mainnet, .testnet] + var type: WalletConnectHandlerType { - return .evm + .evm } var nameTag: String { - return "eip155" + "eip155" } - let supportNetwork: [Flow.ChainID] = [.mainnet, .testnet] - var suppportEVMChainID: [String] { supportNetwork.compactMap { $0.evmChainID }.map { String($0) } } @@ -64,7 +69,9 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { reference = chains.first(where: { $0.namespace == nameTag })?.reference } if let chains = sessionProposal.optionalNamespaces?[nameTag]?.chains { - reference = chains.filter { $0.namespace == nameTag && suppportEVMChainID.contains($0.reference) }.compactMap { $0.reference }.sorted().last + reference = chains + .filter { $0.namespace == nameTag && suppportEVMChainID.contains($0.reference) } + .compactMap { $0.reference }.sorted().last } switch reference { case "646": @@ -78,17 +85,30 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { } } - func approveSessionNamespaces(sessionProposal: Session.Proposal) throws -> [String: SessionNamespace] { + func approveSessionNamespaces( + sessionProposal: Session + .Proposal + ) throws -> [String: SessionNamespace] { guard let account = EVMAccountManager.shared.accounts.first?.address.addHexPrefix() else { return [:] } // Following properties are used to support all the required and optional namespaces for the testing purposes let supportedMethods = WalletConnectEVMMethod.allCases.map(\.rawValue) - let supportedEvents = Set(sessionProposal.requiredNamespaces.flatMap { $0.value.events } + (sessionProposal.optionalNamespaces?.flatMap { $0.value.events } ?? [])) + let supportedEvents = Set( + sessionProposal.requiredNamespaces + .flatMap { $0.value.events } + + (sessionProposal.optionalNamespaces?.flatMap { $0.value.events } ?? []) + ) - let supportedChains = supportNetwork.compactMap(\.evmChainIDString).compactMap { Blockchain(namespace: nameTag, reference: $0) } + let supportedChains = supportNetwork.compactMap(\.evmChainIDString).compactMap { Blockchain( + namespace: nameTag, + reference: $0 + ) } - let supportedAccounts = Array(supportedChains).map { WalletConnectSign.Account(blockchain: $0, address: account)! } + let supportedAccounts = Array(supportedChains).map { WalletConnectSign.Account( + blockchain: $0, + address: account + )! } let sessionNamespaces: [String: SessionNamespace] = try AutoNamespaces.build( sessionProposal: sessionProposal, @@ -100,7 +120,11 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { return sessionNamespaces } - func handlePersonalSignRequest(request: Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + func handlePersonalSignRequest( + request: Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { guard let data = message(sessionRequest: request) else { cancel() return @@ -109,10 +133,12 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { let title = pair?.peer?.name ?? "unknown" let url = pair?.peer?.url ?? "unknown" let logo = pair?.peer?.icons.first - let vm = BrowserSignMessageViewModel(title: title, - url: url, - logo: logo, - cadence: data.hexString) { result in + let vm = BrowserSignMessageViewModel( + title: title, + url: url, + logo: logo, + cadence: data.hexString + ) { result in if result { guard let addrStr = WalletManager.shared.getPrimaryWalletAddress() else { HUD.error(title: "invalid_address".localized) @@ -127,7 +153,12 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { return } let keyIndex = BigUInt(WalletManager.shared.keyIndex) - let proof = COAOwnershipProof(keyIninces: [keyIndex], address: address.data, capabilityPath: "evm", signatures: [sig]) + let proof = COAOwnershipProof( + keyIninces: [keyIndex], + address: address.data, + capabilityPath: "evm", + signatures: [sig] + ) guard let encoded = RLP.encode(proof.rlpList) else { return } @@ -140,7 +171,11 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { Router.route(to: RouteMap.Explore.signMessage(vm)) } - func handleSendTransactionRequest(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + func handleSendTransactionRequest( + request: WalletConnectSign.Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { let pair = try? Pair.instance.getPairing(for: request.topic) let title = pair?.peer?.name ?? "unknown" let url = pair?.peer?.url ?? "unknown" @@ -162,17 +197,24 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { .uint64(receiveModel.gasValue), ] - let vm = BrowserAuthzViewModel(title: title, - url: url, - logo: logo, - cadence: originCadence, - arguments: args.toArguments()) { result in + let vm = BrowserAuthzViewModel( + title: title, + url: url, + logo: logo, + cadence: originCadence, + arguments: args.toArguments() + ) { result in Task { if !result { cancel() } - let txid = try await FlowNetwork.sendTransaction(amount: receiveModel.amount, data: receiveModel.dataValue, toAddress: toAddr, gas: receiveModel.gasValue) + let txid = try await FlowNetwork.sendTransaction( + amount: receiveModel.amount, + data: receiveModel.dataValue, + toAddress: toAddr, + gas: receiveModel.gasValue + ) let holder = TransactionManager.TransactionHolder(id: txid, type: .transferCoin) TransactionManager.shared.newTransaction(holder: holder) let tixResult = try await txid.onceSealed() @@ -194,30 +236,39 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { cancel() } } - - func handleSignTypedDataV4(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + + func handleSignTypedDataV4( + request: WalletConnectSign.Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { let pair = try? Pair.instance.getPairing(for: request.topic) let title = pair?.peer?.name ?? "unknown" let url = pair?.peer?.url ?? "unknown" let logo = pair?.peer?.icons.first - + do { let list = try request.params.get([String].self) let evmAddress = EVMAccountManager.shared.accounts.first?.showAddress.lowercased() - + if list.count != 2 { cancel() return } - - var dataStr: String = "" + + var dataStr = "" if list[0].lowercased() == evmAddress { dataStr = list[1] - }else { + } else { dataStr = list[0] } - - let vm = BrowserSignTypedMessageViewModel(title: title, urlString: url, logo: logo, rawString: dataStr) { result in + + let vm = BrowserSignTypedMessageViewModel( + title: title, + urlString: url, + logo: logo, + rawString: dataStr + ) { result in if result { do { @@ -234,27 +285,62 @@ struct WalletConnectEVMHandler: WalletConnectChildHandlerProtocol { return } let keyIndex = BigUInt(WalletManager.shared.keyIndex) - let proof = COAOwnershipProof(keyIninces: [keyIndex], address: address.data, capabilityPath: "evm", signatures: [sig]) + let proof = COAOwnershipProof( + keyIninces: [keyIndex], + address: address.data, + capabilityPath: "evm", + signatures: [sig] + ) guard let encoded = RLP.encode(proof.rlpList) else { return } - confirm(encoded.hexString.addHexPrefix()) - }catch { + confirm(encoded.hexString.addHexPrefix()) + } catch { cancel() } - + } else { cancel() } } Router.route(to: RouteMap.Explore.signTypedMessage(vm)) - - }catch { + + } catch { log.error("[EVM] handleSignTypedDataV4 \(error)", context: error) cancel() } } + + func handleWatchAsset( + request: Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { + guard let model = try? request.params.get(WalletConnectEVMHandler.WatchAsset.self), + let address = model.options?.address else { + cancel() + return + } + Task { + HUD.loading() + let manager = WalletManager.shared.customTokenManager + guard let token = try await manager.findToken(evmAddress: address) else { + HUD.dismissLoading() + DispatchQueue.main.async { + confirm("false") + } + return + } + HUD.dismissLoading() + let callback: BoolClosure = { result in + DispatchQueue.main.async { + confirm(result ? "true" : "false") + } + } + Router.route(to: RouteMap.Wallet.addTokenSheet(token, callback)) + } + } } extension WalletConnectEVMHandler { @@ -265,6 +351,23 @@ extension WalletConnectEVMHandler { } private func signWithMessage(data: Data) -> Data? { - return WalletManager.shared.signSync(signableData: data) + WalletManager.shared.signSync(signableData: data) + } +} + +// MARK: WalletConnectEVMHandler.WatchAsset + +extension WalletConnectEVMHandler { + private struct WatchAsset: Codable { + struct Info: Codable { + let address: String? + } + + let options: WatchAsset.Info? + let type: String? + + var isERC20: Bool { + type?.lowercased() == "ERC20".lowercased() + } } } diff --git a/FRW/Services/Manager/WalletConnect/WalletConnectFlowHandler.swift b/FRW/Services/Manager/WalletConnect/WalletConnectFlowHandler.swift index 8844b330..e74a5956 100644 --- a/FRW/Services/Manager/WalletConnect/WalletConnectFlowHandler.swift +++ b/FRW/Services/Manager/WalletConnect/WalletConnectFlowHandler.swift @@ -11,11 +11,11 @@ import WalletConnectSign struct WalletConnectFlowHandler: WalletConnectChildHandlerProtocol { var type: WalletConnectHandlerType { - return .flow + .flow } var nameTag: String { - return "flow" + "flow" } func chainId(sessionProposal: Session.Proposal) -> Flow.ChainID? { @@ -27,31 +27,61 @@ struct WalletConnectFlowHandler: WalletConnectChildHandlerProtocol { return Flow.ChainID(name: reference.lowercased()) } - func approveSessionNamespaces(sessionProposal: Session.Proposal) throws -> [String: SessionNamespace] { + func approveSessionNamespaces( + sessionProposal: Session + .Proposal + ) throws -> [String: SessionNamespace] { guard let account = WalletManager.shared.getPrimaryWalletAddress() else { return [:] } var sessionNamespaces = [String: SessionNamespace]() - sessionProposal.requiredNamespaces.forEach { - let caip2Namespace = $0.key - let proposalNamespace = $0.value + for requiredNamespace in sessionProposal.requiredNamespaces { + let caip2Namespace = requiredNamespace.key + let proposalNamespace = requiredNamespace.value if let chains = proposalNamespace.chains { - let accounts = Array(chains.compactMap { WalletConnectSign.Account($0.absoluteString + ":\(account)") }) - let sessionNamespace = SessionNamespace(accounts: accounts, methods: proposalNamespace.methods, events: proposalNamespace.events) + let accounts = Array( + chains + .compactMap { WalletConnectSign.Account($0.absoluteString + ":\(account)") } + ) + let sessionNamespace = SessionNamespace( + accounts: accounts, + methods: proposalNamespace.methods, + events: proposalNamespace.events + ) sessionNamespaces[caip2Namespace] = sessionNamespace } } return sessionNamespaces } - func handlePersonalSignRequest(request _: Request, confirm _: @escaping (String) -> Void, cancel _: @escaping () -> Void) {} + func handlePersonalSignRequest( + request _: Request, + confirm _: @escaping (String) -> Void, + cancel _: @escaping () -> Void + ) {} - func handleSendTransactionRequest(request _: WalletConnectSign.Request, confirm _: @escaping (String) -> Void, cancel: @escaping () -> Void) { + func handleSendTransactionRequest( + request _: WalletConnectSign.Request, + confirm _: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { cancel() } - - func handleSignTypedDataV4(request: Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + + func handleSignTypedDataV4( + request _: Request, + confirm _: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { + cancel() + } + + func handleWatchAsset( + request _: Request, + confirm _: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { cancel() } } diff --git a/FRW/Services/Manager/WalletConnect/WalletConnectHandler.swift b/FRW/Services/Manager/WalletConnect/WalletConnectHandler.swift index a562ac84..21a60f3f 100644 --- a/FRW/Services/Manager/WalletConnect/WalletConnectHandler.swift +++ b/FRW/Services/Manager/WalletConnect/WalletConnectHandler.swift @@ -9,39 +9,17 @@ import Flow import Foundation import WalletConnectSign +// MARK: - WalletConnectHandler + // https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#evm-gateway-endpoints struct WalletConnectHandler { - private var allowNamespaces: [String] { - return [ - flowHandler.nameTag, - EVMHandler.nameTag, - ] - } - - private let flowHandler = WalletConnectFlowHandler() - private let EVMHandler = WalletConnectEVMHandler() - - private func current(sessionProposal: Session.Proposal) -> WalletConnectChildHandlerProtocol { - let namespaces = namespaceTag(sessionProposal: sessionProposal) - if namespaces.contains(EVMHandler.nameTag) { - return EVMHandler - } - return flowHandler - } - - private func current(request: WalletConnectSign.Request) -> WalletConnectChildHandlerProtocol { - let chainId = request.chainId - if chainId.namespace.contains(EVMHandler.nameTag) { - return EVMHandler - } - return flowHandler - } + // MARK: Internal func isAllowedSession(sessionProposal: Session.Proposal) -> Bool { let namespaces = namespaceTag(sessionProposal: sessionProposal) let result = allowNamespaces.filter { namespaces.contains($0) } - return result.count > 0 + return !result.isEmpty } func chainReference(sessionProposal: Session.Proposal) -> String? { @@ -60,7 +38,10 @@ struct WalletConnectHandler { return handle.chainId(sessionProposal: sessionProposal) } - func approveSessionNamespaces(sessionProposal: Session.Proposal) throws -> [String: SessionNamespace] { + func approveSessionNamespaces( + sessionProposal: Session + .Proposal + ) throws -> [String: SessionNamespace] { let handle = current(sessionProposal: sessionProposal) return try handle.approveSessionNamespaces(sessionProposal: sessionProposal) } @@ -70,24 +51,74 @@ struct WalletConnectHandler { return handle.type } - func handlePersonalSignRequest(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + func handlePersonalSignRequest( + request: WalletConnectSign.Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { let handle = current(request: request) handle.handlePersonalSignRequest(request: request, confirm: confirm, cancel: cancel) } - func handleSendTransactionRequest(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + func handleSendTransactionRequest( + request: WalletConnectSign.Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { let handle = current(request: request) handle.handleSendTransactionRequest(request: request, confirm: confirm, cancel: cancel) } - - func handleSignTypedDataV4(request: WalletConnectSign.Request, confirm: @escaping (String) -> Void, cancel: @escaping () -> Void) { + + func handleSignTypedDataV4( + request: WalletConnectSign.Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { let handle = current(request: request) handle.handleSignTypedDataV4(request: request, confirm: confirm, cancel: cancel) } + + func handleWatchAsset( + request: WalletConnectSign.Request, + confirm: @escaping (String) -> Void, + cancel: @escaping () -> Void + ) { + let handle = current(request: request) + handle.handleWatchAsset(request: request, confirm: confirm, cancel: cancel) + } + + // MARK: Private + + private let flowHandler = WalletConnectFlowHandler() + private let EVMHandler = WalletConnectEVMHandler() + + private var allowNamespaces: [String] { + [ + flowHandler.nameTag, + EVMHandler.nameTag, + ] + } + + private func current(sessionProposal: Session.Proposal) -> WalletConnectChildHandlerProtocol { + let namespaces = namespaceTag(sessionProposal: sessionProposal) + if namespaces.contains(EVMHandler.nameTag) { + return EVMHandler + } + return flowHandler + } + + private func current(request: WalletConnectSign.Request) -> WalletConnectChildHandlerProtocol { + let chainId = request.chainId + if chainId.namespace.contains(EVMHandler.nameTag) { + return EVMHandler + } + return flowHandler + } } extension WalletConnectHandler { private func namespaceTag(sessionProposal: Session.Proposal) -> [String] { - return Array(sessionProposal.requiredNamespaces.keys) + Array((sessionProposal.optionalNamespaces ?? [:]).keys) + Array(sessionProposal.requiredNamespaces.keys) + + Array((sessionProposal.optionalNamespaces ?? [:]).keys) } } diff --git a/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift b/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift index a5dc8798..6da82345 100644 --- a/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift +++ b/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift @@ -21,40 +21,28 @@ import WalletConnectUtils import WalletCore import Web3Wallet -class WalletConnectManager: ObservableObject { - static let shared = WalletConnectManager() - - @Published - var activeSessions: [Session] = [] - - @Published - var activePairings: [Pairing] = [] - - @Published var pendingRequests: [WalletConnectSign.Request] = [] - - var onClientConnected: (() -> Void)? - - private var publishers = [AnyCancellable]() - private var pendingRequestCheckTimer: Timer? - private var handler = WalletConnectHandler() - - var currentProposal: Session.Proposal? - var currentRequest: WalletConnectSign.Request? - var currentSessionInfo: SessionInfo? - var currentRequestInfo: RequestInfo? - var currentMessageInfo: RequestMessageInfo? - - @Published var setSessions: [Session] = [] - - private var syncAccountFlag: Bool = false - - private var cacheReqeust:[String] = [] +// MARK: - WalletConnectManager +class WalletConnectManager: ObservableObject { + // MARK: Lifecycle init() { - let redirect = try! AppMetadata.Redirect(native: "frw://", universal: "https://frw-link.lilico.app") - let metadata = AppMetadata(name: "Flow Wallet", description: "Digital wallet created for everyone.", url: "https://frw-link.lilico.app", icons: ["https://frw-link.lilico.app/logo.png"], redirect: redirect) - Networking.configure(groupIdentifier: AppGroupName, projectId: LocalEnvManager.shared.walletConnectProjectID, socketFactory: SocketFactory()) + let redirect = try! AppMetadata.Redirect( + native: "frw://", + universal: "https://frw-link.lilico.app" + ) + let metadata = AppMetadata( + name: "Flow Wallet", + description: "Digital wallet created for everyone.", + url: "https://frw-link.lilico.app", + icons: ["https://frw-link.lilico.app/logo.png"], + redirect: redirect + ) + Networking.configure( + groupIdentifier: AppGroupName, + projectId: LocalEnvManager.shared.walletConnectProjectID, + socketFactory: SocketFactory() + ) Pair.configure(metadata: metadata) Sign.configure(crypto: DefaultCryptoProvider()) Web3Wallet.configure(metadata: metadata, crypto: DefaultCryptoProvider()) @@ -82,17 +70,45 @@ class WalletConnectManager: ObservableObject { } }.store(in: &publishers) - NotificationCenter.default.addObserver(self, selector: #selector(reloadPendingRequests), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(reloadPendingRequests), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) } + // MARK: Internal + + static let shared = WalletConnectManager() + + @Published + var activeSessions: [Session] = [] + + @Published + var activePairings: [Pairing] = [] + + @Published + var pendingRequests: [WalletConnectSign.Request] = [] + + var onClientConnected: (() -> Void)? + + var currentProposal: Session.Proposal? + var currentRequest: WalletConnectSign.Request? + var currentSessionInfo: SessionInfo? + var currentRequestInfo: RequestInfo? + var currentMessageInfo: RequestMessageInfo? + + @Published + var setSessions: [Session] = [] + func connect(link: String) { debugPrint("WalletConnectManager -> connect(), Thread: \(Thread.isMainThread)") print("[RESPONDER] Pairing to: \(link)") Task { do { if let removedLink = link.removingPercentEncoding, - let uri = WalletConnectURI(string: removedLink) - { + let uri = WalletConnectURI(string: removedLink) { // TODO: commit #if DEBUG // if Pair.instance.getPairings().contains(where: { $0.topic == uri.topic }) { @@ -131,8 +147,17 @@ class WalletConnectManager: ObservableObject { self.activePairings = activePairings } - func encodeAccountProof(address: String, nonce: String, appIdentifier: String, includeDomaintag: Bool = true) -> Data? { - let list: [Any] = [appIdentifier.data(using: .utf8) ?? Data(), Data(hex: address), Data(hex: nonce)] + func encodeAccountProof( + address: String, + nonce: String, + appIdentifier: String, + includeDomaintag: Bool = true + ) -> Data? { + let list: [Any] = [ + appIdentifier.data(using: .utf8) ?? Data(), + Data(hex: address), + Data(hex: nonce), + ] guard let rlp = RLP.encode(list) else { return nil } @@ -227,6 +252,16 @@ class WalletConnectManager: ObservableObject { }.store(in: &publishers) } + // MARK: Private + + private var publishers = [AnyCancellable]() + private var pendingRequestCheckTimer: Timer? + private var handler = WalletConnectHandler() + + private var syncAccountFlag: Bool = false + + private var cacheReqeust: [String] = [] + private func navigateBackTodApp(topic: String) { // TODO: #six // WalletConnectRouter.Router.goBack() @@ -242,7 +277,13 @@ extension WalletConnectManager { private func startPendingRequestCheckTimer() { stopPendingRequestCheckTimer() - let timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(reloadPendingRequests), userInfo: nil, repeats: true) + let timer = Timer.scheduledTimer( + timeInterval: 5, + target: self, + selector: #selector(reloadPendingRequests), + userInfo: nil, + repeats: true + ) RunLoop.main.add(timer, forMode: .common) pendingRequestCheckTimer = timer @@ -255,13 +296,18 @@ extension WalletConnectManager { } } - @objc func reloadPendingRequests() { + @objc + func reloadPendingRequests() { if UserManager.shared.isLoggedIn { - pendingRequests = Sign.instance.getPendingRequests().map { (request: Request, _: VerifyContext?) in + pendingRequests = Sign.instance.getPendingRequests().map { ( + request: Request, + _: VerifyContext? + ) in request } - WalletNewsHandler.shared.refreshWalletConnectNews(pendingRequests.map { $0.toLocalNews() }) + WalletNewsHandler.shared + .refreshWalletConnectNews(pendingRequests.map { $0.toLocalNews() }) } } } @@ -279,7 +325,8 @@ extension WalletConnectManager { guard network == LocalUserDefaults.shared.flowNetwork.toFlowType() else { rejectSession(proposal: sessionProposal) let current = LocalUserDefaults.shared.flowNetwork - guard let toNetwork = LocalUserDefaults.FlowNetworkType(chainId: network) else { return } + guard let toNetwork = LocalUserDefaults.FlowNetworkType(chainId: network) + else { return } Router.route(to: RouteMap.Explore.switchNetwork(current, toNetwork, nil)) return } @@ -296,11 +343,13 @@ extension WalletConnectManager { address = EVMAccountManager.shared.accounts.first?.showAddress ?? "" } currentSessionInfo = info - let authnVM = BrowserAuthnViewModel(title: info.name, - url: info.dappURL, - logo: info.iconURL, - walletAddress: address, - network: network) { result in + let authnVM = BrowserAuthnViewModel( + title: info.name, + url: info.dappURL, + logo: info.iconURL, + walletAddress: address, + network: network + ) { result in if result { // TODO: Handle network mismatch self.approveSession(proposal: sessionProposal) @@ -310,18 +359,17 @@ extension WalletConnectManager { } Router.route(to: RouteMap.Explore.authn(authnVM)) - } func handleRequest(_ sessionRequest: WalletConnectSign.Request) { let address = WalletManager.shared.address.hex.addHexPrefix() let keyId = WalletManager.shared.keyIndex - + if cacheReqeust.contains(sessionRequest.id.string) { return } cacheReqeust.append(sessionRequest.id.string) - + switch sessionRequest.method { case FCLWalletConnectMethod.authn.rawValue: @@ -332,56 +380,104 @@ extension WalletConnectManager { var services = [ // Since fcl-js is not implement pre-authz, hence we disable it for now - serviceDefinition(address: RemoteConfigManager.shared.payer, keyId: RemoteConfigManager.shared.keyIndex, type: .preAuthz), + serviceDefinition( + address: RemoteConfigManager.shared.payer, + keyId: RemoteConfigManager.shared.keyIndex, + type: .preAuthz + ), serviceDefinition(address: address, keyId: keyId, type: .authn), serviceDefinition(address: address, keyId: keyId, type: .authz), serviceDefinition(address: address, keyId: keyId, type: .userSignature), ] - + SecurityManager.shared.openIgnoreOnce() if let model = try? JSONDecoder().decode(BaseConfigRequest.self, from: data), let nonce = model.accountProofNonce ?? model.nonce, let appIdentifier = model.appIdentifier, - let data = self.encodeAccountProof(address: address, nonce: nonce, appIdentifier: appIdentifier), - let signedData = try? await WalletManager.shared.sign(signableData: data) - { - services.append(accountProofServiceDefinition(address: address, keyId: keyId, nonce: nonce, signature: signedData.hexValue)) + let data = self.encodeAccountProof( + address: address, + nonce: nonce, + appIdentifier: appIdentifier + ), + let signedData = try? await WalletManager.shared.sign(signableData: data) { + services.append(accountProofServiceDefinition( + address: address, + keyId: keyId, + nonce: nonce, + signature: signedData.hexValue + )) } - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: address, fType: "AuthnResponse", fVsn: "1.0.0", - services: services), - reason: nil, - compositeSignature: nil) - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: address, + fType: "AuthnResponse", + fVsn: "1.0.0", + services: services + ), + reason: nil, + compositeSignature: nil + ) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(result)) + ) self.navigateBackTodApp(topic: sessionRequest.topic) } catch { print("[WALLET] Respond Error: \(error.localizedDescription)") rejectRequest(request: sessionRequest) } } - case FCLWalletConnectMethod.preAuthz.rawValue: - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: address, fType: "AuthnResponse", fVsn: "1.0.0", - services: nil, - proposer: serviceDefinition(address: address, keyId: keyId, type: .authz), - payer: - [serviceDefinition(address: RemoteConfigManager.shared.payer, keyId: RemoteConfigManager.shared.keyIndex, type: .authz)], - authorization: [serviceDefinition(address: address, keyId: keyId, type: .authz)]), - reason: nil, - compositeSignature: nil) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: address, + fType: "AuthnResponse", + fVsn: "1.0.0", + services: nil, + proposer: serviceDefinition( + address: address, + keyId: keyId, + type: .authz + ), + payer: + [serviceDefinition( + address: RemoteConfigManager.shared + .payer, + keyId: RemoteConfigManager.shared + .keyIndex, + type: .authz + )], + authorization: [serviceDefinition( + address: address, + keyId: keyId, + type: .authz + )] + ), + reason: nil, + compositeSignature: nil + ) Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(result))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(result)) + ) } catch { print("[WALLET] Respond Error: \(error.localizedDescription)") rejectRequest(request: sessionRequest) } } - case FCLWalletConnectMethod.authz.rawValue: do { @@ -398,8 +494,7 @@ extension WalletConnectManager { var model: Signable? if let data = Data(base64Encoded: json), data.isGzipped, - let uncompressData = try? data.gunzipped() - { + let uncompressData = try? data.gunzipped() { model = try JSONDecoder().decode(Signable.self, from: uncompressData) } else if let data = json.data(using: .utf8) { model = try JSONDecoder().decode(Signable.self, from: data) @@ -410,17 +505,38 @@ extension WalletConnectManager { } if model.roles.payer, !model.roles.proposer, !model.roles.authorizer { - approvePayerRequest(request: sessionRequest, model: model, message: model.message) + approvePayerRequest( + request: sessionRequest, + model: model, + message: model.message + ) navigateBackTodApp(topic: sessionRequest.topic) return } if let session = activeSessions.first(where: { $0.topic == sessionRequest.topic }) { - let request = RequestInfo(cadence: model.cadence ?? "", agrument: model.args, name: session.peer.name, descriptionText: session.peer.description, dappURL: session.peer.url, iconURL: session.peer.icons.first ?? "", chains: Set(arrayLiteral: sessionRequest.chainId), methods: nil, pendingRequests: [], message: model.message) + let request = RequestInfo( + cadence: model.cadence ?? "", + agrument: model.args, + name: session.peer.name, + descriptionText: session.peer.description, + dappURL: session.peer.url, + iconURL: session.peer.icons.first ?? "", + chains: Set(arrayLiteral: sessionRequest.chainId), + methods: nil, + pendingRequests: [], + message: model.message + ) currentRequestInfo = request - let authzVM = BrowserAuthzViewModel(title: request.name, url: request.dappURL, logo: request.iconURL, cadence: request.cadence, arguments: request.agrument) { result in + let authzVM = BrowserAuthzViewModel( + title: request.name, + url: request.dappURL, + logo: request.iconURL, + cadence: request.cadence, + arguments: request.agrument + ) { result in if result { self.approveRequest(request: sessionRequest, requestInfo: request) } else { @@ -440,7 +556,6 @@ extension WalletConnectManager { log.error("WalletConnectManager -> Respond Error:", context: error) rejectRequest(request: sessionRequest) } - case FCLWalletConnectMethod.userSignature.rawValue: do { @@ -453,8 +568,7 @@ extension WalletConnectManager { var model: SignableMessage? if let data = Data(base64Encoded: json), data.isGzipped, - let uncompressData = try? data.gunzipped() - { + let uncompressData = try? data.gunzipped() { model = try JSONDecoder().decode(SignableMessage.self, from: uncompressData) } else if let data = json.data(using: .utf8) { model = try JSONDecoder().decode(SignableMessage.self, from: data) @@ -464,12 +578,29 @@ extension WalletConnectManager { } if let session = activeSessions.first(where: { $0.topic == sessionRequest.topic }) { - let request = RequestMessageInfo(name: session.peer.name, descriptionText: session.peer.description, dappURL: session.peer.url, iconURL: session.peer.icons.first ?? "", chains: Set(arrayLiteral: sessionRequest.chainId), methods: nil, pendingRequests: [], message: model.message) + let request = RequestMessageInfo( + name: session.peer.name, + descriptionText: session.peer.description, + dappURL: session.peer.url, + iconURL: session.peer.icons.first ?? "", + chains: Set(arrayLiteral: sessionRequest.chainId), + methods: nil, + pendingRequests: [], + message: model.message + ) currentMessageInfo = request - let vm = BrowserSignMessageViewModel(title: request.name, url: request.dappURL, logo: request.iconURL, cadence: request.message) { result in + let vm = BrowserSignMessageViewModel( + title: request.name, + url: request.dappURL, + logo: request.iconURL, + cadence: request.message + ) { result in if result { - self.approveRequestMessage(request: sessionRequest, requestInfo: request) + self.approveRequestMessage( + request: sessionRequest, + requestInfo: request + ) } else { self.rejectRequest(request: sessionRequest) } @@ -486,7 +617,11 @@ extension WalletConnectManager { Task { do { let param = try WalletConnectSyncDevice.packageUserInfo() - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(param)) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(param) + ) } catch { log.error("[WALLET] Respond Error: [accountInfo] \(error.localizedDescription)") rejectRequest(request: sessionRequest) @@ -495,15 +630,22 @@ extension WalletConnectManager { case FCLWalletConnectMethod.addDeviceInfo.rawValue: Task { do { - let res = try sessionRequest.params.get(SyncInfo.SyncResponse.self) + let res = try sessionRequest.params + .get(SyncInfo.SyncResponse.self) let viewModel = SyncAddDeviceViewModel(with: res.data!) { result in if result { Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(""))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable("")) + ) } catch { self.rejectRequest(request: sessionRequest) - print("[WALLET] Request Error: [addDeviceInfo] \(error.localizedDescription)") + print( + "[WALLET] Request Error: [addDeviceInfo] \(error.localizedDescription)" + ) } } } else { @@ -518,11 +660,15 @@ extension WalletConnectManager { } case WalletConnectEVMMethod.personalSign.rawValue: log.info("[EVM] sign person") - + handler.handlePersonalSignRequest(request: sessionRequest) { signStr in Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(signStr))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(signStr)) + ) } catch { self.rejectRequest(request: sessionRequest) log.error("[EVM] Request Error: [personalSign] \(error)") @@ -537,7 +683,11 @@ extension WalletConnectManager { handler.handleSendTransactionRequest(request: sessionRequest) { signStr in Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(signStr))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(signStr)) + ) } catch { self.rejectRequest(request: sessionRequest) log.error("[EVM] Request Error: [sendTransaction] \(error)") @@ -553,6 +703,8 @@ extension WalletConnectManager { handleSignTypedData(sessionRequest) case WalletConnectEVMMethod.signTypedDataV4.rawValue: handleSignTypedData(sessionRequest) + case WalletConnectEVMMethod.watchAsset.rawValue: + handleWatchAsset(sessionRequest) default: rejectRequest(request: sessionRequest, reason: "unspport method") } @@ -572,10 +724,16 @@ extension WalletConnectManager { let user = try WalletConnectSyncDevice.parseAccount(data: data) Router.route(to: RouteMap.RestoreLogin.syncAccount(user)) } catch { - log.error("[WALLET] Respond Error: [account info] \(error.localizedDescription)") + log + .error( + "[WALLET] Respond Error: [account info] \(error.localizedDescription)" + ) } } else if WalletConnectSyncDevice.isDevice(request: request, with: response) { - NotificationCenter.default.post(name: .syncDeviceStatusDidChanged, object: WalletConnectSyncDevice.SyncResult.success) + NotificationCenter.default.post( + name: .syncDeviceStatusDidChanged, + object: WalletConnectSyncDevice.SyncResult.success + ) } case let .error(error): if WalletConnectSyncDevice.isDevice(request: request, with: response) { @@ -586,12 +744,16 @@ extension WalletConnectManager { HUD.error(title: "process_failed_text".localized) } } - + private func handleSignTypedData(_ sessionRequest: WalletConnectSign.Request) { handler.handleSignTypedDataV4(request: sessionRequest) { signStr in Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(signStr))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(signStr)) + ) } catch { self.rejectRequest(request: sessionRequest) log.error("[EVM] Request Error: [signTypedDataV4] \(error)") @@ -602,13 +764,35 @@ extension WalletConnectManager { self.rejectRequest(request: sessionRequest) } } + + private func handleWatchAsset(_ sessionRequest: WalletConnectSign.Request) { + handler.handleWatchAsset(request: sessionRequest) { result in + Task { + do { + try await Sign.instance + .respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(result)) + ) + } catch { + self.rejectRequest(request: sessionRequest) + log.error("[EVM] Add Custom Token Error: \(error)") + } + } + + } cancel: { + log.error("[EVM] invalid token") + self.rejectRequest(request: sessionRequest) + } + } } // MARK: - Action extension WalletConnectManager { private func approveSession(proposal: Session.Proposal) { - guard let account = WalletManager.shared.getPrimaryWalletAddress() else { + guard WalletManager.shared.getPrimaryWalletAddress() != nil else { return } @@ -627,7 +811,10 @@ extension WalletConnectManager { private func rejectSession(proposal: Session.Proposal) { Task { do { - try await Sign.instance.rejectSession(proposalId: proposal.id, reason: .userRejected) + try await Sign.instance.rejectSession( + proposalId: proposal.id, + reason: .userRejected + ) HUD.success(title: "rejected".localized) } catch { HUD.error(title: "reject_failed".localized) @@ -645,12 +832,27 @@ extension WalletConnectManager { let data = Data(requestInfo.message.hexValue) let signedData = try await WalletManager.shared.sign(signableData: data) let signature = signedData.hexValue - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: account, fType: "CompositeSignature", fVsn: "1.0.0", services: nil, keyId: 0, signature: signature), - reason: nil, - compositeSignature: nil) - - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: account, + fType: "CompositeSignature", + fVsn: "1.0.0", + services: nil, + keyId: 0, + signature: signature + ), + reason: nil, + compositeSignature: nil + ) + + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "approved".localized) } catch { @@ -670,13 +872,31 @@ extension WalletConnectManager { do { let tx = model.voucher.toFCLVoucher() let data = Data(message.hexValue) - let signedData = try await RemoteConfigManager.shared.sign(voucher: tx, signableData: data) + let signedData = try await RemoteConfigManager.shared.sign( + voucher: tx, + signableData: data + ) let signature = signedData.hexValue - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: account, fType: "CompositeSignature", fVsn: "1.0.0", services: nil, keyId: 0, signature: signature), - reason: nil, - compositeSignature: nil) - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: account, + fType: "CompositeSignature", + fVsn: "1.0.0", + services: nil, + keyId: 0, + signature: signature + ), + reason: nil, + compositeSignature: nil + ) + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "payer_approved".localized) } catch { log.error("WalletConnectManager -> approveRequest failed", context: error) @@ -686,13 +906,21 @@ extension WalletConnectManager { } private func rejectRequest(request: Request, reason: String = "User reject request") { - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .declined, - reason: reason, - compositeSignature: nil) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .declined, + reason: reason, + compositeSignature: nil + ) Task { do { - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "rejected".localized) } catch { log.error("WalletConnectManager -> approveRequest failed", context: error) @@ -714,11 +942,26 @@ extension WalletConnectManager { let data = Flow.DomainTag.user.normalize + Data(requestInfo.message.hexValue) let signedData = try await WalletManager.shared.sign(signableData: data) let signature = signedData.hexValue - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: account, fType: "CompositeSignature", fVsn: "1.0.0", services: nil, keyId: 0, signature: signature), - reason: nil, - compositeSignature: nil) - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: account, + fType: "CompositeSignature", + fVsn: "1.0.0", + services: nil, + keyId: 0, + signature: signature + ), + reason: nil, + compositeSignature: nil + ) + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "approved".localized) } catch { debugPrint("WalletConnectManager -> approveRequestMessage failed: \(error)") @@ -752,7 +995,8 @@ extension WalletConnectManager { Task { do { - self.currentRequest = try await WalletConnectSyncDevice.requestSyncAccount(in: session) + self.currentRequest = try await WalletConnectSyncDevice + .requestSyncAccount(in: session) } catch { // TODO: log.error("[sync]-account: send sync account requst failed") @@ -770,22 +1014,25 @@ extension WalletConnectManager { } func findSession(topic: String) -> Session? { - return activeSessions.first(where: { $0.topic == topic }) + activeSessions.first(where: { $0.topic == topic }) } } extension WalletConnectSign.Request { func toLocalNews() -> RemoteConfigManager.News { - return RemoteConfigManager.News(id: topic, - priority: .urgent, - type: .message, - title: "Pending Request - \(name ?? "Unknown")", - body: "You have a pending request from \((dappURL?.host) ?? "Unknown").", - icon: logoURL?.absoluteString ?? AppPlaceholder.image, - image: nil, - url: nil, - expiryTime: .distantFuture, - displayType: .click, - flag: .walletconnect, conditions: nil) + RemoteConfigManager.News( + id: topic, + priority: .urgent, + type: .message, + title: "Pending Request - \(name ?? "Unknown")", + body: "You have a pending request from \((dappURL?.host) ?? "Unknown").", + icon: logoURL?.absoluteString ?? AppPlaceholder.image, + image: nil, + url: nil, + expiryTime: .distantFuture, + displayType: .click, + flag: .walletconnect, + conditions: nil + ) } } diff --git a/FRW/Services/Manager/WalletManager.swift b/FRW/Services/Manager/WalletManager.swift index 45910d6a..85c3fab9 100644 --- a/FRW/Services/Manager/WalletManager.swift +++ b/FRW/Services/Manager/WalletManager.swift @@ -5,18 +5,17 @@ // Created by Hao Fu on 30/12/21. // +import BigInt import Combine import Flow import FlowWalletCore import Foundation import KeychainAccess import Kingfisher -import WalletCore import UIKit -import web3swift +import WalletCore import Web3Core -import BigInt - +import web3swift // MARK: - Define @@ -38,47 +37,79 @@ extension WalletManager { } } +// MARK: - WalletManager + class WalletManager: ObservableObject { - static let shared = WalletManager() + // MARK: Lifecycle - @Published var walletInfo: UserWalletResponse? { - didSet { - // TODO: remove after update new Flow Wallet SDK - updateFlowAccount() + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(reset), + name: .willResetWallet, + object: nil + ) + + if UserManager.shared.activatedUID != nil { + restoreMnemonicForCurrentUser() + loadCacheData() } + + UserManager.shared.$activatedUID + .receive(on: DispatchQueue.main) + .map { $0 } + .sink { _ in + self.clearFlowAccount() + self.reloadWalletInfo() + }.store(in: &cancellableSet) } - @Published var supportedCoins: [TokenModel]? - @Published var evmSupportedCoins: [TokenModel]? - @Published var activatedCoins: [TokenModel] = [] - @Published var coinBalances: [String: Double] = [:] - @Published var childAccount: ChildAccount? = nil - @Published var evmAccount: EVMAccountManager.Account? = nil + // MARK: Internal - var accessibleManager: ChildAccountManager.AccessibleManager = .init() + static let shared = WalletManager() - private var childAccountInited: Bool = false + @Published + var supportedCoins: [TokenModel]? + @Published + var evmSupportedCoins: [TokenModel]? + @Published + var activatedCoins: [TokenModel] = [] + @Published + var coinBalances: [String: Double] = [:] + @Published + var childAccount: ChildAccount? = nil + @Published + var evmAccount: EVMAccountManager.Account? = nil + + var accessibleManager: ChildAccountManager.AccessibleManager = .init() - private var hdWallet: HDWallet? var flowAccountKey: Flow.AccountKey? // TODO: remove after update Flow Wallet SDK var phraseAccountkey: Flow.AccountKey? - var mainKeychain = Keychain(service: (Bundle.main.bundleIdentifier ?? defaultBundleID) + ".local") - .label("Lilico app backup") - .synchronizable(false) - .accessibility(.whenUnlocked) - - private var walletInfoRetryTimer: Timer? - private var cancellableSet = Set() + var mainKeychain = + Keychain(service: (Bundle.main.bundleIdentifier ?? defaultBundleID) + ".local") + .label("Lilico app backup") + .synchronizable(false) + .accessibility(.whenUnlocked) var walletAccount: WalletAccount = .init() - @Published var balanceProvider = BalanceProvider() - - var customTokenManager: CustomTokenManager = CustomTokenManager() + @Published + var balanceProvider = BalanceProvider() + + var customTokenManager: CustomTokenManager = .init() + + @Published + var walletInfo: UserWalletResponse? { + didSet { + // TODO: remove after update new Flow Wallet SDK + updateFlowAccount() + } + } var currentAccount: WalletAccount.User { - WalletManager.shared.walletAccount.readInfo(at: getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "") + WalletManager.shared.walletAccount + .readInfo(at: getWatchAddressOrChildAccountAddressOrPrimaryAddress() ?? "") } var defaultSigners: [FlowSigner] { @@ -88,28 +119,6 @@ class WalletManager: ObservableObject { return [WalletManager.shared] } - private var retryCheckCount = 1 - - - private var isShow: Bool = false - - init() { - NotificationCenter.default.addObserver(self, selector: #selector(reset), name: .willResetWallet, object: nil) - - if UserManager.shared.activatedUID != nil { - restoreMnemonicForCurrentUser() - loadCacheData() - } - - UserManager.shared.$activatedUID - .receive(on: DispatchQueue.main) - .map { $0 } - .sink { _ in - self.clearFlowAccount() - self.reloadWalletInfo() - }.store(in: &cancellableSet) - } - func bindChildAccountManager() { ChildAccountManager.shared.$selectedChildAccount .receive(on: DispatchQueue.main) @@ -145,19 +154,41 @@ class WalletManager: ObservableObject { .store(in: &cancellableSet) } + // MARK: Private + + private var childAccountInited: Bool = false + + private var hdWallet: HDWallet? + private var walletInfoRetryTimer: Timer? + private var cancellableSet = Set() + + private var retryCheckCount = 1 + + private var isShow: Bool = false + private func loadCacheData() { guard let uid = UserManager.shared.activatedUID else { return } let cacheWalletInfo = MultiAccountStorage.shared.getWalletInfo(uid) Task { - let cacheSupportedCoins = try? await PageCache.cache.get(forKey: CacheKeys.supportedCoins.rawValue, type: [TokenModel].self) - let cacheActivatedCoins = try? await PageCache.cache.get(forKey: CacheKeys.activatedCoins.rawValue, type: [TokenModel].self) - let cacheBalances = try? await PageCache.cache.get(forKey: CacheKeys.coinBalances.rawValue, type: [String: Double].self) + let cacheSupportedCoins = try? await PageCache.cache.get( + forKey: CacheKeys.supportedCoins.rawValue, + type: [TokenModel].self + ) + let cacheActivatedCoins = try? await PageCache.cache.get( + forKey: CacheKeys.activatedCoins.rawValue, + type: [TokenModel].self + ) + let cacheBalances = try? await PageCache.cache.get( + forKey: CacheKeys.coinBalances.rawValue, + type: [String: Double].self + ) DispatchQueue.main.async { self.walletInfo = cacheWalletInfo - if let cacheSupportedCoins = cacheSupportedCoins, let cacheActivatedCoins = cacheActivatedCoins { + if let cacheSupportedCoins = cacheSupportedCoins, + let cacheActivatedCoins = cacheActivatedCoins { self.supportedCoins = cacheSupportedCoins self.activatedCoins = cacheActivatedCoins } @@ -174,11 +205,11 @@ class WalletManager: ObservableObject { extension WalletManager { var isSelectedChildAccount: Bool { - return childAccount != nil + childAccount != nil } var isSelectedEVMAccount: Bool { - return evmAccount != nil + evmAccount != nil } var selectedAccountIcon: String { @@ -278,15 +309,15 @@ extension WalletManager { guard let address = address, !address.isEmpty else { return false } - return EVMAccountManager.shared.accounts + return !EVMAccountManager.shared.accounts .filter { $0.showAddress.lowercased().contains(address.lowercased()) - }.count > 0 + }.isEmpty } - + func isMain() -> Bool { - - guard let currentAddress = getWatchAddressOrChildAccountAddressOrPrimaryAddress(), !currentAddress.isEmpty else { + guard let currentAddress = getWatchAddressOrChildAccountAddressOrPrimaryAddress(), + !currentAddress.isEmpty else { return false } guard let primaryAddress = getPrimaryWalletAddress() else { @@ -311,7 +342,8 @@ extension WalletManager { flowAccountKey = nil } - @objc private func reset() { + @objc + private func reset() { debugPrint("WalletManager: reset start") resetProperties() @@ -341,7 +373,7 @@ extension WalletManager { extension WalletManager { func getCurrentMnemonic() -> String? { - return hdWallet?.mnemonic + hdWallet?.mnemonic } func getCurrentPublicKey() -> String? { @@ -352,24 +384,24 @@ extension WalletManager { } func getCurrentPrivateKey() -> String? { - return hdWallet?.getPrivateKey() + hdWallet?.getPrivateKey() } func getCurrentFlowAccountKey() -> Flow.AccountKey? { - return hdWallet?.flowAccountKey + hdWallet?.flowAccountKey } func getPrimaryWalletAddress() -> String? { - return walletInfo?.currentNetworkWalletModel?.getAddress + walletInfo?.currentNetworkWalletModel?.getAddress } func getFlowNetworkTypeAddress(network: LocalUserDefaults.FlowNetworkType) -> String? { - return walletInfo?.getNetworkWalletModel(network: network)?.getAddress + walletInfo?.getNetworkWalletModel(network: network)?.getAddress } /// get custom watch address first, then primary address, this method is only used for tab2. func getPrimaryWalletAddressOrCustomWatchAddress() -> String? { - return LocalUserDefaults.shared.customWatchAddress ?? getPrimaryWalletAddress() + LocalUserDefaults.shared.customWatchAddress ?? getPrimaryWalletAddress() } /// watch address -> child account address -> primary address @@ -394,7 +426,9 @@ extension WalletManager { } var isPreviewEnabled: Bool { - return walletInfo?.wallets?.first(where: { $0.chainId == LocalUserDefaults.FlowNetworkType.previewnet.rawValue })?.getAddress != nil + walletInfo?.wallets? + .first(where: { $0.chainId == LocalUserDefaults.FlowNetworkType.previewnet.rawValue })? + .getAddress != nil } func isTokenActivated(symbol: String) -> Bool { @@ -418,7 +452,8 @@ extension WalletManager { } func getBalance(bySymbol symbol: String) -> Double { - return coinBalances[symbol] ?? coinBalances[symbol.lowercased()] ?? coinBalances[symbol.uppercased()] ?? 0 + coinBalances[symbol] ?? coinBalances[symbol.lowercased()] ?? + coinBalances[symbol.uppercased()] ?? 0 } func currentContact() -> Contact { @@ -428,7 +463,16 @@ extension WalletManager { user = WalletManager.shared.walletAccount.readInfo(at: addr) } - let contact = Contact(address: address, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: nil, user: user) + let contact = Contact( + address: address, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: nil, + user: user + ) return contact } } @@ -440,7 +484,8 @@ extension WalletManager { func asyncCreateWalletAddressFromServer() { Task { do { - let _: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.User.userAddress) + let _: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.User.userAddress) debugPrint("WalletManager -> asyncCreateWalletAddressFromServer success") } catch { debugPrint("WalletManager -> asyncCreateWalletAddressFromServer failed") @@ -451,7 +496,13 @@ extension WalletManager { private func startWalletInfoRetryTimer() { debugPrint("WalletManager -> startWalletInfoRetryTimer") stopWalletInfoRetryTimer() - let timer = Timer.scheduledTimer(timeInterval: WalletManager.walletFetchInterval, target: self, selector: #selector(onWalletInfoRetryTimer), userInfo: nil, repeats: false) + let timer = Timer.scheduledTimer( + timeInterval: WalletManager.walletFetchInterval, + target: self, + selector: #selector(onWalletInfoRetryTimer), + userInfo: nil, + repeats: false + ) walletInfoRetryTimer = timer RunLoop.main.add(timer, forMode: .common) } @@ -463,7 +514,8 @@ extension WalletManager { } } - @objc private func onWalletInfoRetryTimer() { + @objc + private func onWalletInfoRetryTimer() { debugPrint("WalletManager -> onWalletInfoRetryTimer") reloadWalletInfo() } @@ -506,7 +558,8 @@ extension WalletManager { Task { do { if retryCheckCount % 4 == 0 { - let _: Network.EmptyResponse = try await Network.requestWithRawModel(FRWAPI.User.manualCheck) + let _: Network.EmptyResponse = try await Network + .requestWithRawModel(FRWAPI.User.manualCheck) } retryCheckCount += 1 } catch { @@ -548,7 +601,11 @@ extension WalletManager { encodedData = Data() } - try set(toMainKeychain: encodedData, forKey: getMnemonicStoreKey(uid: uid), comment: "Lilico user uid: \(uid)") + try set( + toMainKeychain: encodedData, + forKey: getMnemonicStoreKey(uid: uid), + comment: "Lilico user uid: \(uid)" + ) DispatchQueue.main.async { self.resetProperties() @@ -562,9 +619,11 @@ extension WalletManager { extension WalletManager { func getMnemonicFromKeychain(uid: String) -> String? { if var encryptedData = getEncryptedMnemonicData(uid: uid), - var decryptedData = try? WalletManager.decryptionChaChaPoly(key: uid, data: encryptedData), - var mnemonic = String(data: decryptedData, encoding: .utf8) - { + var decryptedData = try? WalletManager.decryptionChaChaPoly( + key: uid, + data: encryptedData + ), + var mnemonic = String(data: decryptedData, encoding: .utf8) { defer { encryptedData = Data() decryptedData = Data() @@ -591,9 +650,14 @@ extension WalletManager { private func restoreMnemonicFromKeychain(uid: String) -> Bool { do { if var encryptedData = getEncryptedMnemonicData(uid: uid) { - debugPrint("WalletManager -> start restore mnemonic from keychain, uid = \(uid), encryptedData.count = \(encryptedData.count)") - - var decryptedData = try WalletManager.decryptionChaChaPoly(key: uid, data: encryptedData) + debugPrint( + "WalletManager -> start restore mnemonic from keychain, uid = \(uid), encryptedData.count = \(encryptedData.count)" + ) + + var decryptedData = try WalletManager.decryptionChaChaPoly( + key: uid, + data: encryptedData + ) defer { encryptedData = Data() decryptedData = Data() @@ -608,7 +672,9 @@ extension WalletManager { } } } catch { - debugPrint("WalletManager -> restoreMnemonicFromKeyChain failed: uid = \(uid), error = \(error)") + debugPrint( + "WalletManager -> restoreMnemonicFromKeyChain failed: uid = \(uid), error = \(error)" + ) } return false @@ -628,11 +694,11 @@ extension WalletManager { extension WalletManager { private func getMnemonicStoreKey(uid: String) -> String { - return "\(WalletManager.mnemonicStoreKeyPrefix).\(uid)" + "\(WalletManager.mnemonicStoreKeyPrefix).\(uid)" } private func getEncryptedMnemonicData(uid: String) -> Data? { - return getData(fromMainKeychain: getMnemonicStoreKey(uid: uid)) + getData(fromMainKeychain: getMnemonicStoreKey(uid: uid)) } } @@ -659,7 +725,8 @@ extension WalletManager { } private func fetchSupportedCoins() async throws { - let tokenResponse: SingleTokenResponse = try await Network.requestWithRawModel(GithubEndpoint.ftTokenList) + let tokenResponse: SingleTokenResponse = try await Network + .requestWithRawModel(GithubEndpoint.ftTokenList) let coins: [TokenModel] = tokenResponse.conversion() let validCoins = coins.filter { $0.getAddress()?.isEmpty == false } DispatchQueue.main.sync { @@ -670,7 +737,7 @@ extension WalletManager { } private func fetchActivatedCoins() async throws { - guard let supportedCoins = supportedCoins, supportedCoins.count != 0 else { + guard let supportedCoins = supportedCoins, !supportedCoins.isEmpty else { DispatchQueue.main.sync { self.activatedCoins.removeAll() } @@ -687,15 +754,18 @@ extension WalletManager { var enabledList: [String: Bool] = [:] if let account = ChildAccountManager.shared.selectedChildAccount { - enabledList = try await FlowNetwork.linkedAccountEnabledTokenList(address: account.showAddress) + enabledList = try await FlowNetwork + .linkedAccountEnabledTokenList(address: account.showAddress) } else { - enabledList = try await FlowNetwork.checkTokensEnable(address: Flow.Address(hex: address)) + enabledList = try await FlowNetwork + .checkTokensEnable(address: Flow.Address(hex: address)) } var list = [TokenModel]() for (_, value) in enabledList.enumerated() { if value.value { - let model = supportedCoins.first { $0.contractId.lowercased() == value.key.lowercased() } + let model = supportedCoins + .first { $0.contractId.lowercased() == value.key.lowercased() } if let model = model { list.append(model) } @@ -715,7 +785,8 @@ extension WalletManager { return } do { - let tokenResponse: SingleTokenResponse = try await Network.requestWithRawModel(GithubEndpoint.EVMTokenList) + let tokenResponse: SingleTokenResponse = try await Network + .requestWithRawModel(GithubEndpoint.EVMTokenList) let coins: [TokenModel] = tokenResponse.conversion() DispatchQueue.main.async { self.evmSupportedCoins = coins @@ -768,44 +839,41 @@ extension WalletManager { guard var tokenModel = tokenModel, let symbol = tokenModel.symbol else { return } let list = try await EVMAccountManager.shared.fetchTokens() - + DispatchQueue.main.sync { log.info("[EVM] load balance success \(balance)") tokenModel.flowIdentifier = tokenModel.contractId self.activatedCoins = [tokenModel] self.coinBalances = [symbol: balance.doubleValue] - list.forEach { item in + for item in list { if item.flowBalance > 0 { let result = self.evmSupportedCoins?.first(where: { model in model.getAddress()?.lowercased() == item.address.lowercased() }) if let result = result { self.activatedCoins.append(result) - } else { - self.activatedCoins.append(item.toTokenModel()) + self.coinBalances[item.symbol] = item.flowBalance } - self.coinBalances[item.symbol] = item.flowBalance } } } } - + private func fetchCustomBalance() async throws { - guard let evmAddress = EVMAccountManager.shared.selectedAccount?.showAddress else { + guard (EVMAccountManager.shared.selectedAccount?.showAddress) != nil else { return } await customTokenManager.fetchAllEVMBalance() let list = customTokenManager.list DispatchQueue.main.sync { - list.forEach { token in + for token in list { addCustomToken(token: token) } } } - + func addCustomToken(token: CustomToken) { - DispatchQueue.main.async { self.activatedCoins.append(token.toToken()) let balance = token.balance ?? BigUInt(0) @@ -818,6 +886,15 @@ extension WalletManager { } } + func deleteCustomToken(token: CustomToken) { + DispatchQueue.main.async { + self.activatedCoins.removeAll { model in + model.getAddress() == token.address && model.name == token.name + } + self.coinBalances[token.symbol] = nil + } + } + func fetchAccessible() async throws { try await accessibleManager.fetchFT() } @@ -839,7 +916,11 @@ extension WalletManager { try mainKeychain.set(value, key: key) } - private func set(toMainKeychain value: Data, forKey key: String, comment: String? = nil) throws { + private func set( + toMainKeychain value: Data, + forKey key: String, + comment: String? = nil + ) throws { if let comment = comment { try mainKeychain.comment(comment).set(value, key: key) } else { @@ -848,14 +929,18 @@ extension WalletManager { } private func getString(fromMainKeychain key: String) -> String? { - return try? mainKeychain.getString(key) + try? mainKeychain.getString(key) } private func getData(fromMainKeychain key: String) -> Data? { - return try? mainKeychain.getData(key) + try? mainKeychain.getData(key) } - static func encryptionAES(key: String, iv: String = LocalEnvManager.shared.aesIV, data: Data) throws -> Data { + static func encryptionAES( + key: String, + iv: String = LocalEnvManager.shared.aesIV, + data: Data + ) throws -> Data { guard var keyData = key.data(using: .utf8), let ivData = iv.data(using: .utf8) else { throw LLError.aesKeyEncryptionFailed } @@ -865,13 +950,18 @@ extension WalletManager { keyData = keyData.paddingZeroRight(blockSize: 16) } - guard let encrypted = AES.encryptCBC(key: keyData, data: data, iv: ivData, mode: .pkcs7) else { + guard let encrypted = AES.encryptCBC(key: keyData, data: data, iv: ivData, mode: .pkcs7) + else { throw LLError.aesEncryptionFailed } return encrypted } - static func decryptionAES(key: String, iv: String = LocalEnvManager.shared.aesIV, data: Data) throws -> Data { + static func decryptionAES( + key: String, + iv: String = LocalEnvManager.shared.aesIV, + data: Data + ) throws -> Data { guard var keyData = key.data(using: .utf8), let ivData = iv.data(using: .utf8) else { throw LLError.aesKeyEncryptionFailed } @@ -882,7 +972,8 @@ extension WalletManager { keyData = keyData.paddingZeroRight(blockSize: 16) } - guard let decrypted = AES.decryptCBC(key: keyData, data: data, iv: ivData, mode: .pkcs7) else { + guard let decrypted = AES.decryptCBC(key: keyData, data: data, iv: ivData, mode: .pkcs7) + else { throw LLError.aesEncryptionFailed } return decrypted @@ -903,6 +994,8 @@ extension WalletManager { } } +// MARK: FlowSigner + extension WalletManager: FlowSigner { public var address: Flow.Address { guard let address = WalletManager.shared.getPrimaryWalletAddress() else { @@ -941,13 +1034,14 @@ extension WalletManager: FlowSigner { HUD.error(title: "verify_failed".localized) throw WalletError.securityVerifyFailed } - + if flowAccountKey == nil { try await findFlowAccount() } if userSecretSign() { - if let userId = walletInfo?.id, let data = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey { + if let userId = walletInfo?.id, + let data = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey { let sec = try WallectSecureEnclave(privateKey: data) let signature = try sec.sign(data: signableData) return signature @@ -958,7 +1052,10 @@ extension WalletManager: FlowSigner { throw LLError.emptyWallet } - var privateKey = hdWallet.getKeyByCurve(curve: .secp256k1, derivationPath: WalletManager.flowPath) + var privateKey = hdWallet.getKeyByCurve( + curve: .secp256k1, + derivationPath: WalletManager.flowPath + ) let hashedData = Hash.sha256(data: signableData) defer { @@ -984,7 +1081,8 @@ extension WalletManager: FlowSigner { try await findFlowAccount() } if userSecretSign() { - if let userId = walletInfo?.id, let data = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey { + if let userId = walletInfo?.id, + let data = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey { let sec = try WallectSecureEnclave(privateKey: data) let signature = try sec.sign(data: signableData) return signature @@ -995,7 +1093,10 @@ extension WalletManager: FlowSigner { throw LLError.emptyWallet } - var privateKey = hdWallet.getKeyByCurve(curve: .secp256k1, derivationPath: WalletManager.flowPath) + var privateKey = hdWallet.getKeyByCurve( + curve: .secp256k1, + derivationPath: WalletManager.flowPath + ) let hashedData = Hash.sha256(data: signableData) defer { @@ -1012,10 +1113,10 @@ extension WalletManager: FlowSigner { } public func signSync(signableData: Data) -> Data? { - if userSecretSign() { do { - if let userId = walletInfo?.id, let data = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey { + if let userId = walletInfo?.id, + let data = try WallectSecureEnclave.Store.fetchModel(by: userId)?.publicKey { let sec = try WallectSecureEnclave(privateKey: data) let signature = try sec.sign(data: signableData) return signature @@ -1029,7 +1130,10 @@ extension WalletManager: FlowSigner { return nil } - var privateKey = hdWallet.getKeyByCurve(curve: .secp256k1, derivationPath: WalletManager.flowPath) + var privateKey = hdWallet.getKeyByCurve( + curve: .secp256k1, + derivationPath: WalletManager.flowPath + ) let hashedData = Hash.sha256(data: signableData) defer { @@ -1099,57 +1203,73 @@ extension WalletManager: FlowSigner { } } } - - + @discardableResult func warningIfKeyIsInvalid(userId: String, markHide: Bool = false) -> Bool { - if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: userId), !mnemonic.isEmpty, mnemonic.split(separator: " ").count != 15 { + if let mnemonic = WalletManager.shared.getMnemonicFromKeychain(uid: userId), + !mnemonic.isEmpty, mnemonic.split(separator: " ").count != 15 { return false } do { let model = try WallectSecureEnclave.Store.fetchModel(by: userId) let list = try WallectSecureEnclave.Store.fetchAllModel(by: userId) - if model == nil && list.count > 0 { + if model == nil && !list.isEmpty { DispatchQueue.main.async { if self.isShow { return } self.isShow = true - let alertVC = BetterAlertController(title: "Something__is__wrong::message".localized, message: "profile_key_invalid".localized, preferredStyle: .alert) - - let cancelAction = UIAlertAction(title: "action_cancel".localized, style: .cancel) { _ in + let alertVC = BetterAlertController( + title: "Something__is__wrong::message".localized, + message: "profile_key_invalid".localized, + preferredStyle: .alert + ) + + let cancelAction = UIAlertAction( + title: "action_cancel".localized, + style: .cancel + ) { _ in self.isShow = false } - let restoreAction = UIAlertAction(title: "Restore Profile".localized, style: .default) { _ in + let restoreAction = UIAlertAction( + title: "Restore Profile".localized, + style: .default + ) { _ in self.isShow = false Router.route(to: RouteMap.RestoreLogin.restoreList) } alertVC.modalPresentationStyle = .overFullScreen alertVC.addAction(cancelAction) alertVC.addAction(restoreAction) - + if markHide { - let hideAction = UIAlertAction(title: "Hide Profile".localized, style: .default) { _ in + let hideAction = UIAlertAction( + title: "Hide Profile".localized, + style: .default + ) { _ in self.isShow = false do { try WallectSecureEnclave.Store.hideInvalidKey(by: userId) UserManager.shared.deleteLoginUID(userId) - }catch { - log.error("[SecureEnclave] hide key for \(userId) failed. \(error.localizedDescription)") + } catch { + log + .error( + "[SecureEnclave] hide key for \(userId) failed. \(error.localizedDescription)" + ) } } alertVC.addAction(hideAction) } Router.topNavigationController()?.present(alertVC, animated: true) } - + return true } - }catch { + } catch { return true } - + return false } } @@ -1198,10 +1318,12 @@ extension HDWallet { var flowAccountKey: Flow.AccountKey { let p256PublicKey = getPublicKey() let key = Flow.PublicKey(hex: String(p256PublicKey)) - return Flow.AccountKey(publicKey: key, - signAlgo: .ECDSA_SECP256k1, - hashAlgo: .SHA2_256, - weight: 1000) + return Flow.AccountKey( + publicKey: key, + signAlgo: .ECDSA_SECP256k1, + hashAlgo: .SHA2_256, + weight: 1000 + ) } var flowAccountP256Key: Flow.AccountKey { @@ -1212,19 +1334,23 @@ extension HDWallet { .hexValue .dropPrefix("04") let key = Flow.PublicKey(hex: String(p256PublicKey)) - return Flow.AccountKey(publicKey: key, - signAlgo: .ECDSA_P256, - hashAlgo: .SHA2_256, - weight: 1000) + return Flow.AccountKey( + publicKey: key, + signAlgo: .ECDSA_P256, + hashAlgo: .SHA2_256, + weight: 1000 + ) } } extension Flow.AccountKey { func toCodableModel() -> AccountKey { - return AccountKey(hashAlgo: hashAlgo.index, - publicKey: publicKey.hex, - signAlgo: signAlgo.index, - weight: weight) + AccountKey( + hashAlgo: hashAlgo.index, + publicKey: publicKey.hex, + signAlgo: signAlgo.index, + weight: weight + ) } } diff --git a/FRW/Services/Network/FRW/Model/Request/AddressBookRequest.swift b/FRW/Services/Network/FRW/Model/Request/AddressBookRequest.swift index 55a9e905..d6199db4 100644 --- a/FRW/Services/Network/FRW/Model/Request/AddressBookRequest.swift +++ b/FRW/Services/Network/FRW/Model/Request/AddressBookRequest.swift @@ -1,5 +1,5 @@ // -// AddressBookAddRequest.swift +// AddressBookRequest.swift // Flow Wallet // // Created by Selina on 2/6/2022. @@ -7,6 +7,8 @@ import Foundation +// MARK: - AddressBookAddRequest + struct AddressBookAddRequest: Codable { let contactName: String let address: String @@ -15,6 +17,8 @@ struct AddressBookAddRequest: Codable { let username: String? } +// MARK: - AddressBookEditRequest + struct AddressBookEditRequest: Codable { let id: Int let contactName: String diff --git a/FRW/Services/Network/FRW/Model/Response/EVMTokenResponse.swift b/FRW/Services/Network/FRW/Model/Response/EVMTokenResponse.swift index 8b5cc70c..df3bb94e 100644 --- a/FRW/Services/Network/FRW/Model/Response/EVMTokenResponse.swift +++ b/FRW/Services/Network/FRW/Model/Response/EVMTokenResponse.swift @@ -1,5 +1,5 @@ // -// EVMResponse.swift +// EVMTokenResponse.swift // FRW // // Created by cat on 2024/4/29. @@ -10,6 +10,8 @@ import Foundation import Web3Core import web3swift +// MARK: - EVMTokenResponse + struct EVMTokenResponse: Codable { let chainId: Int let address: String @@ -20,19 +22,6 @@ struct EVMTokenResponse: Codable { let balance: String? let flowIdentifier: String? - func toTokenModel() -> TokenModel { - - let model = TokenModel(name: name, - address: FlowNetworkModel(mainnet: address, testnet: address, crescendo: address, previewnet: address), - contractName: "", - storagePath: FlowTokenStoragePath(balance: "", vault: "", receiver: ""), - decimal: decimals, - icon: .init(string: logoURI), - symbol: symbol, - website: nil, evmAddress: nil, flowIdentifier: flowIdentifier, balance: BigUInt(balance ?? "-1")) - return model - } - var flowBalance: Double { guard let bal = balance, let value = BigUInt(bal) else { return 0 @@ -45,8 +34,36 @@ struct EVMTokenResponse: Codable { ).doubleValue return result } + + func toTokenModel() -> TokenModel { + let model = TokenModel( + name: name, + address: FlowNetworkModel( + mainnet: address, + testnet: address, + crescendo: address, + previewnet: address + ), + contractName: "", + storagePath: FlowTokenStoragePath( + balance: "", + vault: "", + receiver: "" + ), + decimal: decimals, + icon: .init(string: logoURI), + symbol: symbol, + website: nil, + evmAddress: nil, + flowIdentifier: flowIdentifier, + balance: BigUInt(balance ?? "-1") + ) + return model + } } +// MARK: - EVMCollection + struct EVMCollection: Codable { let chainId: Int let address: String @@ -62,26 +79,104 @@ struct EVMCollection: Codable { func toNFTCollection() -> NFTCollection { let contractName = flowIdentifier?.split(separator: ".")[2] ?? "" let contractAddress = flowIdentifier?.split(separator: ".")[1] ?? "" - let info = NFTCollectionInfo(id: flowIdentifier ?? "", name: name, contractName: String(contractName), address: String(contractAddress), logo: logoURI, banner: nil, officialWebsite: nil, description: nil, path: ContractPath(storagePath: "", publicPath: "", privatePath: nil, publicCollectionName: nil, publicType: nil, privateType: nil), evmAddress: address, flowIdentifier: flowIdentifier) - let list = nfts.map { NFTModel($0.toNFT(collectionAddress: String(contractAddress), contractName: String(contractName)), in: info) } - let model = NFTCollection(collection: info, - count: nfts.count, - ids: nftIds, - evmNFTs: list) + let info = NFTCollectionInfo( + id: flowIdentifier ?? "", + name: name, + contractName: String(contractName), + address: String(contractAddress), + logo: logoURI, + banner: nil, + officialWebsite: nil, + description: nil, + path: ContractPath( + storagePath: "", + publicPath: "", + privatePath: nil, + publicCollectionName: nil, + publicType: nil, + privateType: nil + ), + evmAddress: address, + flowIdentifier: flowIdentifier + ) + let list = nfts.map { NFTModel( + $0 + .toNFT( + collectionAddress: String(contractAddress), + contractName: String(contractName) + ), + in: info + ) } + let model = NFTCollection( + collection: info, + count: nfts.count, + ids: nftIds, + evmNFTs: list + ) return model } } +// MARK: - EVMNFT + struct EVMNFT: Codable { let id: String let name: String let thumbnail: String func toNFT() -> NFTResponse { - NFTResponse(id: id, name: name, description: nil, thumbnail: thumbnail, externalURL: nil, contractAddress: nil, evmAddress: nil, address: nil, collectionID: nil, collectionName: nil, collectionDescription: nil, collectionSquareImage: nil, collectionExternalURL: nil, collectionContractName: nil, collectionBannerImage: nil, traits: nil, postMedia: NFTPostMedia(title: nil, image: thumbnail, description: nil, video: nil, isSvg: nil)) + NFTResponse( + id: id, + name: name, + description: nil, + thumbnail: thumbnail, + externalURL: nil, + contractAddress: nil, + evmAddress: nil, + address: nil, + collectionID: nil, + collectionName: nil, + collectionDescription: nil, + collectionSquareImage: nil, + collectionExternalURL: nil, + collectionContractName: nil, + collectionBannerImage: nil, + traits: nil, + postMedia: NFTPostMedia( + title: nil, + image: thumbnail, + description: nil, + video: nil, + isSvg: nil + ) + ) } func toNFT(collectionAddress: String, contractName: String) -> NFTResponse { - NFTResponse(id: id, name: name, description: nil, thumbnail: thumbnail, externalURL: nil, contractAddress: collectionAddress, evmAddress: nil, address: nil, collectionID: nil, collectionName: nil, collectionDescription: nil, collectionSquareImage: nil, collectionExternalURL: nil, collectionContractName: contractName, collectionBannerImage: nil, traits: nil, postMedia: NFTPostMedia(title: nil, image: thumbnail, description: nil, video: nil, isSvg: nil)) + NFTResponse( + id: id, + name: name, + description: nil, + thumbnail: thumbnail, + externalURL: nil, + contractAddress: collectionAddress, + evmAddress: nil, + address: nil, + collectionID: nil, + collectionName: nil, + collectionDescription: nil, + collectionSquareImage: nil, + collectionExternalURL: nil, + collectionContractName: contractName, + collectionBannerImage: nil, + traits: nil, + postMedia: NFTPostMedia( + title: nil, + image: thumbnail, + description: nil, + video: nil, + isSvg: nil + ) + ) } } diff --git a/FRW/Services/Network/FRW/Model/Response/UserResponses.swift b/FRW/Services/Network/FRW/Model/Response/UserResponses.swift index 886ea488..9d9adefa 100644 --- a/FRW/Services/Network/FRW/Model/Response/UserResponses.swift +++ b/FRW/Services/Network/FRW/Model/Response/UserResponses.swift @@ -1,5 +1,5 @@ // -// RegisterResponse.swift +// UserResponses.swift // Flow Wallet // // Created by Hao Fu on 3/1/22. @@ -7,22 +7,30 @@ import Foundation +// MARK: - CheckUserResponse + struct CheckUserResponse: Codable { let unique: Bool let username: String } +// MARK: - LoginResponse + struct LoginResponse: Codable { let customToken: String let id: String } +// MARK: - RegisterResponse + struct RegisterResponse: Codable { let customToken: String let id: String let txId: String? } +// MARK: - UserInfoResponse + struct UserInfoResponse: Codable { let avatar: String let nickname: String @@ -30,6 +38,8 @@ struct UserInfoResponse: Codable { let `private`: Int } +// MARK: - UserWalletResponse + struct UserWalletResponse: Codable { let id: String // let primaryWallet: Int @@ -49,14 +59,19 @@ struct UserWalletResponse: Codable { // } var currentNetworkWalletModel: WalletResponse? { - return wallets?.first(where: { $0.chainId == LocalUserDefaults.shared.flowNetwork.rawValue && $0.blockchain != nil }) + wallets? + .first(where: { + $0.chainId == LocalUserDefaults.shared.flowNetwork.rawValue && $0.blockchain != nil + }) } func getNetworkWalletModel(network: LocalUserDefaults.FlowNetworkType) -> WalletResponse? { - return wallets?.first(where: { $0.chainId == network.rawValue && $0.blockchain != nil }) + wallets?.first(where: { $0.chainId == network.rawValue && $0.blockchain != nil }) } } +// MARK: - WalletResponse + struct WalletResponse: Codable { let color: String? let icon: String? @@ -74,14 +89,16 @@ struct WalletResponse: Codable { } var getAddress: String? { - return blockchain?.first?.address + blockchain?.first?.address } var getName: String? { - return blockchain?.first?.name + blockchain?.first?.name } } +// MARK: - BlockChainResponse + struct BlockChainResponse: Codable { let id: Int let chainId: String @@ -90,6 +107,8 @@ struct BlockChainResponse: Codable { let coins: [String]? } +// MARK: - UserSearchResponse + struct UserSearchResponse: Codable { let users: [UserInfo]? } diff --git a/FRW/Services/Network/FRWWeb/request/SwapEstimateRequest.swift b/FRW/Services/Network/FRWWeb/request/SwapEstimateRequest.swift index 2bca1c19..e0ed012a 100644 --- a/FRW/Services/Network/FRWWeb/request/SwapEstimateRequest.swift +++ b/FRW/Services/Network/FRWWeb/request/SwapEstimateRequest.swift @@ -1,5 +1,5 @@ // -// OtherRequests.swift +// SwapEstimateRequest.swift // Flow Wallet // // Created by Selina on 26/9/2022. diff --git a/FRW/Services/Network/FirebaseAPI/Model/PayAsSignerModel.swift b/FRW/Services/Network/FirebaseAPI/Model/PayAsSignerModel.swift index f9a5e7e5..768213b7 100644 --- a/FRW/Services/Network/FirebaseAPI/Model/PayAsSignerModel.swift +++ b/FRW/Services/Network/FirebaseAPI/Model/PayAsSignerModel.swift @@ -1,5 +1,5 @@ // -// PayAsSigner.swift +// PayAsSignerModel.swift // Flow Wallet // // Created by Hao Fu on 8/9/2022. @@ -8,16 +8,9 @@ import Flow import Foundation -struct FCLVoucher: Codable { - let cadence: Flow.Script - let payer: Flow.Address - let refBlock: Flow.ID - let arguments: [Flow.Argument] - let proposalKey: ProposalKey - let computeLimit: UInt64 - let authorizers: [Flow.Address] - let payloadSigs: [Signature] +// MARK: - FCLVoucher +struct FCLVoucher: Codable { struct ProposalKey: Codable { let address: Flow.Address let keyId: Int @@ -29,40 +22,64 @@ struct FCLVoucher: Codable { let keyId: Int let sig: String } + + let cadence: Flow.Script + let payer: Flow.Address + let refBlock: Flow.ID + let arguments: [Flow.Argument] + let proposalKey: ProposalKey + let computeLimit: UInt64 + let authorizers: [Flow.Address] + let payloadSigs: [Signature] } +// MARK: - SignPayerResponse + struct SignPayerResponse: Codable { let envelopeSigs: FCLVoucher.Signature } +// MARK: - SignPayerRequest + struct SignPayerRequest: Codable { let transaction: FCLVoucher let message: PayerMessage } -struct PayerMessage: Codable { - let envelopeMessage: String +// MARK: - PayerMessage +struct PayerMessage: Codable { enum CodingKeys: String, CodingKey { case envelopeMessage = "envelope_message" } + + let envelopeMessage: String } extension Flow.Transaction { var voucher: FCLVoucher { - FCLVoucher(cadence: script, - payer: payer, - refBlock: referenceBlockId, - arguments: arguments, - proposalKey: FCLVoucher.ProposalKey(address: proposalKey.address, - keyId: proposalKey.keyIndex, - sequenceNum: UInt64(proposalKey.sequenceNumber)), - computeLimit: UInt64(gasLimit), - authorizers: authorizers, - payloadSigs: payloadSignatures.compactMap { - FCLVoucher.Signature(address: $0.address, - keyId: $0.keyIndex, - sig: $0.signature.hexValue) - }) + FCLVoucher( + cadence: script, + payer: payer, + refBlock: referenceBlockId, + arguments: arguments, + proposalKey: FCLVoucher.ProposalKey( + address: proposalKey.address, + keyId: proposalKey.keyIndex, + sequenceNum: UInt64( + proposalKey + .sequenceNumber + ) + ), + computeLimit: UInt64(gasLimit), + authorizers: authorizers, + payloadSigs: payloadSignatures.compactMap { + FCLVoucher.Signature( + address: $0.address, + keyId: $0.keyIndex, + sig: $0.signature.hexValue + ) + } + ) } } diff --git a/FRW/Services/Network/FlixAuditEndpoint/FlixAuditEndpoint.swift b/FRW/Services/Network/FlixAuditEndpoint/FlixAuditEndpoint.swift index c4db7f01..2b761172 100644 --- a/FRW/Services/Network/FlixAuditEndpoint/FlixAuditEndpoint.swift +++ b/FRW/Services/Network/FlixAuditEndpoint/FlixAuditEndpoint.swift @@ -1,5 +1,5 @@ // -// FlowAuditotEndpoint.swift +// FlixAuditEndpoint.swift // Flow Wallet // // Created by Hao Fu on 14/9/2022. @@ -8,23 +8,29 @@ import Foundation import Moya -struct FlixAuditRequest: Codable { - let cadenceBase64: String - let network: String +// MARK: - FlixAuditRequest +struct FlixAuditRequest: Codable { enum CodingKeys: String, CodingKey { case cadenceBase64 = "cadence_base64" case network } + + let cadenceBase64: String + let network: String } +// MARK: - FlixAuditEndpoint + enum FlixAuditEndpoint { case template(FlixAuditRequest) } +// MARK: TargetType + extension FlixAuditEndpoint: TargetType { var baseURL: URL { - return URL(string: "https://flix.flow.com")! + URL(string: "https://flix.flow.com")! } var path: String { diff --git a/FRW/Services/Network/FlowNetwork.swift b/FRW/Services/Network/FlowNetwork.swift index e10e2f56..ac819302 100644 --- a/FRW/Services/Network/FlowNetwork.swift +++ b/FRW/Services/Network/FlowNetwork.swift @@ -11,6 +11,8 @@ import Flow import Foundation import Web3Core +// MARK: - FlowNetwork + enum FlowNetwork { static func setup() { let type = LocalUserDefaults.shared.flowNetwork.toFlowType() @@ -60,35 +62,45 @@ extension FlowNetwork { } } - static func transferToken(to address: Flow.Address, amount: Decimal, token: TokenModel) async throws -> Flow.ID { + static func transferToken( + to address: Flow.Address, + amount: Decimal, + token: TokenModel + ) async throws -> Flow.ID { let cadenceString = TokenCadence.tokenTransfer(token: token, at: flow.chainID) let currentAdd = WalletManager.shared.getPrimaryWalletAddress() ?? "" let keyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } + + payer { + RemoteConfigManager.shared.payer + } + + proposer { + Flow.TransactionProposalKey( + address: Flow.Address(hex: currentAdd), + keyIndex: keyIndex + ) + } + + authorizers { + Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") + } + + arguments { + [.ufix64(amount), .address(address)] + } + + gasLimit { + 9999 + } } - - payer { - RemoteConfigManager.shared.payer - } - - proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: currentAdd), keyIndex: keyIndex) - } - - authorizers { - Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") - } - - arguments { - [.ufix64(amount), .address(address)] - } - - gasLimit { - 9999 - } - }) + ) } static func minFlowBalance() async throws -> Double { @@ -96,7 +108,10 @@ extension FlowNetwork { throw LLError.invalidAddress } let cadenceString = CadenceManager.shared.current.basic?.getAccountMinFlow?.toFunc() ?? "" - let result: Decimal = try await fetch(cadence: cadenceString, arguments: [.address(Flow.Address(hex: fromAddress))]) + let result: Decimal = try await fetch( + cadence: cadenceString, + arguments: [.address(Flow.Address(hex: fromAddress))] + ) return result.doubleValue } } @@ -112,37 +127,45 @@ extension FlowNetwork { return result } - static func addCollection(at address: Flow.Address, collection: NFTCollectionInfo) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.collection?.enableNFTStorage?.toFunc() ?? "" + static func addCollection( + at address: Flow.Address, + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.collection?.enableNFTStorage? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: originCadence) let fromKeyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) + } - authorizers { - address - } + authorizers { + address + } - gasLimit { - 9999 + gasLimit { + 9999 + } } - }) + ) } static func transferNFT(to address: Flow.Address, nft: NFTModel) async throws -> Flow.ID { var nftCollection = nft.collection if nftCollection == nil { - nftCollection = await NFTCollectionConfig.share.get(from: nft.response.contractAddress ?? "") + nftCollection = await NFTCollectionConfig.share + .get(from: nft.response.contractAddress ?? "") } guard let collection = nftCollection else { @@ -164,33 +187,40 @@ extension FlowNetwork { nftTransfer = CadenceManager.shared.current.collection?.sendNFT?.toFunc() ?? "" } - let cadenceString = collection.formatCadence(script: nft.isNBA ? nbaNFTTransfer : nftTransfer) + let cadenceString = collection + .formatCadence(script: nft.isNBA ? nbaNFTTransfer : nftTransfer) let fromKeyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } - - payer { - RemoteConfigManager.shared.payer - } - - proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: fromAddress), keyIndex: fromKeyIndex) - } - - authorizers { - Flow.Address(hex: fromAddress) - } - - arguments { - [.address(address), .uint64(tokenIdInt)] - } - - gasLimit { - 9999 + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } + + payer { + RemoteConfigManager.shared.payer + } + + proposer { + Flow.TransactionProposalKey( + address: Flow.Address(hex: fromAddress), + keyIndex: fromKeyIndex + ) + } + + authorizers { + Flow.Address(hex: fromAddress) + } + + arguments { + [.address(address), .uint64(tokenIdInt)] + } + + gasLimit { + 9999 + } } - }) + ) } } @@ -202,7 +232,10 @@ extension FlowNetwork { return try await fetch(cadence: cadence, arguments: [.string(domain)]) } - static func queryAddressByDomainFlowns(domain: String, root: String = "fn") async throws -> String { + static func queryAddressByDomainFlowns( + domain: String, + root: String = "fn" + ) async throws -> String { let cadence = CadenceManager.shared.current.basic?.getFlownsAddress?.toFunc() ?? "" let realDomain = domain @@ -215,77 +248,121 @@ extension FlowNetwork { // MARK: - Inbox extension FlowNetwork { - static func claimInboxToken(domain: String, key: String, coin: TokenModel, amount: Decimal, root: String = Contact.DomainType.meow.domain) async throws -> Flow.ID { + static func claimInboxToken( + domain: String, + key: String, + coin: TokenModel, + amount: Decimal, + root: String = Contact.DomainType.meow.domain + ) async throws -> Flow.ID { guard let address = WalletManager.shared.getPrimaryWalletAddress() else { throw LLError.invalidAddress } - let cadenceString = coin.formatCadence(cadence: CadenceManager.shared.current.domain?.claimFTFromInbox?.toFunc() ?? "") + let cadenceString = coin + .formatCadence( + cadence: CadenceManager.shared.current.domain?.claimFTFromInbox? + .toFunc() ?? "" + ) let fromKeyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } - - payer { - RemoteConfigManager.shared.payer + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } + + payer { + RemoteConfigManager.shared.payer + } + + proposer { + Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: fromKeyIndex + ) + } + + authorizers { + Flow.Address(hex: address) + } + + arguments { + [.string(domain), .string(root), .string(key), .ufix64(amount)] + } + + gasLimit { + 9999 + } } - - proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: address), keyIndex: fromKeyIndex) - } - - authorizers { - Flow.Address(hex: address) - } - - arguments { - [.string(domain), .string(root), .string(key), .ufix64(amount)] - } - - gasLimit { - 9999 - } - }) + ) } - static func claimInboxNFT(domain: String, key: String, collection: NFTCollectionInfo, itemId: UInt64, root: String = Contact.DomainType.meow.domain) async throws -> Flow.ID { + static func claimInboxNFT( + domain: String, + key: String, + collection: NFTCollectionInfo, + itemId: UInt64, + root: String = Contact.DomainType.meow.domain + ) async throws -> Flow.ID { guard let address = WalletManager.shared.getPrimaryWalletAddress() else { throw LLError.invalidAddress } - let cadenceString = collection.formatCadence(script: CadenceManager.shared.current.domain?.claimNFTFromInbox?.toFunc() ?? "") + let cadenceString = collection + .formatCadence( + script: CadenceManager.shared.current.domain?.claimNFTFromInbox? + .toFunc() ?? "" + ) let fromKeyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } - - payer { - RemoteConfigManager.shared.payer - } - - proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: address), keyIndex: fromKeyIndex) - } - - authorizers { - Flow.Address(hex: address) - } - - arguments { - [.string(domain), .string(root), .string(key), .uint64(itemId)] - } - - gasLimit { - 9999 + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } + + payer { + RemoteConfigManager.shared.payer + } + + proposer { + Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: fromKeyIndex + ) + } + + authorizers { + Flow.Address(hex: address) + } + + arguments { + [.string(domain), .string(root), .string(key), .uint64(itemId)] + } + + gasLimit { + 9999 + } } - }) + ) } } // MARK: - Swap extension FlowNetwork { - static func swapToken(swapPaths: [String], tokenInMax: Decimal, tokenOutMin: Decimal, tokenInVaultPath: String, tokenOutSplit: [Decimal], tokenInSplit: [Decimal], tokenOutVaultPath: String, tokenOutReceiverPath: String, tokenOutBalancePath: String, deadline: Decimal, isFrom: Bool) async throws -> Flow.ID { + static func swapToken( + swapPaths: [String], + tokenInMax: Decimal, + tokenOutMin: Decimal, + tokenInVaultPath: String, + tokenOutSplit: [Decimal], + tokenInSplit: [Decimal], + tokenOutVaultPath: String, + tokenOutReceiverPath: String, + tokenOutBalancePath: String, + deadline: Decimal, + isFrom: Bool + ) async throws -> Flow.ID { guard let address = WalletManager.shared.getPrimaryWalletAddress() else { throw LLError.invalidAddress } @@ -293,7 +370,8 @@ extension FlowNetwork { let tokenName = String(swapPaths.last?.split(separator: ".").last ?? "") let tokenAddress = String(swapPaths.last?.split(separator: ".")[1] ?? "").addHexPrefix() - let fromCadence = CadenceManager.shared.current.swap?.SwapExactTokensForTokens?.toFunc() ?? "" + let fromCadence = CadenceManager.shared.current.swap?.SwapExactTokensForTokens? + .toFunc() ?? "" let toCadence = CadenceManager.shared.current.swap?.SwapTokensForExactTokens?.toFunc() ?? "" var cadenceString = isFrom ? fromCadence : toCadence cadenceString = cadenceString @@ -317,35 +395,41 @@ extension FlowNetwork { args.append(.path(Flow.Argument.Path(domain: "public", identifier: tokenOutReceiverPath))) args.append(.path(Flow.Argument.Path(domain: "public", identifier: tokenOutBalancePath))) let fromKeyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } - - payer { - RemoteConfigManager.shared.payer - } - - proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: address), keyIndex: fromKeyIndex) - } - - authorizers { - Flow.Address(hex: address) - } - - arguments { - args + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } + + payer { + RemoteConfigManager.shared.payer + } + + proposer { + Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: fromKeyIndex + ) + } + + authorizers { + Flow.Address(hex: address) + } + + arguments { + args + } + + gasLimit { + 9999 + } } - - gasLimit { - 9999 - } - }) + ) } } -// MARK: - Stake +// MARK: - LilicoError enum LilicoError: Error { case emptyWallet @@ -364,7 +448,8 @@ extension FlowNetwork { return try await fetch(cadence: cadence, arguments: [.address(address)]) } - static func claimUnstake(nodeID: String, delegatorId: Int, amount: Decimal) async throws -> Flow.ID { + static func claimUnstake(nodeID: String, delegatorId: Int, amount: Decimal) async throws -> Flow + .ID { guard let walletAddress = WalletManager.shared.getPrimaryWalletAddress() else { throw LilicoError.emptyWallet } @@ -395,7 +480,11 @@ extension FlowNetwork { } } - static func reStakeUnstake(nodeID: String, delegatorId: Int, amount: Decimal) async throws -> Flow.ID { + static func reStakeUnstake( + nodeID: String, + delegatorId: Int, + amount: Decimal + ) async throws -> Flow.ID { guard let walletAddress = WalletManager.shared.getPrimaryWalletAddress() else { throw LilicoError.emptyWallet } @@ -426,7 +515,8 @@ extension FlowNetwork { } } - static func claimReward(nodeID: String, delegatorId: Int, amount: Decimal) async throws -> Flow.ID { + static func claimReward(nodeID: String, delegatorId: Int, amount: Decimal) async throws -> Flow + .ID { guard let walletAddress = WalletManager.shared.getPrimaryWalletAddress() else { throw LilicoError.emptyWallet } @@ -457,7 +547,11 @@ extension FlowNetwork { } } - static func reStakeReward(nodeID: String, delegatorId: Int, amount: Decimal) async throws -> Flow.ID { + static func reStakeReward( + nodeID: String, + delegatorId: Int, + amount: Decimal + ) async throws -> Flow.ID { guard let walletAddress = WalletManager.shared.getPrimaryWalletAddress() else { throw LilicoError.emptyWallet } @@ -497,23 +591,26 @@ extension FlowNetwork { } let address = Flow.Address(hex: walletAddress) let fromKeyIndex = WalletManager.shared.keyIndex - let txId = try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } + let txId = try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) + } - authorizers { - address + authorizers { + address + } } - }) + ) let result = try await txId.onceSealed() @@ -530,98 +627,112 @@ extension FlowNetwork { let cadenceString = cadenceOrigin.replace(by: ScriptAddress.addressMap()) let address = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let fromKeyIndex = WalletManager.shared.keyIndex - let txId = try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } + let txId = try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) + } - authorizers { - address - } + authorizers { + address + } - arguments { - [.string(providerId), .ufix64(Decimal(amount))] - } + arguments { + [.string(providerId), .ufix64(Decimal(amount))] + } - gasLimit { - 9999 + gasLimit { + 9999 + } } - }) + ) return txId } - static func stakeFlow(providerId: String, delegatorId: Int, amount: Double) async throws -> Flow.ID { + static func stakeFlow(providerId: String, delegatorId: Int, amount: Double) async throws -> Flow + .ID { let cadenceOrigin = CadenceManager.shared.current.staking?.createStake?.toFunc() ?? "" let cadenceString = cadenceOrigin.replace(by: ScriptAddress.addressMap()) let address = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let fromKeyIndex = WalletManager.shared.keyIndex - let txId = try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } + let txId = try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) + } - authorizers { - address - } + authorizers { + address + } - arguments { - [.string(providerId), .uint32(UInt32(delegatorId)), .ufix64(Decimal(amount))] - } + arguments { + [.string(providerId), .uint32(UInt32(delegatorId)), .ufix64(Decimal(amount))] + } - gasLimit { - 9999 + gasLimit { + 9999 + } } - }) + ) return txId } - static func unstakeFlow(providerId: String, delegatorId: Int, amount: Double) async throws -> Flow.ID { + static func unstakeFlow( + providerId: String, + delegatorId: Int, + amount: Double + ) async throws -> Flow.ID { let cadenceOrigin = CadenceManager.shared.current.staking?.unstake?.toFunc() ?? "" let cadenceString = cadenceOrigin.replace(by: ScriptAddress.addressMap()) let address = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let fromKeyIndex = WalletManager.shared.keyIndex - let txId = try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } + let txId = try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) + } - authorizers { - address - } + authorizers { + address + } - arguments { - [.string(providerId), .uint32(UInt32(delegatorId)), .ufix64(Decimal(amount))] - } + arguments { + [.string(providerId), .uint32(UInt32(delegatorId)), .ufix64(Decimal(amount))] + } - gasLimit { - 9999 + gasLimit { + 9999 + } } - }) + ) return txId } @@ -652,8 +763,10 @@ extension FlowNetwork { let address = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let cadence = CadenceManager.shared.current.staking?.getDelegatesIndo?.toFunc() ?? "" let replacedCadence = cadence.replace(by: ScriptAddress.addressMap()) - let rawResponse = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: replacedCadence), - arguments: [.address(address)]) + let rawResponse = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: replacedCadence), + arguments: [.address(address)] + ) let response = try JSONDecoder().decode(StakingDelegatorInner.self, from: rawResponse.data) debugPrint("FlowNetwork -> getDelegatorInfo, response = \(response)") @@ -687,45 +800,52 @@ extension FlowNetwork { } static func unlinkChildAccount(_ address: String) async throws -> Flow.ID { - let cadenceOrigin = CadenceManager.shared.current.hybridCustody?.unlinkChildAccount?.toFunc() ?? "" + let cadenceOrigin = CadenceManager.shared.current.hybridCustody?.unlinkChildAccount? + .toFunc() ?? "" let cadenceString = cadenceOrigin.replace(by: ScriptAddress.addressMap()) let walletAddress = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let fromKeyIndex = WalletManager.shared.keyIndex - let txId = try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } + let txId = try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: walletAddress, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: walletAddress, keyIndex: fromKeyIndex) + } - authorizers { - walletAddress - } + authorizers { + walletAddress + } - arguments { - [.address(Flow.Address(hex: address))] - } + arguments { + [.address(Flow.Address(hex: address))] + } - gasLimit { - 9999 + gasLimit { + 9999 + } } - }) + ) return txId } static func queryChildAccountMeta(_ address: String) async throws -> [ChildAccount] { let address = Flow.Address(hex: address) - let cadence = CadenceManager.shared.current.hybridCustody?.getChildAccountMeta?.toFunc() ?? "" + let cadence = CadenceManager.shared.current.hybridCustody?.getChildAccountMeta? + .toFunc() ?? "" let replacedCadence = cadence.replace(by: ScriptAddress.addressMap()) - let rawResponse = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: replacedCadence), - arguments: [.address(address)]) + let rawResponse = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: replacedCadence), + arguments: [.address(address)] + ) guard let decode = rawResponse.decode() as? [String: Any?] else { return [] @@ -747,146 +867,259 @@ extension FlowNetwork { return result } - static func editChildAccountMeta(_ address: String, name: String, desc: String, thumbnail: String) async throws -> Flow.ID { - let editChildAccount = CadenceManager.shared.current.hybridCustody?.editChildAccount?.toFunc() ?? "" + static func editChildAccountMeta( + _ address: String, + name: String, + desc: String, + thumbnail: String + ) async throws -> Flow.ID { + let editChildAccount = CadenceManager.shared.current.hybridCustody?.editChildAccount? + .toFunc() ?? "" let cadenceString = editChildAccount.replace(by: ScriptAddress.addressMap()) let walletAddress = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let fromKeyIndex = WalletManager.shared.keyIndex - let txId = try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - cadenceString - } - - payer { - RemoteConfigManager.shared.payer - } - - proposer { - Flow.TransactionProposalKey(address: walletAddress, keyIndex: fromKeyIndex) - } - - authorizers { - walletAddress - } - - arguments { - [.address(Flow.Address(hex: address)), .string(name), .string(desc), .string(thumbnail)] - } - - gasLimit { - 9999 + let txId = try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + cadenceString + } + + payer { + RemoteConfigManager.shared.payer + } + + proposer { + Flow.TransactionProposalKey(address: walletAddress, keyIndex: fromKeyIndex) + } + + authorizers { + walletAddress + } + + arguments { + [ + .address(Flow.Address(hex: address)), + .string(name), + .string(desc), + .string(thumbnail), + ] + } + + gasLimit { + 9999 + } } - }) + ) return txId } static func fetchAccessibleCollection(parent: String, child: String) async throws -> [String] { - let cadence = CadenceManager.shared.current.hybridCustody?.getChildAccountAllowTypes?.toFunc() ?? "" + let cadence = CadenceManager.shared.current.hybridCustody?.getChildAccountAllowTypes? + .toFunc() ?? "" let cadenceString = cadence.replace(by: ScriptAddress.addressMap()) let parentAddress = Flow.Address(hex: parent) let childAddress = Flow.Address(hex: child) let response = try await flow.accessAPI - .executeScriptAtLatestBlock(script: Flow.Script(text: cadenceString), arguments: [.address(parentAddress), .address(childAddress)]) + .executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceString), + arguments: [.address(parentAddress), .address(childAddress)] + ) let result = try response.decode([String].self) return result } - static func fetchAccessibleFT(parent: String, child: String) async throws -> [FlowModel.TokenInfo] { - let accessible = CadenceManager.shared.current.hybridCustody?.getAccessibleCoinInfo?.toFunc() ?? "" + static func fetchAccessibleFT( + parent: String, + child: String + ) async throws -> [FlowModel.TokenInfo] { + let accessible = CadenceManager.shared.current.hybridCustody?.getAccessibleCoinInfo? + .toFunc() ?? "" let cadenceString = accessible.replace(by: ScriptAddress.addressMap()) let parentAddress = Flow.Address(hex: parent) let childAddress = Flow.Address(hex: child) let response = try await flow.accessAPI - .executeScriptAtLatestBlock(script: Flow.Script(text: cadenceString), arguments: [.address(parentAddress), .address(childAddress)]) + .executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceString), + arguments: [.address(parentAddress), .address(childAddress)] + ) .decode([FlowModel.TokenInfo].self) return response } // on child, move nft to parent - static func moveNFTToParent(nftId: UInt64, childAddress: String, identifier: String, collection: NFTCollectionInfo) async throws -> Flow.ID { - let accessible = CadenceManager.shared.current.hybridCustody?.transferChildNFT?.toFunc() ?? "" + static func moveNFTToParent( + nftId: UInt64, + childAddress: String, + identifier: String, + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let accessible = CadenceManager.shared.current.hybridCustody?.transferChildNFT? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) let childAddress = Flow.Address(hex: childAddress) - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(childAddress), .string(identifier), .uint64(nftId)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [.address(childAddress), .string(identifier), .uint64(nftId)] + ) } // on parent, move nft to child - static func moveNFTToChild(nftId: UInt64, childAddress: String, identifier: String, collection: NFTCollectionInfo) async throws -> Flow.ID { - let accessible = CadenceManager.shared.current.hybridCustody?.transferNFTToChild?.toFunc() ?? "" + static func moveNFTToChild( + nftId: UInt64, + childAddress: String, + identifier: String, + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let accessible = CadenceManager.shared.current.hybridCustody?.transferNFTToChild? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(Flow.Address(hex: childAddress)), .string(identifier), .uint64(nftId)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [ + .address(Flow.Address(hex: childAddress)), + .string(identifier), + .uint64(nftId), + ] + ) } // send NFT from child to other wallet - static func sendChildNFT(nftId: UInt64, childAddress: String, toAddress: String, identifier: String, collection: NFTCollectionInfo) async throws -> Flow.ID { + static func sendChildNFT( + nftId: UInt64, + childAddress: String, + toAddress: String, + identifier: String, + collection: NFTCollectionInfo + ) async throws -> Flow.ID { let accessible = CadenceManager.shared.current.hybridCustody?.sendChildNFT?.toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) let childAddr = Flow.Address(hex: childAddress) let toAddr = Flow.Address(hex: toAddress) - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(childAddr), .address(toAddr), .string(identifier), .uint64(nftId)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [ + .address(childAddr), + .address(toAddr), + .string(identifier), + .uint64(nftId), + ] + ) } // Send NFT from child to child - static func sendChildNFTToChild(nftId: UInt64, childAddress: String, toAddress: String, identifier: String, collection: NFTCollectionInfo) async throws -> Flow.ID { - let accessible = CadenceManager.shared.current.hybridCustody?.sendChildNFTToChild?.toFunc() ?? "" + static func sendChildNFTToChild( + nftId: UInt64, + childAddress: String, + toAddress: String, + identifier: String, + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let accessible = CadenceManager.shared.current.hybridCustody?.sendChildNFTToChild? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) let childAddr = Flow.Address(hex: childAddress) let toAddr = Flow.Address(hex: toAddress) - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(childAddr), .address(toAddr), .string(identifier), .uint64(nftId)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [ + .address(childAddr), + .address(toAddr), + .string(identifier), + .uint64(nftId), + ] + ) } static func linkedAccountEnabledTokenList(address: String) async throws -> [String: Bool] { - let cadence = CadenceManager.shared.current.ft?.isLinkedAccountTokenListEnabled?.toFunc() ?? "" + let cadence = CadenceManager.shared.current.ft?.isLinkedAccountTokenListEnabled? + .toFunc() ?? "" return try await fetch(at: Flow.Address(hex: address), by: cadence) } - static func checkChildLinkedCollections(parent: String, child: String, identifier: String, collection _: NFTCollectionInfo) async throws -> Bool { - let cadence = CadenceManager.shared.current.hybridCustody?.checkChildLinkedCollections?.toFunc() ?? "" + static func checkChildLinkedCollections( + parent: String, + child: String, + identifier: String, + collection _: NFTCollectionInfo + ) async throws -> Bool { + let cadence = CadenceManager.shared.current.hybridCustody?.checkChildLinkedCollections? + .toFunc() ?? "" let cadenceString = cadence.replace(by: ScriptAddress.addressMap()) let parentAddress = Flow.Address(hex: parent) let childAddress = Flow.Address(hex: child) let response = try await flow.accessAPI - .executeScriptAtLatestBlock(script: Flow.Script(text: cadenceString), - arguments: [ - .address(parentAddress), - .address(childAddress), - .string(identifier), - ]) + .executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceString), + arguments: [ + .address(parentAddress), + .address(childAddress), + .string(identifier), + ] + ) let result = try response.decode(Bool.self) return result } - static func batchMoveNFTToParent(childAddr address: String, identifier: String, ids: [UInt64], collection: NFTCollectionInfo) async throws -> Flow.ID { - let accessible = CadenceManager.shared.current.hybridCustody?.batchTransferChildNFT?.toFunc() ?? "" + static func batchMoveNFTToParent( + childAddr address: String, + identifier: String, + ids: [UInt64], + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let accessible = CadenceManager.shared.current.hybridCustody?.batchTransferChildNFT? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) let childAddress = Flow.Address(hex: address) let idMaped = ids.map { Flow.Cadence.FValue.uint64($0) } let ident = identifier.split(separator: "/").last.map { String($0) } ?? identifier - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(childAddress), .string(ident), .array(idMaped)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [.address(childAddress), .string(ident), .array(idMaped)] + ) } - static func batchMoveNFTToChild(childAddr address: String, identifier: String, ids: [UInt64], collection: NFTCollectionInfo) async throws -> Flow.ID { - let accessible = CadenceManager.shared.current.hybridCustody?.batchTransferNFTToChild?.toFunc() ?? "" + static func batchMoveNFTToChild( + childAddr address: String, + identifier: String, + ids: [UInt64], + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let accessible = CadenceManager.shared.current.hybridCustody?.batchTransferNFTToChild? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) let childAddress = Flow.Address(hex: address) let idMaped = ids.map { Flow.Cadence.FValue.uint64($0) } let ident = identifier.split(separator: "/").last.map { String($0) } ?? identifier - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(childAddress), .string(ident), .array(idMaped)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [.address(childAddress), .string(ident), .array(idMaped)] + ) } - static func batchSendChildNFTToChild(fromAddress: String, toAddress: String, identifier: String, ids: [UInt64], collection: NFTCollectionInfo) async throws -> Flow.ID { - let accessible = CadenceManager.shared.current.hybridCustody?.batchSendChildNFTToChild?.toFunc() ?? "" + static func batchSendChildNFTToChild( + fromAddress: String, + toAddress: String, + identifier: String, + ids: [UInt64], + collection: NFTCollectionInfo + ) async throws -> Flow.ID { + let accessible = CadenceManager.shared.current.hybridCustody?.batchSendChildNFTToChild? + .toFunc() ?? "" let cadenceString = collection.formatCadence(script: accessible) let fromAddr = Flow.Address(hex: fromAddress) let toAddr = Flow.Address(hex: toAddress) let idMaped = ids.map { Flow.Cadence.FValue.uint64($0) } let ident = identifier.split(separator: "/").last.map { String($0) } ?? identifier - return try await sendTransaction(cadenceStr: cadenceString, argumentList: [.address(fromAddr), .address(toAddr), .string(ident), .array(idMaped)]) + return try await sendTransaction( + cadenceStr: cadenceString, + argumentList: [.address(fromAddr), .address(toAddr), .string(ident), .array(idMaped)] + ) } } @@ -914,7 +1147,7 @@ extension FlowNetwork { } static func getAccountAtLatestBlock(address: String) async throws -> Flow.Account { - return try await flow.accessAPI.getAccountAtLatestBlock(address: Flow.Address(hex: address)) + try await flow.accessAPI.getAccountAtLatestBlock(address: Flow.Address(hex: address)) } static func getLastBlockAccountKeyId(address: String) async throws -> Int { @@ -925,7 +1158,10 @@ extension FlowNetwork { static func checkStorageInfo() async throws -> Flow.StorageInfo { let address = Flow.Address(hex: WalletManager.shared.getPrimaryWalletAddress() ?? "") let cadence = CadenceManager.shared.current.basic?.getStorageInfo?.toFunc() ?? "" - let response = try await flow.accessAPI.executeScriptAtLatestBlock(cadence: cadence, arguments: [.address(address)]).decode(Flow.StorageInfo.self) + let response = try await flow.accessAPI.executeScriptAtLatestBlock( + cadence: cadence, + arguments: [.address(address)] + ).decode(Flow.StorageInfo.self) return response } } @@ -933,19 +1169,30 @@ extension FlowNetwork { // MARK: - Base extension FlowNetwork { - private static func fetch(at address: Flow.Address, by cadence: String) async throws -> T { + private static func fetch( + at address: Flow.Address, + by cadence: String + ) async throws -> T { let replacedCadence = cadence.replace(by: ScriptAddress.addressMap()) - let response = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: replacedCadence), - arguments: [.address(address)]) + let response = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: replacedCadence), + arguments: [.address(address)] + ) let model: T = try response.decode() return model } - private static func fetch(cadence: String, arguments: [Flow.Cadence.FValue]) async throws -> T { + private static func fetch( + cadence: String, + arguments: [Flow.Cadence.FValue] + ) async throws -> T { let replacedCadence = cadence.replace(by: ScriptAddress.addressMap()) - let response = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: replacedCadence), arguments: arguments) + let response = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: replacedCadence), + arguments: arguments + ) let model: T = try response.decode() return model } @@ -987,30 +1234,37 @@ extension Flow.TransactionResult { extension FlowNetwork { static func revokeAccountKey(by index: Int, at address: Flow.Address) async throws -> Flow.ID { let fromKeyIndex = WalletManager.shared.keyIndex - return try await flow.sendTransaction(signers: WalletManager.shared.defaultSigners, builder: { - cadence { - CadenceManager.shared.current.basic?.revokeKey?.toFunc() ?? "" - } + return try await flow.sendTransaction( + signers: WalletManager.shared.defaultSigners, + builder: { + cadence { + CadenceManager.shared.current.basic?.revokeKey?.toFunc() ?? "" + } - payer { - RemoteConfigManager.shared.payer - } + payer { + RemoteConfigManager.shared.payer + } - proposer { - Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) - } + proposer { + Flow.TransactionProposalKey(address: address, keyIndex: fromKeyIndex) + } - authorizers { - address - } + authorizers { + address + } - arguments { - [.int(index)] + arguments { + [.int(index)] + } } - }) + ) } - static func addKeyToAccount(address: Flow.Address, accountKey: Flow.AccountKey, signers: [FlowSigner]) async throws -> Flow.ID { + static func addKeyToAccount( + address: Flow.Address, + accountKey: Flow.AccountKey, + signers: [FlowSigner] + ) async throws -> Flow.ID { let originCadence = CadenceManager.shared.current.basic?.addKey?.toFunc() ?? "" let fromKeyIndex = WalletManager.shared.keyIndex return try await flow.sendTransaction(signers: signers) { @@ -1039,7 +1293,13 @@ extension FlowNetwork { } } - static func addKeyWithMulti(address: Flow.Address, keyIndex: Int, sequenceNum: Int64, accountKey: Flow.AccountKey, signers: [FlowSigner]) async throws -> Flow.ID { + static func addKeyWithMulti( + address: Flow.Address, + keyIndex: Int, + sequenceNum: Int64, + accountKey: Flow.AccountKey, + signers: [FlowSigner] + ) async throws -> Flow.ID { let originCadence = CadenceManager.shared.current.basic?.addKey?.toFunc() ?? "" return try await flow.sendTransaction(signers: signers) { cadence { @@ -1059,7 +1319,11 @@ extension FlowNetwork { } proposer { - Flow.TransactionProposalKey(address: address, keyIndex: keyIndex, sequenceNumber: sequenceNum) + Flow.TransactionProposalKey( + address: address, + keyIndex: keyIndex, + sequenceNumber: sequenceNum + ) } authorizers { address @@ -1089,7 +1353,10 @@ extension FlowNetwork { } proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: fromAddress), keyIndex: fromKeyIndex) + Flow.TransactionProposalKey( + address: Flow.Address(hex: fromAddress), + keyIndex: fromKeyIndex + ) } authorizers { @@ -1108,7 +1375,10 @@ extension FlowNetwork { } let originCadence = CadenceManager.shared.current.evm?.getCoaAddr?.toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: cadenceStr), arguments: [.address(Flow.Address(hex: fromAddress))]).decode(String.self) + let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceStr), + arguments: [.address(Flow.Address(hex: fromAddress))] + ).decode(String.self) return resonpse } @@ -1119,7 +1389,10 @@ extension FlowNetwork { let originCadence = CadenceManager.shared.current.evm?.getCoaBalance?.toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: cadenceStr), arguments: [.address(Flow.Address(hex: fromAddress))]) + let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceStr), + arguments: [.address(Flow.Address(hex: fromAddress))] + ) let result = try resonpse.decode(Decimal.self) return result } @@ -1148,7 +1421,12 @@ extension FlowNetwork { } /// coa -> eoa - static func sendTransaction(amount: String, data: Data?, toAddress: String, gas: UInt64) async throws -> Flow.ID { + static func sendTransaction( + amount: String, + data: Data?, + toAddress: String, + gas: UInt64 + ) async throws -> Flow.ID { guard let amountParse = Decimal(string: amount) else { throw WalletError.insufficientBalance } @@ -1181,8 +1459,10 @@ extension FlowNetwork { } // transferFlowToEvmAddress - static func sendFlowToEvm(evmAddress: String, amount: Decimal, gas: UInt64) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.evm?.transferFlowToEvmAddress?.toFunc() ?? "" + static func sendFlowToEvm(evmAddress: String, amount: Decimal, gas: UInt64) async throws -> Flow + .ID { + let originCadence = CadenceManager.shared.current.evm?.transferFlowToEvmAddress? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .string(evmAddress), @@ -1192,8 +1472,10 @@ extension FlowNetwork { } /// transferFlowFromCoaToFlow - static func sendFlowTokenFromCoaToFlow(amount: Decimal, address: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.evm?.transferFlowFromCoaToFlow?.toFunc() ?? "" + static func sendFlowTokenFromCoaToFlow(amount: Decimal, address: String) async throws -> Flow + .ID { + let originCadence = CadenceManager.shared.current.evm?.transferFlowFromCoaToFlow? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .ufix64(amount), @@ -1201,8 +1483,13 @@ extension FlowNetwork { ]) } - static func sendNoFlowTokenToEVM(vaultIdentifier: String, amount: Decimal, recipient: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.bridge?.bridgeTokensToEvmAddressV2?.toFunc() ?? "" + static func sendNoFlowTokenToEVM( + vaultIdentifier: String, + amount: Decimal, + recipient: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.bridge?.bridgeTokensToEvmAddressV2? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) let amountValue = Flow.Cadence.FValue.ufix64(amount) @@ -1213,9 +1500,17 @@ extension FlowNetwork { ]) } - static func bridgeToken(vaultIdentifier: String, amount: Decimal, fromEvm: Bool, decimals: Int) async throws -> Flow.ID { - let originCadence = (fromEvm ? CadenceManager.shared.current.bridge?.bridgeTokensFromEvmV2?.toFunc() - : CadenceManager.shared.current.bridge?.bridgeTokensToEvmV2?.toFunc()) ?? "" + static func bridgeToken( + vaultIdentifier: String, + amount: Decimal, + fromEvm: Bool, + decimals: Int + ) async throws -> Flow.ID { + let originCadence = ( + fromEvm ? CadenceManager.shared.current.bridge?.bridgeTokensFromEvmV2? + .toFunc() + : CadenceManager.shared.current.bridge?.bridgeTokensToEvmV2?.toFunc() + ) ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) var amountValue = Flow.Cadence.FValue.ufix64(amount) if let result = Utilities.parseToBigUInt(amount.description, decimals: decimals), fromEvm { @@ -1227,8 +1522,13 @@ extension FlowNetwork { ]) } - static func bridgeTokensFromEvmToFlow(identifier: String, amount: BigUInt, receiver: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.bridge?.bridgeTokensFromEvmToFlowV2?.toFunc() ?? "" + static func bridgeTokensFromEvmToFlow( + identifier: String, + amount: BigUInt, + receiver: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.bridge?.bridgeTokensFromEvmToFlowV2? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) let amountValue = Flow.Cadence.FValue.uint256(amount) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ @@ -1240,11 +1540,19 @@ extension FlowNetwork { // - static func bridgeNFTToEVM(identifier: String, ids: [UInt64], fromEvm: Bool) async throws -> Flow.ID { - let originCadence = (fromEvm ? CadenceManager.shared.current.bridge?.batchBridgeNFTFromEvmV2?.toFunc() - : CadenceManager.shared.current.bridge?.batchBridgeNFTToEvmV2?.toFunc()) ?? "" + static func bridgeNFTToEVM( + identifier: String, + ids: [UInt64], + fromEvm: Bool + ) async throws -> Flow.ID { + let originCadence = ( + fromEvm ? CadenceManager.shared.current.bridge? + .batchBridgeNFTFromEvmV2?.toFunc() + : CadenceManager.shared.current.bridge?.batchBridgeNFTToEvmV2?.toFunc() + ) ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - let idMaped = fromEvm ? ids.map { Flow.Cadence.FValue.uint256(BigUInt($0)) } : ids.map { Flow.Cadence.FValue.uint64($0) } + let idMaped = fromEvm ? ids.map { Flow.Cadence.FValue.uint256(BigUInt($0)) } : ids + .map { Flow.Cadence.FValue.uint64($0) } return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .string(identifier), @@ -1252,8 +1560,13 @@ extension FlowNetwork { ]) } - static func bridgeNFTToAnyEVM(identifier: String, id: String, toAddress: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.bridge?.bridgeNFTToEvmAddressV2?.toFunc() ?? "" + static func bridgeNFTToAnyEVM( + identifier: String, + id: String, + toAddress: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.bridge?.bridgeNFTToEvmAddressV2? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) guard let nftId = UInt64(id) else { throw NFTError.invalidTokenId @@ -1266,8 +1579,13 @@ extension FlowNetwork { ]) } - static func bridgeNFTFromEVMToAnyFlow(identifier: String, id: String, receiver: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.bridge?.bridgeNFTFromEvmToFlowV2?.toFunc() ?? "" + static func bridgeNFTFromEVMToAnyFlow( + identifier: String, + id: String, + receiver: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.bridge?.bridgeNFTFromEvmToFlowV2? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) guard let nftId = BigUInt(id) else { @@ -1280,37 +1598,51 @@ extension FlowNetwork { .address(Flow.Address(hex: receiver)), ]) } - - static func checkCoaLink(address: String) async throws -> Bool? { + + static func checkCoaLink(address _: String) async throws -> Bool? { guard let fromAddress = WalletManager.shared.getPrimaryWalletAddress() else { throw LLError.invalidAddress } let originCadence = CadenceManager.shared.current.evm?.checkCoaLink?.toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: cadenceStr), arguments: [.address(Flow.Address(hex: fromAddress))]).decode(Bool?.self) + let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceStr), + arguments: [.address(Flow.Address(hex: fromAddress))] + ).decode(Bool?.self) return resonpse } - + static func coaLink() async throws -> Flow.ID { let originCadence = CadenceManager.shared.current.evm?.coaLink?.toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: []) } + /// evm contract address, eg. 0x7f27352D5F83Db87a5A3E00f4B07Cc2138D8ee52 static func getAssociatedFlowIdentifier(address: String) async throws -> String? { - let originCadence = CadenceManager.shared.current.bridge?.getAssociatedFlowIdentifier?.toFunc() ?? "" + let originCadence = CadenceManager.shared.current.bridge?.getAssociatedFlowIdentifier? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock(script: Flow.Script(text: cadenceStr), arguments: [.string(address)]).decode( + let resonpse = try await flow.accessAPI.executeScriptAtLatestBlock( + script: Flow.Script(text: cadenceStr), + arguments: [.string(address)] + ).decode( String?.self ) return resonpse } } -//MARK: Bridge between Child and EVM +// MARK: Bridge between Child and EVM + extension FlowNetwork { - static func bridgeChildNFTToEvm(nft identifier: String, id: UInt64, child: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildNFTToEvm?.toFunc() ?? "" + static func bridgeChildNFTToEvm( + nft identifier: String, + id: UInt64, + child: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildNFTToEvm? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .string(identifier), @@ -1318,48 +1650,68 @@ extension FlowNetwork { .address(Flow.Address(hex: child)), ]) } - - static func bridgeChildNFTFromEvm(nft identifier: String, id: UInt64, child: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildNFTFromEvm?.toFunc() ?? "" + + static func bridgeChildNFTFromEvm( + nft identifier: String, + id: UInt64, + child: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildNFTFromEvm? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - + let nftId = BigUInt(id) - + return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .string(identifier), .address(Flow.Address(hex: child)), .uint256(nftId), ]) } - - static func batchBridgeChildNFTToCoa(nft identifier: String, ids: [UInt64], child: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.hybridCustody?.batchBridgeChildNFTToEvm?.toFunc() ?? "" + + static func batchBridgeChildNFTToCoa( + nft identifier: String, + ids: [UInt64], + child: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.hybridCustody?.batchBridgeChildNFTToEvm? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - + let idMaped = ids.map { Flow.Cadence.FValue.uint64($0) } - + return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .string(identifier), .address(Flow.Address(hex: child)), .array(idMaped), ]) } - - static func batchBridgeChildNFTFromCoa(nft identifier: String, ids: [UInt64], child: String) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.hybridCustody?.batchBridgeChildNFTFromEvm?.toFunc() ?? "" + + static func batchBridgeChildNFTFromCoa( + nft identifier: String, + ids: [UInt64], + child: String + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.hybridCustody?.batchBridgeChildNFTFromEvm? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) - + let idMaped = ids.map { Flow.Cadence.FValue.uint64($0) } - + return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ .string(identifier), .address(Flow.Address(hex: child)), .array(idMaped), ]) } - - static func bridgeChildTokenToCoa(vaultIdentifier: String, child:String, amount: Decimal) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildFTToEvm?.toFunc() ?? "" + + static func bridgeChildTokenToCoa( + vaultIdentifier: String, + child: String, + amount: Decimal + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildFTToEvm? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) let amountValue = Flow.Cadence.FValue.ufix64(amount) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ @@ -1368,9 +1720,14 @@ extension FlowNetwork { amountValue, ]) } - - static func bridgeChildTokenFromCoa(vaultIdentifier: String, child:String, amount: Decimal) async throws -> Flow.ID { - let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildFTFromEvm?.toFunc() ?? "" + + static func bridgeChildTokenFromCoa( + vaultIdentifier: String, + child: String, + amount: Decimal + ) async throws -> Flow.ID { + let originCadence = CadenceManager.shared.current.hybridCustody?.bridgeChildFTFromEvm? + .toFunc() ?? "" let cadenceStr = originCadence.replace(by: ScriptAddress.addressMap()) let amountValue = Flow.Cadence.FValue.ufix64(amount) return try await sendTransaction(cadenceStr: cadenceStr, argumentList: [ @@ -1382,7 +1739,10 @@ extension FlowNetwork { } extension FlowNetwork { - private static func sendTransaction(cadenceStr: String, argumentList: [Flow.Cadence.FValue]) async throws -> Flow.ID { + private static func sendTransaction( + cadenceStr: String, + argumentList: [Flow.Cadence.FValue] + ) async throws -> Flow.ID { let fromKeyIndex = WalletManager.shared.keyIndex guard let fromAddress = WalletManager.shared.getPrimaryWalletAddress() else { throw LLError.invalidAddress @@ -1399,7 +1759,10 @@ extension FlowNetwork { argumentList } proposer { - Flow.TransactionProposalKey(address: Flow.Address(hex: fromAddress), keyIndex: fromKeyIndex) + Flow.TransactionProposalKey( + address: Flow.Address(hex: fromAddress), + keyIndex: fromKeyIndex + ) } authorizers { @@ -1429,6 +1792,6 @@ extension UInt8 { extension String { func compareVersion(to version: String) -> ComparisonResult { - return compare(version, options: .numeric) + compare(version, options: .numeric) } } diff --git a/FRW/Services/Network/GithubEndpoint.swift b/FRW/Services/Network/GithubEndpoint.swift index 71266dee..7df40d2d 100644 --- a/FRW/Services/Network/GithubEndpoint.swift +++ b/FRW/Services/Network/GithubEndpoint.swift @@ -8,6 +8,8 @@ import Foundation import Moya +// MARK: - GithubEndpoint + enum GithubEndpoint { case collections case ftTokenList @@ -15,9 +17,11 @@ enum GithubEndpoint { case EVMTokenList } +// MARK: TargetType + extension GithubEndpoint: TargetType { var baseURL: URL { - return URL(string: "https://raw.githubusercontent.com")! + URL(string: "https://raw.githubusercontent.com")! } var path: String { @@ -44,7 +48,6 @@ extension GithubEndpoint: TargetType { return "/Outblock/token-list-jsons/outblock/jsons/previewnet/flow/default.json" } } - case .EVMNFTList: switch LocalUserDefaults.shared.flowNetwork { case .testnet: diff --git a/FRW/Services/Router/Coordinator.swift b/FRW/Services/Router/Coordinator.swift index 2bfd46dc..44a0cb07 100644 --- a/FRW/Services/Router/Coordinator.swift +++ b/FRW/Services/Router/Coordinator.swift @@ -8,6 +8,9 @@ import Combine import SwiftUI import UIKit + +// MARK: - AppTabType + // import Lottie enum AppTabType { @@ -17,22 +20,18 @@ enum AppTabType { case profile } +// MARK: - AppTabBarPageProtocol + protocol AppTabBarPageProtocol { static func tabTag() -> AppTabType static func iconName() -> String static func color() -> Color } -final class Coordinator { - let window: UIWindow - lazy var rootNavi: UINavigationController? = nil +// MARK: - Coordinator - private lazy var privateView: AppPrivateView = { - let view = AppPrivateView() - return view - }() - - private var cancelSets = Set() +final class Coordinator { + // MARK: Lifecycle init(window: UIWindow) { self.window = window @@ -43,10 +42,25 @@ final class Coordinator { } }.store(in: &cancelSets) - NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(didEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(didBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) } + // MARK: Internal + + let window: UIWindow + lazy var rootNavi: UINavigationController? = nil + func showRootView() { if LocalUserDefaults.shared.onBoardingShown { showNormalView() @@ -54,6 +68,15 @@ final class Coordinator { showOnBoardingView() } } + + // MARK: Private + + private lazy var privateView: AppPrivateView = { + let view = AppPrivateView() + return view + }() + + private var cancelSets = Set() } extension Coordinator { @@ -91,7 +114,8 @@ extension Coordinator { // MARK: - Private Screen extension Coordinator { - @objc private func didEnterBackground() { + @objc + private func didEnterBackground() { privateView.alpha = 1 privateView.removeFromSuperview() privateView.frame = window.bounds @@ -101,7 +125,8 @@ extension Coordinator { } } - @objc private func didBecomeActive() { + @objc + private func didBecomeActive() { UIView.animate(withDuration: 0.25) { self.privateView.alpha = 0 } completion: { _ in diff --git a/FRW/Services/Router/RouteMap.swift b/FRW/Services/Router/RouteMap.swift index 28cae871..ef787bcf 100644 --- a/FRW/Services/Router/RouteMap.swift +++ b/FRW/Services/Router/RouteMap.swift @@ -1,5 +1,5 @@ // -// RouterMap.swift +// RouteMap.swift // Flow Wallet // // Created by Selina on 25/7/2022. @@ -11,12 +11,15 @@ import SwiftUI import UIKit +// MARK: - RouteMap + enum RouteMap {} typealias EmptyClosure = () -> Void typealias SwitchNetworkClosure = (LocalUserDefaults.FlowNetworkType) -> Void +typealias BoolClosure = (Bool) -> Void -// MARK: - Restore Login +// MARK: - RouteMap.RestoreLogin extension RouteMap { enum RestoreLogin { @@ -39,6 +42,8 @@ extension RouteMap { } } +// MARK: - RouteMap.RestoreLogin + RouterTarget + extension RouteMap.RestoreLogin: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -57,7 +62,6 @@ extension RouteMap.RestoreLogin: RouterTarget { case let .syncDevice(vm): let vc = CustomHostingController(rootView: SyncAddDeviceView(viewModel: vm)) Router.topPresentedController().present(vc, animated: true, completion: nil) - case .restoreList: navi.push(content: RestoreListView()) case .restoreMulti: @@ -76,7 +80,7 @@ extension RouteMap.RestoreLogin: RouterTarget { } } -// MARK: - Register +// MARK: - RouteMap.Register extension RouteMap { enum Register { @@ -85,6 +89,8 @@ extension RouteMap { } } +// MARK: - RouteMap.Register + RouterTarget + extension RouteMap.Register: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -96,7 +102,7 @@ extension RouteMap.Register: RouterTarget { } } -// MARK: - Backup +// MARK: - RouteMap.Backup extension RouteMap { enum Backup { @@ -113,7 +119,10 @@ extension RouteMap { case createPin case confirmPin(String) - case verityPin(MultiBackupVerifyPinViewModel.From, MultiBackupVerifyPinViewModel.VerifyCallback) + case verityPin( + MultiBackupVerifyPinViewModel.From, + MultiBackupVerifyPinViewModel.VerifyCallback + ) case introduction(IntroductionView.Topic, EmptyClosure, Bool) case thingsNeedKnowOnBackup @@ -122,6 +131,8 @@ extension RouteMap { } } +// MARK: - RouteMap.Backup + RouterTarget + extension RouteMap.Backup: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -140,7 +151,6 @@ extension RouteMap.Backup: RouterTarget { navi.push(content: BackupPasswordView(backupType: type)) case .backupManual: navi.push(content: ManualBackupView()) - case .backupList: navi.push(content: BackupListView()) case let .multiBackup(items): @@ -173,7 +183,7 @@ extension RouteMap.Backup: RouterTarget { } } -// MARK: - Wallet +// MARK: - RouteMap.Wallet extension RouteMap { enum Wallet { @@ -205,9 +215,12 @@ extension RouteMap { case chooseChild(MoveAccountsViewModel) case addCustomToken case showCustomToken(CustomToken) + case addTokenSheet(CustomToken, BoolClosure) } } +// MARK: - RouteMap.Wallet + RouterTarget + extension RouteMap.Wallet: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -239,9 +252,17 @@ extension RouteMap.Wallet: RouterTarget { let vc = TransactionListViewController(contractId: contractId) navi.pushViewController(vc, animated: true) case let .swap(fromToken): - navi.present(content: fromToken != nil ? SwapView(defaultFromToken: fromToken) : SwapView()) + navi + .present( + content: fromToken != nil ? SwapView(defaultFromToken: fromToken) : + SwapView() + ) case let .selectToken(selectedToken, disableTokens, callback): - let vm = AddTokenViewModel(selectedToken: selectedToken, disableTokens: disableTokens, selectCallback: callback) + let vm = AddTokenViewModel( + selectedToken: selectedToken, + disableTokens: disableTokens, + selectCallback: callback + ) navi.present(content: AddTokenView(vm: vm)) case .stakingList: navi.push(content: StakingListView()) @@ -257,7 +278,8 @@ extension RouteMap.Wallet: RouterTarget { let vc = CustomHostingController(rootView: StakeAmountView.StakeSetupView(vm: vm)) Router.topPresentedController().present(vc, animated: true, completion: nil) case .backToTokenDetail: - if let existVC = navi.viewControllers.first(where: { $0 as? RouteableUIHostingController != nil }) { + if let existVC = navi.viewControllers + .first(where: { $0 as? RouteableUIHostingController != nil }) { navi.popToViewController(existVC, animated: true) return } @@ -284,23 +306,38 @@ extension RouteMap.Wallet: RouterTarget { let vc = PresentHostingController(rootView: MoveAssetsView()) navi.present(vc, animated: true, completion: nil) case let .moveToken(tokenModel): - let vc = PresentHostingController(rootView: MoveTokenView(tokenModel: tokenModel, isPresent: .constant(true))) + let vc = PresentHostingController(rootView: MoveTokenView( + tokenModel: tokenModel, + isPresent: .constant(true) + )) navi.present(vc, animated: true, completion: nil) case let .selectMoveToken(token, callback): - let vm = AddTokenViewModel(selectedToken: token, disableTokens: [], selectCallback: callback) + let vm = AddTokenViewModel( + selectedToken: token, + disableTokens: [], + selectCallback: callback + ) Router.topPresentedController().present(content: AddTokenView(vm: vm)) case let .chooseChild(model): let vc = PresentHostingController(rootView: MoveAccountsView(viewModel: model)) Router.topPresentedController().present(vc, animated: true, completion: nil) case .addCustomToken: navi.push(content: AddCustomTokenView()) - case .showCustomToken(let token): + case let .showCustomToken(token): navi.push(content: CustomTokenDetailView(token: token)) + case let .addTokenSheet(token, callback): + let vc = PresentHostingController( + rootView: AddTokenSheetView( + customToken: token, + callback: callback + ) + ) + navi.present(vc, completion: nil) } } } -// MARK: - Profile +// MARK: - RouteMap.Profile extension RouteMap { enum Profile { @@ -339,6 +376,8 @@ extension RouteMap { } } +// MARK: - RouteMap.Profile + RouterTarget + extension RouteMap.Profile: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -361,22 +400,30 @@ extension RouteMap.Profile: RouterTarget { // navi.push(content: BackupPatternView()) // return #endif - if let existVC = navi.viewControllers.first(where: { $0.navigationItem.title == "backup".localized }) { + if let existVC = navi.viewControllers + .first(where: { $0.navigationItem.title == "backup".localized }) { navi.popToViewController(existVC, animated: true) return } navi.push(content: ProfileBackupView()) case let .walletSetting(animated, address): - Router.coordinator.rootNavi?.push(content: WalletSettingView(address: address), animated: animated) + Router.coordinator.rootNavi?.push( + content: WalletSettingView(address: address), + animated: animated + ) case .walletConnect: navi.push(content: WalletConnectView()) case let .privateKey(animated): Router.coordinator.rootNavi?.push(content: PrivateKeyView(), animated: animated) case let .manualBackup(animated): - Router.coordinator.rootNavi?.push(content: RecoveryPhraseView(backupMode: true), animated: animated) + Router.coordinator.rootNavi?.push( + content: RecoveryPhraseView(backupMode: true), + animated: animated + ) case let .security(animated): - if let existVC = Router.coordinator.rootNavi?.viewControllers.first(where: { $0.navigationItem.title == "security".localized }) { + if let existVC = Router.coordinator.rootNavi?.viewControllers + .first(where: { $0.navigationItem.title == "security".localized }) { navi.popToViewController(existVC, animated: animated) return } @@ -400,7 +447,8 @@ extension RouteMap.Profile: RouterTarget { let vm = ChildAccountDetailEditViewModel(childAccount: childAccount) navi.push(content: ChildAccountDetailEditView(vm: vm)) case .backToAccountSetting: - if let existVC = navi.viewControllers.first(where: { $0 as? RouteableUIHostingController != nil }) { + if let existVC = navi.viewControllers + .first(where: { $0 as? RouteableUIHostingController != nil }) { navi.popToViewController(existVC, animated: true) return } @@ -415,7 +463,6 @@ extension RouteMap.Profile: RouterTarget { navi.push(content: DevicesInfoView(info: model)) case .keychain: navi.push(content: KeychainListView()) - case .walletList: navi.push(content: WalletListView()) case .wallpaper: @@ -426,7 +473,7 @@ extension RouteMap.Profile: RouterTarget { } } -// MARK: - AddressBook +// MARK: - RouteMap.AddressBook extension RouteMap { enum AddressBook { @@ -437,6 +484,8 @@ extension RouteMap { } } +// MARK: - RouteMap.AddressBook + RouterTarget + extension RouteMap.AddressBook: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -452,7 +501,7 @@ extension RouteMap.AddressBook: RouterTarget { } } -// MARK: - PinCode +// MARK: - RouteMap.PinCode extension RouteMap { enum PinCode { @@ -463,6 +512,8 @@ extension RouteMap { } } +// MARK: - RouteMap.PinCode + RouterTarget + extension RouteMap.PinCode: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -487,7 +538,7 @@ extension RouteMap.PinCode: RouterTarget { } } -// MARK: - NFT +// MARK: - RouteMap.NFT extension RouteMap { enum NFT { @@ -501,6 +552,8 @@ extension RouteMap { } } +// MARK: - RouteMap.NFT + RouterTarget + extension RouteMap.NFT: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -513,7 +566,11 @@ extension RouteMap.NFT: RouterTarget { case .addCollection: navi.push(content: NFTAddCollectionView()) case let .send(nft, contact, childAccount): - let vc = CustomHostingController(rootView: NFTTransferView(nft: nft, target: contact, fromChildAccount: childAccount)) + let vc = CustomHostingController(rootView: NFTTransferView( + nft: nft, + target: contact, + fromChildAccount: childAccount + )) Router.topPresentedController().present(vc, animated: true, completion: nil) case .AR: print("") @@ -523,7 +580,7 @@ extension RouteMap.NFT: RouterTarget { } } -// MARK: - Transaction +// MARK: - RouteMap.Transaction extension RouteMap { enum Transaction { @@ -531,6 +588,8 @@ extension RouteMap { } } +// MARK: - RouteMap.Transaction + RouterTarget + extension RouteMap.Transaction: RouterTarget { func onPresent(navi _: UINavigationController) { switch self { @@ -544,7 +603,7 @@ extension RouteMap.Transaction: RouterTarget { } } -// MARK: - Explore +// MARK: - RouteMap.Explore extension RouteMap { enum Explore { @@ -558,11 +617,17 @@ extension RouteMap { case bookmark case linkChildAccount(ChildAccountLinkViewModel) case dapps - case switchNetwork(LocalUserDefaults.FlowNetworkType, LocalUserDefaults.FlowNetworkType, SwitchNetworkClosure?) + case switchNetwork( + LocalUserDefaults.FlowNetworkType, + LocalUserDefaults.FlowNetworkType, + SwitchNetworkClosure? + ) case signTypedMessage(BrowserSignTypedMessageViewModel) } } +// MARK: - RouteMap.Explore + RouterTarget + extension RouteMap.Explore: RouterTarget { func onPresent(navi: UINavigationController) { switch self { @@ -574,7 +639,6 @@ extension RouteMap.Explore: RouterTarget { } else { UIApplication.shared.open(url) } - case let .safariBrowser(url): let vc = SFSafariViewController(url: url) navi.present(vc, animated: true) @@ -585,7 +649,10 @@ extension RouteMap.Explore: RouterTarget { let vc = CustomHostingController(rootView: BrowserAuthzView(vm: vm), showLarge: true) Router.topPresentedController().present(vc, animated: true, completion: nil) case let .signMessage(vm): - let vc = CustomHostingController(rootView: BrowserSignMessageView(vm: vm), showLarge: true) + let vc = CustomHostingController( + rootView: BrowserSignMessageView(vm: vm), + showLarge: true + ) Router.topPresentedController().present(vc, animated: true, completion: nil) case .searchExplore: let inputVC = BrowserSearchInputViewController() @@ -612,8 +679,11 @@ extension RouteMap.Explore: RouterTarget { case let .switchNetwork(from, to, callback): let vc = CustomHostingController(rootView: NetworkSwitchPopView(from: from, to: to)) Router.topPresentedController().present(vc, animated: true, completion: nil) - case .signTypedMessage(let viewModel): - let vc = CustomHostingController(rootView: BrowserSignTypedMessageView(viewModel: viewModel), showLarge: true) + case let .signTypedMessage(viewModel): + let vc = CustomHostingController( + rootView: BrowserSignTypedMessageView(viewModel: viewModel), + showLarge: true + ) Router.topPresentedController().present(vc, animated: true, completion: nil) } } diff --git a/FRW/Services/Router/RouteableUIHostingController.swift b/FRW/Services/Router/RouteableUIHostingController.swift index 3f54e4d6..664182b2 100644 --- a/FRW/Services/Router/RouteableUIHostingController.swift +++ b/FRW/Services/Router/RouteableUIHostingController.swift @@ -8,7 +8,9 @@ import SwiftUI import UIKit -typealias RouteableView = View & RouterContentDelegate +typealias RouteableView = RouterContentDelegate & View + +// MARK: - RouterContentDelegate protocol RouterContentDelegate { /// UINavigationBar use this to smooth push animation @@ -31,15 +33,15 @@ protocol RouterContentDelegate { extension RouterContentDelegate { var isNavigationBarHidden: Bool { - return false + false } var navigationBarTitleDisplayMode: NavigationBarItem.TitleDisplayMode { - return .inline + .inline } var forceColorScheme: UIUserInterfaceStyle? { - return nil + nil } func backButtonAction() { @@ -49,11 +51,24 @@ extension RouterContentDelegate { func configNavigationItem(_: UINavigationItem) {} } -class RouteableUIHostingController: UIHostingController, UIPopoverPresentationControllerDelegate { +// MARK: - RouteableUIHostingController + +class RouteableUIHostingController: UIHostingController, + UIPopoverPresentationControllerDelegate { + // MARK: Lifecycle + override init(rootView: Content) { super.init(rootView: rootView) } + @available(*, unavailable) + @MainActor + dynamic required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + override func viewDidLoad() { super.viewDidLoad() @@ -64,12 +79,19 @@ class RouteableUIHostingController: UIHostingController< overrideUserInterfaceStyle = style } - let backItem = UIBarButtonItem(image: UIImage(systemName: "arrow.backward"), style: .plain, target: self, action: #selector(onBackButtonAction)) + let backItem = UIBarButtonItem( + image: UIImage(systemName: "arrow.backward"), + style: .plain, + target: self, + action: #selector(onBackButtonAction) + ) backItem.tintColor = UIColor(named: "button.color") navigationItem.leftBarButtonItem = backItem - navigationController?.navigationBar.prefersLargeTitles = rootView.navigationBarTitleDisplayMode == .large - navigationItem.largeTitleDisplayMode = rootView.navigationBarTitleDisplayMode == .large ? .always : .never + navigationController?.navigationBar.prefersLargeTitles = rootView + .navigationBarTitleDisplayMode == .large + navigationItem.largeTitleDisplayMode = rootView + .navigationBarTitleDisplayMode == .large ? .always : .never rootView.configNavigationItem(navigationItem) @@ -94,16 +116,15 @@ class RouteableUIHostingController: UIHostingController< } } - @objc private func onBackButtonAction() { - rootView.backButtonAction() + @objc + func presentationControllerDidDismiss(_: UIPresentationController) { + log.debug("[Route] ----") } - @available(*, unavailable) - @MainActor dynamic required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + // MARK: Private - @objc func presentationControllerDidDismiss(_: UIPresentationController) { - log.debug("[Route] ----") + @objc + private func onBackButtonAction() { + rootView.backButtonAction() } } diff --git a/FRW/Services/Track/EventTrack+Account.swift b/FRW/Services/Track/EventTrack+Account.swift index d3f57d3f..f1862ae1 100644 --- a/FRW/Services/Track/EventTrack+Account.swift +++ b/FRW/Services/Track/EventTrack+Account.swift @@ -10,35 +10,36 @@ import Foundation extension EventTrack.Account { static func create( key: String, - signAlgo: String, + signAlgo: String, hashAlgo: String, isSecure: Bool = true, isSeed: Bool - = false) { - EventTrack - .send(event: EventTrack.Account.created, properties: [ + = false + ) { + EventTrack + .send(event: EventTrack.Account.created, properties: [ "public_key": key, "is_secure_enclave": isSecure, "is_seed_phrase": isSeed, "sign_algo": signAlgo, - "hash_algo": hashAlgo - ]) + "hash_algo": hashAlgo, + ]) } - + static func createdTimeStart() { EventTrack.timeBegin(event: EventTrack.Account.createdTime) } - + static func createdTimeEnd() { EventTrack.timeEnd(event: EventTrack.Account.createdTime) } - + static func recovered(address: String, machanism: String, methods: [String]) { EventTrack .send(event: EventTrack.Account.recovered, properties: [ "address": address, "mechanism": machanism, - "methods": methods + "methods": methods, ]) } } diff --git a/FRW/Services/Track/EventTrack+Backup.swift b/FRW/Services/Track/EventTrack+Backup.swift index 95210a91..7eda39cd 100644 --- a/FRW/Services/Track/EventTrack+Backup.swift +++ b/FRW/Services/Track/EventTrack+Backup.swift @@ -18,7 +18,7 @@ extension EventTrack.Backup { "providers": source, ]) } - + static func multiCreatedFailed(source: String) { guard let address = WalletManager.shared.getPrimaryWalletAddress() else { return diff --git a/FRW/Services/Track/EventTrack+General.swift b/FRW/Services/Track/EventTrack+General.swift index 284d9b24..717741a3 100644 --- a/FRW/Services/Track/EventTrack+General.swift +++ b/FRW/Services/Track/EventTrack+General.swift @@ -8,14 +8,13 @@ import Foundation extension EventTrack.General { - - static func rpcError(error: String, scriptId: String) { EventTrack.send(event: EventTrack.General.rpcError, properties: [ "error": error, - "script_id": scriptId + "script_id": scriptId, ]) } + /// StakeAmountViewModel stake static func delegationCreated( address: String, @@ -26,9 +25,10 @@ extension EventTrack.General { .send(event: EventTrack.General.delegationCreated, properties: [ "address": address, "node_id": nodeId, - "amount": amount + "amount": amount, ]) } + /// BuyProvderView button action static func rampClick(source: String) { EventTrack @@ -36,6 +36,7 @@ extension EventTrack.General { "source": source, ]) } + /// home page buy button clicked static func security(type: String) { EventTrack diff --git a/FRW/Services/Track/EventTrack+Transaction.swift b/FRW/Services/Track/EventTrack+Transaction.swift index 9525fcf5..6a3c2fb6 100644 --- a/FRW/Services/Track/EventTrack+Transaction.swift +++ b/FRW/Services/Track/EventTrack+Transaction.swift @@ -8,7 +8,6 @@ import Foundation extension EventTrack.Transaction { - /// cadence decode script ,then SHA256 static func flowSigned( cadence: String, @@ -25,48 +24,50 @@ extension EventTrack.Transaction { "authorizers": authorizers, "proposer": proposer, "payer": payer, - "success": success + "success": success, ]) } - + static func evmSigned(flowAddress: String, evmAddress: String, txId: String, success: Bool) { EventTrack .send(event: EventTrack.Transaction.flowSigned, properties: [ "flow_address": flowAddress, "evm_address": evmAddress, "id": txId, - "success": success + "success": success, ]) } - + static func ftTransfer( from: String, to: String, type: String, - amount: Double - , identifier: String) { - EventTrack - .send(event: EventTrack.Transaction.FTTransfer, properties: [ - "from_address": from, - "to_address": to, - "type": type, - "amount": amount, - "ft_identifier": identifier - ]) + amount: Double, + identifier: String + ) { + EventTrack + .send(event: EventTrack.Transaction.FTTransfer, properties: [ + "from_address": from, + "to_address": to, + "type": type, + "amount": amount, + "ft_identifier": identifier, + ]) } - + static func NFTTransfer( from: String, to: String, type: String, - amount: Double - , identifier: String) { - EventTrack - .send(event: EventTrack.Transaction.FTTransfer, properties: [ - "from_address": from, - "to_address": to, - "type": type, - "ft_identifier": identifier - ]) - } + amount _: Double, + identifier: String + ) { + EventTrack + .send(event: EventTrack.Transaction.FTTransfer, properties: [ + "from_address": from, + "to_address": to, + "type": type, + "ft_identifier": identifier, + ]) + } } diff --git a/FRW/Services/Track/EventTrack.swift b/FRW/Services/Track/EventTrack.swift index c5520668..af084890 100644 --- a/FRW/Services/Track/EventTrack.swift +++ b/FRW/Services/Track/EventTrack.swift @@ -9,26 +9,18 @@ import Foundation import Mixpanel class EventTrack { - + // MARK: Internal + static func start(token: String) { Mixpanel.initialize(token: token) Mixpanel.mainInstance().registerSuperProperties(common()) -#if DEBUG + #if DEBUG Mixpanel.mainInstance().loggingEnabled = true -#endif + #endif } - - /// super properties - private static func common() -> [String: String] { - var param: [String: String] = [:] - - let scriptVersion = CadenceManager.shared.version - param["cadence_script_version"] = scriptVersion - - return param - } - //MARK: - Action - + + // MARK: - Action + /// call when switch user static func switchUser() { guard let uid = UserManager.shared.activatedUID else { @@ -37,22 +29,34 @@ class EventTrack { } Mixpanel.mainInstance().identify(distinctId: uid) } - + static func updateNetwork() { // flow_network } - + static func send(event: EventTrackNameProtocol, properties: [String: MixpanelType]? = nil) { Mixpanel .mainInstance() .track(event: event.name, properties: properties) } - + static func timeBegin(event: EventTrackNameProtocol) { Mixpanel.mainInstance().time(event: event.name) } - + static func timeEnd(event: EventTrackNameProtocol, properties: [String: MixpanelType]? = nil) { Mixpanel.mainInstance().track(event: event.name, properties: properties) } + + // MARK: Private + + /// super properties + private static func common() -> [String: String] { + var param: [String: String] = [:] + + let scriptVersion = CadenceManager.shared.version + param["cadence_script_version"] = scriptVersion + + return param + } } diff --git a/FRW/Services/Track/EventTrackName.swift b/FRW/Services/Track/EventTrackName.swift index 6bf77095..a78ee423 100644 --- a/FRW/Services/Track/EventTrackName.swift +++ b/FRW/Services/Track/EventTrackName.swift @@ -7,62 +7,73 @@ import Foundation +// MARK: - EventTrackNameProtocol + protocol EventTrackNameProtocol { var name: String { get } } -//MARK: General +// MARK: - EventTrack.General + extension EventTrack { enum General: String, EventTrackNameProtocol { case rpcError = "script_error" case delegationCreated = "delegation_created" case rampClicked = "on_ramp_clicked" case securityTool = "security_tool" - + + // MARK: Internal + var name: String { - return self.rawValue + rawValue } } - } -//MARK: Backup +// MARK: - EventTrack.Backup + extension EventTrack { enum Backup: String, EventTrackNameProtocol { case multiCreated = "multi_backup_created" case multiCreationFailed = "multi_backup_creation_failed" - + + // MARK: Internal + var name: String { - return self.rawValue + rawValue } } } -//MARK: Transaction +// MARK: - EventTrack.Transaction + extension EventTrack { enum Transaction: String, EventTrackNameProtocol { - case flowSigned = "cadence_transaction_signed" case evmSigned = "evm_transaction_signed" case FTTransfer = "ft_transfer" case NFTTransfer = "nft_transfer" - + + // MARK: Internal + var name: String { - self.rawValue + rawValue } } } -//MARK: Account +// MARK: - EventTrack.Account + extension EventTrack { enum Account: String, EventTrackNameProtocol { - case created = "account_created" case createdTime = "account_creation_time" case recovered = "account_recovered" - + + // MARK: Internal + var name: String { - self.rawValue + rawValue } } } diff --git a/FRW/Tools/JSONStorage.swift b/FRW/Tools/JSONStorage.swift index 8a6847ff..430e29de 100644 --- a/FRW/Tools/JSONStorage.swift +++ b/FRW/Tools/JSONStorage.swift @@ -1,5 +1,5 @@ // -// FileStorage.swift +// JSONStorage.swift // Flow Wallet // // Created by cat on 2022/5/13. @@ -8,23 +8,24 @@ import Foundation import Haneke +// MARK: - JSONStorage + @propertyWrapper struct JSONStorage { - var value: T? - let key: String + // MARK: Lifecycle init(key: String) { self.key = key if let jsonData = UserDefaults.standard.data(forKey: theKey) { let decoder = JSONDecoder() - value = try? decoder.decode(T.self, from: jsonData) + self.value = try? decoder.decode(T.self, from: jsonData) } } - private var theKey: String { - // TODO: fileName: {userId_filename} - return key - } + // MARK: Internal + + var value: T? + let key: String var wrappedValue: T? { set { @@ -37,12 +38,21 @@ struct JSONStorage { value } } + + // MARK: Private + + private var theKey: String { + // TODO: fileName: {userId_filename} + key + } } +// MARK: - JSONTestReader + @propertyWrapper struct JSONTestReader { - var value: T? - let fileName: String + // MARK: Lifecycle + init(fileName: String) { self.fileName = fileName if let path = Bundle.main.path(forResource: fileName, ofType: "json") { @@ -50,11 +60,16 @@ struct JSONTestReader { let url = URL(fileURLWithPath: path) let jsonData = try Data(contentsOf: url) let decoder = JSONDecoder() - value = try? decoder.decode(T.self, from: jsonData) + self.value = try? decoder.decode(T.self, from: jsonData) } catch {} } } + // MARK: Internal + + var value: T? + let fileName: String + var wrappedValue: T? { set { value = newValue diff --git a/FRW/Tools/JSONValue.swift b/FRW/Tools/JSONValue.swift index 0a998711..7e2108e0 100644 --- a/FRW/Tools/JSONValue.swift +++ b/FRW/Tools/JSONValue.swift @@ -7,6 +7,8 @@ import Foundation +// MARK: - JSONValue + enum JSONValue { case string(String) case number(Double) @@ -15,6 +17,8 @@ enum JSONValue { case bool(Bool) case null + // MARK: Lifecycle + init(_ value: Any) { if let stringValue = value as? String { self = .string(stringValue) @@ -37,50 +41,49 @@ enum JSONValue { extension JSONValue { var rawValue: Any { switch self { - case .string(let value): + case let .string(value): return value - case .number(let value): + case let .number(value): return value - case .object(let value): + case let .object(value): return value.mapValues { $0.rawValue } - case .array(let value): + case let .array(value): return value.map { $0.rawValue } - case .bool(let value): + case let .bool(value): return value case .null: return NSNull() } } - + func toString() -> String { - switch self { - case .string(let value): - return "\(value)" - case .number(let value): - return "\(value)" - case .object(let dictionary): - let objectString = dictionary.map { "\($0): \($1.toString())" } - .joined(separator: ", ") - return "{ \(objectString) }" - case .array(let array): - let arrayString = array.map { $0.toString() }.joined(separator: ", ") - return "[ \(arrayString) ]" - case .bool(let value): - return value ? "true" : "false" - case .null: - return "null" - } + switch self { + case let .string(value): + return "\(value)" + case let .number(value): + return "\(value)" + case let .object(dictionary): + let objectString = dictionary.map { "\($0): \($1.toString())" } + .joined(separator: ", ") + return "{ \(objectString) }" + case let .array(array): + let arrayString = array.map { $0.toString() }.joined(separator: ", ") + return "[ \(arrayString) ]" + case let .bool(value): + return value ? "true" : "false" + case .null: + return "null" } + } } extension JSONValue { - static func parse(jsonString: String) -> JSONValue? { guard let jsonData = jsonString.data(using: .utf8) else { print("Invalid JSON string") return nil } - + do { let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) return JSONValue(jsonObject) @@ -90,5 +93,3 @@ extension JSONValue { } } } - - diff --git a/FRW/Tools/ThirdParty/BMChineseSort/BMChineseUtils.swift b/FRW/Tools/ThirdParty/BMChineseSort/BMChineseUtils.swift index 7310ffc1..0031affe 100644 --- a/FRW/Tools/ThirdParty/BMChineseSort/BMChineseUtils.swift +++ b/FRW/Tools/ThirdParty/BMChineseSort/BMChineseUtils.swift @@ -1,5 +1,5 @@ // -// BMChineseSort.swift +// BMChineseUtils.swift // // // Created by Baymax on 2018/10/29. @@ -8,21 +8,27 @@ import Foundation +// MARK: - BMChineseSortModel + /// 封装用于排序的 单位 模型 class BMChineseSortModel { + // MARK: Public + + public static func < (lhs: BMChineseSortModel, rhs: BMChineseSortModel) -> Bool { + lhs.pinYin < rhs.pinYin + } + + // MARK: Internal + // 进行比较的字符串 var string = "" // 字符串对应的拼音 首字母 var pinYin = "" // 需要比较的对象 var object: Element? - - public static func < (lhs: BMChineseSortModel, rhs: BMChineseSortModel) -> Bool { - return lhs.pinYin < rhs.pinYin - } } -// MARK: - ---------------------- 实现 ------------------------ +// MARK: - BMChineseSort + BMChineseSortProtocol extension BMChineseSort: BMChineseSortProtocol { public static func transformChinese(_ word: String) -> String { @@ -36,13 +42,16 @@ extension BMChineseSort: BMChineseSortProtocol { return "" } - public static func sortAndGroup(objectArray: [T]?, - key: String?, - finish: @escaping (_ success: Bool, - _ unGroupedArr: [T], - _ sectionTitleArr: [String], - _ sortedObjArr: [[T]]) -> Void) - { + public static func sortAndGroup( + objectArray: [T]?, + key: String?, + finish: @escaping ( + _ success: Bool, + _ unGroupedArr: [T], + _ sectionTitleArr: [String], + _ sortedObjArr: [[T]] + ) -> Void + ) { BMChineseSort.share.sortAndGroup(objectArray: objectArray, key: key, finish: finish) } @@ -52,22 +61,25 @@ extension BMChineseSort: BMChineseSortProtocol { /// - objectArray: 需要排序的数组 /// - key: 需要排序的字段,字符串数组传nil /// - finish: 排序后回调 - func sortAndGroup(objectArray: [T]?, - key: String?, - finish: @escaping (_ success: Bool, - _ unGroupedArr: [T], - _ sectionTitleArr: [String], - _ sortedObjArr: [[T]]) -> Void) - { + func sortAndGroup( + objectArray: [T]?, + key: String?, + finish: @escaping ( + _ success: Bool, + _ unGroupedArr: [T], + _ sectionTitleArr: [String], + _ sortedObjArr: [[T]] + ) -> Void + ) { // 返回空 - if objectArray == nil || objectArray!.count == 0 { + if objectArray == nil || objectArray!.isEmpty { finish(true, [], [], []) return } // key是否正确 let firstoObj = objectArray?.first if firstoObj is String { - if key != nil, key?.count != 0 { + if key != nil, key?.isEmpty != true { logMsg("** warning ** :当前排序对象为字符串类型,key无法生效") } } else { @@ -145,7 +157,7 @@ extension BMChineseSort: BMChineseSortProtocol { unSortedArr.append(obj) if firstLetter != lastTitle { // 保存上条分组 信息 - if newSection.count != 0 { + if !newSection.isEmpty { sectionTitleArr.append(lastTitle) sortedObjArr.append(newSection) newSection = [T]() @@ -157,7 +169,7 @@ extension BMChineseSort: BMChineseSortProtocol { } } // 保存最后一组信息 - if newSection.count != 0 { + if !newSection.isEmpty { sectionTitleArr.append(lastTitle) sortedObjArr.append(newSection) } @@ -192,7 +204,7 @@ extension BMChineseSort: BMChineseSortProtocol { } // 去除首尾空白字符 model.string = model.string.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) - if model.string.count == 0 { + if model.string.isEmpty { model.string = specialCharSectionTitle } else { let prefix = String(model.string.prefix(1)) @@ -220,7 +232,7 @@ extension BMChineseSort: BMChineseSortProtocol { for (key, value) in polyphoneMapping { newChinese = newChinese.replacingOccurrences(of: key, with: value) } - var resultStr: String = "" + var resultStr = "" let wordsArr = BMChineseSort.transformChinese(chinese).components(separatedBy: " ") /// 如果word是小写 为汉字转的拼音 提取首字符 否则 保留全部 @@ -255,7 +267,7 @@ extension BMChineseSort: BMChineseSortProtocol { str = str.replacingOccurrences(of: key, with: value) } - var resultStr: String = "" + var resultStr = "" for scalar in str.unicodeScalars { let code = Int(scalar.value) if code >= 65, code <= 90 { @@ -292,17 +304,229 @@ extension BMChineseSort: BMChineseSortProtocol { // 汉字码表对应的首字母 private let firstLetterArray = - "ydkqsxnwzssxjbymgcczqpssqbycdscdqldylybssjgyqzjjfgcclzznwdwzjljpfyynnjjtmynzwzhflzppqhgccyynmjqyxxgd" + "nnsnsjnjnsnnmlnrxyfsngnnnnqzggllyjlnyzssecykyyhqwjssggyxyqyjtwktjhychmnxjtlhjyqbyxdldwrrjnwysrldzjpc" + "bzjjbrcfslnczstzfxxchtrqggddlyccssymmrjcyqzpwwjjyfcrwfdfzqpyddwyxkyjawjffxjbcftzyhhycyswccyxsclcxxwz" + "cxnbgnnxbxlzsqsbsjpysazdhmdzbqbscwdzzyytzhbtsyyfzgntnxjywqnknphhlxgybfmjnbjhhgqtjcysxstkzglyckglysmz" + "xyalmeldccxgzyrjxjzlnjzcqkcnnjwhjczccqljststbnhbtyxceqxkkwjyflzqlyhjxspsfxlmpbysxxxytccnylllsjxfhjxp" + "jbtffyabyxbcczbzyclwlczggbtssmdtjcxpthyqtgjjxcjfzkjzjqnlzwlslhdzbwjncjzyzsqnycqynzcjjwybrtwpyftwexcs" + "kdzctbyhyzqyyjxzcfbzzmjyxxsdczottbzljwfckscsxfyrlrygmbdthjxsqjccsbxyytswfbjdztnbcnzlcyzzpsacyzzsqqcs" + "hzqydxlbpjllmqxqydzxsqjtzpxlcglqdcwzfhctdjjsfxjejjtlbgxsxjmyjjqpfzasyjnsydjxkjcdjsznbartcclnjqmwnqnc" + "lllkbdbzzsyhqcltwlccrshllzntylnewyzyxczxxgdkdmtcedejtsyyssdqdfmxdbjlkrwnqlybglxnlgtgxbqjdznyjsjyjcjm" + "rnymgrcjczgjmzmgxmmryxkjnymsgmzzymknfxmbdtgfbhcjhkylpfmdxlxjjsmsqgzsjlqdldgjycalcmzcsdjllnxdjffffjcn" + "fnnffpfkhkgdpqxktacjdhhzdddrrcfqyjkqccwjdxhwjlyllzgcfcqjsmlzpbjjblsbcjggdckkdezsqcckjgcgkdjtjllzycxk" + "lqccgjcltfpcqczgwbjdqyzjjbyjhsjddwgfsjgzkcjctllfspkjgqjhzzljplgjgjjthjjyjzccmlzlyqbgjwmljkxzdznjqsyz" + "mljlljkywxmkjlhskjhbmclyymkxjqlbmllkmdxxkwyxwslmlpsjqqjqxyqfjtjdxmxxllcrqbsyjbgwynnggbcnxpjtgpapfgdj" + "qbhbncfjyzjkjkhxqfgqckfhygkhdkllsdjqxpqyaybnqsxqnszswhbsxwhxwbzzxdmndjbsbkbbzklylxgwxjjwaqzmywsjqlsj" + "xxjqwjeqxnchetlzalyyyszzpnkyzcptlshtzcfycyxyljsdcjqagyslcllyyysslqqqnldxzsccscadycjysfsgbfrsszqsbxjp" + "sjysdrckgjlgtkzjzbdktcsyqpyhstcldjnhmymcgxyzhjdctmhltxzhylamoxyjcltyfbqqjpfbdfehthsqhzywwcncxcdwhowg" + "yjlegmdqcwgfjhcsntmydolbygnqwesqpwnmlrydzszzlyqpzgcwxhnxpyxshmdqjgztdppbfbhzhhjyfdzwkgkzbldnzsxhqeeg" + "zxylzmmzyjzgszxkhkhtxexxgylyapsthxdwhzydpxagkydxbhnhnkdnjnmyhylpmgecslnzhkxxlbzzlbmlsfbhhgsgyyggbhsc" + "yajtxglxtzmcwzydqdqmngdnllszhngjzwfyhqswscelqajynytlsxthaznkzzsdhlaxxtwwcjhqqtddwzbcchyqzflxpslzqgpz" + "sznglydqtbdlxntctajdkywnsyzljhhdzckryyzywmhychhhxhjkzwsxhdnxlyscqydpslyzwmypnkxyjlkchtyhaxqsyshxasmc" + "hkdscrsgjpwqsgzjlwwschsjhsqnhnsngndantbaalczmsstdqjcjktscjnxplggxhhgoxzcxpdmmhldgtybynjmxhmrzplxjzck" + "zxshflqxxcdhxwzpckczcdytcjyxqhlxdhypjqxnlsyydzozjnhhqezysjyayxkypdgxddnsppyzndhthrhxydpcjjhtcnnctlhb" + "ynyhmhzllnnxmylllmdcppxhmxdkycyrdltxjchhznxclcclylnzsxnjzzlnnnnwhyqsnjhxynttdkyjpychhyegkcwtwlgjrlgg" + "tgtygyhpyhylqyqgcwyqkpyyettttlhyylltyttsylnyzwgywgpydqqzzdqnnkcqnmjjzzbxtqfjkdffbtkhzkbxdjjkdjjtlbwf" + "zpptkqtztgpdwntpjyfalqmkgxbcclzfhzcllllanpnxtjklcclgyhdzfgyddgcyyfgydxkssendhykdndknnaxxhbpbyyhxccga" + "pfqyjjdmlxcsjzllpcnbsxgjyndybwjspcwjlzkzddtacsbkzdyzypjzqsjnkktknjdjgyepgtlnyqnacdntcyhblgdzhbbydmjr" + "egkzyheyybjmcdtafzjzhgcjnlghldwxjjkytcyksssmtwcttqzlpbszdtwcxgzagyktywxlnlcpbclloqmmzsslcmbjcsdzkydc" + "zjgqjdsmcytzqqlnzqzxssbpkdfqmddzzsddtdmfhtdycnaqjqkypbdjyyxtljhdrqxlmhkydhrnlklytwhllrllrcxylbnsrnzz" + "symqzzhhkyhxksmzsyzgcxfbnbsqlfzxxnnxkxwymsddyqnggqmmyhcdzttfgyyhgsbttybykjdnkyjbelhdypjqnfxfdnkzhqks" + "byjtzbxhfdsbdaswpawajldyjsfhblcnndnqjtjnchxfjsrfwhzfmdrfjyxwzpdjkzyjympcyznynxfbytfyfwygdbnzzzdnytxz" + "emmqbsqehxfznbmflzzsrsyqjgsxwzjsprytjsjgskjjgljjynzjjxhgjkymlpyyycxycgqzswhwlyrjlpxslcxmnsmwklcdnkny" + "npsjszhdzeptxmwywxyysywlxjqcqxzdclaeelmcpjpclwbxsqhfwrtfnjtnqjhjqdxhwlbyccfjlylkyynldxnhycstyywncjtx" + "ywtrmdrqnwqcmfjdxzmhmayxnwmyzqtxtlmrspwwjhanbxtgzypxyyrrclmpamgkqjszycymyjsnxtplnbappypylxmyzkynldgy" + "jzcchnlmzhhanqnbgwqtzmxxmllhgdzxnhxhrxycjmffxywcfsbssqlhnndycannmtcjcypnxnytycnnymnmsxndlylysljnlxys" + "sqmllyzlzjjjkyzzcsfbzxxmstbjgnxnchlsnmcjscyznfzlxbrnnnylmnrtgzqysatswryhyjzmgdhzgzdwybsscskxsyhytsxg" + "cqgxzzbhyxjscrhmkkbsczjyjymkqhzjfnbhmqhysnjnzybknqmcjgqhwlsnzswxkhljhyybqcbfcdsxdldspfzfskjjzwzxsddx" + "jseeegjscssygclxxnwwyllymwwwgydkzjggggggsycknjwnjpcxbjjtqtjwdsspjxcxnzxnmelptfsxtllxcljxjjljsxctnswx" + "lennlyqrwhsycsqnybyaywjejqfwqcqqcjqgxaldbzzyjgkgxbltqyfxjltpydkyqhpmatlcndnkxmtxynhklefxdllegqtymsaw" + "hzmljtkynxlyjzljeeyybqqffnlyxhdsctgjhxywlkllxqkcctnhjlqmkkzgcyygllljdcgydhzwypysjbzjdzgyzzhywyfqdtyz" + "szyezklymgjjhtsmqwyzljyywzcsrkqyqltdxwcdrjalwsqzwbdcqyncjnnszjlncdcdtlzzzacqqzzddxyblxcbqjylzllljddz" + "jgyqyjzyxnyyyexjxksdaznyrdlzyyynjlslldyxjcykywnqcclddnyyynycgczhjxcclgzqjgnwnncqqjysbzzxyjxjnxjfzbsb" + "dsfnsfpzxhdwztdmpptflzzbzdmyypqjrsdzsqzsqxbdgcpzswdwcsqzgmdhzxmwwfybpngphdmjthzsmmbgzmbzjcfzhfcbbnmq" + "dfmbcmcjxlgpnjbbxgyhyyjgptzgzmqbqdcgybjxlwnkydpdymgcftpfxyztzxdzxtgkptybbclbjaskytssqyymscxfjhhlslls" + "jpqjjqaklyldlycctsxmcwfgngbqxllllnyxtyltyxytdpjhnhgnkbyqnfjyyzbyyessessgdyhfhwtcqbsdzjtfdmxhcnjzymqw" + "srxjdzjqbdqbbsdjgnfbknbxdkqhmkwjjjgdllthzhhyyyyhhsxztyyyccbdbpypzyccztjpzywcbdlfwzcwjdxxhyhlhwczxjtc" + "nlcdpxnqczczlyxjjcjbhfxwpywxzpcdzzbdccjwjhmlxbqxxbylrddgjrrctttgqdczwmxfytmmzcwjwxyywzzkybzcccttqnhx" + "nwxxkhkfhtswoccjybcmpzzykbnnzpbthhjdlszddytyfjpxyngfxbyqxzbhxcpxxtnzdnnycnxsxlhkmzxlthdhkghxxsshqyhh" + "cjyxglhzxcxnhekdtgqxqypkdhentykcnymyyjmkqyyyjxzlthhqtbyqhxbmyhsqckwwyllhcyylnneqxqwmcfbdccmljggxdqkt" + "lxkknqcdgcjwyjjlyhhqyttnwchhxcxwherzjydjccdbqcdgdnyxzdhcqrxcbhztqcbxwgqwyybxhmbymykdyecmqkyaqyngyzsl" + "fnkkqgyssqyshngjctxkzycssbkyxhyylstycxqthysmnscpmmgcccccmnztasmgqzjhklosjylswtmqzyqkdzljqqyplzycztcq" + "qpbbcjzclpkhqcyyxxdtdddsjcxffllchqxmjlwcjcxtspycxndtjshjwhdqqqckxyamylsjhmlalygxcyydmamdqmlmcznnyybz" + "xkyflmcncmlhxrcjjhsylnmtjggzgywjxsrxcwjgjqhqzdqjdcjjskjkgdzcgjjyjylxzxxcdqhhheslmhlfsbdjsyyshfyssczq" + "lpbdrfnztzdkykhsccgkwtqzckmsynbcrxqbjyfaxpzzedzcjykbcjwhyjbqzzywnyszptdkzpfpbaztklqnhbbzptpptyzzybhn" + "ydcpzmmcycqmcjfzzdcmnlfpbplngqjtbttajzpzbbdnjkljqylnbzqhksjznggqstzkcxchpzsnbcgzkddzqanzgjkdrtlzldwj" + "njzlywtxndjzjhxnatncbgtzcsskmljpjytsnwxcfjwjjtkhtzplbhsnjssyjbhbjyzlstlsbjhdnwqpslmmfbjdwajyzccjtbnn" + "nzwxxcdslqgdsdpdzgjtqqpsqlyyjzlgyhsdlctcbjtktyczjtqkbsjlgnnzdncsgpynjzjjyyknhrpwszxmtncszzyshbyhyzax" + "ywkcjtllckjjtjhgcssxyqyczbynnlwqcglzgjgqyqcczssbcrbcskydznxjsqgxssjmecnstjtpbdlthzwxqwqczexnqczgwesg" + "ssbybstscslccgbfsdqnzlccglllzghzcthcnmjgyzazcmsksstzmmzckbjygqljyjppldxrkzyxccsnhshhdznlzhzjjcddcbcj" + "xlbfqbczztpqdnnxljcthqzjgylklszzpcjdscqjhjqkdxgpbajynnsmjtzdxlcjyryynhjbngzjkmjxltbsllrzpylssznxjhll" + "hyllqqzqlsymrcncxsljmlzltzldwdjjllnzggqxppskyggggbfzbdkmwggcxmcgdxjmcjsdycabxjdlnbcddygskydqdxdjjyxh" + "saqazdzfslqxxjnqzylblxxwxqqzbjzlfbblylwdsljhxjyzjwtdjcyfqzqzzdzsxzzqlzcdzfxhwspynpqzmlpplffxjjnzzyls" + "jnyqzfpfzgsywjjjhrdjzzxtxxglghtdxcskyswmmtcwybazbjkshfhgcxmhfqhyxxyzftsjyzbxyxpzlchmzmbxhzzssyfdmncw" + "dabazlxktcshhxkxjjzjsthygxsxyyhhhjwxkzxssbzzwhhhcwtzzzpjxsyxqqjgzyzawllcwxznxgyxyhfmkhydwsqmnjnaycys" + "pmjkgwcqhylajgmzxhmmcnzhbhxclxdjpltxyjkdyylttxfqzhyxxsjbjnayrsmxyplckdnyhlxrlnllstycyyqygzhhsccsmcct" + "zcxhyqfpyyrpbflfqnntszlljmhwtcjqyzwtlnmlmdwmbzzsnzrbpdddlqjjbxtcsnzqqygwcsxfwzlxccrszdzmcyggdyqsgtnn" + "nlsmymmsyhfbjdgyxccpshxczcsbsjyygjmpbwaffyfnxhydxzylremzgzzyndsznlljcsqfnxxkptxzgxjjgbmyyssnbtylbnlh" + "bfzdcyfbmgqrrmzszxysjtznnydzzcdgnjafjbdknzblczszpsgcycjszlmnrznbzzldlnllysxsqzqlcxzlsgkbrxbrbzcycxzj" + "zeeyfgklzlnyhgzcgzlfjhgtgwkraajyzkzqtsshjjxdzyznynnzyrzdqqhgjzxsszbtkjbbfrtjxllfqwjgclqtymblpzdxtzag" + "bdhzzrbgjhwnjtjxlkscfsmwlldcysjtxkzscfwjlbnntzlljzllqblcqmqqcgcdfpbphzczjlpyyghdtgwdxfczqyyyqysrclqz" + "fklzzzgffcqnwglhjycjjczlqzzyjbjzzbpdcsnnjgxdqnknlznnnnpsntsdyfwwdjzjysxyyczcyhzwbbyhxrylybhkjksfxtjj" + "mmchhlltnyymsxxyzpdjjycsycwmdjjkqyrhllngpngtlyycljnnnxjyzfnmlrgjjtyzbsyzmsjyjhgfzqmsyxrszcytlrtqzsst" + "kxgqkgsptgxdnjsgcqcqhmxggztqydjjznlbznxqlhyqgggthqscbyhjhhkyygkggcmjdzllcclxqsftgjslllmlcskctbljszsz" + "mmnytpzsxqhjcnnqnyexzqzcpshkzzyzxxdfgmwqrllqxrfztlystctmjcsjjthjnxtnrztzfqrhcgllgcnnnnjdnlnnytsjtlny" + "xsszxcgjzyqpylfhdjsbbdczgjjjqzjqdybssllcmyttmqnbhjqmnygjyeqyqmzgcjkpdcnmyzgqllslnclmholzgdylfzslncnz" + "lylzcjeshnyllnxnjxlyjyyyxnbcljsswcqqnnyllzldjnllzllbnylnqchxyyqoxccqkyjxxxyklksxeyqhcqkkkkcsnyxxyqxy" + "gwtjohthxpxxhsnlcykychzzcbwqbbwjqcscszsslcylgddsjzmmymcytsdsxxscjpqqsqylyfzychdjynywcbtjsydchcyddjlb" + "djjsodzyqyskkyxdhhgqjyohdyxwgmmmazdybbbppbcmnnpnjzsmtxerxjmhqdntpjdcbsnmssythjtslmltrcplzszmlqdsdmjm" + "qpnqdxcfrnnfsdqqyxhyaykqyddlqyyysszbydslntfgtzqbzmchdhczcwfdxtmqqsphqwwxsrgjcwnntzcqmgwqjrjhtqjbbgwz" + "fxjhnqfxxqywyyhyscdydhhqmrmtmwctbszppzzglmzfollcfwhmmsjzttdhlmyffytzzgzyskjjxqyjzqbhmbzclyghgfmshpcf" + "zsnclpbqsnjyzslxxfpmtyjygbxlldlxpzjyzjyhhzcywhjylsjexfszzywxkzjlnadymlymqjpwxxhxsktqjezrpxxzghmhwqpw" + "qlyjjqjjzszcnhjlchhnxjlqwzjhbmzyxbdhhypylhlhlgfwlcfyytlhjjcwmscpxstkpnhjxsntyxxtestjctlsslstdlllwwyh" + "dnrjzsfgxssyczykwhtdhwjglhtzdqdjzxxqgghltzphcsqfclnjtclzpfstpdynylgmjllycqhynspchylhqyqtmzymbywrfqyk" + "jsyslzdnjmpxyyssrhzjnyqtqdfzbwwdwwrxcwggyhxmkmyyyhmxmzhnksepmlqqmtcwctmxmxjpjjhfxyyzsjzhtybmstsyjznq" + "jnytlhynbyqclcycnzwsmylknjxlggnnpjgtysylymzskttwlgsmzsylmpwlcwxwqcssyzsyxyrhssntsrwpccpwcmhdhhxzdzyf" + "jhgzttsbjhgyglzysmyclllxbtyxhbbzjkssdmalhhycfygmqypjyjqxjllljgclzgqlycjcctotyxmtmshllwlqfxymzmklpszz" + "cxhkjyclctyjcyhxsgyxnnxlzwpyjpxhjwpjpwxqqxlxsdhmrslzzydwdtcxknstzshbsccstplwsscjchjlcgchssphylhfhhxj" + "sxallnylmzdhzxylsxlmzykcldyahlcmddyspjtqjzlngjfsjshctsdszlblmssmnyymjqbjhrzwtyydchjljapzwbgqxbkfnbjd" + "llllyylsjydwhxpsbcmljpscgbhxlqhyrljxyswxhhzlldfhlnnymjljyflyjycdrjlfsyzfsllcqyqfgqyhnszlylmdtdjcnhbz" + "llnwlqxygyyhbmgdhxxnhlzzjzxczzzcyqzfngwpylcpkpykpmclgkdgxzgxwqbdxzzkzfbddlzxjtpjpttbythzzdwslcpnhslt" + "jxxqlhyxxxywzyswttzkhlxzxzpyhgzhknfsyhntjrnxfjcpjztwhplshfcrhnslxxjxxyhzqdxqwnnhyhmjdbflkhcxcwhjfyjc" + "fpqcxqxzyyyjygrpynscsnnnnchkzdyhflxxhjjbyzwttxnncyjjymswyxqrmhxzwfqsylznggbhyxnnbwttcsybhxxwxyhhxyxn" + "knyxmlywrnnqlxbbcljsylfsytjzyhyzawlhorjmnsczjxxxyxchcyqryxqzddsjfslyltsffyxlmtyjmnnyyyxltzcsxqclhzxl" + "wyxzhnnlrxkxjcdyhlbrlmbrdlaxksnlljlyxxlynrylcjtgncmtlzllcyzlpzpzyawnjjfybdyyzsepckzzqdqpbpsjpdyttbdb" + "bbyndycncpjmtmlrmfmmrwyfbsjgygsmdqqqztxmkqwgxllpjgzbqrdjjjfpkjkcxbljmswldtsjxldlppbxcwkcqqbfqbccajzg" + "mykbhyhhzykndqzybpjnspxthlfpnsygyjdbgxnhhjhzjhstrstldxskzysybmxjlxyslbzyslzxjhfybqnbylljqkygzmcyzzym" + "ccslnlhzhwfwyxzmwyxtynxjhbyymcysbmhysmydyshnyzchmjjmzcaahcbjbbhblytylsxsnxgjdhkxxtxxnbhnmlngsltxmrhn" + "lxqqxmzllyswqgdlbjhdcgjyqyymhwfmjybbbyjyjwjmdpwhxqldyapdfxxbcgjspckrssyzjmslbzzjfljjjlgxzgyxyxlszqkx" + "bexyxhgcxbpndyhwectwwcjmbtxchxyqqllxflyxlljlssnwdbzcmyjclwswdczpchqekcqbwlcgydblqppqzqfnqdjhymmcxtxd" + "rmzwrhxcjzylqxdyynhyyhrslnrsywwjjymtltllgtqcjzyabtckzcjyccqlysqxalmzynywlwdnzxqdllqshgpjfjljnjabcqzd" + "jgthhsstnyjfbswzlxjxrhgldlzrlzqzgsllllzlymxxgdzhgbdphzpbrlwnjqbpfdwonnnhlypcnjccndmbcpbzzncyqxldomzb" + "lzwpdwyygdstthcsqsccrsssyslfybnntyjszdfndpdhtqzmbqlxlcmyffgtjjqwftmnpjwdnlbzcmmcngbdzlqlpnfhyymjylsd" + "chdcjwjcctljcldtljjcbddpndsszycndbjlggjzxsxnlycybjjxxcbylzcfzppgkcxqdzfztjjfjdjxzbnzyjqctyjwhdyczhym" + "djxttmpxsplzcdwslshxypzgtfmlcjtacbbmgdewycyzxdszjyhflystygwhkjyylsjcxgywjcbllcsnddbtzbsclyzczzssqdll" + "mjyyhfllqllxfdyhabxggnywyypllsdldllbjcyxjznlhljdxyyqytdlllbngpfdfbbqbzzmdpjhgclgmjjpgaehhbwcqxajhhhz" + "chxyphjaxhlphjpgpzjqcqzgjjzzgzdmqyybzzphyhybwhazyjhykfgdpfqsdlzmljxjpgalxzdaglmdgxmmzqwtxdxxpfdmmssy" + "mpfmdmmkxksyzyshdzkjsysmmzzzmdydyzzczxbmlstmdyemxckjmztyymzmzzmsshhdccjewxxkljsthwlsqlyjzllsjssdppmh" + "nlgjczyhmxxhgncjmdhxtkgrmxfwmckmwkdcksxqmmmszzydkmsclcmpcjmhrpxqpzdsslcxkyxtwlkjyahzjgzjwcjnxyhmmbml" + "gjxmhlmlgmxctkzmjlyscjsyszhsyjzjcdajzhbsdqjzgwtkqxfkdmsdjlfmnhkzqkjfeypzyszcdpynffmzqykttdzzefmzlbnp" + "plplpbpszalltnlkckqzkgenjlwalkxydpxnhsxqnwqnkxqclhyxxmlnccwlymqyckynnlcjnszkpyzkcqzqljbdmdjhlasqlbyd" + "wqlwdgbqcryddztjybkbwszdxdtnpjdtcnqnfxqqmgnseclstbhpwslctxxlpwydzklnqgzcqapllkqcylbqmqczqcnjslqzdjxl" + "ddhpzqdljjxzqdjyzhhzlkcjqdwjppypqakjyrmpzbnmcxkllzllfqpylllmbsglzysslrsysqtmxyxzqzbscnysyztffmzzsmzq" + "hzssccmlyxwtpzgxzjgzgsjzgkddhtqggzllbjdzlsbzhyxyzhzfywxytymsdnzzyjgtcmtnxqyxjscxhslnndlrytzlryylxqht" + "xsrtzcgyxbnqqzfhykmzjbzymkbpnlyzpblmcnqyzzzsjztjctzhhyzzjrdyzhnfxklfzslkgjtctssyllgzrzbbjzzklpkbczys" + "nnyxbjfbnjzzxcdwlzyjxzzdjjgggrsnjkmsmzjlsjywqsnyhqjsxpjztnlsnshrnynjtwchglbnrjlzxwjqxqkysjycztlqzybb" + "ybyzjqdwgyzcytjcjxckcwdkkzxsnkdnywwyyjqyytlytdjlxwkcjnklccpzcqqdzzqlcsfqchqqgssmjzzllbjjzysjhtsjdysj" + "qjpdszcdchjkjzzlpycgmzndjxbsjzzsyzyhgxcpbjydssxdzncglqmbtsfcbfdzdlznfgfjgfsmpnjqlnblgqcyyxbqgdjjqsrf" + "kztjdhczklbsdzcfytplljgjhtxzcsszzxstjygkgckgynqxjplzbbbgcgyjzgczqszlbjlsjfzgkqqjcgycjbzqtldxrjnbsxxp" + "zshszycfwdsjjhxmfczpfzhqhqmqnknlyhtycgfrzgnqxcgpdlbzcsczqlljblhbdcypscppdymzzxgyhckcpzjgslzlnscnsldl" + "xbmsdlddfjmkdqdhslzxlsznpqpgjdlybdskgqlbzlnlkyyhzttmcjnqtzzfszqktlljtyyllnllqyzqlbdzlslyyzxmdfszsnxl" + "xznczqnbbwskrfbcylctnblgjpmczzlstlxshtzcyzlzbnfmqnlxflcjlyljqcbclzjgnsstbrmhxzhjzclxfnbgxgtqncztmsfz" + "kjmssncljkbhszjntnlzdntlmmjxgzjyjczxyhyhwrwwqnztnfjscpyshzjfyrdjsfscjzbjfzqzchzlxfxsbzqlzsgyftzdcszx" + "zjbjpszkjrhxjzcgbjkhcggtxkjqglxbxfgtrtylxqxhdtsjxhjzjjcmzlcqsbtxwqgxtxxhxftsdkfjhzyjfjxnzldlllcqsqqz" + "qwqxswqtwgwbzcgcllqzbclmqjtzgzyzxljfrmyzflxnsnxxjkxrmjdzdmmyxbsqbhgzmwfwygmjlzbyytgzyccdjyzxsngnyjyz" + "nbgpzjcqsyxsxrtfyzgrhztxszzthcbfclsyxzlzqmzlmplmxzjssfsbysmzqhxxnxrxhqzzzsslyflczjrcrxhhzxqndshxsjjh" + "qcjjbcynsysxjbqjpxzqplmlxzkyxlxcnlcycxxzzlxdlllmjyhzxhyjwkjrwyhcpsgnrzlfzwfzznsxgxflzsxzzzbfcsyjdbrj" + "krdhhjxjljjtgxjxxstjtjxlyxqfcsgswmsbctlqzzwlzzkxjmltmjyhsddbxgzhdlbmyjfrzfcgclyjbpmlysmsxlszjqqhjzfx" + "gfqfqbphngyyqxgztnqwyltlgwgwwhnlfmfgzjmgmgbgtjflyzzgzyzaflsspmlbflcwbjztljjmzlpjjlymqtmyyyfbgygqzgly" + "zdxqyxrqqqhsxyyqxygjtyxfsfsllgnqcygycwfhcccfxpylypllzqxxxxxqqhhsshjzcftsczjxspzwhhhhhapylqnlpqafyhxd" + "ylnkmzqgggddesrenzltzgchyppcsqjjhclljtolnjpzljlhymhezdydsqycddhgznndzclzywllznteydgnlhslpjjbdgwxpcnn" + "tycklkclwkllcasstknzdnnjttlyyzssysszzryljqkcgdhhyrxrzydgrgcwcgzqffbppjfzynakrgywyjpqxxfkjtszzxswzddf" + "bbqtbgtzkznpzfpzxzpjszbmqhkyyxyldkljnypkyghgdzjxxeaxpnznctzcmxcxmmjxnkszqnmnlwbwwqjjyhclstmcsxnjcxxt" + "pcnfdtnnpglllzcjlspblpgjcdtnjjlyarscffjfqwdpgzdwmrzzcgodaxnssnyzrestyjwjyjdbcfxnmwttbqlwstszgybljpxg" + "lbnclgpcbjftmxzljylzxcltpnclcgxtfzjshcrxsfysgdkntlbyjcyjllstgqcbxnhzxbxklylhzlqzlnzcqwgzlgzjncjgcmnz" + "zgjdzxtzjxycyycxxjyyxjjxsssjstsstdppghtcsxwzdcsynptfbchfbblzjclzzdbxgcjlhpxnfzflsyltnwbmnjhszbmdnbcy" + "sccldnycndqlyjjhmqllcsgljjsyfpyyccyltjantjjpwycmmgqyysxdxqmzhszxbftwwzqswqrfkjlzjqqyfbrxjhhfwjgzyqac" + "myfrhcyybynwlpexcczsyyrlttdmqlrkmpbgmyyjprkznbbsqyxbhyzdjdnghpmfsgbwfzmfqmmbzmzdcgjlnnnxyqgmlrygqccy" + "xzlwdkcjcggmcjjfyzzjhycfrrcmtznzxhkqgdjxccjeascrjthpljlrzdjrbcqhjdnrhylyqjsymhzydwcdfryhbbydtssccwbx" + "glpzmlzjdqsscfjmmxjcxjytycghycjwynsxlfemwjnmkllswtxhyyyncmmcyjdqdjzglljwjnkhpzggflccsczmcbltbhbqjxqd" + "jpdjztghglfjawbzyzjltstdhjhctcbchflqmpwdshyytqwcnntjtlnnmnndyyyxsqkxwyyflxxnzwcxypmaelyhgjwzzjbrxxaq" + "jfllpfhhhytzzxsgqjmhspgdzqwbwpjhzjdyjcqwxkthxsqlzyymysdzgnqckknjlwpnsyscsyzlnmhqsyljxbcxtlhzqzpcycyk" + "pppnsxfyzjjrcemhszmnxlxglrwgcstlrsxbygbzgnxcnlnjlclynymdxwtzpalcxpqjcjwtcyyjlblxbzlqmyljbghdslssdmxm" + "bdczsxyhamlczcpjmcnhjyjnsykchskqmczqdllkablwjqsfmocdxjrrlyqchjmybyqlrhetfjzfrfksryxfjdwtsxxywsqjysly" + "xwjhsdlxyyxhbhawhwjcxlmyljcsqlkydttxbzslfdxgxsjkhsxxybssxdpwncmrptqzczenygcxqfjxkjbdmljzmqqxnoxslyxx" + "lylljdzptymhbfsttqqwlhsgynlzzalzxclhtwrrqhlstmypyxjjxmnsjnnbryxyjllyqyltwylqyfmlkljdnlltfzwkzhljmlhl" + "jnljnnlqxylmbhhlnlzxqchxcfxxlhyhjjgbyzzkbxscqdjqdsndzsygzhhmgsxcsymxfepcqwwrbpyyjqryqcyjhqqzyhmwffhg" + "zfrjfcdbxntqyzpcyhhjlfrzgpbxzdbbgrqstlgdgylcqmgchhmfywlzyxkjlypjhsywmqqggzmnzjnsqxlqsyjtcbehsxfszfxz" + "wfllbcyyjdytdthwzsfjmqqyjlmqsxlldttkghybfpwdyysqqrnqwlgwdebzwcyygcnlkjxtmxmyjsxhybrwfymwfrxyymxysctz" + "ztfykmldhqdlgyjnlcryjtlpsxxxywlsbrrjwxhqybhtydnhhxmmywytycnnmnssccdalwztcpqpyjllqzyjswjwzzmmglmxclmx" + "nzmxmzsqtzppjqblpgxjzhfljjhycjsrxwcxsncdlxsyjdcqzxslqyclzxlzzxmxqrjmhrhzjbhmfljlmlclqnldxzlllfyprgjy" + "nxcqqdcmqjzzxhnpnxzmemmsxykynlxsxtljxyhwdcwdzhqyybgybcyscfgfsjnzdrzzxqxrzrqjjymcanhrjtldbpyzbstjhxxz" + "ypbdwfgzzrpymnnkxcqbyxnbnfyckrjjcmjegrzgyclnnzdnkknsjkcljspgyyclqqjybzssqlllkjftbgtylcccdblsppfylgyd" + "tzjqjzgkntsfcxbdkdxxhybbfytyhbclnnytgdhryrnjsbtcsnyjqhklllzslydxxwbcjqsbxnpjzjzjdzfbxxbrmladhcsnclbj" + "dstblprznswsbxbcllxxlzdnzsjpynyxxyftnnfbhjjjgbygjpmmmmsszljmtlyzjxswxtyledqpjmpgqzjgdjlqjwjqllsdgjgy" + "gmscljjxdtygjqjjjcjzcjgdzdshqgzjggcjhqxsnjlzzbxhsgzxcxyljxyxyydfqqjhjfxdhctxjyrxysqtjxyefyyssyxjxncy" + "zxfxcsxszxyyschshxzzzgzzzgfjdldylnpzgsjaztyqzpbxcbdztzczyxxyhhscjshcggqhjhgxhsctmzmehyxgebtclzkkwytj" + "zrslekestdbcyhqqsayxcjxwwgsphjszsdncsjkqcxswxfctynydpccczjqtcwjqjzzzqzljzhlsbhpydxpsxshhezdxfptjqyzc" + "xhyaxncfzyyhxgnqmywntzsjbnhhgymxmxqcnssbcqsjyxxtyyhybcqlmmszmjzzllcogxzaajzyhjmchhcxzsxsdznleyjjzjbh" + "zwjzsqtzpsxzzdsqjjjlnyazphhyysrnqzthzhnyjyjhdzxzlswclybzyecwcycrylchzhzydzydyjdfrjjhtrsqtxyxjrjhojyn" + "xelxsfsfjzghpzsxzszdzcqzbyyklsgsjhczshdgqgxyzgxchxzjwyqwgyhksseqzzndzfkwyssdclzstsymcdhjxxyweyxczayd" + "mpxmdsxybsqmjmzjmtjqlpjyqzcgqhyjhhhqxhlhdldjqcfdwbsxfzzyyschtytyjbhecxhjkgqfxbhyzjfxhwhbdzfyzbchpnpg" + "dydmsxhkhhmamlnbyjtmpxejmcthqbzyfcgtyhwphftgzzezsbzegpbmdskftycmhbllhgpzjxzjgzjyxzsbbqsczzlzscstpgxm" + "jsfdcczjzdjxsybzlfcjsazfgszlwbczzzbyztzynswyjgxzbdsynxlgzbzfygczxbzhzftpbgzgejbstgkdmfhyzzjhzllzzgjq" + "zlsfdjsscbzgpdlfzfzszyzyzsygcxsnxxchczxtzzljfzgqsqqxcjqccccdjcdszzyqjccgxztdlgscxzsyjjqtcclqdqztqchq" + "qyzynzzzpbkhdjfcjfztypqyqttynlmbdktjcpqzjdzfpjsbnjlgyjdxjdcqkzgqkxclbzjtcjdqbxdjjjstcxnxbxqmslyjcxnt" + "jqwwcjjnjjlllhjcwqtbzqqczczpzzdzyddcyzdzccjgtjfzdprntctjdcxtqzdtjnplzbcllctdsxkjzqdmzlbznbtjdcxfczdb" + "czjjltqqpldckztbbzjcqdcjwynllzlzccdwllxwzlxrxntqjczxkjlsgdnqtddglnlajjtnnynkqlldzntdnycygjwyxdxfrsqs" + "tcdenqmrrqzhhqhdldazfkapbggpzrebzzykyqspeqjjglkqzzzjlysyhyzwfqznlzzlzhwcgkypqgnpgblplrrjyxcccgyhsfzf" + "wbzywtgzxyljczwhncjzplfflgskhyjdeyxhlpllllcygxdrzelrhgklzzyhzlyqszzjzqljzflnbhgwlczcfjwspyxzlzlxgccp" + "zbllcxbbbbnbbcbbcrnnzccnrbbnnldcgqyyqxygmqzwnzytyjhyfwtehznjywlccntzyjjcdedpwdztstnjhtymbjnyjzlxtsst" + "phndjxxbyxqtzqddtjtdyztgwscszqflshlnzbcjbhdlyzjyckwtydylbnydsdsycctyszyyebgexhqddwnygyclxtdcystqnygz" + "ascsszzdzlcclzrqxyywljsbymxshzdembbllyyllytdqyshymrqnkfkbfxnnsbychxbwjyhtqbpbsbwdzylkgzskyghqzjxhxjx" + "gnljkzlyycdxlfwfghljgjybxblybxqpqgntzplncybxdjyqydymrbeyjyyhkxxstmxrczzjwxyhybmcflyzhqyzfwxdbxbcwzms" + "lpdmyckfmzklzcyqycclhxfzlydqzpzygyjyzmdxtzfnnyttqtzhgsfcdmlccytzxjcytjmkslpzhysnwllytpzctzccktxdhxxt" + "qcyfksmqccyyazhtjplylzlyjbjxtfnyljyynrxcylmmnxjsmybcsysslzylljjgyldzdlqhfzzblfndsqkczfyhhgqmjdsxyctt" + "xnqnjpyybfcjtyyfbnxejdgyqbjrcnfyyqpghyjsyzngrhtknlnndzntsmgklbygbpyszbydjzsstjztsxzbhbscsbzczptqfzlq" + "flypybbjgszmnxdjmtsyskkbjtxhjcegbsmjyjzcstmljyxrczqscxxqpyzhmkyxxxjcljyrmyygadyskqlnadhrskqxzxztcggz" + "dlmlwxybwsyctbhjhcfcwzsxwwtgzlxqshnyczjxemplsrcgltnzntlzjcyjgdtclglbllqpjmzpapxyzlaktkdwczzbncctdqqz" + "qyjgmcdxltgcszlmlhbglkznnwzndxnhlnmkydlgxdtwcfrjerctzhydxykxhwfzcqshknmqqhzhhymjdjskhxzjzbzzxympajnm" + "ctbxlsxlzynwrtsqgscbptbsgzwyhtlkssswhzzlyytnxjgmjrnsnnnnlskztxgxlsammlbwldqhylakqcqctmycfjbslxclzjcl" + "xxknbnnzlhjphqplsxsckslnhpsfqcytxjjzljldtzjjzdlydjntptnndskjfsljhylzqqzlbthydgdjfdbyadxdzhzjnthqbykn" + "xjjqczmlljzkspldsclbblnnlelxjlbjycxjxgcnlcqplzlznjtsljgyzdzpltqcssfdmnycxgbtjdcznbgbqyqjwgkfhtnbyqzq" + "gbkpbbyzmtjdytblsqmbsxtbnpdxklemyycjynzdtldykzzxtdxhqshygmzsjycctayrzlpwltlkxslzcggexclfxlkjrtlqjaqz" + "ncmbqdkkcxglczjzxjhptdjjmzqykqsecqzdshhadmlzfmmzbgntjnnlhbyjbrbtmlbyjdzxlcjlpldlpcqdhlhzlycblcxccjad" + "qlmzmmsshmybhbnkkbhrsxxjmxmdznnpklbbrhgghfchgmnklltsyyycqlcskymyehywxnxqywbawykqldnntndkhqcgdqktgpkx" + "hcpdhtwnmssyhbwcrwxhjmkmzngwtmlkfghkjyldyycxwhyyclqhkqhtdqkhffldxqwytyydesbpkyrzpjfyyzjceqdzzdlattpb" + "fjllcxdlmjsdxegwgsjqxcfbssszpdyzcxznyxppzydlyjccpltxlnxyzyrscyyytylwwndsahjsygyhgywwaxtjzdaxysrltdps" + "syxfnejdxyzhlxlllzhzsjnyqyqyxyjghzgjcyjchzlycdshhsgczyjscllnxzjjyyxnfsmwfpyllyllabmddhwzxjmcxztzpmlq" + "chsfwzynctlndywlslxhymmylmbwwkyxyaddxylldjpybpwnxjmmmllhafdllaflbnhhbqqjqzjcqjjdjtffkmmmpythygdrjrdd" + "wrqjxnbysrmzdbyytbjhpymyjtjxaahggdqtmystqxkbtzbkjlxrbyqqhxmjjbdjntgtbxpgbktlgqxjjjcdhxqdwjlwrfmjgwqh" + "cnrxswgbtgygbwhswdwrfhwytjjxxxjyzyslphyypyyxhydqpxshxyxgskqhywbdddpplcjlhqeewjgsyykdpplfjthkjltcyjhh" + "jttpltzzcdlyhqkcjqysteeyhkyzyxxyysddjkllpymqyhqgxqhzrhbxpllnqydqhxsxxwgdqbshyllpjjjthyjkyphthyyktyez" + "yenmdshlzrpqfbnfxzbsftlgxsjbswyysksflxlpplbbblnsfbfyzbsjssylpbbffffsscjdstjsxtryjcyffsyzyzbjtlctsbsd" + "hrtjjbytcxyyeylycbnebjdsysyhgsjzbxbytfzwgenhhhthjhhxfwgcstbgxklstyymtmbyxjskzscdyjrcythxzfhmymcxlzns" + "djtxtxrycfyjsbsdyerxhljxbbdeynjghxgckgscymblxjmsznskgxfbnbbthfjyafxwxfbxmyfhdttcxzzpxrsywzdlybbktyqw" + "qjbzypzjznjpzjlztfysbttslmptzrtdxqsjehbnylndxljsqmlhtxtjecxalzzspktlzkqqyfsyjywpcpqfhjhytqxzkrsgtksq" + "czlptxcdyyzsslzslxlzmacpcqbzyxhbsxlzdltztjtylzjyytbzypltxjsjxhlbmytxcqrblzssfjzztnjytxmyjhlhpblcyxqj" + "qqkzzscpzkswalqsplczzjsxgwwwygyatjbbctdkhqhkgtgpbkqyslbxbbckbmllndzstbklggqkqlzbkktfxrmdkbftpzfrtppm" + "ferqnxgjpzsstlbztpszqzsjdhljqlzbpmsmmsxlqqnhknblrddnhxdkddjcyyljfqgzlgsygmjqjkhbpmxyxlytqwlwjcpbmjxc" + "yzydrjbhtdjyeqshtmgsfyplwhlzffnynnhxqhpltbqpfbjwjdbygpnxtbfzjgnnntjshxeawtzylltyqbwjpgxghnnkndjtmszs" + "qynzggnwqtfhclssgmnnnnynzqqxncjdqgzdlfnykljcjllzlmzznnnnsshthxjlzjbbhqjwwycrdhlyqqjbeyfsjhthnrnwjhwp" + "slmssgzttygrqqwrnlalhmjtqjsmxqbjjzjqzyzkxbjqxbjxshzssfglxmxnxfghkzszggslcnnarjxhnlllmzxelglxydjytlfb" + "kbpnlyzfbbhptgjkwetzhkjjxzxxglljlstgshjjyqlqzfkcgnndjsszfdbctwwseqfhqjbsaqtgypjlbxbmmywxgslzhglsgnyf" + "ljbyfdjfngsfmbyzhqffwjsyfyjjphzbyyzffwotjnlmftwlbzgyzqxcdjygzyyryzynyzwegazyhjjlzrthlrmgrjxzclnnnljj" + "yhtbwjybxxbxjjtjteekhwslnnlbsfazpqqbdlqjjtyyqlyzkdksqjnejzldqcgjqnnjsncmrfqthtejmfctyhypymhydmjncfgy" + "yxwshctxrljgjzhzcyyyjltkttntmjlzclzzayyoczlrlbszywjytsjyhbyshfjlykjxxtmzyyltxxypslqyjzyzyypnhmymdyyl" + "blhlsyygqllnjjymsoycbzgdlyxylcqyxtszegxhzglhwbljheyxtwqmakbpqcgyshhegqcmwyywljyjhyyzlljjylhzyhmgsljl" + "jxcjjyclycjbcpzjzjmmwlcjlnqljjjlxyjmlszljqlycmmgcfmmfpqqmfxlqmcffqmmmmhnznfhhjgtthxkhslnchhyqzxtmmqd" + "cydyxyqmyqylddcyaytazdcymdydlzfffmmycqcwzzmabtbyctdmndzggdftypcgqyttssffwbdttqssystwnjhjytsxxylbyyhh" + "whxgzxwznnqzjzjjqjccchykxbzszcnjtllcqxynjnckycynccqnxyewyczdcjycchyjlbtzyycqwlpgpyllgktltlgkgqbgychj" + "xy" + "ydkqsxnwzssxjbymgcczqpssqbycdscdqldylybssjgyqzjjfgcclzznwdwzjljpfyynnjjtmynzwzhflzppqhgccyynmjqyxxgd" + + "nnsnsjnjnsnnmlnrxyfsngnnnnqzggllyjlnyzssecykyyhqwjssggyxyqyjtwktjhychmnxjtlhjyqbyxdldwrrjnwysrldzjpc" + + "bzjjbrcfslnczstzfxxchtrqggddlyccssymmrjcyqzpwwjjyfcrwfdfzqpyddwyxkyjawjffxjbcftzyhhycyswccyxsclcxxwz" + + "cxnbgnnxbxlzsqsbsjpysazdhmdzbqbscwdzzyytzhbtsyyfzgntnxjywqnknphhlxgybfmjnbjhhgqtjcysxstkzglyckglysmz" + + "xyalmeldccxgzyrjxjzlnjzcqkcnnjwhjczccqljststbnhbtyxceqxkkwjyflzqlyhjxspsfxlmpbysxxxytccnylllsjxfhjxp" + + "jbtffyabyxbcczbzyclwlczggbtssmdtjcxpthyqtgjjxcjfzkjzjqnlzwlslhdzbwjncjzyzsqnycqynzcjjwybrtwpyftwexcs" + + "kdzctbyhyzqyyjxzcfbzzmjyxxsdczottbzljwfckscsxfyrlrygmbdthjxsqjccsbxyytswfbjdztnbcnzlcyzzpsacyzzsqqcs" + + "hzqydxlbpjllmqxqydzxsqjtzpxlcglqdcwzfhctdjjsfxjejjtlbgxsxjmyjjqpfzasyjnsydjxkjcdjsznbartcclnjqmwnqnc" + + "lllkbdbzzsyhqcltwlccrshllzntylnewyzyxczxxgdkdmtcedejtsyyssdqdfmxdbjlkrwnqlybglxnlgtgxbqjdznyjsjyjcjm" + + "rnymgrcjczgjmzmgxmmryxkjnymsgmzzymknfxmbdtgfbhcjhkylpfmdxlxjjsmsqgzsjlqdldgjycalcmzcsdjllnxdjffffjcn" + + "fnnffpfkhkgdpqxktacjdhhzdddrrcfqyjkqccwjdxhwjlyllzgcfcqjsmlzpbjjblsbcjggdckkdezsqcckjgcgkdjtjllzycxk" + + "lqccgjcltfpcqczgwbjdqyzjjbyjhsjddwgfsjgzkcjctllfspkjgqjhzzljplgjgjjthjjyjzccmlzlyqbgjwmljkxzdznjqsyz" + + "mljlljkywxmkjlhskjhbmclyymkxjqlbmllkmdxxkwyxwslmlpsjqqjqxyqfjtjdxmxxllcrqbsyjbgwynnggbcnxpjtgpapfgdj" + + "qbhbncfjyzjkjkhxqfgqckfhygkhdkllsdjqxpqyaybnqsxqnszswhbsxwhxwbzzxdmndjbsbkbbzklylxgwxjjwaqzmywsjqlsj" + + "xxjqwjeqxnchetlzalyyyszzpnkyzcptlshtzcfycyxyljsdcjqagyslcllyyysslqqqnldxzsccscadycjysfsgbfrsszqsbxjp" + + "sjysdrckgjlgtkzjzbdktcsyqpyhstcldjnhmymcgxyzhjdctmhltxzhylamoxyjcltyfbqqjpfbdfehthsqhzywwcncxcdwhowg" + + "yjlegmdqcwgfjhcsntmydolbygnqwesqpwnmlrydzszzlyqpzgcwxhnxpyxshmdqjgztdppbfbhzhhjyfdzwkgkzbldnzsxhqeeg" + + "zxylzmmzyjzgszxkhkhtxexxgylyapsthxdwhzydpxagkydxbhnhnkdnjnmyhylpmgecslnzhkxxlbzzlbmlsfbhhgsgyyggbhsc" + + "yajtxglxtzmcwzydqdqmngdnllszhngjzwfyhqswscelqajynytlsxthaznkzzsdhlaxxtwwcjhqqtddwzbcchyqzflxpslzqgpz" + + "sznglydqtbdlxntctajdkywnsyzljhhdzckryyzywmhychhhxhjkzwsxhdnxlyscqydpslyzwmypnkxyjlkchtyhaxqsyshxasmc" + + "hkdscrsgjpwqsgzjlwwschsjhsqnhnsngndantbaalczmsstdqjcjktscjnxplggxhhgoxzcxpdmmhldgtybynjmxhmrzplxjzck" + + "zxshflqxxcdhxwzpckczcdytcjyxqhlxdhypjqxnlsyydzozjnhhqezysjyayxkypdgxddnsppyzndhthrhxydpcjjhtcnnctlhb" + + "ynyhmhzllnnxmylllmdcppxhmxdkycyrdltxjchhznxclcclylnzsxnjzzlnnnnwhyqsnjhxynttdkyjpychhyegkcwtwlgjrlgg" + + "tgtygyhpyhylqyqgcwyqkpyyettttlhyylltyttsylnyzwgywgpydqqzzdqnnkcqnmjjzzbxtqfjkdffbtkhzkbxdjjkdjjtlbwf" + + "zpptkqtztgpdwntpjyfalqmkgxbcclzfhzcllllanpnxtjklcclgyhdzfgyddgcyyfgydxkssendhykdndknnaxxhbpbyyhxccga" + + "pfqyjjdmlxcsjzllpcnbsxgjyndybwjspcwjlzkzddtacsbkzdyzypjzqsjnkktknjdjgyepgtlnyqnacdntcyhblgdzhbbydmjr" + + "egkzyheyybjmcdtafzjzhgcjnlghldwxjjkytcyksssmtwcttqzlpbszdtwcxgzagyktywxlnlcpbclloqmmzsslcmbjcsdzkydc" + + "zjgqjdsmcytzqqlnzqzxssbpkdfqmddzzsddtdmfhtdycnaqjqkypbdjyyxtljhdrqxlmhkydhrnlklytwhllrllrcxylbnsrnzz" + + "symqzzhhkyhxksmzsyzgcxfbnbsqlfzxxnnxkxwymsddyqnggqmmyhcdzttfgyyhgsbttybykjdnkyjbelhdypjqnfxfdnkzhqks" + + "byjtzbxhfdsbdaswpawajldyjsfhblcnndnqjtjnchxfjsrfwhzfmdrfjyxwzpdjkzyjympcyznynxfbytfyfwygdbnzzzdnytxz" + + "emmqbsqehxfznbmflzzsrsyqjgsxwzjsprytjsjgskjjgljjynzjjxhgjkymlpyyycxycgqzswhwlyrjlpxslcxmnsmwklcdnkny" + + "npsjszhdzeptxmwywxyysywlxjqcqxzdclaeelmcpjpclwbxsqhfwrtfnjtnqjhjqdxhwlbyccfjlylkyynldxnhycstyywncjtx" + + "ywtrmdrqnwqcmfjdxzmhmayxnwmyzqtxtlmrspwwjhanbxtgzypxyyrrclmpamgkqjszycymyjsnxtplnbappypylxmyzkynldgy" + + "jzcchnlmzhhanqnbgwqtzmxxmllhgdzxnhxhrxycjmffxywcfsbssqlhnndycannmtcjcypnxnytycnnymnmsxndlylysljnlxys" + + "sqmllyzlzjjjkyzzcsfbzxxmstbjgnxnchlsnmcjscyznfzlxbrnnnylmnrtgzqysatswryhyjzmgdhzgzdwybsscskxsyhytsxg" + + "cqgxzzbhyxjscrhmkkbsczjyjymkqhzjfnbhmqhysnjnzybknqmcjgqhwlsnzswxkhljhyybqcbfcdsxdldspfzfskjjzwzxsddx" + + "jseeegjscssygclxxnwwyllymwwwgydkzjggggggsycknjwnjpcxbjjtqtjwdsspjxcxnzxnmelptfsxtllxcljxjjljsxctnswx" + + "lennlyqrwhsycsqnybyaywjejqfwqcqqcjqgxaldbzzyjgkgxbltqyfxjltpydkyqhpmatlcndnkxmtxynhklefxdllegqtymsaw" + + "hzmljtkynxlyjzljeeyybqqffnlyxhdsctgjhxywlkllxqkcctnhjlqmkkzgcyygllljdcgydhzwypysjbzjdzgyzzhywyfqdtyz" + + "szyezklymgjjhtsmqwyzljyywzcsrkqyqltdxwcdrjalwsqzwbdcqyncjnnszjlncdcdtlzzzacqqzzddxyblxcbqjylzllljddz" + + "jgyqyjzyxnyyyexjxksdaznyrdlzyyynjlslldyxjcykywnqcclddnyyynycgczhjxcclgzqjgnwnncqqjysbzzxyjxjnxjfzbsb" + + "dsfnsfpzxhdwztdmpptflzzbzdmyypqjrsdzsqzsqxbdgcpzswdwcsqzgmdhzxmwwfybpngphdmjthzsmmbgzmbzjcfzhfcbbnmq" + + "dfmbcmcjxlgpnjbbxgyhyyjgptzgzmqbqdcgybjxlwnkydpdymgcftpfxyztzxdzxtgkptybbclbjaskytssqyymscxfjhhlslls" + + "jpqjjqaklyldlycctsxmcwfgngbqxllllnyxtyltyxytdpjhnhgnkbyqnfjyyzbyyessessgdyhfhwtcqbsdzjtfdmxhcnjzymqw" + + "srxjdzjqbdqbbsdjgnfbknbxdkqhmkwjjjgdllthzhhyyyyhhsxztyyyccbdbpypzyccztjpzywcbdlfwzcwjdxxhyhlhwczxjtc" + + "nlcdpxnqczczlyxjjcjbhfxwpywxzpcdzzbdccjwjhmlxbqxxbylrddgjrrctttgqdczwmxfytmmzcwjwxyywzzkybzcccttqnhx" + + "nwxxkhkfhtswoccjybcmpzzykbnnzpbthhjdlszddytyfjpxyngfxbyqxzbhxcpxxtnzdnnycnxsxlhkmzxlthdhkghxxsshqyhh" + + "cjyxglhzxcxnhekdtgqxqypkdhentykcnymyyjmkqyyyjxzlthhqtbyqhxbmyhsqckwwyllhcyylnneqxqwmcfbdccmljggxdqkt" + + "lxkknqcdgcjwyjjlyhhqyttnwchhxcxwherzjydjccdbqcdgdnyxzdhcqrxcbhztqcbxwgqwyybxhmbymykdyecmqkyaqyngyzsl" + + "fnkkqgyssqyshngjctxkzycssbkyxhyylstycxqthysmnscpmmgcccccmnztasmgqzjhklosjylswtmqzyqkdzljqqyplzycztcq" + + "qpbbcjzclpkhqcyyxxdtdddsjcxffllchqxmjlwcjcxtspycxndtjshjwhdqqqckxyamylsjhmlalygxcyydmamdqmlmcznnyybz" + + "xkyflmcncmlhxrcjjhsylnmtjggzgywjxsrxcwjgjqhqzdqjdcjjskjkgdzcgjjyjylxzxxcdqhhheslmhlfsbdjsyyshfyssczq" + + "lpbdrfnztzdkykhsccgkwtqzckmsynbcrxqbjyfaxpzzedzcjykbcjwhyjbqzzywnyszptdkzpfpbaztklqnhbbzptpptyzzybhn" + + "ydcpzmmcycqmcjfzzdcmnlfpbplngqjtbttajzpzbbdnjkljqylnbzqhksjznggqstzkcxchpzsnbcgzkddzqanzgjkdrtlzldwj" + + "njzlywtxndjzjhxnatncbgtzcsskmljpjytsnwxcfjwjjtkhtzplbhsnjssyjbhbjyzlstlsbjhdnwqpslmmfbjdwajyzccjtbnn" + + "nzwxxcdslqgdsdpdzgjtqqpsqlyyjzlgyhsdlctcbjtktyczjtqkbsjlgnnzdncsgpynjzjjyyknhrpwszxmtncszzyshbyhyzax" + + "ywkcjtllckjjtjhgcssxyqyczbynnlwqcglzgjgqyqcczssbcrbcskydznxjsqgxssjmecnstjtpbdlthzwxqwqczexnqczgwesg" + + "ssbybstscslccgbfsdqnzlccglllzghzcthcnmjgyzazcmsksstzmmzckbjygqljyjppldxrkzyxccsnhshhdznlzhzjjcddcbcj" + + "xlbfqbczztpqdnnxljcthqzjgylklszzpcjdscqjhjqkdxgpbajynnsmjtzdxlcjyryynhjbngzjkmjxltbsllrzpylssznxjhll" + + "hyllqqzqlsymrcncxsljmlzltzldwdjjllnzggqxppskyggggbfzbdkmwggcxmcgdxjmcjsdycabxjdlnbcddygskydqdxdjjyxh" + + "saqazdzfslqxxjnqzylblxxwxqqzbjzlfbblylwdsljhxjyzjwtdjcyfqzqzzdzsxzzqlzcdzfxhwspynpqzmlpplffxjjnzzyls" + + "jnyqzfpfzgsywjjjhrdjzzxtxxglghtdxcskyswmmtcwybazbjkshfhgcxmhfqhyxxyzftsjyzbxyxpzlchmzmbxhzzssyfdmncw" + + "dabazlxktcshhxkxjjzjsthygxsxyyhhhjwxkzxssbzzwhhhcwtzzzpjxsyxqqjgzyzawllcwxznxgyxyhfmkhydwsqmnjnaycys" + + "pmjkgwcqhylajgmzxhmmcnzhbhxclxdjpltxyjkdyylttxfqzhyxxsjbjnayrsmxyplckdnyhlxrlnllstycyyqygzhhsccsmcct" + + "zcxhyqfpyyrpbflfqnntszlljmhwtcjqyzwtlnmlmdwmbzzsnzrbpdddlqjjbxtcsnzqqygwcsxfwzlxccrszdzmcyggdyqsgtnn" + + "nlsmymmsyhfbjdgyxccpshxczcsbsjyygjmpbwaffyfnxhydxzylremzgzzyndsznlljcsqfnxxkptxzgxjjgbmyyssnbtylbnlh" + + "bfzdcyfbmgqrrmzszxysjtznnydzzcdgnjafjbdknzblczszpsgcycjszlmnrznbzzldlnllysxsqzqlcxzlsgkbrxbrbzcycxzj" + + "zeeyfgklzlnyhgzcgzlfjhgtgwkraajyzkzqtsshjjxdzyznynnzyrzdqqhgjzxsszbtkjbbfrtjxllfqwjgclqtymblpzdxtzag" + + "bdhzzrbgjhwnjtjxlkscfsmwlldcysjtxkzscfwjlbnntzlljzllqblcqmqqcgcdfpbphzczjlpyyghdtgwdxfczqyyyqysrclqz" + + "fklzzzgffcqnwglhjycjjczlqzzyjbjzzbpdcsnnjgxdqnknlznnnnpsntsdyfwwdjzjysxyyczcyhzwbbyhxrylybhkjksfxtjj" + + "mmchhlltnyymsxxyzpdjjycsycwmdjjkqyrhllngpngtlyycljnnnxjyzfnmlrgjjtyzbsyzmsjyjhgfzqmsyxrszcytlrtqzsst" + + "kxgqkgsptgxdnjsgcqcqhmxggztqydjjznlbznxqlhyqgggthqscbyhjhhkyygkggcmjdzllcclxqsftgjslllmlcskctbljszsz" + + "mmnytpzsxqhjcnnqnyexzqzcpshkzzyzxxdfgmwqrllqxrfztlystctmjcsjjthjnxtnrztzfqrhcgllgcnnnnjdnlnnytsjtlny" + + "xsszxcgjzyqpylfhdjsbbdczgjjjqzjqdybssllcmyttmqnbhjqmnygjyeqyqmzgcjkpdcnmyzgqllslnclmholzgdylfzslncnz" + + "lylzcjeshnyllnxnjxlyjyyyxnbcljsswcqqnnyllzldjnllzllbnylnqchxyyqoxccqkyjxxxyklksxeyqhcqkkkkcsnyxxyqxy" + + "gwtjohthxpxxhsnlcykychzzcbwqbbwjqcscszsslcylgddsjzmmymcytsdsxxscjpqqsqylyfzychdjynywcbtjsydchcyddjlb" + + "djjsodzyqyskkyxdhhgqjyohdyxwgmmmazdybbbppbcmnnpnjzsmtxerxjmhqdntpjdcbsnmssythjtslmltrcplzszmlqdsdmjm" + + "qpnqdxcfrnnfsdqqyxhyaykqyddlqyyysszbydslntfgtzqbzmchdhczcwfdxtmqqsphqwwxsrgjcwnntzcqmgwqjrjhtqjbbgwz" + + "fxjhnqfxxqywyyhyscdydhhqmrmtmwctbszppzzglmzfollcfwhmmsjzttdhlmyffytzzgzyskjjxqyjzqbhmbzclyghgfmshpcf" + + "zsnclpbqsnjyzslxxfpmtyjygbxlldlxpzjyzjyhhzcywhjylsjexfszzywxkzjlnadymlymqjpwxxhxsktqjezrpxxzghmhwqpw" + + "qlyjjqjjzszcnhjlchhnxjlqwzjhbmzyxbdhhypylhlhlgfwlcfyytlhjjcwmscpxstkpnhjxsntyxxtestjctlsslstdlllwwyh" + + "dnrjzsfgxssyczykwhtdhwjglhtzdqdjzxxqgghltzphcsqfclnjtclzpfstpdynylgmjllycqhynspchylhqyqtmzymbywrfqyk" + + "jsyslzdnjmpxyyssrhzjnyqtqdfzbwwdwwrxcwggyhxmkmyyyhmxmzhnksepmlqqmtcwctmxmxjpjjhfxyyzsjzhtybmstsyjznq" + + "jnytlhynbyqclcycnzwsmylknjxlggnnpjgtysylymzskttwlgsmzsylmpwlcwxwqcssyzsyxyrhssntsrwpccpwcmhdhhxzdzyf" + + "jhgzttsbjhgyglzysmyclllxbtyxhbbzjkssdmalhhycfygmqypjyjqxjllljgclzgqlycjcctotyxmtmshllwlqfxymzmklpszz" + + "cxhkjyclctyjcyhxsgyxnnxlzwpyjpxhjwpjpwxqqxlxsdhmrslzzydwdtcxknstzshbsccstplwsscjchjlcgchssphylhfhhxj" + + "sxallnylmzdhzxylsxlmzykcldyahlcmddyspjtqjzlngjfsjshctsdszlblmssmnyymjqbjhrzwtyydchjljapzwbgqxbkfnbjd" + + "llllyylsjydwhxpsbcmljpscgbhxlqhyrljxyswxhhzlldfhlnnymjljyflyjycdrjlfsyzfsllcqyqfgqyhnszlylmdtdjcnhbz" + + "llnwlqxygyyhbmgdhxxnhlzzjzxczzzcyqzfngwpylcpkpykpmclgkdgxzgxwqbdxzzkzfbddlzxjtpjpttbythzzdwslcpnhslt" + + "jxxqlhyxxxywzyswttzkhlxzxzpyhgzhknfsyhntjrnxfjcpjztwhplshfcrhnslxxjxxyhzqdxqwnnhyhmjdbflkhcxcwhjfyjc" + + "fpqcxqxzyyyjygrpynscsnnnnchkzdyhflxxhjjbyzwttxnncyjjymswyxqrmhxzwfqsylznggbhyxnnbwttcsybhxxwxyhhxyxn" + + "knyxmlywrnnqlxbbcljsylfsytjzyhyzawlhorjmnsczjxxxyxchcyqryxqzddsjfslyltsffyxlmtyjmnnyyyxltzcsxqclhzxl" + + "wyxzhnnlrxkxjcdyhlbrlmbrdlaxksnlljlyxxlynrylcjtgncmtlzllcyzlpzpzyawnjjfybdyyzsepckzzqdqpbpsjpdyttbdb" + + "bbyndycncpjmtmlrmfmmrwyfbsjgygsmdqqqztxmkqwgxllpjgzbqrdjjjfpkjkcxbljmswldtsjxldlppbxcwkcqqbfqbccajzg" + + "mykbhyhhzykndqzybpjnspxthlfpnsygyjdbgxnhhjhzjhstrstldxskzysybmxjlxyslbzyslzxjhfybqnbylljqkygzmcyzzym" + + "ccslnlhzhwfwyxzmwyxtynxjhbyymcysbmhysmydyshnyzchmjjmzcaahcbjbbhblytylsxsnxgjdhkxxtxxnbhnmlngsltxmrhn" + + "lxqqxmzllyswqgdlbjhdcgjyqyymhwfmjybbbyjyjwjmdpwhxqldyapdfxxbcgjspckrssyzjmslbzzjfljjjlgxzgyxyxlszqkx" + + "bexyxhgcxbpndyhwectwwcjmbtxchxyqqllxflyxlljlssnwdbzcmyjclwswdczpchqekcqbwlcgydblqppqzqfnqdjhymmcxtxd" + + "rmzwrhxcjzylqxdyynhyyhrslnrsywwjjymtltllgtqcjzyabtckzcjyccqlysqxalmzynywlwdnzxqdllqshgpjfjljnjabcqzd" + + "jgthhsstnyjfbswzlxjxrhgldlzrlzqzgsllllzlymxxgdzhgbdphzpbrlwnjqbpfdwonnnhlypcnjccndmbcpbzzncyqxldomzb" + + "lzwpdwyygdstthcsqsccrsssyslfybnntyjszdfndpdhtqzmbqlxlcmyffgtjjqwftmnpjwdnlbzcmmcngbdzlqlpnfhyymjylsd" + + "chdcjwjcctljcldtljjcbddpndsszycndbjlggjzxsxnlycybjjxxcbylzcfzppgkcxqdzfztjjfjdjxzbnzyjqctyjwhdyczhym" + + "djxttmpxsplzcdwslshxypzgtfmlcjtacbbmgdewycyzxdszjyhflystygwhkjyylsjcxgywjcbllcsnddbtzbsclyzczzssqdll" + + "mjyyhfllqllxfdyhabxggnywyypllsdldllbjcyxjznlhljdxyyqytdlllbngpfdfbbqbzzmdpjhgclgmjjpgaehhbwcqxajhhhz" + + "chxyphjaxhlphjpgpzjqcqzgjjzzgzdmqyybzzphyhybwhazyjhykfgdpfqsdlzmljxjpgalxzdaglmdgxmmzqwtxdxxpfdmmssy" + + "mpfmdmmkxksyzyshdzkjsysmmzzzmdydyzzczxbmlstmdyemxckjmztyymzmzzmsshhdccjewxxkljsthwlsqlyjzllsjssdppmh" + + "nlgjczyhmxxhgncjmdhxtkgrmxfwmckmwkdcksxqmmmszzydkmsclcmpcjmhrpxqpzdsslcxkyxtwlkjyahzjgzjwcjnxyhmmbml" + + "gjxmhlmlgmxctkzmjlyscjsyszhsyjzjcdajzhbsdqjzgwtkqxfkdmsdjlfmnhkzqkjfeypzyszcdpynffmzqykttdzzefmzlbnp" + + "plplpbpszalltnlkckqzkgenjlwalkxydpxnhsxqnwqnkxqclhyxxmlnccwlymqyckynnlcjnszkpyzkcqzqljbdmdjhlasqlbyd" + + "wqlwdgbqcryddztjybkbwszdxdtnpjdtcnqnfxqqmgnseclstbhpwslctxxlpwydzklnqgzcqapllkqcylbqmqczqcnjslqzdjxl" + + "ddhpzqdljjxzqdjyzhhzlkcjqdwjppypqakjyrmpzbnmcxkllzllfqpylllmbsglzysslrsysqtmxyxzqzbscnysyztffmzzsmzq" + + "hzssccmlyxwtpzgxzjgzgsjzgkddhtqggzllbjdzlsbzhyxyzhzfywxytymsdnzzyjgtcmtnxqyxjscxhslnndlrytzlryylxqht" + + "xsrtzcgyxbnqqzfhykmzjbzymkbpnlyzpblmcnqyzzzsjztjctzhhyzzjrdyzhnfxklfzslkgjtctssyllgzrzbbjzzklpkbczys" + + "nnyxbjfbnjzzxcdwlzyjxzzdjjgggrsnjkmsmzjlsjywqsnyhqjsxpjztnlsnshrnynjtwchglbnrjlzxwjqxqkysjycztlqzybb" + + "ybyzjqdwgyzcytjcjxckcwdkkzxsnkdnywwyyjqyytlytdjlxwkcjnklccpzcqqdzzqlcsfqchqqgssmjzzllbjjzysjhtsjdysj" + + "qjpdszcdchjkjzzlpycgmzndjxbsjzzsyzyhgxcpbjydssxdzncglqmbtsfcbfdzdlznfgfjgfsmpnjqlnblgqcyyxbqgdjjqsrf" + + "kztjdhczklbsdzcfytplljgjhtxzcsszzxstjygkgckgynqxjplzbbbgcgyjzgczqszlbjlsjfzgkqqjcgycjbzqtldxrjnbsxxp" + + "zshszycfwdsjjhxmfczpfzhqhqmqnknlyhtycgfrzgnqxcgpdlbzcsczqlljblhbdcypscppdymzzxgyhckcpzjgslzlnscnsldl" + + "xbmsdlddfjmkdqdhslzxlsznpqpgjdlybdskgqlbzlnlkyyhzttmcjnqtzzfszqktlljtyyllnllqyzqlbdzlslyyzxmdfszsnxl" + + "xznczqnbbwskrfbcylctnblgjpmczzlstlxshtzcyzlzbnfmqnlxflcjlyljqcbclzjgnsstbrmhxzhjzclxfnbgxgtqncztmsfz" + + "kjmssncljkbhszjntnlzdntlmmjxgzjyjczxyhyhwrwwqnztnfjscpyshzjfyrdjsfscjzbjfzqzchzlxfxsbzqlzsgyftzdcszx" + + "zjbjpszkjrhxjzcgbjkhcggtxkjqglxbxfgtrtylxqxhdtsjxhjzjjcmzlcqsbtxwqgxtxxhxftsdkfjhzyjfjxnzldlllcqsqqz" + + "qwqxswqtwgwbzcgcllqzbclmqjtzgzyzxljfrmyzflxnsnxxjkxrmjdzdmmyxbsqbhgzmwfwygmjlzbyytgzyccdjyzxsngnyjyz" + + "nbgpzjcqsyxsxrtfyzgrhztxszzthcbfclsyxzlzqmzlmplmxzjssfsbysmzqhxxnxrxhqzzzsslyflczjrcrxhhzxqndshxsjjh" + + "qcjjbcynsysxjbqjpxzqplmlxzkyxlxcnlcycxxzzlxdlllmjyhzxhyjwkjrwyhcpsgnrzlfzwfzznsxgxflzsxzzzbfcsyjdbrj" + + "krdhhjxjljjtgxjxxstjtjxlyxqfcsgswmsbctlqzzwlzzkxjmltmjyhsddbxgzhdlbmyjfrzfcgclyjbpmlysmsxlszjqqhjzfx" + + "gfqfqbphngyyqxgztnqwyltlgwgwwhnlfmfgzjmgmgbgtjflyzzgzyzaflsspmlbflcwbjztljjmzlpjjlymqtmyyyfbgygqzgly" + + "zdxqyxrqqqhsxyyqxygjtyxfsfsllgnqcygycwfhcccfxpylypllzqxxxxxqqhhsshjzcftsczjxspzwhhhhhapylqnlpqafyhxd" + + "ylnkmzqgggddesrenzltzgchyppcsqjjhclljtolnjpzljlhymhezdydsqycddhgznndzclzywllznteydgnlhslpjjbdgwxpcnn" + + "tycklkclwkllcasstknzdnnjttlyyzssysszzryljqkcgdhhyrxrzydgrgcwcgzqffbppjfzynakrgywyjpqxxfkjtszzxswzddf" + + "bbqtbgtzkznpzfpzxzpjszbmqhkyyxyldkljnypkyghgdzjxxeaxpnznctzcmxcxmmjxnkszqnmnlwbwwqjjyhclstmcsxnjcxxt" + + "pcnfdtnnpglllzcjlspblpgjcdtnjjlyarscffjfqwdpgzdwmrzzcgodaxnssnyzrestyjwjyjdbcfxnmwttbqlwstszgybljpxg" + + "lbnclgpcbjftmxzljylzxcltpnclcgxtfzjshcrxsfysgdkntlbyjcyjllstgqcbxnhzxbxklylhzlqzlnzcqwgzlgzjncjgcmnz" + + "zgjdzxtzjxycyycxxjyyxjjxsssjstsstdppghtcsxwzdcsynptfbchfbblzjclzzdbxgcjlhpxnfzflsyltnwbmnjhszbmdnbcy" + + "sccldnycndqlyjjhmqllcsgljjsyfpyyccyltjantjjpwycmmgqyysxdxqmzhszxbftwwzqswqrfkjlzjqqyfbrxjhhfwjgzyqac" + + "myfrhcyybynwlpexcczsyyrlttdmqlrkmpbgmyyjprkznbbsqyxbhyzdjdnghpmfsgbwfzmfqmmbzmzdcgjlnnnxyqgmlrygqccy" + + "xzlwdkcjcggmcjjfyzzjhycfrrcmtznzxhkqgdjxccjeascrjthpljlrzdjrbcqhjdnrhylyqjsymhzydwcdfryhbbydtssccwbx" + + "glpzmlzjdqsscfjmmxjcxjytycghycjwynsxlfemwjnmkllswtxhyyyncmmcyjdqdjzglljwjnkhpzggflccsczmcbltbhbqjxqd" + + "jpdjztghglfjawbzyzjltstdhjhctcbchflqmpwdshyytqwcnntjtlnnmnndyyyxsqkxwyyflxxnzwcxypmaelyhgjwzzjbrxxaq" + + "jfllpfhhhytzzxsgqjmhspgdzqwbwpjhzjdyjcqwxkthxsqlzyymysdzgnqckknjlwpnsyscsyzlnmhqsyljxbcxtlhzqzpcycyk" + + "pppnsxfyzjjrcemhszmnxlxglrwgcstlrsxbygbzgnxcnlnjlclynymdxwtzpalcxpqjcjwtcyyjlblxbzlqmyljbghdslssdmxm" + + "bdczsxyhamlczcpjmcnhjyjnsykchskqmczqdllkablwjqsfmocdxjrrlyqchjmybyqlrhetfjzfrfksryxfjdwtsxxywsqjysly" + + "xwjhsdlxyyxhbhawhwjcxlmyljcsqlkydttxbzslfdxgxsjkhsxxybssxdpwncmrptqzczenygcxqfjxkjbdmljzmqqxnoxslyxx" + + "lylljdzptymhbfsttqqwlhsgynlzzalzxclhtwrrqhlstmypyxjjxmnsjnnbryxyjllyqyltwylqyfmlkljdnlltfzwkzhljmlhl" + + "jnljnnlqxylmbhhlnlzxqchxcfxxlhyhjjgbyzzkbxscqdjqdsndzsygzhhmgsxcsymxfepcqwwrbpyyjqryqcyjhqqzyhmwffhg" + + "zfrjfcdbxntqyzpcyhhjlfrzgpbxzdbbgrqstlgdgylcqmgchhmfywlzyxkjlypjhsywmqqggzmnzjnsqxlqsyjtcbehsxfszfxz" + + "wfllbcyyjdytdthwzsfjmqqyjlmqsxlldttkghybfpwdyysqqrnqwlgwdebzwcyygcnlkjxtmxmyjsxhybrwfymwfrxyymxysctz" + + "ztfykmldhqdlgyjnlcryjtlpsxxxywlsbrrjwxhqybhtydnhhxmmywytycnnmnssccdalwztcpqpyjllqzyjswjwzzmmglmxclmx" + + "nzmxmzsqtzppjqblpgxjzhfljjhycjsrxwcxsncdlxsyjdcqzxslqyclzxlzzxmxqrjmhrhzjbhmfljlmlclqnldxzlllfyprgjy" + + "nxcqqdcmqjzzxhnpnxzmemmsxykynlxsxtljxyhwdcwdzhqyybgybcyscfgfsjnzdrzzxqxrzrqjjymcanhrjtldbpyzbstjhxxz" + + "ypbdwfgzzrpymnnkxcqbyxnbnfyckrjjcmjegrzgyclnnzdnkknsjkcljspgyyclqqjybzssqlllkjftbgtylcccdblsppfylgyd" + + "tzjqjzgkntsfcxbdkdxxhybbfytyhbclnnytgdhryrnjsbtcsnyjqhklllzslydxxwbcjqsbxnpjzjzjdzfbxxbrmladhcsnclbj" + + "dstblprznswsbxbcllxxlzdnzsjpynyxxyftnnfbhjjjgbygjpmmmmsszljmtlyzjxswxtyledqpjmpgqzjgdjlqjwjqllsdgjgy" + + "gmscljjxdtygjqjjjcjzcjgdzdshqgzjggcjhqxsnjlzzbxhsgzxcxyljxyxyydfqqjhjfxdhctxjyrxysqtjxyefyyssyxjxncy" + + "zxfxcsxszxyyschshxzzzgzzzgfjdldylnpzgsjaztyqzpbxcbdztzczyxxyhhscjshcggqhjhgxhsctmzmehyxgebtclzkkwytj" + + "zrslekestdbcyhqqsayxcjxwwgsphjszsdncsjkqcxswxfctynydpccczjqtcwjqjzzzqzljzhlsbhpydxpsxshhezdxfptjqyzc" + + "xhyaxncfzyyhxgnqmywntzsjbnhhgymxmxqcnssbcqsjyxxtyyhybcqlmmszmjzzllcogxzaajzyhjmchhcxzsxsdznleyjjzjbh" + + "zwjzsqtzpsxzzdsqjjjlnyazphhyysrnqzthzhnyjyjhdzxzlswclybzyecwcycrylchzhzydzydyjdfrjjhtrsqtxyxjrjhojyn" + + "xelxsfsfjzghpzsxzszdzcqzbyyklsgsjhczshdgqgxyzgxchxzjwyqwgyhksseqzzndzfkwyssdclzstsymcdhjxxyweyxczayd" + + "mpxmdsxybsqmjmzjmtjqlpjyqzcgqhyjhhhqxhlhdldjqcfdwbsxfzzyyschtytyjbhecxhjkgqfxbhyzjfxhwhbdzfyzbchpnpg" + + "dydmsxhkhhmamlnbyjtmpxejmcthqbzyfcgtyhwphftgzzezsbzegpbmdskftycmhbllhgpzjxzjgzjyxzsbbqsczzlzscstpgxm" + + "jsfdcczjzdjxsybzlfcjsazfgszlwbczzzbyztzynswyjgxzbdsynxlgzbzfygczxbzhzftpbgzgejbstgkdmfhyzzjhzllzzgjq" + + "zlsfdjsscbzgpdlfzfzszyzyzsygcxsnxxchczxtzzljfzgqsqqxcjqccccdjcdszzyqjccgxztdlgscxzsyjjqtcclqdqztqchq" + + "qyzynzzzpbkhdjfcjfztypqyqttynlmbdktjcpqzjdzfpjsbnjlgyjdxjdcqkzgqkxclbzjtcjdqbxdjjjstcxnxbxqmslyjcxnt" + + "jqwwcjjnjjlllhjcwqtbzqqczczpzzdzyddcyzdzccjgtjfzdprntctjdcxtqzdtjnplzbcllctdsxkjzqdmzlbznbtjdcxfczdb" + + "czjjltqqpldckztbbzjcqdcjwynllzlzccdwllxwzlxrxntqjczxkjlsgdnqtddglnlajjtnnynkqlldzntdnycygjwyxdxfrsqs" + + "tcdenqmrrqzhhqhdldazfkapbggpzrebzzykyqspeqjjglkqzzzjlysyhyzwfqznlzzlzhwcgkypqgnpgblplrrjyxcccgyhsfzf" + + "wbzywtgzxyljczwhncjzplfflgskhyjdeyxhlpllllcygxdrzelrhgklzzyhzlyqszzjzqljzflnbhgwlczcfjwspyxzlzlxgccp" + + "zbllcxbbbbnbbcbbcrnnzccnrbbnnldcgqyyqxygmqzwnzytyjhyfwtehznjywlccntzyjjcdedpwdztstnjhtymbjnyjzlxtsst" + + "phndjxxbyxqtzqddtjtdyztgwscszqflshlnzbcjbhdlyzjyckwtydylbnydsdsycctyszyyebgexhqddwnygyclxtdcystqnygz" + + "ascsszzdzlcclzrqxyywljsbymxshzdembbllyyllytdqyshymrqnkfkbfxnnsbychxbwjyhtqbpbsbwdzylkgzskyghqzjxhxjx" + + "gnljkzlyycdxlfwfghljgjybxblybxqpqgntzplncybxdjyqydymrbeyjyyhkxxstmxrczzjwxyhybmcflyzhqyzfwxdbxbcwzms" + + "lpdmyckfmzklzcyqycclhxfzlydqzpzygyjyzmdxtzfnnyttqtzhgsfcdmlccytzxjcytjmkslpzhysnwllytpzctzccktxdhxxt" + + "qcyfksmqccyyazhtjplylzlyjbjxtfnyljyynrxcylmmnxjsmybcsysslzylljjgyldzdlqhfzzblfndsqkczfyhhgqmjdsxyctt" + + "xnqnjpyybfcjtyyfbnxejdgyqbjrcnfyyqpghyjsyzngrhtknlnndzntsmgklbygbpyszbydjzsstjztsxzbhbscsbzczptqfzlq" + + "flypybbjgszmnxdjmtsyskkbjtxhjcegbsmjyjzcstmljyxrczqscxxqpyzhmkyxxxjcljyrmyygadyskqlnadhrskqxzxztcggz" + + "dlmlwxybwsyctbhjhcfcwzsxwwtgzlxqshnyczjxemplsrcgltnzntlzjcyjgdtclglbllqpjmzpapxyzlaktkdwczzbncctdqqz" + + "qyjgmcdxltgcszlmlhbglkznnwzndxnhlnmkydlgxdtwcfrjerctzhydxykxhwfzcqshknmqqhzhhymjdjskhxzjzbzzxympajnm" + + "ctbxlsxlzynwrtsqgscbptbsgzwyhtlkssswhzzlyytnxjgmjrnsnnnnlskztxgxlsammlbwldqhylakqcqctmycfjbslxclzjcl" + + "xxknbnnzlhjphqplsxsckslnhpsfqcytxjjzljldtzjjzdlydjntptnndskjfsljhylzqqzlbthydgdjfdbyadxdzhzjnthqbykn" + + "xjjqczmlljzkspldsclbblnnlelxjlbjycxjxgcnlcqplzlznjtsljgyzdzpltqcssfdmnycxgbtjdcznbgbqyqjwgkfhtnbyqzq" + + "gbkpbbyzmtjdytblsqmbsxtbnpdxklemyycjynzdtldykzzxtdxhqshygmzsjycctayrzlpwltlkxslzcggexclfxlkjrtlqjaqz" + + "ncmbqdkkcxglczjzxjhptdjjmzqykqsecqzdshhadmlzfmmzbgntjnnlhbyjbrbtmlbyjdzxlcjlpldlpcqdhlhzlycblcxccjad" + + "qlmzmmsshmybhbnkkbhrsxxjmxmdznnpklbbrhgghfchgmnklltsyyycqlcskymyehywxnxqywbawykqldnntndkhqcgdqktgpkx" + + "hcpdhtwnmssyhbwcrwxhjmkmzngwtmlkfghkjyldyycxwhyyclqhkqhtdqkhffldxqwytyydesbpkyrzpjfyyzjceqdzzdlattpb" + + "fjllcxdlmjsdxegwgsjqxcfbssszpdyzcxznyxppzydlyjccpltxlnxyzyrscyyytylwwndsahjsygyhgywwaxtjzdaxysrltdps" + + "syxfnejdxyzhlxlllzhzsjnyqyqyxyjghzgjcyjchzlycdshhsgczyjscllnxzjjyyxnfsmwfpyllyllabmddhwzxjmcxztzpmlq" + + "chsfwzynctlndywlslxhymmylmbwwkyxyaddxylldjpybpwnxjmmmllhafdllaflbnhhbqqjqzjcqjjdjtffkmmmpythygdrjrdd" + + "wrqjxnbysrmzdbyytbjhpymyjtjxaahggdqtmystqxkbtzbkjlxrbyqqhxmjjbdjntgtbxpgbktlgqxjjjcdhxqdwjlwrfmjgwqh" + + "cnrxswgbtgygbwhswdwrfhwytjjxxxjyzyslphyypyyxhydqpxshxyxgskqhywbdddpplcjlhqeewjgsyykdpplfjthkjltcyjhh" + + "jttpltzzcdlyhqkcjqysteeyhkyzyxxyysddjkllpymqyhqgxqhzrhbxpllnqydqhxsxxwgdqbshyllpjjjthyjkyphthyyktyez" + + "yenmdshlzrpqfbnfxzbsftlgxsjbswyysksflxlpplbbblnsfbfyzbsjssylpbbffffsscjdstjsxtryjcyffsyzyzbjtlctsbsd" + + "hrtjjbytcxyyeylycbnebjdsysyhgsjzbxbytfzwgenhhhthjhhxfwgcstbgxklstyymtmbyxjskzscdyjrcythxzfhmymcxlzns" + + "djtxtxrycfyjsbsdyerxhljxbbdeynjghxgckgscymblxjmsznskgxfbnbbthfjyafxwxfbxmyfhdttcxzzpxrsywzdlybbktyqw" + + "qjbzypzjznjpzjlztfysbttslmptzrtdxqsjehbnylndxljsqmlhtxtjecxalzzspktlzkqqyfsyjywpcpqfhjhytqxzkrsgtksq" + + "czlptxcdyyzsslzslxlzmacpcqbzyxhbsxlzdltztjtylzjyytbzypltxjsjxhlbmytxcqrblzssfjzztnjytxmyjhlhpblcyxqj" + + "qqkzzscpzkswalqsplczzjsxgwwwygyatjbbctdkhqhkgtgpbkqyslbxbbckbmllndzstbklggqkqlzbkktfxrmdkbftpzfrtppm" + + "ferqnxgjpzsstlbztpszqzsjdhljqlzbpmsmmsxlqqnhknblrddnhxdkddjcyyljfqgzlgsygmjqjkhbpmxyxlytqwlwjcpbmjxc" + + "yzydrjbhtdjyeqshtmgsfyplwhlzffnynnhxqhpltbqpfbjwjdbygpnxtbfzjgnnntjshxeawtzylltyqbwjpgxghnnkndjtmszs" + + "qynzggnwqtfhclssgmnnnnynzqqxncjdqgzdlfnykljcjllzlmzznnnnsshthxjlzjbbhqjwwycrdhlyqqjbeyfsjhthnrnwjhwp" + + "slmssgzttygrqqwrnlalhmjtqjsmxqbjjzjqzyzkxbjqxbjxshzssfglxmxnxfghkzszggslcnnarjxhnlllmzxelglxydjytlfb" + + "kbpnlyzfbbhptgjkwetzhkjjxzxxglljlstgshjjyqlqzfkcgnndjsszfdbctwwseqfhqjbsaqtgypjlbxbmmywxgslzhglsgnyf" + + "ljbyfdjfngsfmbyzhqffwjsyfyjjphzbyyzffwotjnlmftwlbzgyzqxcdjygzyyryzynyzwegazyhjjlzrthlrmgrjxzclnnnljj" + + "yhtbwjybxxbxjjtjteekhwslnnlbsfazpqqbdlqjjtyyqlyzkdksqjnejzldqcgjqnnjsncmrfqthtejmfctyhypymhydmjncfgy" + + "yxwshctxrljgjzhzcyyyjltkttntmjlzclzzayyoczlrlbszywjytsjyhbyshfjlykjxxtmzyyltxxypslqyjzyzyypnhmymdyyl" + + "blhlsyygqllnjjymsoycbzgdlyxylcqyxtszegxhzglhwbljheyxtwqmakbpqcgyshhegqcmwyywljyjhyyzlljjylhzyhmgsljl" + + "jxcjjyclycjbcpzjzjmmwlcjlnqljjjlxyjmlszljqlycmmgcfmmfpqqmfxlqmcffqmmmmhnznfhhjgtthxkhslnchhyqzxtmmqd" + + "cydyxyqmyqylddcyaytazdcymdydlzfffmmycqcwzzmabtbyctdmndzggdftypcgqyttssffwbdttqssystwnjhjytsxxylbyyhh" + + "whxgzxwznnqzjzjjqjccchykxbzszcnjtllcqxynjnckycynccqnxyewyczdcjycchyjlbtzyycqwlpgpyllgktltlgkgqbgychj" + + "xy" extension String { subscript(i: Int) -> String { - return self[i ..< i + 1] + self[i..) -> String { - let range = Range(uncheckedBounds: (lower: max(0, min(count, r.lowerBound)), upper: min(count, max(0, r.upperBound)))) + let range = Range(uncheckedBounds: ( + lower: max(0, min(count, r.lowerBound)), + upper: min(count, max(0, r.upperBound)) + )) let start = index(startIndex, offsetBy: range.lowerBound) let end = index(start, offsetBy: range.upperBound - range.lowerBound) - return String(self[start ..< end]) + return String(self[start..>() - - private var selectedCategory: String? - - private var latestSize = CGSize(width: 300, height: 300) - private let buttonSize = CGSize(width: 28, height: 28) - - private let collapseButton = UIButton(frame: .zero) - private let squareButton = UIView() - private let draggablePoint = UIView(frame: .zero) - private let shapeLayer = CAShapeLayer() - private let tableView = UITableView() - private lazy var collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 0 - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.delegate = self - collectionView.dataSource = self - collectionView.backgroundColor = .clear - collectionView.showsHorizontalScrollIndicator = false - collectionView.register(DebugViewCategoryCell.self, forCellWithReuseIdentifier: DebugViewCategoryCell.description()) - return collectionView - }() - - private lazy var clearAllButton: UIButton = { - let button = UIButton() - button.setTitle("Clear", for: .normal) - button.titleLabel?.font = UIFont(name: "CourierNewPS-BoldMT", size: 12.0) - button.addTarget(self, action: #selector(clearAll), for: .touchUpInside) - return button - }() - - public enum Theme { - case dark - case light - - var baseColor: UIColor { - switch self { - case .dark: - return .black - case .light: - return .white - } - } - - var backgroundColor: UIColor { - switch self { - case .dark: - return UIColor.black.withAlphaComponent(0.7) - case .light: - return UIColor.white.withAlphaComponent(0.8) - } - } +// MARK: - DebugViewer - var fontColor: UIColor { - switch self { - case .dark: - return .white - case .light: - return .black - } - } - } - - private lazy var viewerFrameKey: String = "com.dapperlabs.mobile.debug-viewer.frame.\(String(describing: type(of: self)))" - - private var items: CappedCollection { - guard let category = selectedCategory, data.keys.contains(category) else { - return data.first?.value ?? CappedCollection(elements: [], maxCount: 100) - } - return data[category] ?? CappedCollection(elements: [], maxCount: 100) - } - - override public var frame: CGRect { - didSet { - guard frame != .zero else { return } - UserDefaults.standard.setValue(NSCoder.string(for: frame), forKey: viewerFrameKey) - } - } - - override public var center: CGPoint { - didSet { - guard frame != .zero else { return } - UserDefaults.standard.setValue(NSCoder.string(for: frame), forKey: viewerFrameKey) - } - } - - private func updateTheme() { - layer.borderColor = theme.baseColor.cgColor - layer.borderWidth = 2.0 - backgroundColor = theme.backgroundColor - - collapseButton.backgroundColor = theme.baseColor - squareButton.backgroundColor = theme.fontColor - - shapeLayer.strokeColor = layer.borderColor - shapeLayer.fillColor = theme.fontColor.withAlphaComponent(0.7).cgColor - - clearAllButton.setTitleColor(theme.fontColor, for: .normal) - clearAllButton.backgroundColor = theme.baseColor - - collectionView.reloadData() - tableView.reloadData() - } +public class DebugViewer: ResizableView { + // MARK: Lifecycle override init( frame: CGRect @@ -182,7 +71,11 @@ public class DebugViewer: ResizableView { make.left.right.top.equalToSuperview() } - collapseButton.addTarget(self, action: #selector(didPressCollapseButton), for: .touchUpInside) + collapseButton.addTarget( + self, + action: #selector(didPressCollapseButton), + for: .touchUpInside + ) collapseButton.snp.makeConstraints { make in make.size.equalTo(buttonSize) @@ -226,45 +119,62 @@ public class DebugViewer: ResizableView { fatalError("init(coder:) has not been implemented") } - private func addPanGestureRecoginizer(_ view: UIView) { - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewDidPan(_:))) - view.addGestureRecognizer(panGesture) - } + // MARK: Public - @objc private func viewDidPan(_ sender: UIPanGestureRecognizer) { - let translation = sender.translation(in: self) - center = CGPoint(x: center.x + translation.x, y: center.y + translation.y) - sender.setTranslation(CGPoint.zero, in: self) - } + public enum Theme { + case dark + case light - private var isCollapsed: Bool = false { - didSet { - collectionView.isHidden = isCollapsed - clearAllButton.isHidden = isCollapsed - tableView.isHidden = isCollapsed + // MARK: Internal + + var baseColor: UIColor { + switch self { + case .dark: + return .black + case .light: + return .white + } } - } - @objc private func didPressCollapseButton() { - if !isCollapsed { - latestSize = frame.size + var backgroundColor: UIColor { + switch self { + case .dark: + return UIColor.black.withAlphaComponent(0.7) + case .light: + return UIColor.white.withAlphaComponent(0.8) + } } - let targetSize = isCollapsed ? latestSize : collapseButton.frame.size - isCollapsed = !isCollapsed - UIView.animate(withDuration: 0.1) { - self.frame.size = targetSize - } completion: { _ in + + var fontColor: UIColor { + switch self { + case .dark: + return .white + case .light: + return .black + } } } - private var keyWindow: UIWindow? { - return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + public static let shared = DebugViewer() + + public var theme: Theme = .dark { + didSet { + updateTheme() + } } - @objc private func keyWindowChanged() { - guard superview != keyWindow else { return } - removeFromSuperview() - keyWindow?.addSubview(self) + override public var frame: CGRect { + didSet { + guard frame != .zero else { return } + UserDefaults.standard.setValue(NSCoder.string(for: frame), forKey: viewerFrameKey) + } + } + + override public var center: CGPoint { + didSet { + guard frame != .zero else { return } + UserDefaults.standard.setValue(NSCoder.string(for: frame), forKey: viewerFrameKey) + } } public func show(theme: Theme = .dark) { @@ -273,8 +183,7 @@ public class DebugViewer: ResizableView { keyWindow?.addSubview(self) } if let storedFrame = UserDefaults.standard.string(forKey: viewerFrameKey), - NSCoder.cgRect(for: storedFrame) != CGRect.zero - { + NSCoder.cgRect(for: storedFrame) != CGRect.zero { frame = NSCoder.cgRect(for: storedFrame) } else { frame.size = latestSize @@ -285,7 +194,8 @@ public class DebugViewer: ResizableView { alwaysShowOnTop() } - @objc public func close() { + @objc + public func close() { isHidden = true UserDefaults.standard.removeObject(forKey: viewerFrameKey) } @@ -296,7 +206,10 @@ public class DebugViewer: ResizableView { } public func addViewModel(category: String, viewModel: DebugViewModel) { - var dataSource: CappedCollection = data[category] ?? CappedCollection(elements: [], maxCount: 100) + var dataSource: CappedCollection = data[category] ?? CappedCollection( + elements: [], + maxCount: 100 + ) dataSource.append(viewModel) data[category] = dataSource if selectedCategory == nil { @@ -310,7 +223,120 @@ public class DebugViewer: ResizableView { } } - @objc private func clearAll() { + // MARK: Private + + private var data = ThreadSafeDictionary>() + + private var selectedCategory: String? + + private var latestSize = CGSize(width: 300, height: 300) + private let buttonSize = CGSize(width: 28, height: 28) + + private let collapseButton = UIButton(frame: .zero) + private let squareButton = UIView() + private let draggablePoint = UIView(frame: .zero) + private let shapeLayer = CAShapeLayer() + private let tableView = UITableView() + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 0 + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.register( + DebugViewCategoryCell.self, + forCellWithReuseIdentifier: DebugViewCategoryCell.description() + ) + return collectionView + }() + + private lazy var clearAllButton: UIButton = { + let button = UIButton() + button.setTitle("Clear", for: .normal) + button.titleLabel?.font = UIFont(name: "CourierNewPS-BoldMT", size: 12.0) + button.addTarget(self, action: #selector(clearAll), for: .touchUpInside) + return button + }() + + private lazy var viewerFrameKey: String = + "com.dapperlabs.mobile.debug-viewer.frame.\(String(describing: type(of: self)))" + + private var items: CappedCollection { + guard let category = selectedCategory, data.keys.contains(category) else { + return data.first?.value ?? CappedCollection(elements: [], maxCount: 100) + } + return data[category] ?? CappedCollection(elements: [], maxCount: 100) + } + + private var isCollapsed: Bool = false { + didSet { + collectionView.isHidden = isCollapsed + clearAllButton.isHidden = isCollapsed + tableView.isHidden = isCollapsed + } + } + + private var keyWindow: UIWindow? { + UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + } + + private func updateTheme() { + layer.borderColor = theme.baseColor.cgColor + layer.borderWidth = 2.0 + backgroundColor = theme.backgroundColor + + collapseButton.backgroundColor = theme.baseColor + squareButton.backgroundColor = theme.fontColor + + shapeLayer.strokeColor = layer.borderColor + shapeLayer.fillColor = theme.fontColor.withAlphaComponent(0.7).cgColor + + clearAllButton.setTitleColor(theme.fontColor, for: .normal) + clearAllButton.backgroundColor = theme.baseColor + + collectionView.reloadData() + tableView.reloadData() + } + + private func addPanGestureRecoginizer(_ view: UIView) { + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewDidPan(_:))) + view.addGestureRecognizer(panGesture) + } + + @objc + private func viewDidPan(_ sender: UIPanGestureRecognizer) { + let translation = sender.translation(in: self) + center = CGPoint(x: center.x + translation.x, y: center.y + translation.y) + sender.setTranslation(CGPoint.zero, in: self) + } + + @objc + private func didPressCollapseButton() { + if !isCollapsed { + latestSize = frame.size + } + let targetSize = isCollapsed ? latestSize : collapseButton.frame.size + isCollapsed = !isCollapsed + UIView.animate(withDuration: 0.1) { + self.frame.size = targetSize + } completion: { _ in + } + } + + @objc + private func keyWindowChanged() { + guard superview != keyWindow else { return } + removeFromSuperview() + keyWindow?.addSubview(self) + } + + @objc + private func clearAll() { for (category, collection) in data { var mutableCollection = collection mutableCollection.removeAllElements() @@ -324,7 +350,7 @@ public class DebugViewer: ResizableView { extension DebugViewer: UITableViewDataSource, UITableViewDelegate { public func numberOfSections(in _: UITableView) -> Int { - return items.count + items.count } public func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -334,10 +360,17 @@ extension DebugViewer: UITableViewDataSource, UITableViewDelegate { return 0 } - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { let item = items[indexPath.section] - let cell: DebugViewCell = tableView.dequeueReusableCell(withIdentifier: DebugViewCell.description(), for: indexPath) as! DebugViewCell - cell.backgroundColor = (indexPath.section % 2) == 0 ? theme.baseColor.withAlphaComponent(0.6) : theme.baseColor.withAlphaComponent(0.3) + let cell: DebugViewCell = tableView.dequeueReusableCell( + withIdentifier: DebugViewCell.description(), + for: indexPath + ) as! DebugViewCell + cell.backgroundColor = (indexPath.section % 2) == 0 ? theme.baseColor + .withAlphaComponent(0.6) : theme.baseColor.withAlphaComponent(0.3) let showDetails = indexPath.row == 1 cell.configure(event: item, showDetails: showDetails, theme: theme) return cell @@ -351,7 +384,10 @@ extension DebugViewer: UITableViewDataSource, UITableViewDelegate { } } - public func tableView(_: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + public func tableView( + _: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { let item = items[indexPath.section] let contextItem = UIContextualAction(style: .normal, title: "Copy") { _, _, boolValue in if indexPath.row == 0 { @@ -366,9 +402,10 @@ extension DebugViewer: UITableViewDataSource, UITableViewDelegate { } } +// MARK: - DebugViewCell + class DebugViewCell: UITableViewCell { - static var font = UIFont(name: "CourierNewPS-BoldMT", size: 10)! - static var detailFont = UIFont(name: "CourierNewPS-BoldMT", size: 8)! + // MARK: Lifecycle override init( style _: UITableViewCell.CellStyle, reuseIdentifier: String? @@ -395,6 +432,11 @@ class DebugViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } + // MARK: Internal + + static var font = UIFont(name: "CourierNewPS-BoldMT", size: 10)! + static var detailFont = UIFont(name: "CourierNewPS-BoldMT", size: 8)! + func configure(event: DebugViewModel, showDetails: Bool = false, theme: DebugViewer.Theme) { textLabel?.textColor = theme.fontColor if showDetails { @@ -413,7 +455,7 @@ class DebugViewCell: UITableViewCell { } } -// MARK: UICollectionViewDataSource, UICollectionViewDelegate +// MARK: - DebugViewer + UICollectionViewDataSource extension DebugViewer: UICollectionViewDataSource { private func category(_ indexPath: IndexPath) -> String { @@ -430,11 +472,17 @@ extension DebugViewer: UICollectionViewDataSource { } public func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { - return data.keys.count + data.keys.count } - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DebugViewCategoryCell.description(), for: indexPath) as! DebugViewCategoryCell + public func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: DebugViewCategoryCell.description(), + for: indexPath + ) as! DebugViewCategoryCell cell.theme = theme cell.category = category(indexPath) cell.current = isCurrent(indexPath) @@ -442,26 +490,62 @@ extension DebugViewer: UICollectionViewDataSource { } } +// MARK: - DebugViewer + UICollectionViewDelegate + extension DebugViewer: UICollectionViewDelegate { - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + public func collectionView( + _ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath + ) { selectedCategory = category(indexPath) tableView.reloadData() collectionView.reloadSections(IndexSet(integer: 0)) } } +// MARK: - DebugViewer + UICollectionViewDelegateFlowLayout + extension DebugViewer: UICollectionViewDelegateFlowLayout { - public func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { let category = category(indexPath) - let boundingRect = category.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: buttonSize.height), - options: [.usesLineFragmentOrigin, .usesFontLeading], - context: nil) + let boundingRect = category.boundingRect( + with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: buttonSize.height), + options: [.usesLineFragmentOrigin, .usesFontLeading], + context: nil + ) return CGSize(width: boundingRect.width + 40, height: buttonSize.height) } } +// MARK: - DebugViewCategoryCell + class DebugViewCategoryCell: UICollectionViewCell { - private let label = UILabel(frame: .zero) + // MARK: Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.backgroundColor = .black + + label.font = UIFont(name: "CourierNewPS-BoldMT", size: 12.0) + label.textAlignment = .center + contentView.addSubview(label) + label.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + var category: String? { didSet { label.text = category @@ -480,21 +564,7 @@ class DebugViewCategoryCell: UICollectionViewCell { } } - override init(frame: CGRect) { - super.init(frame: frame) - - contentView.backgroundColor = .black - - label.font = UIFont(name: "CourierNewPS-BoldMT", size: 12.0) - label.textAlignment = .center - contentView.addSubview(label) - label.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - } + // MARK: Private - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + private let label = UILabel(frame: .zero) } diff --git a/FRW/Tools/ThirdParty/DebugViewer/ResizeableView.swift b/FRW/Tools/ThirdParty/DebugViewer/ResizeableView.swift index e3fc8ff5..517d0e40 100644 --- a/FRW/Tools/ThirdParty/DebugViewer/ResizeableView.swift +++ b/FRW/Tools/ThirdParty/DebugViewer/ResizeableView.swift @@ -1,5 +1,5 @@ // -// ResizableView.swift +// ResizeableView.swift // // // Created by Jin Kim on 6/13/22. @@ -9,28 +9,22 @@ import Foundation import UIKit public class ResizableView: UIView { - enum Edge { - case topLeft, topRight, bottomLeft, bottomRight, none - } - - static var edgeSize: CGFloat = 44.0 - private typealias `Self` = ResizableView - - var currentEdge: Edge = .none - var touchStart = CGPoint.zero - var minSubviewSize: CGSize = .zero + // MARK: Public override public func touchesBegan(_ touches: Set, with _: UIEvent?) { if let touch = touches.first { touchStart = touch.location(in: self) currentEdge = { - if self.bounds.size.width - touchStart.x < Self.edgeSize, self.bounds.size.height - touchStart.y < Self.edgeSize { + if self.bounds.size.width - touchStart.x < Self.edgeSize, + self.bounds.size.height - touchStart.y < Self.edgeSize { return .bottomRight } else if touchStart.x < Self.edgeSize, touchStart.y < Self.edgeSize { return .topLeft - } else if self.bounds.size.width - touchStart.x < Self.edgeSize, touchStart.y < Self.edgeSize { + } else if self.bounds.size.width - touchStart.x < Self.edgeSize, + touchStart.y < Self.edgeSize { return .topRight - } else if touchStart.x < Self.edgeSize, self.bounds.size.height - touchStart.y < Self.edgeSize { + } else if touchStart.x < Self.edgeSize, + self.bounds.size.height - touchStart.y < Self.edgeSize { return .bottomLeft } return .none @@ -53,16 +47,39 @@ public class ResizableView: UIView { switch currentEdge { case .topLeft: - frame = CGRect(x: originX + deltaWidth, y: originY + deltaHeight, width: width - deltaWidth, height: height - deltaHeight) + frame = CGRect( + x: originX + deltaWidth, + y: originY + deltaHeight, + width: width - deltaWidth, + height: height - deltaHeight + ) case .topRight: - frame = CGRect(x: originX, y: originY + deltaHeight, width: width + deltaWidth, height: height - deltaHeight) + frame = CGRect( + x: originX, + y: originY + deltaHeight, + width: width + deltaWidth, + height: height - deltaHeight + ) case .bottomRight: - frame = CGRect(x: originX, y: originY, width: width + deltaWidth, height: height + deltaHeight) + frame = CGRect( + x: originX, + y: originY, + width: width + deltaWidth, + height: height + deltaHeight + ) case .bottomLeft: - frame = CGRect(x: originX + deltaWidth, y: originY, width: width - deltaWidth, height: height + deltaHeight) + frame = CGRect( + x: originX + deltaWidth, + y: originY, + width: width - deltaWidth, + height: height + deltaHeight + ) default: // Moving - center = CGPoint(x: center.x + currentPoint.x - touchStart.x, y: center.y + currentPoint.y - touchStart.y) + center = CGPoint( + x: center.x + currentPoint.x - touchStart.x, + y: center.y + currentPoint.y - touchStart.y + ) } // If the frame size gets smaller than minimum subview size, we are not able to drag the edge to increase the size // Always expose the edge @@ -80,4 +97,20 @@ public class ResizableView: UIView { override public func touchesEnded(_: Set, with _: UIEvent?) { currentEdge = .none } + + // MARK: Internal + + enum Edge { + case topLeft, topRight, bottomLeft, bottomRight, none + } + + static var edgeSize: CGFloat = 44.0 + + var currentEdge: Edge = .none + var touchStart = CGPoint.zero + var minSubviewSize: CGSize = .zero + + // MARK: Private + + private typealias `Self` = ResizableView } diff --git a/FRW/Tools/ThirdParty/FloatingButton/Utils.swift b/FRW/Tools/ThirdParty/FloatingButton/Utils.swift index df63e9c4..68772079 100644 --- a/FRW/Tools/ThirdParty/FloatingButton/Utils.swift +++ b/FRW/Tools/ThirdParty/FloatingButton/Utils.swift @@ -1,5 +1,5 @@ // -// SwiftUIView.swift +// Utils.swift // // // Created by Alisa Mylnikova on 31.03.2023. @@ -15,12 +15,15 @@ extension View { extension Collection where Element == CGPoint { subscript(safe index: Index) -> CGPoint { - return indices.contains(index) ? self[index] : .zero + indices.contains(index) ? self[index] : .zero } } +// MARK: - SizeGetter + struct SizeGetter: ViewModifier { - @Binding var size: CGSize + @Binding + var size: CGSize func body(content: Content) -> some View { content @@ -37,6 +40,8 @@ struct SizeGetter: ViewModifier { } } +// MARK: - SubmenuButtonPreferenceKey + struct SubmenuButtonPreferenceKey: PreferenceKey { typealias Value = [CGSize] @@ -47,13 +52,17 @@ struct SubmenuButtonPreferenceKey: PreferenceKey { } } +// MARK: - SubmenuButtonPreferenceViewSetter + struct SubmenuButtonPreferenceViewSetter: View { var body: some View { GeometryReader { geometry in Rectangle() .fill(Color.clear) - .preference(key: SubmenuButtonPreferenceKey.self, - value: [geometry.frame(in: .global).size]) + .preference( + key: SubmenuButtonPreferenceKey.self, + value: [geometry.frame(in: .global).size] + ) } } } diff --git a/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift b/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift index 6433a6bd..4f205dbf 100644 --- a/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift +++ b/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift @@ -9,103 +9,114 @@ import SwiftUI /// A CALayer that draws a single blob on the screen public class BlobLayer: CAGradientLayer { + // MARK: Lifecycle + init(color: Color) { super.init() - - self.type = .radial + + type = .radial #if os(OSX) autoresizingMask = [.layerWidthSizable, .layerHeightSizable] #endif - + // Set color set(color: color) - + // Center point let position = newPosition() - self.startPoint = position - + startPoint = position + // Radius let radius = newRadius() - self.endPoint = position.displace(by: radius) + endPoint = position.displace(by: radius) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") } - + + // Required by the framework + override public init(layer: Any) { + super.init(layer: layer) + } + + // MARK: Internal + /// Generate a random point on the canvas func newPosition() -> CGPoint { - return CGPoint(x: CGFloat.random(in: 0.0...1.0), - y: CGFloat.random(in: 0.0...1.0)).capped() + CGPoint( + x: CGFloat.random(in: 0.0...1.0), + y: CGFloat.random(in: 0.0...1.0) + ).capped() } - + /// Generate a random radius for the blob func newRadius() -> CGPoint { let size = CGFloat.random(in: 0.15...0.75) - let viewRatio = frame.width/frame.height + let viewRatio = frame.width / frame.height let safeRatio = max(viewRatio.isNaN ? 1 : viewRatio, 1) - let ratio = safeRatio*CGFloat.random(in: 0.25...1.75) - return CGPoint(x: size, - y: size*ratio) + let ratio = safeRatio * CGFloat.random(in: 0.25...1.75) + return CGPoint( + x: size, + y: size * ratio + ) } - + /// Animate the blob to a random point and size on screen at set speed func animate(speed: CGFloat) { guard speed > 0 else { return } - - self.removeAllAnimations() - let currentLayer = self.presentation() ?? self - + + removeAllAnimations() + let currentLayer = presentation() ?? self + let animation = CASpringAnimation() - animation.mass = 10/speed + animation.mass = 10 / speed animation.damping = 50 - animation.duration = 1/speed - + animation.duration = 1 / speed + animation.isRemovedOnCompletion = false animation.fillMode = CAMediaTimingFillMode.forwards - + let position = newPosition() let radius = newRadius() - + // Center point let start = animation.copy() as! CASpringAnimation start.keyPath = "startPoint" start.fromValue = currentLayer.startPoint start.toValue = position - + // Radius let end = animation.copy() as! CASpringAnimation end.keyPath = "endPoint" end.fromValue = currentLayer.endPoint end.toValue = position.displace(by: radius) - - self.startPoint = position - self.endPoint = position.displace(by: radius) - + + startPoint = position + endPoint = position.displace(by: radius) + // Opacity let value = Float.random(in: 0.5...1) let opacity = animation.copy() as! CASpringAnimation opacity.fromValue = self.opacity opacity.toValue = value - + self.opacity = value - - self.add(opacity, forKey: "opacity") - self.add(start, forKey: "startPoint") - self.add(end, forKey: "endPoint") + + add(opacity, forKey: "opacity") + add(start, forKey: "startPoint") + add(end, forKey: "endPoint") } - + /// Set the color of the blob func set(color: Color) { // Converted to the system color so that cgColor isn't nil - self.colors = [SystemColor(color).cgColor, - SystemColor(color).cgColor, - SystemColor(color.opacity(0.0)).cgColor] - self.locations = [0.0, 0.9, 1.0] - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // Required by the framework - public override init(layer: Any) { - super.init(layer: layer) + colors = [ + SystemColor(color).cgColor, + SystemColor(color).cgColor, + SystemColor(color.opacity(0.0)).cgColor, + ] + locations = [0.0, 0.9, 1.0] } } diff --git a/FRW/Tools/ThirdParty/FluidGradient/CGPoint+Extensions.swift b/FRW/Tools/ThirdParty/FluidGradient/CGPoint+Extensions.swift index 200fab1e..1701a250 100644 --- a/FRW/Tools/ThirdParty/FluidGradient/CGPoint+Extensions.swift +++ b/FRW/Tools/ThirdParty/FluidGradient/CGPoint+Extensions.swift @@ -1,6 +1,6 @@ // // CGPoint+Extensions.swift -// +// // // Created by João Gabriel Pozzobon dos Santos on 03/10/22. // @@ -10,13 +10,17 @@ import CoreGraphics extension CGPoint { /// Build a point from an origin and a displacement func displace(by point: CGPoint = .init(x: 0.0, y: 0.0)) -> CGPoint { - return CGPoint(x: self.x+point.x, - y: self.y+point.y) + CGPoint( + x: x + point.x, + y: y + point.y + ) } - + /// Caps the point to the unit space func capped() -> CGPoint { - return CGPoint(x: max(min(x, 1), 0), - y: max(min(y, 1), 0)) + CGPoint( + x: max(min(x, 1), 0), + y: max(min(y, 1), 0) + ) } } diff --git a/FRW/Tools/ThirdParty/FluidGradient/FluidGradient.swift b/FRW/Tools/ThirdParty/FluidGradient/FluidGradient.swift index cccb3ffb..f76330c6 100644 --- a/FRW/Tools/ThirdParty/FluidGradient/FluidGradient.swift +++ b/FRW/Tools/ThirdParty/FluidGradient/FluidGradient.swift @@ -7,33 +7,48 @@ import SwiftUI +// MARK: - FluidGradient + public struct FluidGradient: View { - private var blobs: [Color] - private var highlights: [Color] - private var speed: CGFloat - private var blur: CGFloat - - @State var blurValue: CGFloat = 0.0 - - public init(blobs: [Color], - highlights: [Color] = [], - speed: CGFloat = 1.0, - blur: CGFloat = 0.75) { + // MARK: Lifecycle + + public init( + blobs: [Color], + highlights: [Color] = [], + speed: CGFloat = 1.0, + blur: CGFloat = 0.75 + ) { self.blobs = blobs self.highlights = highlights self.speed = speed self.blur = blur } - + + // MARK: Public + public var body: some View { - Representable(blobs: blobs, - highlights: highlights, - speed: speed, - blurValue: $blurValue) + Representable( + blobs: blobs, + highlights: highlights, + speed: speed, + blurValue: $blurValue + ) .blur(radius: pow(blurValue, blur)) .accessibility(hidden: true) .clipped() } + + // MARK: Internal + + @State + var blurValue: CGFloat = 0.0 + + // MARK: Private + + private var blobs: [Color] + private var highlights: [Color] + private var speed: CGFloat + private var blur: CGFloat } #if os(OSX) @@ -43,89 +58,103 @@ typealias SystemRepresentable = UIViewRepresentable #endif // MARK: - Representable + extension FluidGradient { struct Representable: SystemRepresentable { var blobs: [Color] var highlights: [Color] var speed: CGFloat - - @Binding var blurValue: CGFloat - + + @Binding + var blurValue: CGFloat + func makeView(context: Context) -> FluidGradientView { context.coordinator.view } - - func updateView(_ view: FluidGradientView, context: Context) { + + func updateView(_: FluidGradientView, context: Context) { context.coordinator.create(blobs: blobs, highlights: highlights) DispatchQueue.main.async { context.coordinator.update(speed: speed) } } - -#if os(OSX) + + #if os(OSX) func makeNSView(context: Context) -> FluidGradientView { makeView(context: context) } + func updateNSView(_ view: FluidGradientView, context: Context) { updateView(view, context: context) } -#else + #else func makeUIView(context: Context) -> FluidGradientView { makeView(context: context) } + func updateUIView(_ view: FluidGradientView, context: Context) { updateView(view, context: context) } -#endif - + #endif + func makeCoordinator() -> Coordinator { - Coordinator(blobs: blobs, - highlights: highlights, - speed: speed, - blurValue: $blurValue) + Coordinator( + blobs: blobs, + highlights: highlights, + speed: speed, + blurValue: $blurValue + ) } } - + class Coordinator: FluidGradientDelegate { - var blobs: [Color] - var highlights: [Color] - var speed: CGFloat - var blurValue: Binding - - var view: FluidGradientView - - init(blobs: [Color], - highlights: [Color], - speed: CGFloat, - blurValue: Binding) { + // MARK: Lifecycle + + init( + blobs: [Color], + highlights: [Color], + speed: CGFloat, + blurValue: Binding + ) { self.blobs = blobs self.highlights = highlights self.speed = speed self.blurValue = blurValue - self.view = FluidGradientView(blobs: blobs, - highlights: highlights, - speed: speed) - self.view.delegate = self + self.view = FluidGradientView( + blobs: blobs, + highlights: highlights, + speed: speed + ) + view.delegate = self } - + + // MARK: Internal + + var blobs: [Color] + var highlights: [Color] + var speed: CGFloat + var blurValue: Binding + + var view: FluidGradientView + /// Create blobs and highlights func create(blobs: [Color], highlights: [Color]) { guard blobs != self.blobs || highlights != self.highlights else { return } self.blobs = blobs self.highlights = highlights - + view.create(blobs, layer: view.baseLayer) view.create(highlights, layer: view.highlightLayer) view.update(speed: speed) } - + /// Update speed func update(speed: CGFloat) { guard speed != self.speed else { return } self.speed = speed view.update(speed: speed) } - + func updateBlur(_ value: CGFloat) { blurValue.wrappedValue = value } diff --git a/FRW/Tools/ThirdParty/FluidGradient/FluidGradientView.swift b/FRW/Tools/ThirdParty/FluidGradient/FluidGradientView.swift index ab8e61a9..30f8def2 100644 --- a/FRW/Tools/ThirdParty/FluidGradient/FluidGradientView.swift +++ b/FRW/Tools/ThirdParty/FluidGradient/FluidGradientView.swift @@ -5,68 +5,70 @@ // Created by Oskar Groth on 2021-12-23. // -import SwiftUI import Combine - +import SwiftUI + #if os(OSX) import AppKit + public typealias SystemColor = NSColor public typealias SystemView = NSView #else import UIKit + public typealias SystemColor = UIColor public typealias SystemView = UIView #endif +// MARK: - FluidGradientView + /// A system view that presents an animated gradient with ``CoreAnimation`` public class FluidGradientView: SystemView { - var speed: CGFloat - - let baseLayer = ResizableLayer() - let highlightLayer = ResizableLayer() - - var cancellables = Set() - - weak var delegate: FluidGradientDelegate? - - init(blobs: [Color] = [], - highlights: [Color] = [], - speed: CGFloat = 1.0) { + // MARK: Lifecycle + + init( + blobs: [Color] = [], + highlights: [Color] = [], + speed: CGFloat = 1.0 + ) { self.speed = speed super.init(frame: .zero) - + if let compositingFilter = CIFilter(name: "CIOverlayBlendMode") { highlightLayer.compositingFilter = compositingFilter } - + #if os(OSX) layer = ResizableLayer() - + wantsLayer = true postsFrameChangedNotifications = true - + layer?.delegate = self baseLayer.delegate = self highlightLayer.delegate = self - - self.layer?.addSublayer(baseLayer) - self.layer?.addSublayer(highlightLayer) + + layer?.addSublayer(baseLayer) + layer?.addSublayer(highlightLayer) #else - self.layer.addSublayer(baseLayer) - self.layer.addSublayer(highlightLayer) + layer.addSublayer(baseLayer) + layer.addSublayer(highlightLayer) #endif - + create(blobs, layer: baseLayer) create(highlights, layer: highlightLayer) DispatchQueue.main.async { self.update(speed: speed) } } - - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + // MARK: Public + /// Create blobs and add to specified layer public func create(_ colors: [Color], layer: CALayer) { // Remove blobs at the end if colors are removed @@ -75,7 +77,7 @@ public class FluidGradientView: SystemView { if removeCount > 0 { layer.sublayers?.removeLast(removeCount) } - + for (index, color) in colors.enumerated() { if index < count { if let existing = layer.sublayers?[index] as? BlobLayer { @@ -86,73 +88,92 @@ public class FluidGradientView: SystemView { } } } - + /// Update sublayers and set speed and blur levels public func update(speed: CGFloat) { cancellables.removeAll() self.speed = speed guard speed > 0 else { return } - + let layers = (baseLayer.sublayers ?? []) + (highlightLayer.sublayers ?? []) for layer in layers { if let layer = layer as? BlobLayer { - Timer.publish(every: .random(in: 0.8/speed...1.2/speed), - on: .main, - in: .common) - .autoconnect() - .sink { _ in - #if os(OSX) - let visible = self.window?.occlusionState.contains(.visible) - guard visible == true else { return } - #endif - layer.animate(speed: speed) - } - .store(in: &cancellables) + Timer.publish( + every: .random(in: 0.8 / speed...1.2 / speed), + on: .main, + in: .common + ) + .autoconnect() + .sink { _ in + #if os(OSX) + let visible = self.window?.occlusionState.contains(.visible) + guard visible == true else { return } + #endif + layer.animate(speed: speed) + } + .store(in: &cancellables) } } } - - /// Compute and update new blur value - private func updateBlur() { - delegate?.updateBlur(min(frame.width, frame.height)) - } - + /// Functional methods #if os(OSX) - public override func viewDidMoveToWindow() { + override public func viewDidMoveToWindow() { super.viewDidMoveToWindow() let scale = window?.backingScaleFactor ?? 2 layer?.contentsScale = scale baseLayer.contentsScale = scale highlightLayer.contentsScale = scale - + updateBlur() } - - public override func resize(withOldSuperviewSize oldSize: NSSize) { + + override public func resize(withOldSuperviewSize _: NSSize) { updateBlur() } #else - public override func layoutSubviews() { - layer.frame = self.bounds - baseLayer.frame = self.bounds - highlightLayer.frame = self.bounds - + override public func layoutSubviews() { + layer.frame = bounds + baseLayer.frame = bounds + highlightLayer.frame = bounds + updateBlur() } #endif + + // MARK: Internal + + var speed: CGFloat + + let baseLayer = ResizableLayer() + let highlightLayer = ResizableLayer() + + var cancellables = Set() + + weak var delegate: FluidGradientDelegate? + + // MARK: Private + + /// Compute and update new blur value + private func updateBlur() { + delegate?.updateBlur(min(frame.width, frame.height)) + } } +// MARK: - FluidGradientDelegate + protocol FluidGradientDelegate: AnyObject { func updateBlur(_ value: CGFloat) } #if os(OSX) extension FluidGradientView: CALayerDelegate, NSViewLayerContentScaleDelegate { - public func layer(_ layer: CALayer, - shouldInheritContentsScale newScale: CGFloat, - from window: NSWindow) -> Bool { - return true + public func layer( + _: CALayer, + shouldInheritContentsScale _: CGFloat, + from _: NSWindow + ) -> Bool { + true } } #endif diff --git a/FRW/Tools/ThirdParty/FluidGradient/ResizableLayer.swift b/FRW/Tools/ThirdParty/FluidGradient/ResizableLayer.swift index a53b813d..6ed6ce33 100644 --- a/FRW/Tools/ThirdParty/FluidGradient/ResizableLayer.swift +++ b/FRW/Tools/ThirdParty/FluidGradient/ResizableLayer.swift @@ -9,6 +9,8 @@ import SwiftUI /// An implementation of ``CALayer`` that resizes its sublayers public class ResizableLayer: CALayer { + // MARK: Lifecycle + override init() { super.init() #if os(OSX) @@ -16,16 +18,19 @@ public class ResizableLayer: CALayer { #endif sublayers = [] } - - public override init(layer: Any) { + + override public init(layer: Any) { super.init(layer: layer) } - - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - - public override func layoutSublayers() { + + // MARK: Public + + override public func layoutSublayers() { super.layoutSublayers() sublayers?.forEach { layer in layer.frame = self.frame diff --git a/FRW/Tools/ThirdParty/PopupView/Modifiers.swift b/FRW/Tools/ThirdParty/PopupView/Modifiers.swift index eb58c633..f2514eca 100644 --- a/FRW/Tools/ThirdParty/PopupView/Modifiers.swift +++ b/FRW/Tools/ThirdParty/PopupView/Modifiers.swift @@ -1,5 +1,5 @@ // -// Constructors.swift +// Modifiers.swift // Pods // // Created by Alisa Mylnikova on 11.10.2022. @@ -7,11 +7,12 @@ import SwiftUI -public extension View { - func popup( +extension View { + public func popup( isPresented: Binding, @ViewBuilder view: @escaping () -> PopupContent, - customize: @escaping (Popup.PopupParameters) -> Popup.PopupParameters + customize: @escaping (Popup.PopupParameters) -> Popup + .PopupParameters ) -> some View { modifier( FullscreenPopup( @@ -24,10 +25,11 @@ public extension View { ) } - func popup( + public func popup( item: Binding, @ViewBuilder itemView: @escaping (Item) -> PopupContent, - customize: @escaping (Popup.PopupParameters) -> Popup.PopupParameters + customize: @escaping (Popup.PopupParameters) -> Popup + .PopupParameters ) -> some View { modifier( FullscreenPopup( @@ -40,7 +42,7 @@ public extension View { ) } - func popup( + public func popup( isPresented: Binding, @ViewBuilder view: @escaping () -> PopupContent ) -> some View { @@ -55,7 +57,7 @@ public extension View { ) } - func popup( + public func popup( item: Binding, @ViewBuilder itemView: @escaping (Item) -> PopupContent ) -> some View { diff --git a/FRW/Tools/ThirdParty/PopupView/PopupViewUtils.swift b/FRW/Tools/ThirdParty/PopupView/PopupViewUtils.swift index a38a56b9..67e23973 100644 --- a/FRW/Tools/ThirdParty/PopupView/PopupViewUtils.swift +++ b/FRW/Tools/ThirdParty/PopupView/PopupViewUtils.swift @@ -1,5 +1,5 @@ // -// Utils.swift +// PopupViewUtils.swift // PopupView // // Created by Alisa Mylnikova on 01.06.2022. @@ -9,16 +9,24 @@ import Combine import SwiftUI +// MARK: - DispatchWorkHolder + final class DispatchWorkHolder { var work: DispatchWorkItem? } +// MARK: - ClassReference + final class ClassReference { - var value: T + // MARK: Lifecycle init(_ value: T) { self.value = value } + + // MARK: Internal + + var value: T } extension View { @@ -47,17 +55,17 @@ extension View { @ViewBuilder func addTapIfNotTV(if condition: Bool, onTap: @escaping () -> Void) -> some View { #if os(tvOS) - self + self #else - if condition { - gesture( - TapGesture().onEnded { - onTap() - } - ) - } else { - self - } + if condition { + gesture( + TapGesture().onEnded { + onTap() + } + ) + } else { + self + } #endif } } @@ -65,7 +73,8 @@ extension View { // MARK: - FrameGetter struct FrameGetter: ViewModifier { - @Binding var frame: CGRect + @Binding + var frame: CGRect func body(content: Content) -> some View { content @@ -84,14 +93,17 @@ struct FrameGetter: ViewModifier { } } -internal extension View { +extension View { func frameGetter(_ frame: Binding) -> some View { modifier(FrameGetter(frame: frame)) } } +// MARK: - SafeAreaGetter + struct SafeAreaGetter: ViewModifier { - @Binding var safeArea: EdgeInsets + @Binding + var safeArea: EdgeInsets func body(content: Content) -> some View { content @@ -110,15 +122,26 @@ struct SafeAreaGetter: ViewModifier { } } -public extension View { - func safeAreaGetter(_ safeArea: Binding) -> some View { +extension View { + public func safeAreaGetter(_ safeArea: Binding) -> some View { modifier(SafeAreaGetter(safeArea: safeArea)) } } -// MARK: - AnimationCompletionObserver +// MARK: - AnimationCompletionObserverModifier + +struct AnimationCompletionObserverModifier: AnimatableModifier where Value: VectorArithmetic, + Value: Comparable { + // MARK: Lifecycle + + init(observedValue: Value, completion: @escaping () -> Void) { + self.completion = completion + self.animatableData = observedValue + self.targetValue = observedValue + } + + // MARK: Internal -struct AnimationCompletionObserverModifier: AnimatableModifier where Value: VectorArithmetic, Value: Comparable { /// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes. var animatableData: Value { didSet { @@ -126,18 +149,19 @@ struct AnimationCompletionObserverModifier: AnimatableModifier where Valu } } + func body(content: Content) -> some View { + /// We're not really modifying the view so we can directly return the original input value. + content + } + + // MARK: Private + /// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes. private var targetValue: Value /// The completion callback which is called once the animation completes. private var completion: () -> Void - init(observedValue: Value, completion: @escaping () -> Void) { - self.completion = completion - animatableData = observedValue - targetValue = observedValue - } - /// Verifies whether the current animation is finished and calls the completion callback if true. private func notifyCompletionIfFinished() { guard animatableData == targetValue else { return } @@ -148,25 +172,12 @@ struct AnimationCompletionObserverModifier: AnimatableModifier where Valu self.completion() } } - - func body(content: Content) -> some View { - /// We're not really modifying the view so we can directly return the original input value. - return content - } } -struct AnimatableModifierDouble: AnimatableModifier { - var targetValue: Double - static var done = false - - // SwiftUI gradually varies it from old value to the new value - var animatableData: Double { - didSet { - checkIfFinished() - } - } +// MARK: - AnimatableModifierDouble - var completion: () -> Void +struct AnimatableModifierDouble: AnimatableModifier { + // MARK: Lifecycle // Re-created every time the control argument changes init(bindedValue: Double, completion: @escaping () -> Void) { @@ -176,17 +187,30 @@ struct AnimatableModifierDouble: AnimatableModifier { // and gradually varies the value while the body // is being called to animate. Following line serves the purpose of // associating the extenal argument with the animatableData. - animatableData = bindedValue - targetValue = bindedValue + self.animatableData = bindedValue + self.targetValue = bindedValue AnimatableModifierDouble.done = false } + // MARK: Internal + + static var done = false + + var targetValue: Double + var completion: () -> Void + + // SwiftUI gradually varies it from old value to the new value + var animatableData: Double { + didSet { + checkIfFinished() + } + } + func checkIfFinished() { if AnimatableModifierDouble.done { return } let delta = 0.1 if animatableData > targetValue - delta && - animatableData < targetValue + delta - { + animatableData < targetValue + delta { AnimatableModifierDouble.done = true DispatchQueue.main.async { self.completion() @@ -209,63 +233,75 @@ extension View { #if os(iOS) - extension View { - func transparentNonAnimatingFullScreenCover( - isPresented: Binding, - dismissSource: DismissSource?, - userDismissCallback: @escaping (DismissSource) -> Void, - content: @escaping () -> Content - ) -> some View { - modifier(TransparentNonAnimatableFullScreenModifier(isPresented: isPresented, dismissSource: dismissSource, userDismissCallback: userDismissCallback, fullScreenContent: content)) - } +extension View { + func transparentNonAnimatingFullScreenCover( + isPresented: Binding, + dismissSource: DismissSource?, + userDismissCallback: @escaping (DismissSource) -> Void, + content: @escaping () -> Content + ) -> some View { + modifier(TransparentNonAnimatableFullScreenModifier( + isPresented: isPresented, + dismissSource: dismissSource, + userDismissCallback: userDismissCallback, + fullScreenContent: content + )) } +} - private struct TransparentNonAnimatableFullScreenModifier: ViewModifier { - @Binding var isPresented: Bool - var dismissSource: DismissSource? - var userDismissCallback: (DismissSource) -> Void - let fullScreenContent: () -> (FullScreenContent) +private struct TransparentNonAnimatableFullScreenModifier< + FullScreenContent: View +>: ViewModifier { + @Binding + var isPresented: Bool + var dismissSource: DismissSource? + var userDismissCallback: (DismissSource) -> Void + let fullScreenContent: () -> (FullScreenContent) - func body(content: Content) -> some View { - content - .onChange(of: isPresented) { _ in - UIView.setAnimationsEnabled(false) + func body(content: Content) -> some View { + content + .onChange(of: isPresented) { _ in + UIView.setAnimationsEnabled(false) + } + .fullScreenCover(isPresented: $isPresented, content: { + ZStack { + fullScreenContent() } - .fullScreenCover(isPresented: $isPresented, content: { - ZStack { - fullScreenContent() + .background(FullScreenCoverBackgroundRemovalView()) + .onAppear { + if !UIView.areAnimationsEnabled { + UIView.setAnimationsEnabled(true) } - .background(FullScreenCoverBackgroundRemovalView()) - .onAppear { - if !UIView.areAnimationsEnabled { - UIView.setAnimationsEnabled(true) - } - } - .onDisappear { - userDismissCallback(dismissSource ?? .binding) - if !UIView.areAnimationsEnabled { - UIView.setAnimationsEnabled(true) - } + } + .onDisappear { + userDismissCallback(dismissSource ?? .binding) + if !UIView.areAnimationsEnabled { + UIView.setAnimationsEnabled(true) } - }) - } + } + }) } +} - private struct FullScreenCoverBackgroundRemovalView: UIViewRepresentable { - private class BackgroundRemovalView: UIView { - override func didMoveToWindow() { - super.didMoveToWindow() +private struct FullScreenCoverBackgroundRemovalView: UIViewRepresentable { + // MARK: Internal - superview?.superview?.backgroundColor = .clear - } - } + func makeUIView(context _: Context) -> UIView { + BackgroundRemovalView() + } - func makeUIView(context _: Context) -> UIView { - return BackgroundRemovalView() - } + func updateUIView(_: UIView, context _: Context) {} + + // MARK: Private - func updateUIView(_: UIView, context _: Context) {} + private class BackgroundRemovalView: UIView { + override func didMoveToWindow() { + super.didMoveToWindow() + + superview?.superview?.backgroundColor = .clear + } } +} #endif @@ -273,40 +309,55 @@ extension View { #if os(iOS) - class KeyboardHeightHelper: ObservableObject { - @Published var keyboardHeight: CGFloat = 0 - @Published var keyboardDisplayed: Bool = false +class KeyboardHeightHelper: ObservableObject { + // MARK: Lifecycle - init() { - listenForKeyboardNotifications() - } + init() { + listenForKeyboardNotifications() + } - private func listenForKeyboardNotifications() { - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, - object: nil, - queue: .main) { notification in - guard let userInfo = notification.userInfo, - let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + // MARK: Internal - self.keyboardHeight = keyboardRect.height - self.keyboardDisplayed = true - } + @Published + var keyboardHeight: CGFloat = 0 + @Published + var keyboardDisplayed: Bool = false - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, - object: nil, - queue: .main) { _ in - self.keyboardHeight = 0 - self.keyboardDisplayed = false - } + // MARK: Private + + private func listenForKeyboardNotifications() { + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillShowNotification, + object: nil, + queue: .main + ) { notification in + guard let userInfo = notification.userInfo, + let keyboardRect = + userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + + self.keyboardHeight = keyboardRect.height + self.keyboardDisplayed = true + } + + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillHideNotification, + object: nil, + queue: .main + ) { _ in + self.keyboardHeight = 0 + self.keyboardDisplayed = false } } +} #else - class KeyboardHeightHelper: ObservableObject { - @Published var keyboardHeight: CGFloat = 0 - @Published var keyboardDisplayed: Bool = false - } +class KeyboardHeightHelper: ObservableObject { + @Published + var keyboardHeight: CGFloat = 0 + @Published + var keyboardDisplayed: Bool = false +} #endif @@ -321,11 +372,11 @@ extension CGPoint { extension CGSize { static var screenSize: CGSize { #if os(iOS) || os(tvOS) - return UIScreen.main.bounds.size + return UIScreen.main.bounds.size #elseif os(watchOS) - return WKInterfaceDevice.current().screenBounds.size + return WKInterfaceDevice.current().screenBounds.size #elseif os(macOS) - return NSScreen.main?.frame.size ?? .zero + return NSScreen.main?.frame.size ?? .zero #endif } } diff --git a/FRW/Tools/ThirdParty/ProgressHUD/ProgressHUD.swift b/FRW/Tools/ThirdParty/ProgressHUD/ProgressHUD.swift index ad3d0173..8c88aec1 100644 --- a/FRW/Tools/ThirdParty/ProgressHUD/ProgressHUD.swift +++ b/FRW/Tools/ThirdParty/ProgressHUD/ProgressHUD.swift @@ -12,6 +12,8 @@ import Lottie import UIKit +// MARK: - AnimationType + // ----------------------------------------------------------------------------------------------------------------------------------------------- public enum AnimationType { case systemActivityIndicator @@ -28,6 +30,8 @@ public enum AnimationType { case lottie } +// MARK: - AnimatedIcon + // ----------------------------------------------------------------------------------------------------------------------------------------------- public enum AnimatedIcon { case succeed @@ -35,6 +39,8 @@ public enum AnimatedIcon { case added } +// MARK: - AlertIcon + // ----------------------------------------------------------------------------------------------------------------------------------------------- public enum AlertIcon { case heart @@ -86,64 +92,64 @@ extension AlertIcon { } // ----------------------------------------------------------------------------------------------------------------------------------------------- -public extension ProgressHUD { - class var animationType: AnimationType { +extension ProgressHUD { + public class var animationType: AnimationType { get { shared.animationType } set { shared.animationType = newValue } } - class var colorBackground: UIColor { + public class var colorBackground: UIColor { get { shared.colorBackground } set { shared.colorBackground = newValue } } - class var colorHUD: UIColor { + public class var colorHUD: UIColor { get { shared.colorHUD } set { shared.colorHUD = newValue } } - class var colorStatus: UIColor { + public class var colorStatus: UIColor { get { shared.colorStatus } set { shared.colorStatus = newValue } } - class var colorAnimation: UIColor { + public class var colorAnimation: UIColor { get { shared.colorAnimation } set { shared.colorAnimation = newValue } } - class var colorProgress: UIColor { + public class var colorProgress: UIColor { get { shared.colorProgress } set { shared.colorProgress = newValue } } - class var fontStatus: UIFont { + public class var fontStatus: UIFont { get { shared.fontStatus } set { shared.fontStatus = newValue } } - class var imageSuccess: UIImage { + public class var imageSuccess: UIImage { get { shared.imageSuccess } set { shared.imageSuccess = newValue } } - class var imageError: UIImage { + public class var imageError: UIImage { get { shared.imageError } set { shared.imageError = newValue } } } // ----------------------------------------------------------------------------------------------------------------------------------------------- -public extension ProgressHUD { +extension ProgressHUD { // ------------------------------------------------------------------------------------------------------------------------------------------- - class func dismiss() { + public class func dismiss() { DispatchQueue.main.async { shared.hudHide() } } // ------------------------------------------------------------------------------------------------------------------------------------------- - class func show(_ status: String? = nil, interaction: Bool = true) { + public class func show(_ status: String? = nil, interaction: Bool = true) { DispatchQueue.main.async { shared.setup(status: status, hide: false, interaction: interaction) } @@ -152,7 +158,7 @@ public extension ProgressHUD { // MARK: - // ------------------------------------------------------------------------------------------------------------------------------------------- - class func show(_ status: String? = nil, icon: AlertIcon, interaction: Bool = true) { + public class func show(_ status: String? = nil, icon: AlertIcon, interaction: Bool = true) { let image = icon.image?.withTintColor(shared.colorAnimation, renderingMode: .alwaysOriginal) DispatchQueue.main.async { @@ -161,46 +167,83 @@ public extension ProgressHUD { } // ------------------------------------------------------------------------------------------------------------------------------------------- - class func show(_ status: String? = nil, icon animatedIcon: AnimatedIcon, interaction: Bool = true) { + public class func show( + _ status: String? = nil, + icon animatedIcon: AnimatedIcon, + interaction: Bool = true + ) { DispatchQueue.main.async { - shared.setup(status: status, animatedIcon: animatedIcon, hide: true, interaction: interaction) + shared.setup( + status: status, + animatedIcon: animatedIcon, + hide: true, + interaction: interaction + ) } } // MARK: - // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showSuccess(_ status: String? = nil, image: UIImage? = nil, interaction: Bool = true) { + public class func showSuccess( + _ status: String? = nil, + image: UIImage? = nil, + interaction: Bool = true + ) { DispatchQueue.main.async { - shared.setup(status: status, staticImage: image ?? shared.imageSuccess, hide: true, interaction: interaction) + shared.setup( + status: status, + staticImage: image ?? shared.imageSuccess, + hide: true, + interaction: interaction + ) } } // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showError(_ status: String? = nil, image: UIImage? = nil, interaction: Bool = true) { + public class func showError( + _ status: String? = nil, + image: UIImage? = nil, + interaction: Bool = true + ) { DispatchQueue.main.async { - shared.setup(status: status, staticImage: image ?? shared.imageError, hide: true, interaction: interaction) + shared.setup( + status: status, + staticImage: image ?? shared.imageError, + hide: true, + interaction: interaction + ) } } // MARK: - // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showSucceed(_ status: String? = nil, interaction: Bool = true) { + public class func showSucceed(_ status: String? = nil, interaction: Bool = true) { DispatchQueue.main.async { - shared.setup(status: status, animatedIcon: .succeed, hide: true, interaction: interaction) + shared.setup( + status: status, + animatedIcon: .succeed, + hide: true, + interaction: interaction + ) } } // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showFailed(_ status: String? = nil, interaction: Bool = true) { + public class func showFailed(_ status: String? = nil, interaction: Bool = true) { DispatchQueue.main.async { - shared.setup(status: status, animatedIcon: .failed, hide: true, interaction: interaction) + shared.setup( + status: status, + animatedIcon: .failed, + hide: true, + interaction: interaction + ) } } // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showAdded(_ status: String? = nil, interaction: Bool = true) { + public class func showAdded(_ status: String? = nil, interaction: Bool = true) { DispatchQueue.main.async { shared.setup(status: status, animatedIcon: .added, hide: true, interaction: interaction) } @@ -209,22 +252,56 @@ public extension ProgressHUD { // MARK: - // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showProgress(_ progress: CGFloat, interaction: Bool = false) { + public class func showProgress(_ progress: CGFloat, interaction: Bool = false) { DispatchQueue.main.async { shared.setup(progress: progress, hide: false, interaction: interaction) } } // ------------------------------------------------------------------------------------------------------------------------------------------- - class func showProgress(_ status: String?, _ progress: CGFloat, interaction: Bool = false) { + public class func showProgress( + _ status: String?, + _ progress: CGFloat, + interaction: Bool = false + ) { DispatchQueue.main.async { shared.setup(status: status, progress: progress, hide: false, interaction: interaction) } } } +// MARK: - ProgressHUD + // ----------------------------------------------------------------------------------------------------------------------------------------------- public class ProgressHUD: UIView { + // MARK: Lifecycle + + // ------------------------------------------------------------------------------------------------------------------------------------------- + private convenience init() { + self.init(frame: UIScreen.main.bounds) + alpha = 0 + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + override private init(frame: CGRect) { + super.init(frame: frame) + } + + // MARK: Internal + + // ------------------------------------------------------------------------------------------------------------------------------------------- + static let shared: ProgressHUD = { + let instance = ProgressHUD() + return instance + }() + + // MARK: Private + private var viewBackground: UIView? private var toolbarHUD: UIToolbar? private var labelStatus: UILabel? @@ -245,8 +322,14 @@ public class ProgressHUD: UIView { private var colorProgress = UIColor.lightGray private var fontStatus = UIFont.boldSystemFont(ofSize: 24) - private var imageSuccess = UIImage.checkmark.withTintColor(UIColor.systemGreen, renderingMode: .alwaysOriginal) - private var imageError = UIImage.remove.withTintColor(UIColor.systemRed, renderingMode: .alwaysOriginal) + private var imageSuccess = UIImage.checkmark.withTintColor( + UIColor.systemGreen, + renderingMode: .alwaysOriginal + ) + private var imageError = UIImage.remove.withTintColor( + UIColor.systemRed, + renderingMode: .alwaysOriginal + ) private let keyboardWillShow = UIResponder.keyboardWillShowNotification private let keyboardWillHide = UIResponder.keyboardWillHideNotification @@ -255,32 +338,17 @@ public class ProgressHUD: UIView { private let orientationDidChange = UIDevice.orientationDidChangeNotification - // ------------------------------------------------------------------------------------------------------------------------------------------- - static let shared: ProgressHUD = { - let instance = ProgressHUD() - return instance - }() - - // ------------------------------------------------------------------------------------------------------------------------------------------- - private convenience init() { - self.init(frame: UIScreen.main.bounds) - alpha = 0 - } - - // ------------------------------------------------------------------------------------------------------------------------------------------- - internal required init?(coder: NSCoder) { - super.init(coder: coder) - } - - // ------------------------------------------------------------------------------------------------------------------------------------------- - override private init(frame: CGRect) { - super.init(frame: frame) - } - // MARK: - // ------------------------------------------------------------------------------------------------------------------------------------------- - private func setup(status: String? = nil, progress: CGFloat? = nil, animatedIcon: AnimatedIcon? = nil, staticImage: UIImage? = nil, hide: Bool, interaction: Bool) { + private func setup( + status: String? = nil, + progress: CGFloat? = nil, + animatedIcon: AnimatedIcon? = nil, + staticImage: UIImage? = nil, + hide: Bool, + interaction: Bool + ) { setupNotifications() setupBackground(interaction) setupToolbar() @@ -288,8 +356,10 @@ public class ProgressHUD: UIView { if progress == nil, animatedIcon == nil, staticImage == nil { setupAnimation() } if progress != nil, animatedIcon == nil, staticImage == nil { setupProgress(progress) } - if progress == nil, animatedIcon != nil, staticImage == nil { setupAnimatedIcon(animatedIcon) } - if progress == nil, animatedIcon == nil, staticImage != nil { setupStaticImage(staticImage) } + if progress == nil, animatedIcon != nil, + staticImage == nil { setupAnimatedIcon(animatedIcon) } + if progress == nil, animatedIcon == nil, + staticImage != nil { setupStaticImage(staticImage) } setupSize() setupPosition() @@ -308,11 +378,36 @@ public class ProgressHUD: UIView { // ------------------------------------------------------------------------------------------------------------------------------------------- private func setupNotifications() { if viewBackground == nil { - NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardWillShow, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardWillHide, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardDidShow, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardDidHide, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: orientationDidChange, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(setupPosition(_:)), + name: keyboardWillShow, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(setupPosition(_:)), + name: keyboardWillHide, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(setupPosition(_:)), + name: keyboardDidShow, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(setupPosition(_:)), + name: keyboardDidHide, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(setupPosition(_:)), + name: orientationDidChange, + object: nil + ) } } @@ -398,13 +493,17 @@ public class ProgressHUD: UIView { $0.removeFromSuperlayer() } - if animationType == .systemActivityIndicator { animationSystemActivityIndicator(viewAnimation!) } - if animationType == .horizontalCirclesPulse { animationHorizontalCirclesPulse(viewAnimation!) } + if animationType == + .systemActivityIndicator { animationSystemActivityIndicator(viewAnimation!) } + if animationType == + .horizontalCirclesPulse { animationHorizontalCirclesPulse(viewAnimation!) } if animationType == .lineScaling { animationLineScaling(viewAnimation!) } if animationType == .singleCirclePulse { animationSingleCirclePulse(viewAnimation!) } if animationType == .multipleCirclePulse { animationMultipleCirclePulse(viewAnimation!) } - if animationType == .singleCircleScaleRipple { animationSingleCircleScaleRipple(viewAnimation!) } - if animationType == .multipleCircleScaleRipple { animationMultipleCircleScaleRipple(viewAnimation!) } + if animationType == + .singleCircleScaleRipple { animationSingleCircleScaleRipple(viewAnimation!) } + if animationType == + .multipleCircleScaleRipple { animationMultipleCircleScaleRipple(viewAnimation!) } if animationType == .circleSpinFade { animationCircleSpinFade(viewAnimation!) } if animationType == .lineSpinFade { animationLineSpinFade(viewAnimation!) } if animationType == .circleRotateChase { animationCircleRotateChase(viewAnimation!) } @@ -462,8 +561,14 @@ public class ProgressHUD: UIView { if let text = labelStatus?.text { let sizeMax = CGSize(width: 250, height: 250) - let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: labelStatus?.font as Any] - var rectLabel = text.boundingRect(with: sizeMax, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) + let attributes: [NSAttributedString.Key: Any] = + [NSAttributedString.Key.font: labelStatus?.font as Any] + var rectLabel = text.boundingRect( + with: sizeMax, + options: .usesLineFragmentOrigin, + attributes: attributes, + context: nil + ) width = ceil(rectLabel.size.width) + 60 height = ceil(rectLabel.size.height) + 120 @@ -490,17 +595,21 @@ public class ProgressHUD: UIView { } // ------------------------------------------------------------------------------------------------------------------------------------------- - @objc private func setupPosition(_ notification: Notification? = nil) { + @objc + private func setupPosition(_ notification: Notification? = nil) { var heightKeyboard: CGFloat = 0 var animationDuration: TimeInterval = 0 if let notification = notification { - let frameKeyboard = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? CGRect.zero - animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0 + let frameKeyboard = notification + .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? CGRect.zero + animationDuration = notification + .userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0 if (notification.name == keyboardWillShow) || (notification.name == keyboardDidShow) { heightKeyboard = frameKeyboard.size.height - } else if (notification.name == keyboardWillHide) || (notification.name == keyboardDidHide) { + } else if (notification.name == keyboardWillHide) || + (notification.name == keyboardDidHide) { heightKeyboard = 0 } else { heightKeyboard = keyboardHeight() @@ -513,18 +622,23 @@ public class ProgressHUD: UIView { let screen = mainWindow.bounds let center = CGPoint(x: screen.size.width / 2, y: (screen.size.height - heightKeyboard) / 2) - UIView.animate(withDuration: animationDuration, delay: 0, options: .allowUserInteraction, animations: { - self.toolbarHUD?.center = center - self.viewBackground?.frame = screen - }, completion: nil) + UIView.animate( + withDuration: animationDuration, + delay: 0, + options: .allowUserInteraction, + animations: { + self.toolbarHUD?.center = center + self.viewBackground?.frame = screen + }, + completion: nil + ) } // ------------------------------------------------------------------------------------------------------------------------------------------- private func keyboardHeight() -> CGFloat { if let keyboardWindowClass = NSClassFromString("UIRemoteKeyboardWindow"), let inputSetContainerView = NSClassFromString("UIInputSetContainerView"), - let inputSetHostView = NSClassFromString("UIInputSetHostView") - { + let inputSetHostView = NSClassFromString("UIInputSetHostView") { for window in UIApplication.shared.windows { if window.isKind(of: keyboardWindowClass) { for firstSubView in window.subviews { @@ -554,23 +668,35 @@ public class ProgressHUD: UIView { toolbarHUD?.alpha = 0 toolbarHUD?.transform = CGAffineTransform(scaleX: 1.4, y: 1.4) - UIView.animate(withDuration: 0.15, delay: 0, options: [.allowUserInteraction, .curveEaseIn], animations: { - self.toolbarHUD?.transform = CGAffineTransform(scaleX: 1 / 1.4, y: 1 / 1.4) - self.toolbarHUD?.alpha = 1 - }, completion: nil) + UIView.animate( + withDuration: 0.15, + delay: 0, + options: [.allowUserInteraction, .curveEaseIn], + animations: { + self.toolbarHUD?.transform = CGAffineTransform(scaleX: 1 / 1.4, y: 1 / 1.4) + self.toolbarHUD?.alpha = 1 + }, + completion: nil + ) } } // ------------------------------------------------------------------------------------------------------------------------------------------- private func hudHide() { if alpha == 1 { - UIView.animate(withDuration: 0.15, delay: 0, options: [.allowUserInteraction, .curveEaseIn], animations: { - self.toolbarHUD?.transform = CGAffineTransform(scaleX: 0.3, y: 0.3) - self.toolbarHUD?.alpha = 0 - }, completion: { _ in - self.hudDestroy() - self.alpha = 0 - }) + UIView.animate( + withDuration: 0.15, + delay: 0, + options: [.allowUserInteraction, .curveEaseIn], + animations: { + self.toolbarHUD?.transform = CGAffineTransform(scaleX: 0.3, y: 0.3) + self.toolbarHUD?.alpha = 0 + }, + completion: { _ in + self.hudDestroy() + self.alpha = 0 + } + ) } } @@ -625,11 +751,22 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(arcCenter: CGPoint(x: radius / 2, y: radius / 2), radius: radius / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + let path = UIBezierPath( + arcCenter: CGPoint(x: radius / 2, y: radius / 2), + radius: radius / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) - for i in 0 ..< 3 { + for i in 0..<3 { let layer = CAShapeLayer() - layer.frame = CGRect(x: (radius + spacing) * CGFloat(i), y: ypos, width: radius, height: radius) + layer.frame = CGRect( + x: (radius + spacing) * CGFloat(i), + y: ypos, + width: radius, + height: radius + ) layer.path = path.cgPath layer.fillColor = colorAnimation.cgColor @@ -659,11 +796,19 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: lineWidth, height: height), cornerRadius: width / 2) + let path = UIBezierPath( + roundedRect: CGRect(x: 0, y: 0, width: lineWidth, height: height), + cornerRadius: width / 2 + ) - for i in 0 ..< 5 { + for i in 0..<5 { let layer = CAShapeLayer() - layer.frame = CGRect(x: lineWidth * 2 * CGFloat(i), y: 0, width: lineWidth, height: height) + layer.frame = CGRect( + x: lineWidth * 2 * CGFloat(i), + y: 0, + width: lineWidth, + height: height + ) layer.path = path.cgPath layer.backgroundColor = nil layer.fillColor = colorAnimation.cgColor @@ -699,7 +844,13 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(arcCenter: CGPoint(x: width / 2, y: height / 2), radius: width / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + let path = UIBezierPath( + arcCenter: CGPoint(x: width / 2, y: height / 2), + radius: width / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) let layer = CAShapeLayer() layer.frame = CGRect(x: 0, y: 0, width: width, height: height) @@ -736,9 +887,15 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(arcCenter: CGPoint(x: width / 2, y: height / 2), radius: width / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + let path = UIBezierPath( + arcCenter: CGPoint(x: width / 2, y: height / 2), + radius: width / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) - for i in 0 ..< 3 { + for i in 0..<3 { let layer = CAShapeLayer() layer.frame = CGRect(x: 0, y: 0, width: width, height: height) layer.path = path.cgPath @@ -778,7 +935,13 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(arcCenter: CGPoint(x: width / 2, y: height / 2), radius: width / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + let path = UIBezierPath( + arcCenter: CGPoint(x: width / 2, y: height / 2), + radius: width / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) let layer = CAShapeLayer() layer.frame = CGRect(x: 0, y: 0, width: width, height: height) @@ -820,9 +983,15 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(arcCenter: CGPoint(x: width / 2, y: height / 2), radius: width / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + let path = UIBezierPath( + arcCenter: CGPoint(x: width / 2, y: height / 2), + radius: width / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) - for i in 0 ..< 3 { + for i in 0..<3 { let layer = CAShapeLayer() layer.frame = CGRect(x: 0, y: 0, width: width, height: height) layer.path = path.cgPath @@ -867,16 +1036,27 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(arcCenter: CGPoint(x: radius / 2, y: radius / 2), radius: radius / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + let path = UIBezierPath( + arcCenter: CGPoint(x: radius / 2, y: radius / 2), + radius: radius / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) - for i in 0 ..< 8 { + for i in 0..<8 { let angle = .pi / 4 * CGFloat(i) let layer = CAShapeLayer() layer.path = path.cgPath layer.fillColor = colorAnimation.cgColor layer.backgroundColor = nil - layer.frame = CGRect(x: radiusX * (cos(angle) + 1), y: radiusX * (sin(angle) + 1), width: radius, height: radius) + layer.frame = CGRect( + x: radiusX * (cos(angle) + 1), + y: radiusX * (sin(angle) + 1), + width: radius, + height: radius + ) animation.beginTime = beginTime - beginTimes[i] @@ -909,19 +1089,32 @@ public class ProgressHUD: UIView { animation.repeatCount = HUGE animation.isRemovedOnCompletion = false - let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: lineWidth, height: lineHeight), cornerRadius: lineWidth / 2) + let path = UIBezierPath( + roundedRect: CGRect(x: 0, y: 0, width: lineWidth, height: lineHeight), + cornerRadius: lineWidth / 2 + ) - for i in 0 ..< 8 { + for i in 0..<8 { let angle = .pi / 4 * CGFloat(i) let line = CAShapeLayer() - line.frame = CGRect(x: (containerSize - lineWidth) / 2, y: (containerSize - lineHeight) / 2, width: lineWidth, height: lineHeight) + line.frame = CGRect( + x: (containerSize - lineWidth) / 2, + y: (containerSize - lineHeight) / 2, + width: lineWidth, + height: lineHeight + ) line.path = path.cgPath line.backgroundColor = nil line.fillColor = colorAnimation.cgColor let container = CALayer() - container.frame = CGRect(x: radius * (cos(angle) + 1), y: radius * (sin(angle) + 1), width: containerSize, height: containerSize) + container.frame = CGRect( + x: radius * (cos(angle) + 1), + y: radius * (sin(angle) + 1), + width: containerSize, + height: containerSize + ) container.addSublayer(line) container.sublayerTransform = CATransform3DMakeRotation(.pi / 2 + angle, 0, 0, 1) @@ -943,11 +1136,23 @@ public class ProgressHUD: UIView { let duration: CFTimeInterval = 1.5 - let path = UIBezierPath(arcCenter: CGPoint(x: radius / 2, y: radius / 2), radius: radius / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) - - let pathPosition = UIBezierPath(arcCenter: CGPoint(x: width / 2, y: height / 2), radius: radiusX, startAngle: 1.5 * .pi, endAngle: 3.5 * .pi, clockwise: true) - - for i in 0 ..< 5 { + let path = UIBezierPath( + arcCenter: CGPoint(x: radius / 2, y: radius / 2), + radius: radius / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) + + let pathPosition = UIBezierPath( + arcCenter: CGPoint(x: width / 2, y: height / 2), + radius: radiusX, + startAngle: 1.5 * .pi, + endAngle: 3.5 * .pi, + clockwise: true + ) + + for i in 0..<5 { let rate = Float(i) * 1 / 5 let fromScale = 1 - rate let toScale = 0.2 + rate @@ -986,9 +1191,9 @@ public class ProgressHUD: UIView { let width = view.frame.size.width let height = view.frame.size.height - let beginTime: Double = 0.5 - let durationStart: Double = 1.2 - let durationStop: Double = 0.7 + let beginTime = 0.5 + let durationStart = 1.2 + let durationStop = 0.7 let animationRotation = CABasicAnimation(keyPath: "transform.rotation") animationRotation.byValue = 2 * Float.pi @@ -1014,7 +1219,13 @@ public class ProgressHUD: UIView { animation.isRemovedOnCompletion = false animation.fillMode = .forwards - let path = UIBezierPath(arcCenter: CGPoint(x: width / 2, y: height / 2), radius: width / 2, startAngle: -0.5 * .pi, endAngle: 1.5 * .pi, clockwise: true) + let path = UIBezierPath( + arcCenter: CGPoint(x: width / 2, y: height / 2), + radius: width / 2, + startAngle: -0.5 * .pi, + endAngle: 1.5 * .pi, + clockwise: true + ) let layer = CAShapeLayer() layer.frame = CGRect(x: 0, y: 0, width: width, height: height) @@ -1093,7 +1304,7 @@ public class ProgressHUD: UIView { animation.fillMode = .forwards animation.isRemovedOnCompletion = false - for i in 0 ..< 2 { + for i in 0..<2 { let layer = CAShapeLayer() layer.path = paths[i].cgPath layer.fillColor = UIColor.clear.cgColor @@ -1133,7 +1344,7 @@ public class ProgressHUD: UIView { animation.fillMode = .forwards animation.isRemovedOnCompletion = false - for i in 0 ..< 2 { + for i in 0..<2 { let layer = CAShapeLayer() layer.path = paths[i].cgPath layer.fillColor = UIColor.clear.cgColor @@ -1155,15 +1366,7 @@ public class ProgressHUD: UIView { // ----------------------------------------------------------------------------------------------------------------------------------------------- private class ProgressView: UIView { - var color: UIColor = .systemBackground { - didSet { setupLayers() } - } - - private var progress: CGFloat = 0 - - private var layerCircle = CAShapeLayer() - private var layerProgress = CAShapeLayer() - private var labelPercentage = UILabel() + // MARK: Lifecycle // ------------------------------------------------------------------------------------------------------------------------------------------- convenience init(_ color: UIColor) { @@ -1181,6 +1384,12 @@ private class ProgressView: UIView { super.init(frame: frame) } + // MARK: Internal + + var color: UIColor = .systemBackground { + didSet { setupLayers() } + } + // ------------------------------------------------------------------------------------------------------------------------------------------- override func draw(_ rect: CGRect) { super.draw(rect) @@ -1199,8 +1408,20 @@ private class ProgressView: UIView { let radiusCircle = width / 2 let radiusProgress = width / 2 - 5 - let pathCircle = UIBezierPath(arcCenter: center, radius: radiusCircle, startAngle: -0.5 * .pi, endAngle: 1.5 * .pi, clockwise: true) - let pathProgress = UIBezierPath(arcCenter: center, radius: radiusProgress, startAngle: -0.5 * .pi, endAngle: 1.5 * .pi, clockwise: true) + let pathCircle = UIBezierPath( + arcCenter: center, + radius: radiusCircle, + startAngle: -0.5 * .pi, + endAngle: 1.5 * .pi, + clockwise: true + ) + let pathProgress = UIBezierPath( + arcCenter: center, + radius: radiusProgress, + startAngle: -0.5 * .pi, + endAngle: 1.5 * .pi, + clockwise: true + ) layerCircle.path = pathCircle.cgPath layerCircle.fillColor = UIColor.clear.cgColor @@ -1235,4 +1456,12 @@ private class ProgressView: UIView { progress = value labelPercentage.text = "\(Int(value * 100))%" } + + // MARK: Private + + private var progress: CGFloat = 0 + + private var layerCircle = CAShapeLayer() + private var layerProgress = CAShapeLayer() + private var labelPercentage = UILabel() } diff --git a/FRW/Tools/ThirdParty/SPQRCode/Data/Colors.swift b/FRW/Tools/ThirdParty/SPQRCode/Data/Colors.swift index 341b71f4..66f7c2ed 100644 --- a/FRW/Tools/ThirdParty/SPQRCode/Data/Colors.swift +++ b/FRW/Tools/ThirdParty/SPQRCode/Data/Colors.swift @@ -1,5 +1,5 @@ // -// Color.swift +// Colors.swift // Flow Wallet // // Created by Hao Fu on 6/9/2022. diff --git a/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift b/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift index 6ddc33f1..e1fddb84 100644 --- a/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift +++ b/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift @@ -26,28 +26,12 @@ import SparrowKit import SwiftUI import UIKit -public typealias SPQRCodeCallback = ((SPQRCodeData, SPQRCameraController) -> Void) +public typealias SPQRCodeCallback = (SPQRCodeData, SPQRCameraController) -> Void -open class SPQRCameraController: SPController { - open var detectQRCodeData: ((SPQRCodeData, SPQRCameraController) -> SPQRCodeData?) = { data, _ in data } - open var handledQRCodeData: SPQRCodeCallback? - open var clickQRCodeData: SPQRCodeCallback? +// MARK: - SPQRCameraController - internal var updateTimer: Timer? - internal lazy var captureSession: AVCaptureSession = makeCaptureSession() - internal var qrCodeData: SPQRCodeData? { - didSet { - updateInterface() - didTapHandledButton() - } - } - - // MARK: - Views - - internal let frameLayer = SPQRFrameLayer() - internal let detailView = SPQRDetailButton() - internal lazy var previewLayer = makeVideoPreviewLayer() - internal let maskView = SPQRMaskView() +open class SPQRCameraController: SPController { + // MARK: Lifecycle override public init() { super.init() @@ -59,12 +43,20 @@ open class SPQRCameraController: SPController { fatalError("init(coder:) has not been implemented") } + // MARK: Open + + open var detectQRCodeData: ((SPQRCodeData, SPQRCameraController) -> SPQRCodeData?) = + { data, _ in + data + } + + open var handledQRCodeData: SPQRCodeCallback? + open var clickQRCodeData: SPQRCodeCallback? + override open var prefersStatusBarHidden: Bool { - return true + true } - // MARK: - Lifecycle - override open func viewDidLoad() { super.viewDidLoad() @@ -84,6 +76,41 @@ open class SPQRCameraController: SPController { updateInterface() } + override open func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + previewLayer.frame = .init( + x: .zero, y: .zero, + width: view.layer.bounds.width, + height: view.layer.bounds.height + ) + maskView.frame = previewLayer.frame + } + + // MARK: Internal + + static let supportedCodeTypes = [ + AVMetadataObject.ObjectType.aztec, + AVMetadataObject.ObjectType.qr, + ] + + var updateTimer: Timer? + lazy var captureSession: AVCaptureSession = makeCaptureSession() + + // MARK: - Views + + let frameLayer = SPQRFrameLayer() + let detailView = SPQRDetailButton() + lazy var previewLayer = makeVideoPreviewLayer() + let maskView = SPQRMaskView() + + var qrCodeData: SPQRCodeData? { + didSet { + updateInterface() + didTapHandledButton() + } + } + func stopRunning() { if captureSession.isRunning { captureSession.stopRunning() @@ -92,16 +119,82 @@ open class SPQRCameraController: SPController { // MARK: - Actions - @objc func didTapHandledButton() { + @objc + func didTapHandledButton() { guard let data = qrCodeData else { return } handledQRCodeData?(data, self) } - @objc func didTapCancelButton() { + @objc + func didTapCancelButton() { dismissAnimated() } - @objc private func didTapDetailButtonClick() { + func updateInterface() { + let duration: TimeInterval = 0.22 + if qrCodeData != nil { + detailView.isHidden = false + if case .flowWallet = qrCodeData { + detailView.applyDefaultAppearance(with: .init( + content: .white, + background: UIColor(hex: "#00EF8B") + )) + frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor + } + if case .ethWallet = qrCodeData { + detailView.applyDefaultAppearance(with: .init( + content: .white, + background: UIColor(hex: "#00EF8B") + )) + frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor + } + UIView.animate( + withDuration: duration, + delay: .zero, + options: .curveEaseInOut, + animations: { + self.detailView.transform = .identity + self.detailView.alpha = 1 + } + ) + } else { + UIView.animate( + withDuration: duration, + delay: .zero, + options: .curveEaseInOut, + animations: { + self.detailView.transform = .init(scale: 0.9) + self.detailView.alpha = .zero + }, + completion: { _ in + self.detailView.isHidden = true + } + ) + } + } + + func makeVideoPreviewLayer() -> AVCaptureVideoPreviewLayer { + let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoPreviewLayer.videoGravity = .resizeAspectFill + return videoPreviewLayer + } + + func makeCaptureSession() -> AVCaptureSession { + let captureSession = AVCaptureSession() + guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { fatalError() } + guard let input = try? AVCaptureDeviceInput(device: device) else { fatalError() } + captureSession.addInput(input) + let captureMetadataOutput = AVCaptureMetadataOutput() + captureSession.addOutput(captureMetadataOutput) + captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + captureMetadataOutput.metadataObjectTypes = Self.supportedCodeTypes + return captureSession + } + + // MARK: Private + + @objc + private func didTapDetailButtonClick() { guard let data = qrCodeData else { return } clickQRCodeData?(data, self) } @@ -135,75 +228,12 @@ open class SPQRCameraController: SPController { make.centerY.equalTo(backButton.snp.centerY) } } - - override open func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - previewLayer.frame = .init( - x: .zero, y: .zero, - width: view.layer.bounds.width, - height: view.layer.bounds.height - ) - maskView.frame = previewLayer.frame - } - - // MARK: - Internal - - internal func updateInterface() { - let duration: TimeInterval = 0.22 - if qrCodeData != nil { - detailView.isHidden = false - if case .flowWallet = qrCodeData { - detailView.applyDefaultAppearance(with: .init(content: .white, background: UIColor(hex: "#00EF8B"))) - frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor - } - if case .ethWallet = qrCodeData { - detailView.applyDefaultAppearance(with: .init(content: .white, background: UIColor(hex: "#00EF8B"))) - frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor - } - UIView.animate(withDuration: duration, delay: .zero, options: .curveEaseInOut, animations: { - self.detailView.transform = .identity - self.detailView.alpha = 1 - }) - } else { - UIView.animate(withDuration: duration, delay: .zero, options: .curveEaseInOut, animations: { - self.detailView.transform = .init(scale: 0.9) - self.detailView.alpha = .zero - }, completion: { _ in - self.detailView.isHidden = true - }) - } - } - - internal static let supportedCodeTypes = [ - AVMetadataObject.ObjectType.aztec, - AVMetadataObject.ObjectType.qr, - ] - - internal func makeVideoPreviewLayer() -> AVCaptureVideoPreviewLayer { - let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - videoPreviewLayer.videoGravity = .resizeAspectFill - return videoPreviewLayer - } - - internal func makeCaptureSession() -> AVCaptureSession { - let captureSession = AVCaptureSession() - guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { fatalError() } - guard let input = try? AVCaptureDeviceInput(device: device) else { fatalError() } - captureSession.addInput(input) - let captureMetadataOutput = AVCaptureMetadataOutput() - captureSession.addOutput(captureMetadataOutput) - captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - captureMetadataOutput.metadataObjectTypes = Self.supportedCodeTypes - return captureSession - } } extension UIViewController { var statusBarHeight: CGFloat { - guard - let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let height = scene.statusBarManager?.statusBarFrame.height + guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let height = scene.statusBarManager?.statusBarFrame.height else { return 0 } diff --git a/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRMaskView.swift b/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRMaskView.swift index 4e475ba7..7b8a4248 100644 --- a/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRMaskView.swift +++ b/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRMaskView.swift @@ -8,17 +8,26 @@ import UIKit class SPQRMaskView: UIView { - internal let maskLayer = CAShapeLayer() - internal let maskBorder = UIImageView(image: UIImage(named: "scan_border")) - internal let padding = 35.0 - var top = 72.0 - var statusBarHeight = 20.0 + // MARK: Lifecycle override init(frame: CGRect) { super.init(frame: frame) config() } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let maskLayer = CAShapeLayer() + let maskBorder = UIImageView(image: UIImage(named: "scan_border")) + let padding = 35.0 + var top = 72.0 + var statusBarHeight = 20.0 + func config() { backgroundColor = .clear @@ -39,11 +48,6 @@ class SPQRMaskView: UIView { buildMaskPath() } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: func buildMaskPath() { @@ -76,19 +80,46 @@ class SPQRMaskView: UIView { // 右下圆角 let rightBottomCornerPoint = CGPoint(x: scanX + scanW - cornerRadius, y: scanY + scanH) - let rightBottomCenter = CGPoint(x: scanX + scanW - cornerRadius, y: scanY + scanH - cornerRadius) + let rightBottomCenter = CGPoint( + x: scanX + scanW - cornerRadius, + y: scanY + scanH - cornerRadius + ) scanPath.move(to: leftTopCornerPoint) - scanPath.addArc(withCenter: leftTopCenter, radius: cornerRadius, startAngle: -.pi / 2, endAngle: -.pi, clockwise: false) + scanPath.addArc( + withCenter: leftTopCenter, + radius: cornerRadius, + startAngle: -.pi / 2, + endAngle: -.pi, + clockwise: false + ) scanPath.addLine(to: leftBottomCornerPoint) - scanPath.addArc(withCenter: leftBottomCenter, radius: cornerRadius, startAngle: -.pi, endAngle: -.pi * 1.5, clockwise: false) + scanPath.addArc( + withCenter: leftBottomCenter, + radius: cornerRadius, + startAngle: -.pi, + endAngle: -.pi * 1.5, + clockwise: false + ) scanPath.addLine(to: rightBottomCornerPoint) - scanPath.addArc(withCenter: rightBottomCenter, radius: cornerRadius, startAngle: -.pi * 1.5, endAngle: 0, clockwise: false) + scanPath.addArc( + withCenter: rightBottomCenter, + radius: cornerRadius, + startAngle: -.pi * 1.5, + endAngle: 0, + clockwise: false + ) scanPath.addLine(to: rightTopCornerPoint) - scanPath.addArc(withCenter: rightTopCenter, radius: cornerRadius, startAngle: 0, endAngle: -.pi / 2, clockwise: false) + scanPath.addArc( + withCenter: rightTopCenter, + radius: cornerRadius, + startAngle: 0, + endAngle: -.pi / 2, + clockwise: false + ) scanPath.close() coverPath.append(scanPath) @@ -97,6 +128,6 @@ class SPQRMaskView: UIView { } func topMargin() -> CGFloat { - return top + statusBarHeight + 44 + top + statusBarHeight + 44 } } diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/DraggingDetector.swift b/FRW/Tools/ThirdParty/Snappable/Internal/DraggingDetector.swift index 9b1644b4..7b369a1e 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/DraggingDetector.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/DraggingDetector.swift @@ -1,19 +1,21 @@ import UIKit -internal class DraggingDetector: NSObject, UIScrollViewDelegate { - private let snapMode: SnapMode - - internal var captureSnapID: (() -> SnapID?)? - internal var flickTarget: ((CGPoint) -> SnapID?)? - internal var scrollTo: ((SnapID?) -> Void)? +class DraggingDetector: NSObject, UIScrollViewDelegate { + // MARK: Lifecycle - internal init(snapMode: SnapMode) { + init(snapMode: SnapMode) { self.snapMode = snapMode } + // MARK: Internal + + var captureSnapID: (() -> SnapID?)? + var flickTarget: ((CGPoint) -> SnapID?)? + var scrollTo: ((SnapID?) -> Void)? + // MARK: UIScrollViewDelegate methods - internal func scrollViewWillEndDragging( + func scrollViewWillEndDragging( _: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset _: UnsafeMutablePointer @@ -39,14 +41,14 @@ internal class DraggingDetector: NSObject, UIScrollViewDelegate { } } - internal func scrollViewDidEndScrollingAnimation(_: UIScrollView) { + func scrollViewDidEndScrollingAnimation(_: UIScrollView) { guard case .immediately = snapMode.snapTiming else { return } let currentSnapID = captureSnapID?() scrollTo?(currentSnapID) } - internal func scrollViewDidEndDragging(_: UIScrollView, willDecelerate decelerate: Bool) { + func scrollViewDidEndDragging(_: UIScrollView, willDecelerate decelerate: Bool) { guard case .afterScrolling = snapMode.snapTiming else { return } if !decelerate { @@ -57,10 +59,14 @@ internal class DraggingDetector: NSObject, UIScrollViewDelegate { } } - internal func scrollViewDidEndDecelerating(_: UIScrollView) { + func scrollViewDidEndDecelerating(_: UIScrollView) { guard case .afterScrolling = snapMode.snapTiming else { return } let currentSnapID = captureSnapID?() scrollTo?(currentSnapID) } + + // MARK: Private + + private let snapMode: SnapMode } diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/Environments/CoordinateSpaceNameEnvironmentKey.swift b/FRW/Tools/ThirdParty/Snappable/Internal/Environments/CoordinateSpaceNameEnvironmentKey.swift index c00e3c51..2a7cd9c4 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/Environments/CoordinateSpaceNameEnvironmentKey.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/Environments/CoordinateSpaceNameEnvironmentKey.swift @@ -1,10 +1,12 @@ import SwiftUI -internal struct CoordinateSpaceNameEnvironmentKey: EnvironmentKey { - internal static var defaultValue = UUID() +// MARK: - CoordinateSpaceNameEnvironmentKey + +struct CoordinateSpaceNameEnvironmentKey: EnvironmentKey { + static var defaultValue = UUID() } -internal extension EnvironmentValues { +extension EnvironmentValues { var coordinateSpaceName: UUID { get { self[CoordinateSpaceNameEnvironmentKey.self] } set { self[CoordinateSpaceNameEnvironmentKey.self] = newValue } diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/Environments/SnapAlignmentEnvironmentKey.swift b/FRW/Tools/ThirdParty/Snappable/Internal/Environments/SnapAlignmentEnvironmentKey.swift index 513d4ea8..bed57114 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/Environments/SnapAlignmentEnvironmentKey.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/Environments/SnapAlignmentEnvironmentKey.swift @@ -1,10 +1,12 @@ import SwiftUI -internal struct SnapAlignmentEnvironmentKey: EnvironmentKey { - internal static var defaultValue: SnapAlignment = .center +// MARK: - SnapAlignmentEnvironmentKey + +struct SnapAlignmentEnvironmentKey: EnvironmentKey { + static var defaultValue: SnapAlignment = .center } -internal extension EnvironmentValues { +extension EnvironmentValues { var snapAlignment: SnapAlignment { get { self[SnapAlignmentEnvironmentKey.self] } set { self[SnapAlignmentEnvironmentKey.self] = newValue } diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/CGPoint+Distance.swift b/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/CGPoint+Distance.swift index 664bda0b..4f19373d 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/CGPoint+Distance.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/CGPoint+Distance.swift @@ -1,6 +1,6 @@ import UIKit -internal extension CGPoint { +extension CGPoint { func distance(_ point: CGPoint) -> CGFloat { let xDistance = x - point.x let yDistance = y - point.y diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/SnapAlignment+Convert.swift b/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/SnapAlignment+Convert.swift index 470b4c1d..3b651965 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/SnapAlignment+Convert.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/Extensions/SnapAlignment+Convert.swift @@ -1,6 +1,6 @@ import SwiftUI -internal extension SnapAlignment { +extension SnapAlignment { var unitPoint: UnitPoint { switch self { case .topLeading: diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/Preferences/SnapAnchorPreferenceKey.swift b/FRW/Tools/ThirdParty/Snappable/Internal/Preferences/SnapAnchorPreferenceKey.swift index a9e13307..2477c9b8 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/Preferences/SnapAnchorPreferenceKey.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/Preferences/SnapAnchorPreferenceKey.swift @@ -1,9 +1,9 @@ import SwiftUI -internal struct SnapAnchorPreferenceKey: PreferenceKey { - internal static var defaultValue: [SnapID: CGPoint] = [:] +struct SnapAnchorPreferenceKey: PreferenceKey { + static var defaultValue: [SnapID: CGPoint] = [:] - internal static func reduce( + static func reduce( value: inout [SnapID: CGPoint], nextValue: () -> [SnapID: CGPoint] ) { diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/Types/SnapTiming.swift b/FRW/Tools/ThirdParty/Snappable/Internal/Types/SnapTiming.swift index 337e5cba..b086f007 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/Types/SnapTiming.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/Types/SnapTiming.swift @@ -1,4 +1,4 @@ -internal enum SnapTiming { +enum SnapTiming { case afterScrolling case immediately(withFlick: Bool) } diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnapIDModifier.swift b/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnapIDModifier.swift index 646b5c63..378d156c 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnapIDModifier.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnapIDModifier.swift @@ -1,16 +1,15 @@ import SwiftUI -internal struct SnapIDModifier: ViewModifier where ID: Hashable { - @Environment(\.coordinateSpaceName) private var coordinateSpaceName: UUID - @Environment(\.snapAlignment) private var snapAlignment: SnapAlignment +struct SnapIDModifier: ViewModifier where ID: Hashable { + // MARK: Lifecycle - private let id: ID - - internal init(id: ID) { + init(id: ID) { self.id = id } - internal func body(content: Content) -> some View { + // MARK: Internal + + func body(content: Content) -> some View { content .id(id) .background( @@ -19,10 +18,23 @@ internal struct SnapIDModifier: ViewModifier where ID: Hashable { .preference( key: SnapAnchorPreferenceKey.self, value: [ - id: snapAlignment.point(in: proxy.frame(in: CoordinateSpace.named(coordinateSpaceName))), + id: snapAlignment + .point( + in: proxy + .frame(in: CoordinateSpace.named(coordinateSpaceName)) + ), ] ) } ) } + + // MARK: Private + + @Environment(\.coordinateSpaceName) + private var coordinateSpaceName: UUID + @Environment(\.snapAlignment) + private var snapAlignment: SnapAlignment + + private let id: ID } diff --git a/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnappableModifier.swift b/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnappableModifier.swift index 243872bd..0173ab2a 100644 --- a/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnappableModifier.swift +++ b/FRW/Tools/ThirdParty/Snappable/Internal/ViewModifiers/SnappableModifier.swift @@ -1,25 +1,19 @@ import SwiftUI -internal struct SnappableModifier: ViewModifier { - private let snapAlignment: SnapAlignment - private let snapMode: SnapMode - private let draggingDetector: DraggingDetector - private let coordinateSpaceName: UUID - private let onSelectChanged: ((SnapID?) -> Void)? - - @State private var parentAnchor: CGPoint = .zero - @State private var childSnapAnchors: [SnapID: CGPoint] = [:] - @State private var snapCandidateID: SnapID? +struct SnappableModifier: ViewModifier { + // MARK: Lifecycle - internal init(alignment: SnapAlignment, mode: SnapMode, onChange: ((SnapID?) -> Void)? = nil) { - snapAlignment = alignment - snapMode = mode - draggingDetector = DraggingDetector(snapMode: mode) - coordinateSpaceName = UUID() - onSelectChanged = onChange + init(alignment: SnapAlignment, mode: SnapMode, onChange: ((SnapID?) -> Void)? = nil) { + self.snapAlignment = alignment + self.snapMode = mode + self.draggingDetector = DraggingDetector(snapMode: mode) + self.coordinateSpaceName = UUID() + self.onSelectChanged = onChange } - internal func body(content: Content) -> some View { + // MARK: Internal + + func body(content: Content) -> some View { ScrollViewReader { scrollViewProxy in content .coordinateSpace(name: coordinateSpaceName) @@ -55,7 +49,8 @@ internal struct SnappableModifier: ViewModifier { draggingDetector.captureSnapID = { snapCandidateID } draggingDetector.flickTarget = { velocity in guard let current = snapCandidateID, - let currentAnchor = childSnapAnchors[current] else { return snapCandidateID } + let currentAnchor = childSnapAnchors[current] + else { return snapCandidateID } let willSnap = childSnapAnchors .filter { _, value in @@ -72,7 +67,8 @@ internal struct SnappableModifier: ViewModifier { return willSnap?.key ?? snapCandidateID } draggingDetector.scrollTo = { id in - DispatchQueue.main.async { // Avoid a crash when scrolling is stopped by touch + DispatchQueue.main.async { + // Avoid a crash when scrolling is stopped by touch withAnimation { scrollViewProxy.scrollTo(id, anchor: snapAlignment.unitPoint) } @@ -83,4 +79,19 @@ internal struct SnappableModifier: ViewModifier { } } } + + // MARK: Private + + private let snapAlignment: SnapAlignment + private let snapMode: SnapMode + private let draggingDetector: DraggingDetector + private let coordinateSpaceName: UUID + private let onSelectChanged: ((SnapID?) -> Void)? + + @State + private var parentAnchor: CGPoint = .zero + @State + private var childSnapAnchors: [SnapID: CGPoint] = [:] + @State + private var snapCandidateID: SnapID? } diff --git a/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift b/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift index 59359e7e..95347de0 100644 --- a/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift +++ b/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift @@ -6,13 +6,15 @@ import UIKit /// - `.immediately` /// - `.afterScrolling` public struct SnapMode { + // MARK: Public + /// The default setting for snapping after scrolling. public static let afterScrolling: SnapMode = Self.afterScrolling(decelerationRate: .fast) /// The default setting for snapping immediately. - public static let immediately: SnapMode = Self.immediately(decelerationRate: .normal, withFlick: true) - - internal let decelerationRate: DecelerationRate - internal let snapTiming: SnapTiming + public static let immediately: SnapMode = Self.immediately( + decelerationRate: .normal, + withFlick: true + ) /// An editable setting for snapping after scrolling. /// - Parameter decelerationRate: The deceleration rate of the ScrollView after dragging end. @@ -26,7 +28,15 @@ public struct SnapMode { /// - decelerationRate: The deceleration rate of the ScrollView after dragging end. /// - withFlick: The flag whether or not do snapping in consideration of flicking. /// - Returns: SnapMode, edited as immediately. - public static func immediately(decelerationRate: DecelerationRate, withFlick: Bool = true) -> SnapMode { + public static func immediately( + decelerationRate: DecelerationRate, + withFlick: Bool = true + ) -> SnapMode { SnapMode(decelerationRate: decelerationRate, snapTiming: .immediately(withFlick: withFlick)) } + + // MARK: Internal + + let decelerationRate: DecelerationRate + let snapTiming: SnapTiming } diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/API/Index.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/API/Index.swift index 23880566..1b7416ab 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/API/Index.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/API/Index.swift @@ -6,104 +6,117 @@ import SwiftUI +// MARK: - Index + public struct Index: Equatable { - internal let contentID: AnyHashable - internal let displayPriority: DisplayPriority + // MARK: Internal + + let contentID: AnyHashable + let displayPriority: DisplayPriority + + // MARK: Private private let icon: Image? private let title: Text? } -public extension Index { - enum DisplayPriority: Equatable, Hashable { +// MARK: Index.DisplayPriority + +extension Index { + public enum DisplayPriority: Equatable, Hashable { case standard case increased } } -public extension Index { - init(_ title: Title, - image name: String? = nil, - displayPriority: DisplayPriority = .standard, - contentID: ContentID) +extension Index { + public init( + _ title: Title, + image name: String? = nil, + displayPriority: DisplayPriority = .standard, + contentID: ContentID + ) where Title: StringProtocol, - ContentID: Hashable - { + ContentID: Hashable { self.contentID = AnyHashable(contentID) self.displayPriority = displayPriority self.title = Text(title) if let name = name { - icon = Image(name) + self.icon = Image(name) } else { - icon = nil + self.icon = nil } } - init(_ title: LocalizedStringKey, - image name: String? = nil, - displayPriority: DisplayPriority = .standard, - contentID: ContentID) - where ContentID: Hashable - { + public init( + _ title: LocalizedStringKey, + image name: String? = nil, + displayPriority: DisplayPriority = .standard, + contentID: ContentID + ) + where ContentID: Hashable { self.contentID = AnyHashable(contentID) self.displayPriority = displayPriority self.title = Text(title) if let name = name { - icon = Image(name) + self.icon = Image(name) } else { - icon = nil + self.icon = nil } } - init(_ title: Title, - systemImage name: String?, - displayPriority: DisplayPriority = .standard, - contentID: ContentID) + public init( + _ title: Title, + systemImage name: String?, + displayPriority: DisplayPriority = .standard, + contentID: ContentID + ) where Title: StringProtocol, - ContentID: Hashable - { + ContentID: Hashable { self.contentID = AnyHashable(contentID) self.displayPriority = displayPriority self.title = Text(title) if let name = name { - icon = Image(systemName: name) + self.icon = Image(systemName: name) } else { - icon = nil + self.icon = nil } } - init(_ title: LocalizedStringKey, - systemImage name: String?, - displayPriority: DisplayPriority = .standard, - contentID: ContentID) - where ContentID: Hashable - { + public init( + _ title: LocalizedStringKey, + systemImage name: String?, + displayPriority: DisplayPriority = .standard, + contentID: ContentID + ) + where ContentID: Hashable { self.contentID = AnyHashable(contentID) self.displayPriority = displayPriority self.title = Text(title) if let name = name { - icon = Image(systemName: name) + self.icon = Image(systemName: name) } else { - icon = nil + self.icon = nil } } } -internal extension Index { +extension Index { init(separatorWith contentID: AnyHashable) { self.contentID = contentID - displayPriority = .standard - icon = nil - title = nil + self.displayPriority = .standard + self.icon = nil + self.title = nil } - @ViewBuilder func label() -> some View { + @ViewBuilder + func label() -> some View { if let title = title { if let icon = icon { Label { title } icon: { icon } diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/EnvironmentValues+Extensions.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/EnvironmentValues+Extensions.swift index ec6ebd82..653e5b3c 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/EnvironmentValues+Extensions.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/EnvironmentValues+Extensions.swift @@ -6,7 +6,7 @@ import SwiftUI -internal extension EnvironmentValues { +extension EnvironmentValues { var indexBarBackground: IndexBarBackground { get { self[IndexBarBackgroundKey.self] } set { self[IndexBarBackgroundKey.self] = newValue } @@ -18,10 +18,14 @@ internal extension EnvironmentValues { } } +// MARK: - IndexBarBackgroundKey + private struct IndexBarBackgroundKey: EnvironmentKey { static let defaultValue = IndexBarBackground(contentMode: .fit, view: { AnyView(EmptyView()) }) } +// MARK: - IndexBarInsetsKey + private struct IndexBarInsetsKey: EnvironmentKey { static let defaultValue: EdgeInsets? = nil } diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBar.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBar.swift index 207b8a17..0c72e6df 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBar.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBar.swift @@ -6,11 +6,12 @@ import SwiftUI -internal struct IndexBar: View +// MARK: - IndexBar + +struct IndexBar: View where Indices: Equatable, Indices: RandomAccessCollection, - Indices.Element == Index -{ + Indices.Element == Index { var accessory: ScrollAccessory var indices: Indices var scrollView: ScrollViewProxy @@ -18,35 +19,44 @@ internal struct IndexBar: View var body: some View { GeometryReader { geometry in if accessory.showsIndexBar(indices: indices) { - IndexReducer(frameHeight: geometry.size.height, - indices: indices, - scrollView: scrollView) - .transition(.identity) + IndexReducer( + frameHeight: geometry.size.height, + indices: indices, + scrollView: scrollView + ) + .transition(.identity) } } } } -internal var indexBarInsets: EdgeInsets { +var indexBarInsets: EdgeInsets { EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: labelSize.width) } +// MARK: - IndexReducer + private struct IndexReducer: View where Indices: Equatable, Indices: RandomAccessCollection, - Indices.Element == Index -{ + Indices.Element == Index { + // MARK: Internal + var frameHeight: CGFloat var indices: Indices var scrollView: ScrollViewProxy var body: some View { - IndexLayout(frameHeight: frameHeight, - indices: indices, - reducedIndices: reducedIndices, - scrollView: scrollView) + IndexLayout( + frameHeight: frameHeight, + indices: indices, + reducedIndices: reducedIndices, + scrollView: scrollView + ) } + // MARK: Private + private var reducedIndices: [Index] { var innerIndices = Array(indices) let indexTarget = max(Int(floor((frameHeight - (stackPadding * 2)) / labelSize.height)), 2) @@ -86,7 +96,8 @@ private struct IndexReducer: View } // Evenly remove indices to reach target - let skipLimit = Double(innerIndexTarget) / Double(innerIndices.count + 1 - innerIndexTarget) + let skipLimit = Double(innerIndexTarget) / + Double(innerIndices.count + 1 - innerIndexTarget) var skipCount: Double = 0 innerIndices = innerIndices @@ -111,12 +122,14 @@ private struct IndexReducer: View } } +// MARK: - IndexLayout + private struct IndexLayout: View where Indices: Equatable, Indices: RandomAccessCollection, - Indices.Element == Index -{ - @GestureState private var currentIndex: Index? = nil + Indices.Element == Index { + @GestureState + private var currentIndex: Index? = nil var frameHeight: CGFloat var indices: Indices @@ -148,7 +161,11 @@ private struct IndexLayout: View CGFloat(reducedIndices.count) * labelSize.height } - private func dragUpdating(value: DragGesture.Value, currentIndex: inout Index?, transaction _: inout Transaction) { + private func dragUpdating( + value: DragGesture.Value, + currentIndex: inout Index?, + transaction _: inout Transaction + ) { guard !indices.isEmpty else { return } let dragLocation = value.location.y + ((stackHeight - frameHeight) / 2) @@ -169,6 +186,8 @@ private struct IndexLayout: View } } +// MARK: - IndexStack + private struct IndexStack: View { var frameHeight: CGFloat var reducedIndices: [Index] @@ -190,8 +209,11 @@ private struct IndexStack: View { } } +// MARK: - IndexBarBackgroundView + private struct IndexBarBackgroundView: View { - @Environment(\.indexBarBackground) private var background + @Environment(\.indexBarBackground) + private var background var stackHeight: CGFloat diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBarBackground.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBarBackground.swift index a0a9a6f0..e049d96d 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBarBackground.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/IndexBarBackground.swift @@ -6,7 +6,7 @@ import SwiftUI -internal struct IndexBarBackground { +struct IndexBarBackground { let contentMode: ContentMode let view: () -> AnyView } diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/ScrollAccessory+Extensions.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/ScrollAccessory+Extensions.swift index 97c7fa47..f9ead946 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/ScrollAccessory+Extensions.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/ScrollAccessory+Extensions.swift @@ -6,10 +6,9 @@ import Foundation -internal extension ScrollAccessory { +extension ScrollAccessory { func showsIndexBar(indices: Indices) -> Bool - where Indices: RandomAccessCollection - { + where Indices: RandomAccessCollection { switch self { case .automatic: return !indices.isEmpty case .indexBar: return true @@ -19,8 +18,7 @@ internal extension ScrollAccessory { } func showsScrollIndicator(indices: Indices) -> Bool - where Indices: RandomAccessCollection - { + where Indices: RandomAccessCollection { switch self { case .automatic: return indices.isEmpty case .indexBar: return false diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UITableViewCustomizer.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UITableViewCustomizer.swift index 13c017f5..3ff3ae9c 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UITableViewCustomizer.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UITableViewCustomizer.swift @@ -6,7 +6,7 @@ import SwiftUI -internal struct UITableViewCustomizer: UIViewRepresentable { +struct UITableViewCustomizer: UIViewRepresentable { var showsVerticalScrollIndicator: Bool func makeUIView(context _: Context) -> UIView { diff --git a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UIView+Extensions.swift b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UIView+Extensions.swift index ebff0e16..06a14f99 100644 --- a/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UIView+Extensions.swift +++ b/FRW/Tools/ThirdParty/SwiftUIIndexedList/Internal/UIView+Extensions.swift @@ -6,7 +6,7 @@ import UIKit -internal extension UIView { +extension UIView { func firstUITableView() -> UITableView? { for subview in subviews { if let tableView = subview as? UITableView { diff --git a/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/ImageAnimated.swift b/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/ImageAnimated.swift index 3124c765..71af3af6 100644 --- a/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/ImageAnimated.swift +++ b/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/ImageAnimated.swift @@ -11,7 +11,7 @@ import SwiftUI extension ImageAnimated { static func appRefreshImageNames() -> [String] { var images: [String] = [] - for i in 0 ... 95 { + for i in 0...95 { images.append("refresh-header-seq-\(i)") } @@ -19,17 +19,30 @@ extension ImageAnimated { } } +// MARK: - ImageAnimated + struct ImageAnimated: UIViewRepresentable { + // MARK: Internal + let imageSize: CGSize let imageNames: [String] let duration: Double var isAnimating: Bool = false func makeUIView(context _: Self.Context) -> UIView { - let containerView = UIView(frame: CGRect(x: 0, y: 0, - width: imageSize.width, height: imageSize.height)) + let containerView = UIView(frame: CGRect( + x: 0, + y: 0, + width: imageSize.width, + height: imageSize.height + )) - let animationImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)) + let animationImageView = UIImageView(frame: CGRect( + x: 0, + y: 0, + width: imageSize.width, + height: imageSize.height + )) animationImageView.clipsToBounds = true animationImageView.contentMode = UIView.ContentMode.scaleAspectFill @@ -60,9 +73,11 @@ struct ImageAnimated: UIViewRepresentable { } } + // MARK: Private + private func generateImages() -> [UIImage] { var images = [UIImage]() - imageNames.forEach { imageName in + for imageName in imageNames { if let img = UIImage(named: imageName) { images.append(img) } diff --git a/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/SwiftUIPullToRefresh.swift b/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/SwiftUIPullToRefresh.swift index 05b44591..89a84554 100644 --- a/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/SwiftUIPullToRefresh.swift +++ b/FRW/Tools/ThirdParty/SwiftUIPullToRefresh/SwiftUIPullToRefresh.swift @@ -1,11 +1,15 @@ import SwiftUI +// MARK: - PositionType + // There are two type of positioning views - one that scrolls with the content, // and one that stays fixed private enum PositionType { case fixed, moving } +// MARK: - Position + // This struct is the currency of the Preferences, and has a type // (fixed or moving) and the actual Y-axis value. // It's Equatable because Swift requires it to be. @@ -14,6 +18,8 @@ private struct Position: Equatable { let y: CGFloat } +// MARK: - PositionPreferenceKey + // This might seem weird, but it's necessary due to the funny nature of // how Preferences work. We can't just store the last position and merge // it with the next one - instead we have a queue of all the latest positions. @@ -27,6 +33,8 @@ private struct PositionPreferenceKey: PreferenceKey { } } +// MARK: - PositionIndicator + private struct PositionIndicator: View { let type: PositionType @@ -35,7 +43,10 @@ private struct PositionIndicator: View { // the View itself is an invisible Shape that fills as much as possible Color.clear // Compute the top Y position and emit it to the Preferences queue - .preference(key: PositionPreferenceKey.self, value: [Position(type: type, y: proxy.frame(in: .global).minY)]) + .preference( + key: PositionPreferenceKey.self, + value: [Position(type: type, y: proxy.frame(in: .global).minY)] + ) } } } @@ -52,6 +63,8 @@ public typealias OnRefresh = (@escaping RefreshComplete) -> Void // with it to your liking. public let defaultRefreshThreshold: CGFloat = 60 +// MARK: - RefreshState + // Tracks the state of the RefreshableScrollView - it's either: // 1. waiting for a scroll to happen // 2. has been primed by pulling down beyond THRESHOLD @@ -67,27 +80,20 @@ public typealias RefreshProgressBuilder = (RefreshState) -> Prog // Default color of the rectangle behind the progress spinner public let defaultLoadingViewBackgroundColor = Color.LL.Neutrals.background -public struct RefreshableScrollView: View where Progress: View, Content: View { - let showsIndicators: Bool // if the ScrollView should show indicators - let loadingViewBackgroundColor: Color - let threshold: CGFloat // what height do you have to pull down to trigger the refresh - let onRefresh: OnRefresh // the refreshing action - let progress: RefreshProgressBuilder // custom progress view - let content: () -> Content // the ScrollView content - @State private var offset: CGFloat = 0 - @State private var state = RefreshState.waiting // the current state +// MARK: - RefreshableScrollView - // Haptic Feedback - let pullReleasedFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) +public struct RefreshableScrollView: View where Progress: View, Content: View { + // MARK: Lifecycle // We use a custom constructor to allow for usage of a @ViewBuilder for the content - public init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: @escaping RefreshProgressBuilder, - @ViewBuilder content: @escaping () -> Content) - { + public init( + showsIndicators: Bool = true, + loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, + threshold: CGFloat = defaultRefreshThreshold, + onRefresh: @escaping OnRefresh, + @ViewBuilder progress: @escaping RefreshProgressBuilder, + @ViewBuilder content: @escaping () -> Content + ) { self.showsIndicators = showsIndicators self.loadingViewBackgroundColor = loadingViewBackgroundColor self.threshold = threshold @@ -96,6 +102,8 @@ public struct RefreshableScrollView: View where Progress: Vie self.content = content } + // MARK: Public + public var body: some View { // The root view is a regular ScrollView ScrollView(showsIndicators: showsIndicators) { @@ -158,35 +166,56 @@ public struct RefreshableScrollView: View where Progress: Vie } } } + + // MARK: Internal + + let showsIndicators: Bool // if the ScrollView should show indicators + let loadingViewBackgroundColor: Color + let threshold: CGFloat // what height do you have to pull down to trigger the refresh + let onRefresh: OnRefresh // the refreshing action + let progress: RefreshProgressBuilder // custom progress view + let content: () -> Content // the ScrollView content + // Haptic Feedback + let pullReleasedFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + + // MARK: Private + + @State + private var offset: CGFloat = 0 + @State + private var state = RefreshState.waiting // the current state } // Extension that uses default RefreshActivityIndicator so that you don't have to // specify it every time. -public extension RefreshableScrollView where Progress == RefreshActivityIndicator { - init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder content: @escaping () -> Content) - { - self.init(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: { state in - RefreshActivityIndicator(isAnimating: state == .loading) { - $0.hidesWhenStopped = false - } - }, - content: content) +extension RefreshableScrollView where Progress == RefreshActivityIndicator { + public init( + showsIndicators: Bool = true, + loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, + threshold: CGFloat = defaultRefreshThreshold, + onRefresh: @escaping OnRefresh, + @ViewBuilder content: @escaping () -> Content + ) { + self.init( + showsIndicators: showsIndicators, + loadingViewBackgroundColor: loadingViewBackgroundColor, + threshold: threshold, + onRefresh: onRefresh, + progress: { state in + RefreshActivityIndicator(isAnimating: state == .loading) { + $0.hidesWhenStopped = false + } + }, + content: content + ) } } +// MARK: - RefreshActivityIndicator + // Wraps a UIActivityIndicatorView as a loading spinner that works on all SwiftUI versions. public struct RefreshActivityIndicator: UIViewRepresentable { - public typealias UIView = UIActivityIndicatorView - public var isAnimating: Bool = true - public var configuration = { (_: UIView) in } + // MARK: Lifecycle public init(isAnimating: Bool, configuration: ((UIView) -> Void)? = nil) { self.isAnimating = isAnimating @@ -195,6 +224,13 @@ public struct RefreshActivityIndicator: UIViewRepresentable { } } + // MARK: Public + + public typealias UIView = UIActivityIndicatorView + + public var isAnimating: Bool = true + public var configuration = { (_: UIView) in } + public func makeUIView(context _: UIViewRepresentableContext) -> UIView { UIView() } @@ -206,44 +242,46 @@ public struct RefreshActivityIndicator: UIViewRepresentable { } #if compiler(>=5.5) - // Allows using RefreshableScrollView with an async block. - @available(iOS 15.0, *) - public extension RefreshableScrollView { - init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - action: @escaping @Sendable () async -> Void, - @ViewBuilder progress: @escaping RefreshProgressBuilder, - @ViewBuilder content: @escaping () -> Content) - { - self.init(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: { refreshComplete in - Task { - await action() - refreshComplete() - } - }, - progress: progress, - content: content) - } +// Allows using RefreshableScrollView with an async block. +@available(iOS 15.0, *) +extension RefreshableScrollView { + public init( + showsIndicators: Bool = true, + loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, + threshold: CGFloat = defaultRefreshThreshold, + action: @escaping @Sendable () async -> Void, + @ViewBuilder progress: @escaping RefreshProgressBuilder, + @ViewBuilder content: @escaping () -> Content + ) { + self.init( + showsIndicators: showsIndicators, + loadingViewBackgroundColor: loadingViewBackgroundColor, + threshold: threshold, + onRefresh: { refreshComplete in + Task { + await action() + refreshComplete() + } + }, + progress: progress, + content: content + ) } +} #endif -public struct RefreshableCompat: ViewModifier where Progress: View { - private let showsIndicators: Bool - private let loadingViewBackgroundColor: Color - private let threshold: CGFloat - private let onRefresh: OnRefresh - private let progress: RefreshProgressBuilder +// MARK: - RefreshableCompat - public init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: @escaping RefreshProgressBuilder) - { +public struct RefreshableCompat: ViewModifier where Progress: View { + // MARK: Lifecycle + + public init( + showsIndicators: Bool = true, + loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, + threshold: CGFloat = defaultRefreshThreshold, + onRefresh: @escaping OnRefresh, + @ViewBuilder progress: @escaping RefreshProgressBuilder + ) { self.showsIndicators = showsIndicators self.loadingViewBackgroundColor = loadingViewBackgroundColor self.threshold = threshold @@ -251,62 +289,84 @@ public struct RefreshableCompat: ViewModifier where Progress: View { self.progress = progress } + // MARK: Public + public func body(content: Content) -> some View { - RefreshableScrollView(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: progress) { + RefreshableScrollView( + showsIndicators: showsIndicators, + loadingViewBackgroundColor: loadingViewBackgroundColor, + threshold: threshold, + onRefresh: onRefresh, + progress: progress + ) { content } } + + // MARK: Private + + private let showsIndicators: Bool + private let loadingViewBackgroundColor: Color + private let threshold: CGFloat + private let onRefresh: OnRefresh + private let progress: RefreshProgressBuilder } #if compiler(>=5.5) - @available(iOS 15.0, *) - public extension List { - @ViewBuilder func refreshableCompat(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: @escaping RefreshProgressBuilder) -> some View - { - if #available(iOS 15.0, macOS 12.0, *) { - self.refreshable { - await withCheckedContinuation { cont in - onRefresh { - cont.resume() - } +@available(iOS 15.0, *) +extension List { + @ViewBuilder + public func refreshableCompat( + showsIndicators: Bool = true, + loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, + threshold: CGFloat = defaultRefreshThreshold, + onRefresh: @escaping OnRefresh, + @ViewBuilder progress: @escaping RefreshProgressBuilder + ) -> some View { + if #available(iOS 15.0, macOS 12.0, *) { + self.refreshable { + await withCheckedContinuation { cont in + onRefresh { + cont.resume() } } - } else { - modifier(RefreshableCompat(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: progress)) } + } else { + modifier(RefreshableCompat( + showsIndicators: showsIndicators, + loadingViewBackgroundColor: loadingViewBackgroundColor, + threshold: threshold, + onRefresh: onRefresh, + progress: progress + )) } } +} #endif -public extension View { - @ViewBuilder func refreshableCompat(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: @escaping RefreshProgressBuilder) -> some View - { - modifier(RefreshableCompat(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: progress)) +extension View { + @ViewBuilder + public func refreshableCompat( + showsIndicators: Bool = true, + loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, + threshold: CGFloat = defaultRefreshThreshold, + onRefresh: @escaping OnRefresh, + @ViewBuilder progress: @escaping RefreshProgressBuilder + ) -> some View { + modifier(RefreshableCompat( + showsIndicators: showsIndicators, + loadingViewBackgroundColor: loadingViewBackgroundColor, + threshold: threshold, + onRefresh: onRefresh, + progress: progress + )) } } +// MARK: - TestView + struct TestView: View { - @State private var now = Date() + // MARK: Internal var body: some View { RefreshableScrollView( @@ -315,19 +375,27 @@ struct TestView: View { self.now = Date() done() } - }) { - VStack { - ForEach(1 ..< 20) { - Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") - .padding(.bottom, 10) - } - }.padding() } + ) { + VStack { + ForEach(1..<20) { + Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") + .padding(.bottom, 10) + } + }.padding() + } } + + // MARK: Private + + @State + private var now = Date() } +// MARK: - TestViewWithLargerThreshold + struct TestViewWithLargerThreshold: View { - @State private var now = Date() + // MARK: Internal var body: some View { RefreshableScrollView( @@ -340,105 +408,140 @@ struct TestViewWithLargerThreshold: View { } ) { VStack { - ForEach(1 ..< 20) { + ForEach(1..<20) { Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") .padding(.bottom, 10) } }.padding() } } + + // MARK: Private + + @State + private var now = Date() } +// MARK: - TestViewWithCustomProgress + struct TestViewWithCustomProgress: View { - @State private var now = Date() + // MARK: Internal var body: some View { - RefreshableScrollView(onRefresh: { done in - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self.now = Date() - done() - } - }, - progress: { state in - if state == .waiting { - Text("Pull me down...") - } else if state == .primed { - Text("Now release!") - } else { - Text("Working...") - } - }) { + RefreshableScrollView( + onRefresh: { done in + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + self.now = Date() + done() + } + }, + progress: { state in + if state == .waiting { + Text("Pull me down...") + } else if state == .primed { + Text("Now release!") + } else { + Text("Working...") + } + } + ) { VStack { - ForEach(1 ..< 20) { + ForEach(1..<20) { Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") .padding(.bottom, 10) } }.padding() } } + + // MARK: Private + + @State + private var now = Date() } #if compiler(>=5.5) - @available(iOS 15, *) - struct TestViewWithAsync: View { - @State private var now = Date() - - var body: some View { - RefreshableScrollView(action: { - try? await Task.sleep(nanoseconds: 3_000_000_000) - now = Date() - }, progress: { state in - RefreshActivityIndicator(isAnimating: state == .loading) { - $0.hidesWhenStopped = false - } - }) { - VStack { - ForEach(1 ..< 20) { - Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") - .padding(.bottom, 10) - } - }.padding() +@available(iOS 15, *) +struct TestViewWithAsync: View { + // MARK: Internal + + var body: some View { + RefreshableScrollView(action: { + try? await Task.sleep(nanoseconds: 3_000_000_000) + now = Date() + }, progress: { state in + RefreshActivityIndicator(isAnimating: state == .loading) { + $0.hidesWhenStopped = false } + }) { + VStack { + ForEach(1..<20) { + Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") + .padding(.bottom, 10) + } + }.padding() } } + + // MARK: Private + + @State + private var now = Date() +} #endif +// MARK: - TestViewCompat + struct TestViewCompat: View { - @State private var now = Date() + // MARK: Internal var body: some View { VStack { - ForEach(1 ..< 20) { + ForEach(1..<20) { Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)") .padding(.bottom, 10) } } - .refreshableCompat(showsIndicators: false, - onRefresh: { done in - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self.now = Date() - done() - } - }, progress: { state in - RefreshActivityIndicator(isAnimating: state == .loading) { - $0.hidesWhenStopped = false - } - }) + .refreshableCompat( + showsIndicators: false, + onRefresh: { done in + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + self.now = Date() + done() + } + }, + progress: { state in + RefreshActivityIndicator(isAnimating: state == .loading) { + $0.hidesWhenStopped = false + } + } + ) } + + // MARK: Private + + @State + private var now = Date() } +// MARK: - TestView_Previews + struct TestView_Previews: PreviewProvider { static var previews: some View { TestView() } } +// MARK: - TestViewWithLargerThreshold_Previews + struct TestViewWithLargerThreshold_Previews: PreviewProvider { static var previews: some View { TestViewWithLargerThreshold() } } +// MARK: - TestViewWithCustomProgress_Previews + struct TestViewWithCustomProgress_Previews: PreviewProvider { static var previews: some View { TestViewWithCustomProgress() @@ -446,14 +549,16 @@ struct TestViewWithCustomProgress_Previews: PreviewProvider { } #if compiler(>=5.5) - @available(iOS 15, *) - struct TestViewWithAsync_Previews: PreviewProvider { - static var previews: some View { - TestViewWithAsync() - } +@available(iOS 15, *) +struct TestViewWithAsync_Previews: PreviewProvider { + static var previews: some View { + TestViewWithAsync() } +} #endif +// MARK: - TestViewCompat_Previews + struct TestViewCompat_Previews: PreviewProvider { static var previews: some View { TestViewCompat() diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VLink/VWebLinkType.swift b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VLink/VWebLinkType.swift index 6baaea6c..baa01bab 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VLink/VWebLinkType.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VLink/VWebLinkType.swift @@ -21,15 +21,15 @@ public typealias VWebLinkPreset = DerivedButtonPreset // MARK: - Button extension VWebLinkType { - @ViewBuilder static func webLinkButton( + @ViewBuilder + static func webLinkButton( buttonType: VWebLinkType, isEnabled: Bool, action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content ) -> some View - where Content: View - { - Self.navLinkButton( + where Content: View { + navLinkButton( buttonType: buttonType, isEnabled: isEnabled, action: action, diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VNavigationLink/VNavigationLink.swift b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VNavigationLink/VNavigationLink.swift index 46f31bf6..16e573dc 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VNavigationLink/VNavigationLink.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VNavigationLink/VNavigationLink.swift @@ -7,7 +7,7 @@ import SwiftUI -// MARK: - V Navigation Link +// MARK: - VNavigationLink /// Button component that controls a navigation presentation. /// @@ -85,36 +85,8 @@ import SwiftUI public struct VNavigationLink: View where Destination: View, - Content: View -{ - // MARK: Properties - - private let navLinkButtonType: VNavigationLinkType - - private let state: VNavigationLinkState - - @State private var isActiveInternally: Bool = false - @Binding private var isActiveExternally: Bool - private let stateManagament: ComponentStateManagement - private var isActive: Binding { - .init( - get: { - switch stateManagament { - case .internal: return isActiveInternally - case .external: return isActiveExternally - } - }, - set: { value in - switch stateManagament { - case .internal: isActiveInternally = value - case .external: isActiveExternally = value - } - } - ) - } - - private let destination: Destination - private let content: () -> Content + Content: View { + // MARK: Lifecycle // MARK: Initializers - Preset and Tap @@ -125,10 +97,10 @@ public struct VNavigationLink: View destination: Destination, @ViewBuilder content: @escaping () -> Content ) { - navLinkButtonType = navLinkPreset.buttonType + self.navLinkButtonType = navLinkPreset.buttonType self.state = state _isActiveExternally = .constant(false) - stateManagament = .internal + self.stateManagament = .internal self.destination = destination self.content = content } @@ -140,8 +112,7 @@ public struct VNavigationLink: View destination: Destination, title: String ) - where Content == VText - { + where Content == VText { self.init( preset: navLinkPreset, state: state, @@ -160,10 +131,10 @@ public struct VNavigationLink: View destination: Destination, @ViewBuilder content: @escaping () -> Content ) { - navLinkButtonType = navLinkPreset.buttonType + self.navLinkButtonType = navLinkPreset.buttonType self.state = state _isActiveExternally = isActive - stateManagament = .external + self.stateManagament = .external self.destination = destination self.content = content } @@ -176,8 +147,7 @@ public struct VNavigationLink: View destination: Destination, title: String ) - where Content == VText - { + where Content == VText { self.init( preset: navLinkPreset, state: state, @@ -195,10 +165,10 @@ public struct VNavigationLink: View destination: Destination, @ViewBuilder content: @escaping () -> Content ) { - navLinkButtonType = .custom + self.navLinkButtonType = .custom self.state = state _isActiveExternally = .constant(false) - stateManagament = .internal + self.stateManagament = .internal self.destination = destination self.content = content } @@ -212,23 +182,66 @@ public struct VNavigationLink: View destination: Destination, @ViewBuilder content: @escaping () -> Content ) { - navLinkButtonType = .custom + self.navLinkButtonType = .custom self.state = state _isActiveExternally = isActive - stateManagament = .external + self.stateManagament = .external self.destination = destination self.content = content } + // MARK: Public + // MARK: Body public var body: some View { contentView(isActive: isActive) - .background({ - NavigationLink(destination: destinationView, isActive: isActive, label: { EmptyView() }) - .allowsHitTesting(false) - .opacity(0) - }()) + .background( + NavigationLink( + destination: destinationView, + isActive: isActive, + label: { EmptyView() } + ) + .allowsHitTesting(false) + .opacity(0) + ) + } + + // MARK: Private + + // MARK: Properties + + private let navLinkButtonType: VNavigationLinkType + + private let state: VNavigationLinkState + + @State + private var isActiveInternally: Bool = false + @Binding + private var isActiveExternally: Bool + private let stateManagament: ComponentStateManagement + private let destination: Destination + private let content: () -> Content + + private var isActive: Binding { + .init( + get: { + switch stateManagament { + case .internal: return isActiveInternally + case .external: return isActiveExternally + } + }, + set: { value in + switch stateManagament { + case .internal: isActiveInternally = value + case .external: isActiveExternally = value + } + } + ) + } + + private var destinationView: some View { + destination.environment(\.vNavigationViewBackButtonHidden, false) } private func contentView(isActive: Binding) -> some View { @@ -239,24 +252,12 @@ public struct VNavigationLink: View content: content ) } - - private var destinationView: some View { - destination.environment(\.vNavigationViewBackButtonHidden, false) - } } -// MARK: - Preview +// MARK: - VNavigationLink_Previews struct VNavigationLink_Previews: PreviewProvider { - private static var destination: some View { - VBaseView(title: "Destination", content: { - ZStack(content: { - ColorBook.canvas.edgesIgnoringSafeArea(.all) - - VSheet() - }) - }) - } + // MARK: Internal static var previews: some View { VNavigationView(content: { @@ -275,4 +276,16 @@ struct VNavigationLink_Previews: PreviewProvider { }) }) } + + // MARK: Private + + private static var destination: some View { + VBaseView(title: "Destination", content: { + ZStack(content: { + ColorBook.canvas.edgesIgnoringSafeArea(.all) + + VSheet() + }) + }) + } } diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift index 5e667d0b..65a687a0 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift @@ -1,5 +1,5 @@ // -// VPrimaryButtonModelBordered.swift +// VPrimaryButtonModel.swift // VComponents // // Created by Vakhtang Kontridze on 12/24/20. @@ -11,24 +11,31 @@ import SwiftUI /// Model that describes UI. public struct VPrimaryButtonModel { - // MARK: Properties - - /// Sub-model containing layout properties. - public var layout: Layout = .init() - - /// Sub-model containing color properties. - public var colors: Colors = .init() - - /// Sub-model containing font properties. - public var fonts: Fonts = .init() + // MARK: Lifecycle /// Initializes model with default values. public init() {} + // MARK: Public + // MARK: Layout /// Sub-model containing layout properties. public struct Layout { + // MARK: Lifecycle + + // MARK: Initializers + + /// Initializes sub-model with default values. + public init() {} + + // MARK: Public + + // MARK: Content Margin + + /// Sub-model containing `horizontal` and `vertical` margins. + public typealias ContentMargin = LayoutGroup_HV + // MARK: Properties /// Button height. Defaults to `56`. @@ -40,8 +47,6 @@ public struct VPrimaryButtonModel { /// Button border width. Defaults to `0`. public var borderWidth: CGFloat = 0 - var hasBorder: Bool { borderWidth > 0 } - /// Content margin. Defaults to `15` horizontally and `3` vertically. public var contentMargin: ContentMargin = .init( horizontal: 15, @@ -53,23 +58,36 @@ public struct VPrimaryButtonModel { /// Only visible when state is set to `loading`. public var loaderSpacing: CGFloat = 20 + // MARK: Internal + let loaderWidth: CGFloat = 10 + var hasBorder: Bool { borderWidth > 0 } + } + + // MARK: Colors + + /// Sub-model containing color properties. + public struct Colors { + // MARK: Lifecycle + // MARK: Initializers /// Initializes sub-model with default values. public init() {} - // MARK: Content Margin + // MARK: Public - /// Sub-model containing `horizontal` and `vertical` margins. - public typealias ContentMargin = LayoutGroup_HV - } + // MARK: State Colors - // MARK: Colors + /// Sub-model containing colors for component states. + public typealias StateColors = StateColors_EPLD + + // MARK: State Opacities + + /// Sub-model containing opacities for component states. + public typealias StateOpacities = StateOpacities_PD - /// Sub-model containing color properties. - public struct Colors { // MARK: Properties /// Content opacities. @@ -106,39 +124,41 @@ public struct VPrimaryButtonModel { /// Loader colors. public var loader: Color = ColorBook.primaryInverted + } + + // MARK: Fonts + + /// Sub-model containing font properties. + public struct Fonts { + // MARK: Lifecycle // MARK: Initializers /// Initializes sub-model with default values. public init() {} - // MARK: State Colors - - /// Sub-model containing colors for component states. - public typealias StateColors = StateColors_EPLD - - // MARK: State Opacities - - /// Sub-model containing opacities for component states. - public typealias StateOpacities = StateOpacities_PD - } - - // MARK: Fonts + // MARK: Public - /// Sub-model containing font properties. - public struct Fonts { // MARK: Properties /// Title font. Defaults to system font of size `16` with `semibold` weight. /// /// Only applicable when using init with title. public var title: Font = .system(size: 16, weight: .semibold) + } - // MARK: Initializers + // MARK: Properties - /// Initializes sub-model with default values. - public init() {} - } + /// Sub-model containing layout properties. + public var layout: Layout = .init() + + /// Sub-model containing color properties. + public var colors: Colors = .init() + + /// Sub-model containing font properties. + public var fonts: Fonts = .init() + + // MARK: Internal // MARK: Sub-Models diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Indicators/VPageIndicator/VPageIndicatorInfinite.swift b/FRW/Tools/ThirdParty/VComponents/Components/Indicators/VPageIndicator/VPageIndicatorInfinite.swift index e155108b..aec4bc07 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Indicators/VPageIndicator/VPageIndicatorInfinite.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Indicators/VPageIndicator/VPageIndicatorInfinite.swift @@ -1,5 +1,5 @@ // -// VPageIndicaatorInfinite.swift +// VPageIndicatorInfinite.swift // VComponents // // Created by Vakhtang Kontridze on 2/6/21. @@ -7,25 +7,10 @@ import SwiftUI -// MARK: - V Page Indicator Infinite +// MARK: - VPageIndicatorInfinite struct VPageIndicatorInfinite: View { - // MARK: Properties - - private let model: VPageIndicatorModel - private let visible: Int - private let center: Int - private var side: Int { (visible - center) / 2 } - private var middle: Int { visible / 2 } - - private let total: Int - private let selectedIndex: Int - - private var region: Region { - .init(selectedIndex: selectedIndex, total: total, middle: middle) - } - - private let isLayoutValid: Bool + // MARK: Lifecycle // MARK: Intializers @@ -42,19 +27,67 @@ struct VPageIndicatorInfinite: View { self.total = total self.selectedIndex = selectedIndex - isLayoutValid = visible > center && visible.isOdd && center.isOdd + self.isLayoutValid = visible > center && visible.isOdd && center.isOdd } + // MARK: Public + // MARK: Body - @ViewBuilder public var body: some View { + @ViewBuilder + public var body: some View { switch (isLayoutValid, total) { case (false, _): EmptyView() - case (true, ...visible): VPageIndicatorFinite(model: model, total: total, selectedIndex: selectedIndex) + case (true, ...visible): VPageIndicatorFinite( + model: model, + total: total, + selectedIndex: selectedIndex + ) case (true, _): validBody } } + // MARK: Private + + // MARK: Region + + private enum Region { + // MARK: Cases + + case leftEdge + case center + case rightEdge + + // MARK: Lifecycle + + // MARK: Initializers + + init(selectedIndex: Int, total: Int, middle: Int) { + switch selectedIndex { + case 0.. CGFloat { switch region { case .leftEdge: - guard - let leftEdgeVisibleIndex: Int = leftEdgeVisibleIndex(at: index), - let leftEdgeRightSideIndex: Int = leftEdgeRightSideIndex(at: leftEdgeVisibleIndex) + guard let leftEdgeVisibleIndex: Int = leftEdgeVisibleIndex(at: index), + let leftEdgeRightSideIndex: Int = leftEdgeRightSideIndex(at: leftEdgeVisibleIndex) else { return 1 } @@ -127,9 +163,8 @@ struct VPageIndicatorInfinite: View { return leftEdgeRightSideScale(at: leftEdgeRightSideIndex) case .center: - guard - let visibleIndex: Int = centerVisibleIndex(at: index), - let centerIndexAbsolute: Int = centerIndexAbsolute(at: visibleIndex) + guard let visibleIndex: Int = centerVisibleIndex(at: index), + let centerIndexAbsolute: Int = centerIndexAbsolute(at: visibleIndex) else { return 1 } @@ -137,9 +172,9 @@ struct VPageIndicatorInfinite: View { return centerScale(at: centerIndexAbsolute) case .rightEdge: - guard - let rightEdgeVisibleIndex: Int = rightEdgeVisibleIndex(at: index), - let rightEdgeleftSideIndex: Int = rightEdgeleftSideIndex(at: rightEdgeVisibleIndex) + guard let rightEdgeVisibleIndex: Int = rightEdgeVisibleIndex(at: index), + let rightEdgeleftSideIndex: Int = + rightEdgeleftSideIndex(at: rightEdgeVisibleIndex) else { return 1 } @@ -151,7 +186,7 @@ struct VPageIndicatorInfinite: View { // LEFT private func leftEdgeVisibleIndex(at index: Int) -> Int? { switch index { - case 0 ..< visible: return index + case 0.. Int? { // (5 6) -> (0 1) switch index { - case visible - side ..< visible: return side + index - visible + case visible - side.. Int? { // (0 1 2 3 4 5 6) -> (0 1 _ _ _ 1 0) switch index { - case 0 ..< side: return index - case visible - side ..< visible: return visible - index - 1 + case 0.. Int? { switch index { - case total - visible ..< total: return visible - total + index + case total - visible.. Int? { // (0 1) -> (0 1) switch index { - case 0 ..< side: return index + case 0.. Bool { @@ -45,7 +45,8 @@ extension UIKitTextFieldRepresentable { representable.beginHandler?() } - @objc func textFieldDidChange(_ textField: UITextField) { + @objc + func textFieldDidChange(_ textField: UITextField) { representable.commitText(textField.text ?? "") representable.changeHandler?() } @@ -53,5 +54,11 @@ extension UIKitTextFieldRepresentable { func textFieldDidEndEditing(_: UITextField) { representable.endHandler?() } + + // MARK: Private + + // MARK: Properties + + private let representable: UIKitTextFieldRepresentable } } diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VMenuPicker/VMenuPickerButtonType.swift b/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VMenuPicker/VMenuPickerButtonType.swift index ed76927d..a4898528 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VMenuPicker/VMenuPickerButtonType.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VMenuPicker/VMenuPickerButtonType.swift @@ -28,7 +28,7 @@ extension VMenuPickerButtonType { ) -> some View where Content: View { - Self.menuButton( + menuButton( buttonType: buttonType, isEnabled: isEnabled, label: label diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VSegmentedPicker/VSegmentedPicker.swift b/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VSegmentedPicker/VSegmentedPicker.swift index 425cc295..3b627a31 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VSegmentedPicker/VSegmentedPicker.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VSegmentedPicker/VSegmentedPicker.swift @@ -278,9 +278,7 @@ public struct VSegmentedPicker: View rowContent(data[i]) .padding(model.layout.actualRowContentMargin) .frame(maxWidth: .infinity, maxHeight: .infinity) - .opacity(contentOpacity(for: i)) - .readSize(onChange: { rowWidth = $0.width }) } ) diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VWheelPicker/VWheelPicker.swift b/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VWheelPicker/VWheelPicker.swift index ca65cb39..8093dcb9 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VWheelPicker/VWheelPicker.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Item Pickers/VWheelPicker/VWheelPicker.swift @@ -198,10 +198,8 @@ public struct VWheelPicker: View }) .pickerStyle(WheelPickerStyle()) .labelsHidden() - .disabled(!state.isEnabled) // Luckily, doesn't affect colors .opacity(model.colors.content.for(state)) - .background(model.colors.background.for(state).cornerRadius(model.layout.cornerRadius)) } diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListLayoutType.swift b/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListLayoutType.swift index 210baa39..9150f060 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListLayoutType.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListLayoutType.swift @@ -1,5 +1,5 @@ // -// VGenericListContentLayout.swift +// VBaseListLayoutType.swift // VComponents // // Created by Vakhtang Kontridze on 1/11/21. @@ -34,6 +34,8 @@ public enum VBaseListLayoutType: Int, CaseIterable { /// Component stretches vertically to occupy maximum space, but is constrainted in space given by container. Scrolling may be enabled inside component. case flexible + // MARK: Public + // MARK: Initailizers /// Default value. Set to `flexible`. diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListModel.swift index 4210a23d..29eca594 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Lists/VBaseList/VBaseListModel.swift @@ -1,5 +1,5 @@ // -// VGenericListContent.swift +// VBaseListModel.swift // VComponents // // Created by Vakhtang Kontridze on 1/10/21. @@ -11,26 +11,33 @@ import SwiftUI /// Model that describes UI. public struct VBaseListModel { - // MARK: Properties - - /// Sub-model containing layout properties. - public var layout: Layout = .init() - - /// Sub-model containing color properties. - public var colors: Colors = .init() - - /// Sub-model containing misc properties. - public var misc: Misc = .init() + // MARK: Lifecycle // MARK: Initializers /// Initializes model with default values. public init() {} + // MARK: Public + // MARK: Layout /// Sub-model containing layout properties. public struct Layout { + // MARK: Lifecycle + + // MARK: Initializers + + /// Initializes sub-model with default values. + public init() {} + + // MARK: Public + + // MARK: Horizontal Margins + + /// Sub-model containing `leading` and `trailing` margins. + public typealias HorizontalMargins = LayoutGroup_LT + // MARK: Properties /// Trailing margin. Defaults to `0`. @@ -50,50 +57,63 @@ public struct VBaseListModel { trailing: 0 ) + // MARK: Internal + var dividerMarginVertical: CGFloat { rowSpacing / 2 } var hasDivider: Bool { dividerHeight > 0 } + } + + // MARK: Colors + + /// Sub-model containing color properties. + public struct Colors { + // MARK: Lifecycle // MARK: Initializers /// Initializes sub-model with default values. public init() {} - // MARK: Horizontal Margins - - /// Sub-model containing `leading` and `trailing` margins. - public typealias HorizontalMargins = LayoutGroup_LT - } - - // MARK: Colors + // MARK: Public - /// Sub-model containing color properties. - public struct Colors { // MARK: Properties /// Divider color. public var divider: Color = .init(componentAsset: "BaseList.Divider") + } + + // MARK: Misc + + /// Sub-model containing misc properties. + public struct Misc { + // MARK: Lifecycle // MARK: Initializers /// Initializes sub-model with default values. public init() {} - } - // MARK: Misc + // MARK: Public - /// Sub-model containing misc properties. - public struct Misc { // MARK: Properties /// Indicates if scrolling indicator is shown. Defaults to `true`. public var showIndicator: Bool = true + } - // MARK: Initializers + // MARK: Properties - /// Initializes sub-model with default values. - public init() {} - } + /// Sub-model containing layout properties. + public var layout: Layout = .init() + + /// Sub-model containing color properties. + public var colors: Colors = .init() + + /// Sub-model containing misc properties. + public var misc: Misc = .init() + + // MARK: Internal // MARK: Sub-Models diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Lists/VSectionList/VSectionListSection.swift b/FRW/Tools/ThirdParty/VComponents/Components/Lists/VSectionList/VSectionListSection.swift index 04d490a8..141982b9 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Lists/VSectionList/VSectionListSection.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Lists/VSectionList/VSectionListSection.swift @@ -1,5 +1,5 @@ // -// VSectionListSectionViewModelable.swift +// VSectionListSection.swift // VComponents // // Created by Vakhtang Kontridze on 1/14/21. diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Modals/VDialog/VDialogButtonModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/Modals/VDialog/VDialogButtonModel.swift index ca5202e8..3ed423d4 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Modals/VDialog/VDialogButtonModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Modals/VDialog/VDialogButtonModel.swift @@ -1,5 +1,5 @@ // -// VDialogButtonModelCustom.swift +// VDialogButtonModel.swift // VComponents // // Created by Vakhtang Kontridze on 1/14/21. @@ -7,7 +7,7 @@ import SwiftUI -// MARK: - V Dialog Button Model Model +// MARK: - VDialogButtonModel /// Enum that describes `VDialog` button model, such as `primary`, `secondary`, or `custom`. public enum VDialogButtonModel { @@ -22,6 +22,8 @@ public enum VDialogButtonModel { /// Custom button. case custom(_ model: VDialogButtonModelCustom) + // MARK: Internal + // MARK: Sub-Models var buttonSubModel: VPrimaryButtonModel { @@ -32,6 +34,8 @@ public enum VDialogButtonModel { } } + // MARK: Private + private static let primaryButtonSubModel: VDialogButtonModelCustom = .init( colors: .init( content: .init( @@ -40,7 +44,8 @@ public enum VDialogButtonModel { text: .init( enabled: VDialogButtonModelCustom.primaryButtonReference.colors.textContent.enabled, pressed: VDialogButtonModelCustom.primaryButtonReference.colors.textContent.pressed, - disabled: VDialogButtonModelCustom.primaryButtonReference.colors.textContent.disabled + disabled: VDialogButtonModelCustom.primaryButtonReference.colors.textContent + .disabled ), background: .init( enabled: VDialogButtonModelCustom.primaryButtonReference.colors.background.enabled, @@ -69,23 +74,11 @@ public enum VDialogButtonModel { ) } -// MARK: - V Dialog Button Model Custom +// MARK: - VDialogButtonModelCustom /// Model that describes UI. public struct VDialogButtonModelCustom { - // MARK: Properties - - /// Reference to `VPrimaryButtonModel`. - public static let primaryButtonReference: VPrimaryButtonModel = .init() - - /// Sub-model containing layout properties. - public var layout: Layout - - /// Sub-model containing color properties. - public var colors: Colors - - /// Sub-model containing font properties. - public var fonts: Fonts + // MARK: Lifecycle // MARK: Initializers @@ -96,10 +89,21 @@ public struct VDialogButtonModelCustom { self.fonts = fonts } + // MARK: Public + // MARK: Layout /// Sub-model containing layout properties. public struct Layout { + // MARK: Lifecycle + + // MARK: Initializers + + /// Initializes sub-model with default values. + public init() {} + + // MARK: Public + // MARK: Properties /// Button height. Defaults to `40`. @@ -107,29 +111,13 @@ public struct VDialogButtonModelCustom { /// Button corner radius. Defaults to `20`. public var cornerRadius: CGFloat = 10 - - // MARK: Initializers - - /// Initializes sub-model with default values. - public init() {} } // MARK: Colors /// Sub-model containing color properties. public struct Colors { - // MARK: Properties - - /// Conrent opacities. - public var content: StateOpacities - - /// Text content colors. - /// - /// Only applicable when using init with title. - public var text: StateColors - - /// Background colors. - public var background: StateColors + // MARK: Lifecycle // MARK: Initializers @@ -140,6 +128,8 @@ public struct VDialogButtonModelCustom { self.background = background } + // MARK: Public + // MARK: State Colors /// Sub-model containing colors for component states. @@ -149,23 +139,56 @@ public struct VDialogButtonModelCustom { /// Sub-model containing opacities for component states. public typealias StateOpacities = StateOpacities_P + + // MARK: Properties + + /// Conrent opacities. + public var content: StateOpacities + + /// Text content colors. + /// + /// Only applicable when using init with title. + public var text: StateColors + + /// Background colors. + public var background: StateColors } // MARK: Fonts /// Sub-model containing font properties. public struct Fonts { - // MARK: Properties - - /// Title font. Defaults to system font of size `16` with `semibold` weight. - public var title: Font = primaryButtonReference.fonts.title + // MARK: Lifecycle // MARK: Initializers /// Initializes sub-model with default values. public init() {} + + // MARK: Public + + // MARK: Properties + + /// Title font. Defaults to system font of size `16` with `semibold` weight. + public var title: Font = primaryButtonReference.fonts.title } + // MARK: Properties + + /// Reference to `VPrimaryButtonModel`. + public static let primaryButtonReference: VPrimaryButtonModel = .init() + + /// Sub-model containing layout properties. + public var layout: Layout + + /// Sub-model containing color properties. + public var colors: Colors + + /// Sub-model containing font properties. + public var fonts: Fonts + + // MARK: Fileprivate + // MARK: Sub-Models fileprivate var primaryButtonSubModel: VPrimaryButtonModel { @@ -176,7 +199,8 @@ public struct VDialogButtonModelCustom { model.colors.content = .init( pressedOpacity: colors.content.pressedOpacity, - disabledOpacity: VDialogButtonModelCustom.primaryButtonReference.colors.content.disabledOpacity + disabledOpacity: VDialogButtonModelCustom.primaryButtonReference.colors.content + .disabledOpacity ) model.colors.textContent = .init( diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Modals/VMenu/VMenuButtonType.swift b/FRW/Tools/ThirdParty/VComponents/Components/Modals/VMenu/VMenuButtonType.swift index b5dbb4fb..44c68066 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Modals/VMenu/VMenuButtonType.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Modals/VMenu/VMenuButtonType.swift @@ -1,5 +1,5 @@ // -// VMenuPreset.swift +// VMenuButtonType.swift // VComponents // // Created by Vakhtang Kontridze on 1/28/21. @@ -21,13 +21,13 @@ public typealias VMenuButtonPreset = DerivedButtonPreset // MARK: - Button extension VMenuButtonType { - @ViewBuilder static func menuButton( + @ViewBuilder + static func menuButton( buttonType: VMenuButtonType, isEnabled: Bool, @ViewBuilder label: @escaping () -> Content ) -> some View - where Content: View - { + where Content: View { switch buttonType { case let .primary(model): VPrimaryButton( diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Modals/VSideBar/VSideBarModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/Modals/VSideBar/VSideBarModel.swift index 18f2143a..1038b4c8 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Modals/VSideBar/VSideBarModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Modals/VSideBar/VSideBarModel.swift @@ -1,5 +1,5 @@ // -// VSideBarModelStandard.swift +// VSideBarModel.swift // VComponents // // Created by Vakhtang Kontridze on 12/24/20. @@ -11,35 +11,33 @@ import SwiftUI /// Model that describes UI. public struct VSideBarModel { - // MARK: Properties + // MARK: Lifecycle - /// Reference to `VSheetModel`. - public static let sheetReference: VSheetModel = .init() + // MARK: Initializers - /// Reference to `VModalModel`. - public static let modalReference: VModalModel = .init() + /// Initializes model with default values. + public init() {} - /// Reference to `VHalfModalModel`. - public static let halfModalReference: VHalfModalModel = .init() + // MARK: Public + + // MARK: Layout /// Sub-model containing layout properties. - public var layout: Layout = .init() + public struct Layout { + // MARK: Lifecycle - /// Sub-model containing color properties. - public var colors: Colors = .init() + // MARK: Initializers - /// Sub-model containing animation properties. - public var animations: Animations = .init() + /// Initializes sub-model with default values. + public init() {} - // MARK: Initializers + // MARK: Public - /// Initializes model with default values. - public init() {} + // MARK: Margins - // MARK: Layout + /// Sub-model containing `leading`, `trailing`, `top`, and `bottom` margins. + public typealias Margins = LayoutGroup_LTTB - /// Sub-model containing layout properties. - public struct Layout { // MARK: Properties /// Side bar width. Defaults to `0.67` ratio of screen with. @@ -48,8 +46,6 @@ public struct VSideBarModel { /// Corner radius. Defaults to `15`. public var cornerRadius: CGFloat = modalReference.layout.cornerRadius - var roundCorners: Bool { cornerRadius > 0 } - /// Content margins. Defaults to `10` leading, `10` trailing, `10` top, and `10` bottom. public var contentMargins: Margins = .init( leading: sheetReference.layout.contentMargin, @@ -64,6 +60,13 @@ public struct VSideBarModel { /// Indicates if side bar has margins for safe area on bottom edge. Defaults to `true`. public var hasSafeAreaMarginBottom: Bool = true + /// Distance to drag side bar left to initiate dismiss. Default to `100`. + public var translationToDismiss: CGFloat = 100 + + // MARK: Internal + + var roundCorners: Bool { cornerRadius > 0 } + var edgesToIgnore: Edge.Set { switch (hasSafeAreaMarginTop, hasSafeAreaMarginBottom) { case (false, false): return [.top, .bottom] @@ -72,25 +75,21 @@ public struct VSideBarModel { case (true, true): return [] } } + } - /// Distance to drag side bar left to initiate dismiss. Default to `100`. - public var translationToDismiss: CGFloat = 100 + // MARK: Colors + + /// Sub-model containing color properties. + public struct Colors { + // MARK: Lifecycle // MARK: Initializers /// Initializes sub-model with default values. public init() {} - // MARK: Margins - - /// Sub-model containing `leading`, `trailing`, `top`, and `bottom` margins. - public typealias Margins = LayoutGroup_LTTB - } - - // MARK: Colors + // MARK: Public - /// Sub-model containing color properties. - public struct Colors { // MARK: Properties /// Background color. @@ -98,17 +97,21 @@ public struct VSideBarModel { /// Blinding color. public var blinding: Color = modalReference.colors.blinding - - // MARK: Initializers - - /// Initializes sub-model with default values. - public init() {} } // MARK: Animations /// Sub-model containing animation properties. public struct Animations { + // MARK: Lifecycle + + // MARK: Initializers + + /// Initializes sub-model with default values + public init() {} + + // MARK: Public + // MARK: Properties /// Appear animation. Defaults to `linear` with duration `0.2`. @@ -116,19 +119,37 @@ public struct VSideBarModel { /// Disappear animation. Defaults to `linear` with duration `0.2`. public var disappear: BasicAnimation? = halfModalReference.animations.disappear + } - // MARK: Initializers + // MARK: Properties - /// Initializes sub-model with default values - public init() {} - } + /// Reference to `VSheetModel`. + public static let sheetReference: VSheetModel = .init() + + /// Reference to `VModalModel`. + public static let modalReference: VModalModel = .init() + + /// Reference to `VHalfModalModel`. + public static let halfModalReference: VHalfModalModel = .init() + + /// Sub-model containing layout properties. + public var layout: Layout = .init() + + /// Sub-model containing color properties. + public var colors: Colors = .init() + + /// Sub-model containing animation properties. + public var animations: Animations = .init() + + // MARK: Internal // MARK: Sub-Models var sheetSubModel: VSheetModel { var model: VSheetModel = .init() - model.layout.roundedCorners = layout.roundCorners ? .custom([.topRight, .bottomRight]) : .none + model.layout.roundedCorners = layout + .roundCorners ? .custom([.topRight, .bottomRight]) : .none model.layout.cornerRadius = layout.cornerRadius model.layout.contentMargin = 0 diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewModel.swift index ed40790b..6da03799 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewModel.swift @@ -1,5 +1,5 @@ // -// VNavigationViewFilledModel.swift +// VNavigationViewModel.swift // VComponents // // Created by Vakhtang Kontridze on 12/22/20. @@ -11,20 +11,28 @@ import SwiftUI /// Model that describes UI. public struct VNavigationViewModel { - // MARK: Properties - - /// Sub-model containing color properties. - public var colors: Colors = .init() + // MARK: Lifecycle // MARK: Initializers /// Initializes model with default values. public init() {} + // MARK: Public + // MARK: Colors /// Sub-model containing color properties. public struct Colors { + // MARK: Lifecycle + + // MARK: Initializers + + /// Initializes sub-model with default values. + public init() {} + + // MARK: Public + // MARK: Properties /// Background color. @@ -32,10 +40,10 @@ public struct VNavigationViewModel { /// Navigation bar divider color. public var divider: Color = .init(componentAsset: "NavigationView.Divider") + } - // MARK: Initializers + // MARK: Properties - /// Initializes sub-model with default values. - public init() {} - } + /// Sub-model containing color properties. + public var colors: Colors = .init() } diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewNavigationBar.swift b/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewNavigationBar.swift index 8679a53d..abba9091 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewNavigationBar.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Navigation/VNavigationView/VNavigationViewNavigationBar.swift @@ -1,5 +1,5 @@ // -// VNaviationViewNavigationBar.swift +// VNavigationViewNavigationBar.swift // VComponents // // Created by Vakhtang Kontridze on 12/22/20. @@ -15,7 +15,7 @@ extension View { } } -// MARK: - V Navigation View Navigation Bar +// MARK: - VNaviggationViewNavigationBar struct VNaviggationViewNavigationBar: ViewModifier { func body(content: Content) -> some View { diff --git a/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleModel.swift index 07ce1552..5ec8102e 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleModel.swift @@ -1,5 +1,5 @@ // -// VToggleRightContentModel.swift +// VToggleModel.swift // VComponents // // Created by Vakhtang Kontridze on 12/21/20. diff --git a/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleState.swift b/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleState.swift index 9f736f91..9f6866d1 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleState.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/State Pickers/VToggle/VToggleState.swift @@ -1,5 +1,5 @@ // -// VTogglState.swift +// VToggleState.swift // VComponents // // Created by Vakhtang Kontridze on 19.12.20. diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VRangeSlider/VRangeSlider.swift b/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VRangeSlider/VRangeSlider.swift index 31507009..c4a06913 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VRangeSlider/VRangeSlider.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VRangeSlider/VRangeSlider.swift @@ -109,10 +109,8 @@ public struct VRangeSlider: View { progress(in: proxy) }) .mask(RoundedRectangle(cornerRadius: model.layout.cornerRadius)) - .overlay(thumb(in: proxy, thumb: .low)) .overlay(thumb(in: proxy, thumb: .high)) - .disabled(!state.isEnabled) }) .frame(height: model.layout.height) @@ -127,7 +125,6 @@ public struct VRangeSlider: View { Rectangle() .padding(.leading, progress(in: proxy, thumb: .low)) .padding(.trailing, progress(in: proxy, thumb: .high)) - .foregroundColor(model.colors.progress.for(state)) } @@ -145,7 +142,6 @@ public struct VRangeSlider: View { .offset(x: thumbOffset(in: proxy, thumb: thumb)) }) .frame(maxWidth: .infinity, alignment: .leading) // Must be put into group, as content already has frame - .gesture( DragGesture(minimumDistance: 0) .onChanged { dragChanged(drag: $0, in: proxy, thumb: thumb) } diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VSlider/VSlider.swift b/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VSlider/VSlider.swift index 567ad6e8..d5a93e79 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VSlider/VSlider.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Value Pickers/VSlider/VSlider.swift @@ -73,9 +73,7 @@ public struct VSlider: View { progress(in: proxy) }) .mask(RoundedRectangle(cornerRadius: model.layout.cornerRadius)) - .overlay(thumb(in: proxy)) - .gesture( DragGesture(minimumDistance: 0) .onChanged { dragChanged(drag: $0, in: proxy) } @@ -95,7 +93,6 @@ public struct VSlider: View { private func progress(in proxy: GeometryProxy) -> some View { Rectangle() .frame(width: progressWidth(in: proxy)) - .foregroundColor(model.colors.progress.for(state)) } diff --git a/FRW/Tools/ThirdParty/VComponents/Extensions/SwiftUI/View.ConditionalViewModifiers.swift b/FRW/Tools/ThirdParty/VComponents/Extensions/SwiftUI/View.ConditionalViewModifiers.swift index 78f16a0d..55aa7687 100644 --- a/FRW/Tools/ThirdParty/VComponents/Extensions/SwiftUI/View.ConditionalViewModifiers.swift +++ b/FRW/Tools/ThirdParty/VComponents/Extensions/SwiftUI/View.ConditionalViewModifiers.swift @@ -1,5 +1,5 @@ // -// ViewExtensions.swift +// View.ConditionalViewModifiers.swift // VComponents // // Created by Vakhtang Kontridze on 18.12.20. diff --git a/FRW/Tools/ThirdParty/VComponents/Helpers/ComponentStateManager.swift b/FRW/Tools/ThirdParty/VComponents/Helpers/ComponentStateManager.swift index 8b499fa2..d7db7f8c 100644 --- a/FRW/Tools/ThirdParty/VComponents/Helpers/ComponentStateManager.swift +++ b/FRW/Tools/ThirdParty/VComponents/Helpers/ComponentStateManager.swift @@ -1,5 +1,5 @@ // -// ComponentStateManagement.swift +// ComponentStateManager.swift // VComponents // // Created by Vakhtang Kontridze on 2/3/21. diff --git a/FRW/Tools/ThirdParty/VComponents/Helpers/Edge Insets/LayoutGroup_LTTB.swift b/FRW/Tools/ThirdParty/VComponents/Helpers/Edge Insets/LayoutGroup_LTTB.swift index ea1ce6e5..08ad8ddc 100644 --- a/FRW/Tools/ThirdParty/VComponents/Helpers/Edge Insets/LayoutGroup_LTTB.swift +++ b/FRW/Tools/ThirdParty/VComponents/Helpers/Edge Insets/LayoutGroup_LTTB.swift @@ -1,5 +1,5 @@ // -// LayoutGroups.swift +// LayoutGroup_LTTB.swift // VComponents // // Created by Vakhtang Kontridze on 2/9/21. diff --git a/FRW/UI/Component/Base/BaseDivider.swift b/FRW/UI/Component/Base/BaseDivider.swift index 74a4c78a..002e8e98 100644 --- a/FRW/UI/Component/Base/BaseDivider.swift +++ b/FRW/UI/Component/Base/BaseDivider.swift @@ -1,5 +1,5 @@ // -// CommonBaseView.swift +// BaseDivider.swift // Flow Wallet // // Created by Selina on 19/5/2022. diff --git a/FRW/UI/Component/ConfettiManager.swift b/FRW/UI/Component/ConfettiManager.swift index 86e2364e..d492a9b2 100644 --- a/FRW/UI/Component/ConfettiManager.swift +++ b/FRW/UI/Component/ConfettiManager.swift @@ -8,12 +8,14 @@ import SPConfetti import SwiftUI -struct ConfettiManager { +enum ConfettiManager { static func `default`() { - SPConfettiConfiguration.particlesConfig.colors = [Color.LL.Primary.salmonPrimary.toUIColor()!, - Color.LL.Secondary.mangoNFT.toUIColor()!, - Color.LL.Secondary.navy4.toUIColor()!, - Color.LL.Secondary.violetDiscover.toUIColor()!] + SPConfettiConfiguration.particlesConfig.colors = [ + Color.LL.Primary.salmonPrimary.toUIColor()!, + Color.LL.Secondary.mangoNFT.toUIColor()!, + Color.LL.Secondary.navy4.toUIColor()!, + Color.LL.Secondary.violetDiscover.toUIColor()!, + ] SPConfettiConfiguration.particlesConfig.velocity = 400 SPConfettiConfiguration.particlesConfig.velocityRange = 200 SPConfettiConfiguration.particlesConfig.birthRate = 200 @@ -23,9 +25,11 @@ struct ConfettiManager { static func show() { DispatchQueue.main.async { ConfettiManager.default() - SPConfetti.startAnimating(.fullWidthToDown, - particles: [.triangle, .arc, .polygon, .heart, .star], - duration: 4) + SPConfetti.startAnimating( + .fullWidthToDown, + particles: [.triangle, .arc, .polygon, .heart, .star], + duration: 4 + ) } } } diff --git a/FRW/UI/Component/Like/Support Shapes/CapusuleGroupView.swift b/FRW/UI/Component/Like/Support Shapes/CapusuleGroupView.swift index 472d1d94..78be382d 100644 --- a/FRW/UI/Component/Like/Support Shapes/CapusuleGroupView.swift +++ b/FRW/UI/Component/Like/Support Shapes/CapusuleGroupView.swift @@ -1,5 +1,5 @@ // -// UpperCapsuleView.swift +// CapusuleGroupView.swift // SwiftUI-Animations // // Created by Shubham Singh on 26/09/20. diff --git a/FRW/UI/Component/Like/Support Shapes/ShrinkingCapsule.swift b/FRW/UI/Component/Like/Support Shapes/ShrinkingCapsule.swift index 7833fe97..3d7bd9a4 100644 --- a/FRW/UI/Component/Like/Support Shapes/ShrinkingCapsule.swift +++ b/FRW/UI/Component/Like/Support Shapes/ShrinkingCapsule.swift @@ -27,7 +27,7 @@ struct ShrinkingCapsule: View { .frame(width: 5, height: self.isAnimating ? 10 : 15, alignment: .bottomLeading) .rotationEffect(rotationAngle) }.offset(offset) - .opacity(self.hideCapsule ? 0 : 0.8) + .opacity(hideCapsule ? 0 : 0.8) .animation(Animation.easeIn(duration: animationDuration), value: isAnimating) .onAppear { Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in diff --git a/FRW/UI/Component/SectionItem.swift b/FRW/UI/Component/SectionItem.swift index a35ecaa4..1c3bd944 100644 --- a/FRW/UI/Component/SectionItem.swift +++ b/FRW/UI/Component/SectionItem.swift @@ -1,17 +1,19 @@ // -// ImportTitleHeader.swift +// SectionItem.swift // FRW // // Created by cat on 10/30/24. // -import SwiftUI import Introspect +import SwiftUI + +// MARK: - TitleView struct TitleView: View { let title: String let isStar: Bool - + var body: some View { HStack(spacing: 0) { Text(title) @@ -27,29 +29,33 @@ struct TitleView: View { } } +// MARK: - SingleInputView + struct SingleInputView: View { - enum FocusField: Hashable { case field } - - @Binding var content: String + + @Binding + var content: String var placeholder: String? = "add_custom_token_place".localized - @FocusState private var focusedField: FocusField? + @FocusState + private var focusedField: FocusField? var onSubmit: EmptyClosure? = nil - var onChange: ((String)->())? = nil - + var onChange: ((String) -> Void)? = nil + var body: some View { ZStack(alignment: .center) { - TextField("", text: $content) .keyboardType(.alphabet) .submitLabel(.search) .disableAutocorrection(true) - .modifier(PlaceholderStyle(showPlaceHolder: content.isEmpty, - placeholder: placeholder ?? "", - font: .inter(size: 14, weight: .medium), - color: Color.LL.Neutrals.note)) + .modifier(PlaceholderStyle( + showPlaceHolder: content.isEmpty, + placeholder: placeholder ?? "", + font: .inter(size: 14, weight: .medium), + color: Color.LL.Neutrals.note + )) .font(.inter(size: 14)) .frame(maxWidth: .infinity, maxHeight: .infinity) .onSubmit { @@ -74,40 +80,36 @@ struct SingleInputView: View { } } - - - - +// MARK: - AnimatedSecureTextField public struct AnimatedSecureTextField: View { - private enum FocusedField: Int, Hashable { - case password - } - - @State var isSecure = false - @FocusState private var field: FocusedField? - public let placeholder: String - @Binding var text: String + // MARK: Lifecycle - var textDidChange:(String)->() - - public init(placeholder: String, text: Binding, textDidChange: @escaping (String)->()) { + public init( + placeholder: String, + text: Binding, + textDidChange: @escaping (String) -> Void + ) { self.placeholder = placeholder - self._text = text + _text = text self.textDidChange = textDidChange self.field = .password } + // MARK: Public + + public let placeholder: String + public var body: some View { ZStack(alignment: .leading) { - if text.isEmpty { + if text.isEmpty { HStack { Text(placeholder) .font(.inter(size: 14)) .foregroundStyle(Color.Theme.Text.black3) Spacer() } - // .padding(.horizontal, 16.0) + // .padding(.horizontal, 16.0) .frame(maxWidth: .infinity) .layoutPriority(1) .onTapGesture { @@ -116,7 +118,6 @@ public struct AnimatedSecureTextField: View { } } } - if isSecure { SecureField("", text: $text) @@ -130,7 +131,7 @@ public struct AnimatedSecureTextField: View { .onChange(of: text) { text in textDidChange(text) } - + } else { TextField("", text: $text) .disableAutocorrection(true) @@ -143,12 +144,11 @@ public struct AnimatedSecureTextField: View { .onChange(of: text) { text in textDidChange(text) } - } HStack { Spacer() - + if !text.isEmpty { Button { isSecure.toggle() @@ -168,20 +168,36 @@ public struct AnimatedSecureTextField: View { } } } - .padding(.horizontal,20) + .padding(.horizontal, 20) .frame(height: 64) .background { RoundedRectangle(cornerRadius: 16.0) .foregroundColor(.Theme.Background.bg2) } } -} + // MARK: Internal + + @State + var isSecure = false + @Binding + var text: String + + var textDidChange: (String) -> Void + + // MARK: Private + + private enum FocusedField: Int, Hashable { + case password + } + + @FocusState + private var field: FocusedField? +} #Preview { TitleView(title: "Hello", isStar: false) - - SingleInputView(content: .constant("abc")) { str in - + + SingleInputView(content: .constant("abc")) { _ in } } diff --git a/FRW/UI/Component/TabBarView/TabBarItemView.swift b/FRW/UI/Component/TabBarView/TabBarItemView.swift index 0c587d10..d4707591 100644 --- a/FRW/UI/Component/TabBarView/TabBarItemView.swift +++ b/FRW/UI/Component/TabBarView/TabBarItemView.swift @@ -1,5 +1,5 @@ // -// TabbarItem.swift +// TabBarItemView.swift // Test // // Created by cat on 2022/5/25. @@ -8,18 +8,23 @@ import Lottie import SwiftUI +// MARK: - TabBarItemView + struct TabBarItemView: View { var pageModel: TabBarPageModel - @Binding var selected: T + @Binding + var selected: T var action: () -> Void var animationView: some View { - ResizableLottieView(lottieView: pageModel.lottieView, - color: selected == pageModel.tag ? pageModel.color : Color.gray) - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) + ResizableLottieView( + lottieView: pageModel.lottieView, + color: selected == pageModel.tag ? pageModel.color : Color.gray + ) + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) } var body: some View { @@ -43,20 +48,24 @@ struct TabBarItemView: View { } } +// MARK: - WalletView_Previews + struct WalletView_Previews: PreviewProvider { static let animation = AnimationView(name: "Copy", bundle: .main) static var previews: some View { - ResizableLottieView(lottieView: animation, - color: Color.gray) + ResizableLottieView( + lottieView: animation, + color: Color.gray + ) // LottieView(name: "Coin2", loopMode: .loop) - .aspectRatio(contentMode: .fit) - .frame(width: 300, height: 300) - .frame(maxWidth: .infinity) - .onAppear { - animation.play() - }.onTapGesture { - animation.play() - } + .aspectRatio(contentMode: .fit) + .frame(width: 300, height: 300) + .frame(maxWidth: .infinity) + .onAppear { + animation.play() + }.onTapGesture { + animation.play() + } } } diff --git a/FRW/UI/Component/TabBarView/TabBarView.swift b/FRW/UI/Component/TabBarView/TabBarView.swift index 72f1cccc..6775ecf8 100644 --- a/FRW/UI/Component/TabBarView/TabBarView.swift +++ b/FRW/UI/Component/TabBarView/TabBarView.swift @@ -1,5 +1,5 @@ // -// ContentView.swift +// TabBarView.swift // Test // // Created by cat on 2022/5/23. @@ -7,13 +7,10 @@ import SwiftUI -struct TabBarView: View { - @State var current: T - var pages: [TabBarPageModel] +// MARK: - TabBarView - var maxWidth: CGFloat - @State private var offsetX: CGFloat - @State private var currentIndex: Int +struct TabBarView: View { + // MARK: Lifecycle init(current: T, pages: [TabBarPageModel], maxWidth: CGFloat) { _current = State(initialValue: current) @@ -30,25 +27,38 @@ struct TabBarView: View { _offsetX = State(initialValue: maxWidth * CGFloat(selectIndex)) } + // MARK: Internal + + @State + var current: T + var pages: [TabBarPageModel] + + var maxWidth: CGFloat + var body: some View { VStack(spacing: 0) { tabView - TabBar(pages: pages, - indicatorColor: getCurrentPageModel()?.color ?? .black, - offsetX: $offsetX, - selected: $current) + TabBar( + pages: pages, + indicatorColor: getCurrentPageModel()?.color ?? .black, + offsetX: $offsetX, + selected: $current + ) } } var tabView: some View { TabView(selection: $current) { - ForEach(0 ..< pages.count, id: \.self) { index in + ForEach(0..: View { .coordinateSpace(name: "frameLayer") } + // MARK: Private + + @State + private var offsetX: CGFloat + @State + private var currentIndex: Int + private func offset(index: Int, frame: CGRect) { if currentIndex == index { let x = -frame.origin.x @@ -84,10 +101,12 @@ struct TabBarView: View { } } -// MARK: - Helper +// MARK: - ViewOffsetKey private struct ViewOffsetKey: PreferenceKey { typealias Value = CGRect + static var defaultValue = CGRect.zero + static func reduce(value _: inout Value, nextValue _: () -> Value) {} } diff --git a/FRW/UI/Extension/Color.swift b/FRW/UI/Extension/Color.swift index 428795a5..84ffc0a1 100644 --- a/FRW/UI/Extension/Color.swift +++ b/FRW/UI/Extension/Color.swift @@ -39,20 +39,22 @@ extension UIColor { } func lighter(by percentage: CGFloat = 30.0) -> UIColor { - return adjust(by: abs(percentage)) + adjust(by: abs(percentage)) } func darker(by percentage: CGFloat = 30.0) -> UIColor { - return adjust(by: -1 * abs(percentage)) + adjust(by: -1 * abs(percentage)) } func adjust(by percentage: CGFloat = 30.0) -> UIColor { var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 if getRed(&red, green: &green, blue: &blue, alpha: &alpha) { - return UIColor(red: min(red + percentage / 100, 1.0), - green: min(green + percentage / 100, 1.0), - blue: min(blue + percentage / 100, 1.0), - alpha: alpha) + return UIColor( + red: min(red + percentage / 100, 1.0), + green: min(green + percentage / 100, 1.0), + blue: min(blue + percentage / 100, 1.0), + alpha: alpha + ) } else { return self } @@ -61,43 +63,14 @@ extension UIColor { extension Color { func adjustbyTheme(by percentage: CGFloat = 30.0) -> Color { - return Color(UIColor(self).adjustbyTheme(by: percentage)) + Color(UIColor(self).adjustbyTheme(by: percentage)) } } -// MARK: - Custom Color +// MARK: - Color.LL extension Color { enum LL { - static let background = Color("Background") - static let rebackground = Color("Rebacground") -// static let primary = Color("Primary") - static let orange = Color("accent.green") - static let blue = Color("Blue") - static let yellow = Color("Yellow") - - static let error = Color("Error") - static let success = Color("Success") - static let outline = Color("Outline") - static let disable = Color("Disable") - static let note = Color("Note") - - static let frontColor = Color("FrontColor") - - static let text = Color("Text") - static let flow = Color("Flow") - - static let warning2 = Color("Warning2") - static let warning6 = Color("Warning6") - - static let bgForIcon = Color("BgForIcon") - - static let deepBg = Color("DeepBackground") - - static let neutrals1 = Color("Neutrals1") - - static let stakeMain = Color("other.stakingMain") - /// The primary color palette is used across all the iteractive elemets such as CTA’s(Call to The Action), links, inputs,active states,etc enum Primary { static let salmon1 = Color("primary.salmon1") @@ -186,25 +159,58 @@ extension Color { static let bg3 = Color("other.bg3") static let icon1 = Color("other.icon1") } + + static let background = Color("Background") + static let rebackground = Color("Rebacground") +// static let primary = Color("Primary") + static let orange = Color("accent.green") + static let blue = Color("Blue") + static let yellow = Color("Yellow") + + static let error = Color("Error") + static let success = Color("Success") + static let outline = Color("Outline") + static let disable = Color("Disable") + static let note = Color("Note") + + static let frontColor = Color("FrontColor") + + static let text = Color("Text") + static let flow = Color("Flow") + + static let warning2 = Color("Warning2") + static let warning6 = Color("Warning6") + + static let bgForIcon = Color("BgForIcon") + + static let deepBg = Color("DeepBackground") + + static let neutrals1 = Color("Neutrals1") + + static let stakeMain = Color("other.stakingMain") } } +// MARK: - Color.Flow + extension Color { // 2023-08-21 enum Flow { - static let accessory = Color("accessory") - static let blue = Color("tip.blue") enum Font { static let ascend = Color("font.ascend") static let descend = Color("font.descend") static let inaccessible = Color("font.inaccessible") } + + static let accessory = Color("accessory") + static let blue = Color("tip.blue") } } +// MARK: - Color.Theme + extension Color { enum Theme { - static let evm = Color("evm") enum Accent { static let green = Color("accent.green") static let grey = Color("accent.grey") @@ -231,17 +237,14 @@ extension Color { static let bg2 = Color("BG.2") static let bg3 = Color("BG.3") - + static let fill1 = Color("bg.fill1") - - } - + enum BG { static let bg1 = Color("bg1") static let bg3 = Color("bg3") } - enum Line { static let line = Color("line.black") @@ -256,12 +259,14 @@ extension Color { static let black6 = Color("text.black.6") static let white9 = Color("text.white.9") } + + static let evm = Color("evm") } } extension Color { /// opacity is 0.16 func fixedOpacity() -> Color { - return opacity(0.16) + opacity(0.16) } } diff --git a/FRW/UI/Extension/EventLoop+Future.swift b/FRW/UI/Extension/EventLoop+Future.swift index 89e988a3..3d067a14 100644 --- a/FRW/UI/Extension/EventLoop+Future.swift +++ b/FRW/UI/Extension/EventLoop+Future.swift @@ -7,6 +7,7 @@ import Combine import Foundation + // import NIO // extension EventLoopFuture { diff --git a/FRW/UI/Extension/Logger.swift b/FRW/UI/Extension/Logger.swift index 2d868e91..4b8a0de5 100644 --- a/FRW/UI/Extension/Logger.swift +++ b/FRW/UI/Extension/Logger.swift @@ -9,16 +9,16 @@ import Foundation func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { #if DEBUG - items.forEach { - Swift.print($0, separator: separator, terminator: terminator) - } + for item in items { + Swift.print(item, separator: separator, terminator: terminator) + } #endif } func debugPrint(_ items: Any..., separator: String = " ", terminator: String = "\n") { #if DEBUG - items.forEach { - Swift.debugPrint($0, separator: separator, terminator: terminator) - } + for item in items { + Swift.debugPrint(item, separator: separator, terminator: terminator) + } #endif } diff --git a/FRW/UI/Extension/MoyaAsync.swift b/FRW/UI/Extension/MoyaAsync.swift index 6e601485..66fb9093 100644 --- a/FRW/UI/Extension/MoyaAsync.swift +++ b/FRW/UI/Extension/MoyaAsync.swift @@ -10,22 +10,32 @@ import Moya public typealias AsyncTask = Task -public extension AsyncSequence where Element == Result { - func forEach(_ body: (Element) async throws -> Void) async throws { +extension AsyncSequence where Element == Result { + public func forEach(_ body: (Element) async throws -> Void) async throws { for try await element in self { try await body(element) } } } -internal class AsyncMoyaRequestWrapper { - var performRequest: (CheckedContinuation, Never>) -> Moya.Cancellable? - var cancellable: Moya.Cancellable? +// MARK: - AsyncMoyaRequestWrapper + +class AsyncMoyaRequestWrapper { + // MARK: Lifecycle - init(_ performRequest: @escaping (CheckedContinuation, Never>) -> Moya.Cancellable?) { + init( + _ performRequest: @escaping (CheckedContinuation, Never>) + -> Moya.Cancellable? + ) { self.performRequest = performRequest } + // MARK: Internal + + var performRequest: (CheckedContinuation, Never>) -> Moya + .Cancellable? + var cancellable: Moya.Cancellable? + func perform(continuation: CheckedContinuation, Never>) { cancellable = performRequest(continuation) } @@ -35,11 +45,11 @@ internal class AsyncMoyaRequestWrapper { } } -public extension MoyaProvider { +extension MoyaProvider { /// Async request /// - Parameter target: Entity, with provides Moya.Target protocol /// - Returns: Result type with response and error - func asyncRequest(_ target: Target) async -> Result { + public func asyncRequest(_ target: Target) async -> Result { let asyncRequestWrapper = AsyncMoyaRequestWrapper { [weak self] continuation in guard let self = self else { return nil } return self.request(target) { result in @@ -64,8 +74,11 @@ public extension MoyaProvider { /// Async request with progress using `AsyncStream` /// - Parameter target: Entity, with provides Moya.Target protocol /// - Returns: `AsyncStream>` with Result type of progress and error - func requestWithProgress(_ target: Target) async -> AsyncStream> { - return AsyncStream { stream in + public func requestWithProgress(_ target: Target) async -> AsyncStream> { + AsyncStream { stream in let cancelable = self.request(target) { progress in stream.yield(.success(progress)) } completion: { result in diff --git a/FRW/UI/Extension/Others.swift b/FRW/UI/Extension/Others.swift index 1b1eafd0..261491c8 100644 --- a/FRW/UI/Extension/Others.swift +++ b/FRW/UI/Extension/Others.swift @@ -17,12 +17,12 @@ extension UICollectionReusableView { extension Decimal { var doubleValue: Double { - return NSDecimalNumber(decimal: self).doubleValue + NSDecimalNumber(decimal: self).doubleValue } } extension CaseIterable { static var count: Int { - return Self.allCases.count + allCases.count } } diff --git a/FRW/UI/Extension/String.swift b/FRW/UI/Extension/String.swift index 23fbfee4..8552b137 100644 --- a/FRW/UI/Extension/String.swift +++ b/FRW/UI/Extension/String.swift @@ -18,7 +18,8 @@ extension String { return value } - guard let path = Bundle.main.path(forResource: "en", ofType: "lproj"), let bundle = Bundle(path: path) else { + guard let path = Bundle.main.path(forResource: "en", ofType: "lproj"), + let bundle = Bundle(path: path) else { return value } @@ -26,7 +27,7 @@ extension String { } func localized(_ args: CVarArg...) -> String { - return String.localizedStringWithFormat(localized, args) + String.localizedStringWithFormat(localized, args) } func condenseWhitespace() -> String { @@ -35,14 +36,14 @@ extension String { } func trim() -> String { - return trimmingCharacters(in: .whitespacesAndNewlines) + trimmingCharacters(in: .whitespacesAndNewlines) } func matchRegex(_ regex: String) -> Bool { do { let regex = try NSRegularExpression(pattern: regex, options: []) let matches = regex.matches(in: self, options: [], range: NSMakeRange(0, count)) - return matches.count > 0 + return !matches.isEmpty } catch { return false } @@ -69,18 +70,23 @@ extension String { func replaceBeforeLast(_ delimiter: Character, replacement: String) -> String { if let index = lastIndex(of: delimiter) { - return replacingOccurrences(of: String(delimiter), with: replacement, options: [], range: startIndex ..< index) + return replacingOccurrences( + of: String(delimiter), + with: replacement, + options: [], + range: startIndex.. CGFloat { let string = self as NSString let attr = [NSAttributedString.Key.font: font] - let rect = string.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attr, context: nil) + let rect = string.boundingRect( + with: CGSize( + width: CGFloat.greatestFiniteMagnitude, + height: CGFloat.greatestFiniteMagnitude + ), + options: [.usesLineFragmentOrigin, .usesFontLeading], + attributes: attr, + context: nil + ) let width = ceil(rect.size.width) if let maxWidth = maxWidth { @@ -115,12 +129,21 @@ extension String { } var md5: String { - return Insecure.MD5.hash(data: data(using: .utf8)!).map { String(format: "%02hhx", $0) }.joined() + Insecure.MD5.hash(data: data(using: .utf8)!).map { String(format: "%02hhx", $0) }.joined() } - func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range] { + func ranges( + of substring: String, + options: CompareOptions = [], + locale: Locale? = nil + ) -> [Range] { var ranges: [Range] = [] - while let range = range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex) ..< self.endIndex, locale: locale) { + while let range = range( + of: substring, + options: options, + range: (ranges.last?.upperBound ?? startIndex).. URL? { - guard let encodedString = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + guard let encodedString = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + else { return nil } @@ -174,7 +198,8 @@ extension String { extension String { var canOpenUrl: Bool { - guard let url = URL(string: self), UIApplication.shared.canOpenURL(url) else { return false } + guard let url = URL(string: self), + UIApplication.shared.canOpenURL(url) else { return false } let regEx = "((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+" let predicate = NSPredicate(format: "SELF MATCHES %@", argumentArray: [regEx]) return predicate.evaluate(with: self) @@ -191,7 +216,8 @@ extension String { return url } - guard let encodedString = trim().addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + guard let encodedString = trim() + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil } @@ -212,7 +238,7 @@ extension String { /// Determine string has hexadecimal prefix. /// - returns: `Bool` type. func hasHexPrefix() -> Bool { - return hasPrefix("0x") + hasPrefix("0x") } /// If string has hexadecimal prefix, remove it @@ -237,16 +263,28 @@ extension String { } extension String { + func hasPrefixes(_ prefixes: [String]) -> Bool { + prefixes.contains(where: hasPrefix) + } + func deletingPrefix(_ prefix: String) -> String { guard hasPrefix(prefix) else { return self } return String(dropFirst(prefix.count)) } + + func deletingPrefixes(_ prefixes: [String]) -> String { + prefixes.reduce(self) { $0.deletingPrefix($1) } + } } extension String { var isValidURL: Bool { let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { + if let match = detector.firstMatch( + in: self, + options: [], + range: NSRange(location: 0, length: utf16.count) + ) { // it is a link, if the match covers the whole string return match.range.length == utf16.count } else { diff --git a/FRW/UI/GradientButton/GradientButton.swift b/FRW/UI/GradientButton/GradientButton.swift index ccca1821..02c6eb36 100644 --- a/FRW/UI/GradientButton/GradientButton.swift +++ b/FRW/UI/GradientButton/GradientButton.swift @@ -1,5 +1,5 @@ // -// GridentButton.swift +// GradientButton.swift // Flow Wallet // // Created by Hao Fu on 6/9/2022. @@ -7,7 +7,11 @@ import SwiftUI +// MARK: - GradientButton + struct GradientButton: View { + // MARK: Internal + var buttonTitle: String var buttonAction: () -> Void var gradient1: [Color] = [ @@ -17,26 +21,28 @@ struct GradientButton: View { Color(red: 39 / 255, green: 232 / 255, blue: 1), ] - @State private var angle: Double = 0 - var body: some View { Button(action: buttonAction, label: { GeometryReader { geometry in ZStack { - AngularGradient(gradient: Gradient(colors: gradient1), center: .center, angle: .degrees(angle)) - .blendMode(.overlay) - .blur(radius: 8.0) - .mask( - RoundedRectangle(cornerRadius: 16) - .frame(maxWidth: geometry.size.width - 15) - .frame(height: 50) - .blur(radius: 8) - ) - .onAppear { - withAnimation(.linear(duration: 7)) { - self.angle += 350 - } + AngularGradient( + gradient: Gradient(colors: gradient1), + center: .center, + angle: .degrees(angle) + ) + .blendMode(.overlay) + .blur(radius: 8.0) + .mask( + RoundedRectangle(cornerRadius: 16) + .frame(maxWidth: geometry.size.width - 15) + .frame(height: 50) + .blur(radius: 8) + ) + .onAppear { + withAnimation(.linear(duration: 7)) { + self.angle += 350 } + } GradientText(text: buttonTitle) .font(.headline) .frame(maxWidth: geometry.size.width - 15) @@ -57,8 +63,15 @@ struct GradientButton: View { .frame(height: 50) }).buttonStyle(ScaleButtonStyle()) } + + // MARK: Private + + @State + private var angle: Double = 0 } +// MARK: - GradientButton_Previews + struct GradientButton_Previews: PreviewProvider { static var previews: some View { GradientButton(buttonTitle: "Hello") { @@ -67,6 +80,8 @@ struct GradientButton_Previews: PreviewProvider { } } +// MARK: - GradientText + struct GradientText: View { var text: String = "Text here" @@ -76,11 +91,13 @@ struct GradientText: View { } } -public extension View { - func gradientForeground(colors: [Color]) -> some View { - overlay(LinearGradient(gradient: .init(colors: colors), - startPoint: .topLeading, - endPoint: .bottomTrailing)) - .mask(self) +extension View { + public func gradientForeground(colors: [Color]) -> some View { + overlay(LinearGradient( + gradient: .init(colors: colors), + startPoint: .topLeading, + endPoint: .bottomTrailing + )) + .mask(self) } } diff --git a/FRW/UI/ImageViewer/ImageViewer.swift b/FRW/UI/ImageViewer/ImageViewer.swift index 60a1c9d7..7e5daaef 100644 --- a/FRW/UI/ImageViewer/ImageViewer.swift +++ b/FRW/UI/ImageViewer/ImageViewer.swift @@ -9,29 +9,21 @@ import Kingfisher import SwiftUI import UIKit +// MARK: - ImageViewer + @available(iOS 13.0, *) public struct ImageViewer: View { - @Binding var viewerShown: Bool - var imageURL: String - @State var caption: Text? - @State var closeButtonTopRight: Bool? - - var backgroundColor: Color - var aspectRatio: Binding? - - @State var dragOffset = CGSize.zero - @State var dragOffsetPredicted = CGSize.zero - - var heroAnimation: Namespace.ID - - public init(imageURL: String, - viewerShown: Binding, - backgroundColor: Color = Color(red: 0.12, green: 0.12, blue: 0.12), - heroAnimation: Namespace.ID, - aspectRatio: Binding? = nil, - caption: Text? = nil, - closeButtonTopRight: Bool? = false) - { + // MARK: Lifecycle + + public init( + imageURL: String, + viewerShown: Binding, + backgroundColor: Color = Color(red: 0.12, green: 0.12, blue: 0.12), + heroAnimation: Namespace.ID, + aspectRatio: Binding? = nil, + caption: Text? = nil, + closeButtonTopRight: Bool? = false + ) { self.imageURL = imageURL self.backgroundColor = backgroundColor self.heroAnimation = heroAnimation @@ -41,10 +33,12 @@ public struct ImageViewer: View { _closeButtonTopRight = State(initialValue: closeButtonTopRight) } + // MARK: Public + @ViewBuilder public var body: some View { VStack { - if viewerShown && imageURL.count > 0 { + if viewerShown && !imageURL.isEmpty { ZStack { VStack { HStack { @@ -81,23 +75,35 @@ public struct ImageViewer: View { .offset(x: self.dragOffset.width, y: self.dragOffset.height) .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) .pinchToZoom() - .gesture(DragGesture() - .onChanged { value in - self.dragOffset = value.translation - self.dragOffsetPredicted = value.predictedEndTranslation - } - .onEnded { _ in - if (abs(self.dragOffset.height) + abs(self.dragOffset.width) > 270) || (abs(self.dragOffsetPredicted.height) / abs(self.dragOffset.height) > 2) || (abs(self.dragOffsetPredicted.width) / abs(self.dragOffset.width)) > 2 { - withAnimation(.spring()) { - self.dragOffset = self.dragOffsetPredicted - } - self.viewerShown = false - return + .gesture( + DragGesture() + .onChanged { value in + self.dragOffset = value.translation + self.dragOffsetPredicted = value.predictedEndTranslation } - withAnimation(.interactiveSpring()) { - self.dragOffset = .zero + .onEnded { _ in + if ( + abs(self.dragOffset.height) + + abs(self.dragOffset.width) > 270 + ) || + ( + abs(self.dragOffsetPredicted.height) / + abs(self.dragOffset.height) > 2 + ) || + ( + abs(self.dragOffsetPredicted.width) / + abs(self.dragOffset.width) + ) > 2 { + withAnimation(.spring()) { + self.dragOffset = self.dragOffsetPredicted + } + self.viewerShown = false + return + } + withAnimation(.interactiveSpring()) { + self.dragOffset = .zero + } } - } ) .matchedGeometryEffect(id: "imageView", in: heroAnimation) @@ -128,7 +134,14 @@ public struct ImageViewer: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background( backgroundColor - .opacity(1.0 - Double(abs(self.dragOffset.width) + abs(self.dragOffset.height)) / 1000) + .opacity( + 1.0 - + Double( + abs(self.dragOffset.width) + + abs(self.dragOffset.height) + ) / + 1000 + ) .edgesIgnoringSafeArea(.all) .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) ) @@ -147,9 +160,51 @@ public struct ImageViewer: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) } + + // MARK: Internal + + @Binding + var viewerShown: Bool + var imageURL: String + @State + var caption: Text? + @State + var closeButtonTopRight: Bool? + + var backgroundColor: Color + var aspectRatio: Binding? + + @State + var dragOffset = CGSize.zero + @State + var dragOffsetPredicted = CGSize.zero + + var heroAnimation: Namespace.ID } +// MARK: - PinchZoomView + class PinchZoomView: UIView { + // MARK: Lifecycle + + init() { + super.init(frame: .zero) + + let pinchGesture = UIPinchGestureRecognizer( + target: self, + action: #selector(pinch(gesture:)) + ) + pinchGesture.cancelsTouchesInView = false + addGestureRecognizer(pinchGesture) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + // MARK: Internal + weak var delegate: PinchZoomViewDelgate? private(set) var scale: CGFloat = 0 { @@ -176,37 +231,36 @@ class PinchZoomView: UIView { } } + // MARK: Private + private var startLocation: CGPoint = .zero private var location: CGPoint = .zero private var numberOfTouches: Int = 0 - init() { - super.init(frame: .zero) - - let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:))) - pinchGesture.cancelsTouchesInView = false - addGestureRecognizer(pinchGesture) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - @objc private func pinch(gesture: UIPinchGestureRecognizer) { + @objc + private func pinch(gesture: UIPinchGestureRecognizer) { switch gesture.state { case .began: isPinching = true startLocation = gesture.location(in: self) - anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height) + anchor = UnitPoint( + x: startLocation.x / bounds.width, + y: startLocation.y / bounds.height + ) numberOfTouches = gesture.numberOfTouches case .changed: if gesture.numberOfTouches != numberOfTouches { // If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping. let newLocation = gesture.location(in: self) - let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y) - startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height) + let jumpDifference = CGSize( + width: newLocation.x - location.x, + height: newLocation.y - location.y + ) + startLocation = CGPoint( + x: startLocation.x + jumpDifference.width, + y: startLocation.y + jumpDifference.height + ) numberOfTouches = gesture.numberOfTouches } @@ -214,7 +268,10 @@ class PinchZoomView: UIView { scale = gesture.scale location = gesture.location(in: self) - offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y) + offset = CGSize( + width: location.x - startLocation.x, + height: location.y - startLocation.y + ) case .ended, .cancelled, .failed: withAnimation(.interactiveSpring()) { @@ -223,12 +280,15 @@ class PinchZoomView: UIView { anchor = .center offset = .zero } + default: break } } } +// MARK: - PinchZoomViewDelgate + protocol PinchZoomViewDelgate: AnyObject { func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) @@ -236,31 +296,20 @@ protocol PinchZoomViewDelgate: AnyObject { func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) } -struct PinchZoom: UIViewRepresentable { - @Binding var scale: CGFloat - @Binding var anchor: UnitPoint - @Binding var offset: CGSize - @Binding var isPinching: Bool - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - func makeUIView(context: Context) -> PinchZoomView { - let pinchZoomView = PinchZoomView() - pinchZoomView.delegate = context.coordinator - return pinchZoomView - } - - func updateUIView(_: PinchZoomView, context _: Context) {} +// MARK: - PinchZoom +struct PinchZoom: UIViewRepresentable { class Coordinator: NSObject, PinchZoomViewDelgate { - var pinchZoom: PinchZoom + // MARK: Lifecycle init(_ pinchZoom: PinchZoom) { self.pinchZoom = pinchZoom } + // MARK: Internal + + var pinchZoom: PinchZoom + func pinchZoomView(_: PinchZoomView, didChangePinching isPinching: Bool) { pinchZoom.isPinching = isPinching } @@ -277,19 +326,51 @@ struct PinchZoom: UIViewRepresentable { pinchZoom.offset = offset } } + + @Binding + var scale: CGFloat + @Binding + var anchor: UnitPoint + @Binding + var offset: CGSize + @Binding + var isPinching: Bool + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> PinchZoomView { + let pinchZoomView = PinchZoomView() + pinchZoomView.delegate = context.coordinator + return pinchZoomView + } + + func updateUIView(_: PinchZoomView, context _: Context) {} } +// MARK: - PinchToZoom + struct PinchToZoom: ViewModifier { - @State var scale: CGFloat = 1.0 - @State var anchor: UnitPoint = .center - @State var offset: CGSize = .zero - @State var isPinching: Bool = false + @State + var scale: CGFloat = 1.0 + @State + var anchor: UnitPoint = .center + @State + var offset: CGSize = .zero + @State + var isPinching: Bool = false func body(content: Content) -> some View { content .scaleEffect(scale, anchor: anchor) .offset(offset) - .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) + .overlay(PinchZoom( + scale: $scale, + anchor: $anchor, + offset: $offset, + isPinching: $isPinching + )) } } diff --git a/FRW/UI/SVG/SVGUIView.swift b/FRW/UI/SVG/SVGUIView.swift index e2439546..28fb3dc6 100644 --- a/FRW/UI/SVG/SVGUIView.swift +++ b/FRW/UI/SVG/SVGUIView.swift @@ -1,5 +1,5 @@ // -// Untitled.swift +// SVGUIView.swift // FRW // // Created by cat on 2024/9/23. @@ -8,13 +8,25 @@ import WebKit class SVGUIView: UIView { - var svg: String? - private var html: String = "" + // MARK: Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + buildView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + buildView() + } + // MARK: Internal + + var svg: String? lazy var webView: WKWebView = { let prefs = WKPreferences() #if os(macOS) - if #available(macOS 10.5, *) {} else { prefs.javaEnabled = false } + if #available(macOS 10.5, *) {} else { prefs.javaEnabled = false } #endif if #available(macOS 11, *) {} else { prefs.javaScriptEnabled = false } prefs.javaScriptCanOpenWindowsAutomatically = false @@ -24,7 +36,8 @@ class SVGUIView: UIView { config.allowsAirPlayForMediaPlayback = false let bodyStyle = "body { margin:0; }" - let source = "var node = document.createElement(\"style\"); node.innerHTML = \"\(bodyStyle)\";document.body.appendChild(node);" + let source = + "var node = document.createElement(\"style\"); node.innerHTML = \"\(bodyStyle)\";document.body.appendChild(node);" let script = WKUserScript( source: source, @@ -48,7 +61,7 @@ class SVGUIView: UIView { let webView = WKWebView(frame: .zero, configuration: config) #if !os(macOS) - webView.scrollView.isScrollEnabled = false + webView.scrollView.isScrollEnabled = false #endif // Sometimes necessary to make things show up initially. No idea why. @@ -66,20 +79,6 @@ class SVGUIView: UIView { return webView }() - override init(frame: CGRect) { - super.init(frame: frame) - buildView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - buildView() - } - - private func buildView() { - addSubviews(webView) - } - override func layoutSubviews() { super.layoutSubviews() webView.frame = bounds @@ -90,25 +89,36 @@ class SVGUIView: UIView { webView.loadHTMLString(html, baseURL: nil) } + // MARK: Private + + private var html: String = "" + + private func buildView() { + addSubviews(webView) + } + /// A hacky way to patch the size in the SVG root tag. private func rewriteSVGSize(_ string: String) -> String { guard let startRange = string.range(of: "", range: remainder) else { return string } - let tagRange = startRange.lowerBound ..< endRange.upperBound + let tagRange = startRange.lowerBound.. String { guard let startRange = string.range(of: "", range: remainder) else { return string } - let tagRange = startRange.lowerBound ..< endRange.upperBound + let tagRange = startRange.lowerBound..\(rewriteSVGSize(svg))" ) } - /// A hacky way to patch the size in the SVG root tag. - private func rewriteSVGSize(_ string: String) -> String { - guard let startRange = string.range(of: "", range: remainder) else { - return string - } + // MARK: Internal - let tagRange = startRange.lowerBound ..< endRange.upperBound - let oldTag = string[tagRange] + #if os(macOS) + typealias UXViewRepresentable = NSViewRepresentable + #else + typealias UXViewRepresentable = UIViewRepresentable + #endif - var attrs: [String: String] = { - final class Handler: NSObject, XMLParserDelegate { - var attrs: [String: String]? + // MARK: Private - func parser(_: XMLParser, didStartElement _: String, - namespaceURI _: String?, qualifiedName _: String?, - attributes: [String: String]) - { - self.attrs = attributes - } - } - let parser = XMLParser(data: Data((string[tagRange] + "").utf8)) - let handler = Handler() - parser.delegate = handler + private struct WebView: UXViewRepresentable { + // MARK: Internal - guard parser.parse() else { return [:] } - return handler.attrs ?? [:] - }() + let html: String - if attrs["viewBox"] == nil && - (attrs["width"] != nil || attrs["height"] != nil) - { // convert to viewBox - let w = attrs.removeValue(forKey: "width") ?? "100%" - let h = attrs.removeValue(forKey: "height") ?? "100%" - let x = attrs.removeValue(forKey: "x") ?? "0" - let y = attrs.removeValue(forKey: "y") ?? "0" - attrs["viewBox"] = "\(x) \(y) \(w) \(h)" + #if os(macOS) + func makeNSView(context _: Context) -> WKWebView { + makeWebView() } - attrs.removeValue(forKey: "x") - attrs.removeValue(forKey: "y") - attrs["width"] = "100%" - attrs["height"] = "100%" - func renderTag(_ tag: String, attributes: [String: String]) -> String { - var ms = "<\(tag)" - for (key, value) in attributes { - ms += " \(key)=\"" - ms += value - .replacingOccurrences(of: "&", with: "&") - .replacingOccurrences(of: "<", with: "<") - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "'", with: "'") - .replacingOccurrences(of: "\"", with: """) - ms += "\"" - } - ms += ">" - return ms + func updateNSView(_ webView: WKWebView, context: Context) { + updateWebView(webView, context: context) + } + #else // iOS etc + func makeUIView(context _: Context) -> WKWebView { + makeWebView() } - let newTag = renderTag("svg", attributes: attrs) - return newTag == oldTag - ? string - : string.replacingCharacters(in: tagRange, with: newTag) - } - - #if os(macOS) - typealias UXViewRepresentable = NSViewRepresentable - #else - typealias UXViewRepresentable = UIViewRepresentable - #endif + func updateUIView(_ webView: WKWebView, context: Context) { + updateWebView(webView, context: context) + } + #endif - private struct WebView: UXViewRepresentable { - let html: String + // MARK: Private private func makeWebView() -> WKWebView { let prefs = WKPreferences() #if os(macOS) - if #available(macOS 10.5, *) {} else { prefs.javaEnabled = false } + if #available(macOS 10.5, *) {} else { prefs.javaEnabled = false } #endif if #available(macOS 11, *) {} else { prefs.javaScriptEnabled = false } prefs.javaScriptCanOpenWindowsAutomatically = false @@ -120,7 +85,8 @@ public struct SVGWebView: View { config.allowsAirPlayForMediaPlayback = false let bodyStyle = "body { margin:0; }" - let source = "var node = document.createElement(\"style\"); node.innerHTML = \"\(bodyStyle)\";document.body.appendChild(node);" + let source = + "var node = document.createElement(\"style\"); node.innerHTML = \"\(bodyStyle)\";document.body.appendChild(node);" let script = WKUserScript( source: source, @@ -144,7 +110,7 @@ public struct SVGWebView: View { let webView = WKWebView(frame: .zero, configuration: config) #if !os(macOS) - webView.scrollView.isScrollEnabled = false + webView.scrollView.isScrollEnabled = false #endif webView.loadHTMLString(html, baseURL: nil) @@ -167,30 +133,85 @@ public struct SVGWebView: View { private func updateWebView(_ webView: WKWebView, context _: Context) { webView.loadHTMLString(html, baseURL: nil) } + } - #if os(macOS) - func makeNSView(context _: Context) -> WKWebView { - return makeWebView() - } + private let svg: String - func updateNSView(_ webView: WKWebView, context: Context) { - updateWebView(webView, context: context) - } - #else // iOS etc - func makeUIView(context _: Context) -> WKWebView { - return makeWebView() + /// A hacky way to patch the size in the SVG root tag. + private func rewriteSVGSize(_ string: String) -> String { + guard let startRange = string.range(of: "", range: remainder) else { + return string + } + + let tagRange = startRange.lowerBound..").utf8)) + let handler = Handler() + parser.delegate = handler + + guard parser.parse() else { return [:] } + return handler.attrs ?? [:] + }() - func updateUIView(_ webView: WKWebView, context: Context) { - updateWebView(webView, context: context) + if attrs["viewBox"] == nil && + (attrs["width"] != nil || attrs["height"] != nil) { // convert to viewBox + let w = attrs.removeValue(forKey: "width") ?? "100%" + let h = attrs.removeValue(forKey: "height") ?? "100%" + let x = attrs.removeValue(forKey: "x") ?? "0" + let y = attrs.removeValue(forKey: "y") ?? "0" + attrs["viewBox"] = "\(x) \(y) \(w) \(h)" + } + attrs.removeValue(forKey: "x") + attrs.removeValue(forKey: "y") + attrs["width"] = "100%" + attrs["height"] = "100%" + + func renderTag(_ tag: String, attributes: [String: String]) -> String { + var ms = "<\(tag)" + for (key, value) in attributes { + ms += " \(key)=\"" + ms += value + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "'", with: "'") + .replacingOccurrences(of: "\"", with: """) + ms += "\"" } - #endif + ms += ">" + return ms + } + + let newTag = renderTag("svg", attributes: attrs) + return newTag == oldTag + ? string + : string.replacingCharacters(in: tagRange, with: newTag) } } +// MARK: - SVGWebView_Previews + struct SVGWebView_Previews: PreviewProvider { static var previews: some View { - SVGWebView(svg: + SVGWebView( + svg: """ @@ -213,7 +235,8 @@ struct SVGWebView_Previews: PreviewProvider { - """) - .frame(width: 200, height: 200) + """ + ) + .frame(width: 200, height: 200) } } diff --git a/FRW/UI/Share/ShareActivityItemSource.swift b/FRW/UI/Share/ShareActivityItemSource.swift index aa334414..d7e7eb5b 100644 --- a/FRW/UI/Share/ShareActivityItemSource.swift +++ b/FRW/UI/Share/ShareActivityItemSource.swift @@ -1,5 +1,5 @@ // -// ShareItem.swift +// ShareActivityItemSource.swift // Flow Wallet // // Created by Hao Fu on 6/9/2022. @@ -10,9 +10,7 @@ import LinkPresentation import UIKit class ShareActivityItemSource: NSObject, UIActivityItemSource { - var shareText: String - var shareImage: UIImage - var linkMetaData = LPLinkMetadata() + // MARK: Lifecycle init(shareText: String, shareImage: UIImage) { self.shareText = shareText @@ -22,15 +20,24 @@ class ShareActivityItemSource: NSObject, UIActivityItemSource { super.init() } + // MARK: Internal + + var shareText: String + var shareImage: UIImage + var linkMetaData = LPLinkMetadata() + func activityViewControllerPlaceholderItem(_: UIActivityViewController) -> Any { - return UIImage(named: "AppIcon") as Any + UIImage(named: "AppIcon") as Any } - func activityViewController(_: UIActivityViewController, itemForActivityType _: UIActivity.ActivityType?) -> Any? { - return nil + func activityViewController( + _: UIActivityViewController, + itemForActivityType _: UIActivity.ActivityType? + ) -> Any? { + nil } func activityViewControllerLinkMetadata(_: UIActivityViewController) -> LPLinkMetadata? { - return linkMetaData + linkMetaData } } diff --git a/FRW/UI/UIKit/BetterAlertController.swift b/FRW/UI/UIKit/BetterAlertController.swift index b2625ad8..0f87a52c 100644 --- a/FRW/UI/UIKit/BetterAlertController.swift +++ b/FRW/UI/UIKit/BetterAlertController.swift @@ -7,17 +7,19 @@ import UIKit -class BetterAlertController : UIAlertController { - +class BetterAlertController: UIAlertController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let screenBounds = UIScreen.main.bounds - if (preferredStyle == .actionSheet) { - self.view.center = CGPointMake(screenBounds.size.width*0.5, screenBounds.size.height - (self.view.frame.size.height*0.5) - 8) + if preferredStyle == .actionSheet { + view.center = CGPointMake( + screenBounds.size.width * 0.5, + screenBounds.size.height - (view.frame.size.height * 0.5) - 8 + ) } else { - self.view.center = CGPointMake(screenBounds.size.width*0.5, screenBounds.size.height*0.5) + view.center = CGPointMake(screenBounds.size.width * 0.5, screenBounds.size.height * 0.5) } } } diff --git a/FRWTests/SymmetricEncryptionTests.swift b/FRWTests/SymmetricEncryptionTests.swift index 8ece2546..19dbd8e1 100644 --- a/FRWTests/SymmetricEncryptionTests.swift +++ b/FRWTests/SymmetricEncryptionTests.swift @@ -1,5 +1,5 @@ // -// DapperProTests.swift +// SymmetricEncryptionTests.swift // DapperProTests // // Created by Hao Fu on 7/10/2022. @@ -11,9 +11,11 @@ import XCTest final class SymmetricEncryptionTests: XCTestCase { let key = "0123456789" let seedPhrase = "upgrade snack buzz employ female cute quote kit rack couple toddler glare" - let base64Encrypted = "R1GTsKkvL8lL1MTztxur3NDAvaJv6g6adciYhRxe/Jg1/aY87WbNzdwV2HhWpfSAn6AwezSOZ+nhJLmP1Ck37Zx4SBXU14rVW1Lzw8vcxfLJRSDEW3Cmx4N8jlx78xyMrQCEJTM=" + let base64Encrypted = + "R1GTsKkvL8lL1MTztxur3NDAvaJv6g6adciYhRxe/Jg1/aY87WbNzdwV2HhWpfSAn6AwezSOZ+nhJLmP1Ck37Zx4SBXU14rVW1Lzw8vcxfLJRSDEW3Cmx4N8jlx78xyMrQCEJTM=" - let AESBase64Encrypted = "W1+ejoJBIqZ1vwDxWeic38QjTjXeP0p827gzwHOw5v9YTFQltrvlEaGa336AUAbJbZxfUFBVIgLQbIelN2FQ6rlUUkP7TWIIGD6rdjkr7GSFtCTjHqvkxyTLVlMGeKDIYX9md3I=" + let AESBase64Encrypted = + "W1+ejoJBIqZ1vwDxWeic38QjTjXeP0p827gzwHOw5v9YTFQltrvlEaGa336AUAbJbZxfUFBVIgLQbIelN2FQ6rlUUkP7TWIIGD6rdjkr7GSFtCTjHqvkxyTLVlMGeKDIYX9md3I=" // MARK: - ChaChaPoly