From 46521950a53c1f1fa0467daf63fc7493b51602e3 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 4 Jun 2024 12:40:42 +0200 Subject: [PATCH 01/68] Refactor ResetWallet --- .../ResetWalletClient+Live.swift | 17 +++++++++++++---- .../Features/MainFeature/Main+Reducer.swift | 13 +++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift index ce91386da3..6deb9b97b2 100644 --- a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift +++ b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift @@ -4,16 +4,25 @@ extension ResetWalletClient: DependencyKey { public static let liveValue: Self = { let walletDidResetSubject = AsyncPassthroughSubject() + @Dependency(\.errorQueue) var errorQueue + @Dependency(\.appPreferencesClient) var appPreferencesClient @Dependency(\.cacheClient) var cacheClient @Dependency(\.radixConnectClient) var radixConnectClient @Dependency(\.userDefaults) var userDefaults return Self( resetWallet: { - cacheClient.removeAll() - await radixConnectClient.disconnectAll() - userDefaults.removeAll() - walletDidResetSubject.send(()) + do { + // TODO: Is this the best order? + try await appPreferencesClient.deleteProfileAndFactorSources(true) + cacheClient.removeAll() + await radixConnectClient.disconnectAll() + userDefaults.removeAll() + walletDidResetSubject.send(()) + } catch { + loggerGlobal.error("Failed to delete profile: \(error)") + errorQueue.schedule(error) + } }, walletDidReset: { walletDidResetSubject.eraseToAnyAsyncSequence() diff --git a/RadixWallet/Features/MainFeature/Main+Reducer.swift b/RadixWallet/Features/MainFeature/Main+Reducer.swift index 0cfcf5e1bd..1f48173ce5 100644 --- a/RadixWallet/Features/MainFeature/Main+Reducer.swift +++ b/RadixWallet/Features/MainFeature/Main+Reducer.swift @@ -100,13 +100,14 @@ public struct Main: Sendable, FeatureReducer { private func didResetWalletEffect() -> Effect { .run { send in - for try await _ in resetWalletClient.walletDidReset() { - guard !Task.isCancelled else { return } - try await appPreferencesClient.deleteProfileAndFactorSources(true) - await send(.delegate(.removedWallet)) + do { + for try await _ in resetWalletClient.walletDidReset() { + guard !Task.isCancelled else { return } + await send(.delegate(.removedWallet)) + } + } catch { + loggerGlobal.error("Failed to iterate over walletDidReset: \(error)") } - } catch: { error, _ in - loggerGlobal.error("Failed to delete profile: \(error)") } } From 872ef7ccee401e178da944b7aac12cfdcd64e5ce Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 4 Jun 2024 12:48:47 +0200 Subject: [PATCH 02/68] wip --- .../CloudBackupClient+Live.swift | 27 ++++++++++++++++--- .../Clients/ProfileStore/ProfileStore.swift | 4 +++ .../AppFeature/Overlay/Overlay+Reducer.swift | 4 +++ .../AppFeature/Overlay/Overlay+View.swift | 9 +++---- .../Features/MainFeature/Main+Reducer.swift | 8 +++++- .../RestoreProfileFromBackupCoordinator.swift | 12 +++++---- 6 files changed, 49 insertions(+), 15 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 04595acd91..d11bfc8d1c 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -55,6 +55,7 @@ extension CloudBackupClient { public static func live( profileStore: ProfileStore = .shared ) -> CloudBackupClient { + @Dependency(\.overlayWindowClient) var overlayWindowClient @Dependency(\.secureStorageClient) var secureStorageClient @Dependency(\.userDefaults) var userDefaults @@ -144,11 +145,31 @@ extension CloudBackupClient { return .init( startAutomaticBackups: { - let timer = AsyncTimerSequence(every: .seconds(60)) + let ticks = AsyncTimerSequence(every: .seconds(1)) let profiles = await profileStore.values() + var lastClaimCheck: Date = .now // .distantPast + + print("•• START automaticBackups") + for try await (profile, tick) in combineLatest(profiles, ticks) { + guard !Task.isCancelled else { print("•• CANCEL automaticBackups"); return } + + print("•• tick") + + if tick.timeIntervalSince(lastClaimCheck) > 15 { + print("•• time to check claims") + lastClaimCheck = tick + + let backedUpHeader = try? await getProfileHeader(fetchProfileRecord(.init(recordName: profile.id.uuidString))) + + if true /* let backedUpHeader, await !profileStore.isThisDevice(deviceID: backedUpHeader.lastUsedOnDevice.id) */ { + print("•• different IDs") + // TODO: Cancel/pause task? + overlayWindowClient.scheduleFullScreenIgnoreAction(.init(root: .claimWallet(.init()))) + } else { + print("•• same IDs") + } + } - for try await (profile, _) in combineLatest(profiles, timer) { - guard !Task.isCancelled else { return } guard profile.appPreferences.security.isCloudProfileSyncEnabled else { continue } guard profile.header.isNonEmpty else { continue } diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index a8d1fc3d05..fe25f0747d 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -253,6 +253,10 @@ extension ProfileStore { try importProfile(profile) } + public func isThisDevice(deviceID: DeviceID) -> Bool { + deviceID == deviceInfo.id + } + public func unlockedApp() async -> Profile { loggerGlobal.notice("Unlocking app") let buffered = bufferedOwnershipConflictWhileAppLocked diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift index 1c337754c3..742f90f08c 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift @@ -90,6 +90,10 @@ struct OverlayReducer: Sendable, FeatureReducer { case .hud(.delegate(.dismiss)): return dismiss(&state) + case let .fullScreen(.child(.root(fullScreenAction))): + print("•• fullScreenAction: \(fullScreenAction)") + return .none + case .fullScreen(.delegate(.dismiss)): return dismiss(&state) diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+View.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+View.swift index 1fd409617f..dc31fdb8cb 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+View.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+View.swift @@ -11,12 +11,9 @@ extension OverlayReducer { } var body: some SwiftUI.View { - IfLetStore( - store.destination, - state: /OverlayReducer.Destination.State.hud, - action: OverlayReducer.Destination.Action.hud, - then: { HUD.View(store: $0) } - ) + IfLetStore(store.destination.scope(state: \.hud, action: \.hud)) { + HUD.View(store: $0) + } .destinations(with: store) .task { store.send(.view(.task)) } } diff --git a/RadixWallet/Features/MainFeature/Main+Reducer.swift b/RadixWallet/Features/MainFeature/Main+Reducer.swift index 1f48173ce5..c150fc5ddd 100644 --- a/RadixWallet/Features/MainFeature/Main+Reducer.swift +++ b/RadixWallet/Features/MainFeature/Main+Reducer.swift @@ -49,11 +49,11 @@ public struct Main: Sendable, FeatureReducer { } } - @Dependency(\.appPreferencesClient) var appPreferencesClient @Dependency(\.gatewaysClient) var gatewaysClient @Dependency(\.personasClient) var personasClient @Dependency(\.cloudBackupClient) var cloudBackupClient @Dependency(\.resetWalletClient) var resetWalletClient + @Dependency(\.overlayWindowClient) var overlayWindowClient public init() {} @@ -75,6 +75,12 @@ public struct Main: Sendable, FeatureReducer { startAutomaticBackupsEffect() .merge(with: gatewayValuesEffect()) .merge(with: didResetWalletEffect()) + .merge(with: overlayActionEffect()) + } + } + + private func overlayActionEffect() -> Effect { + .run { _ in } } diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift index 7264744ada..a4f54497ad 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift @@ -22,22 +22,23 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { } public struct Path: Sendable, Hashable, Reducer { + @CasePathable public enum State: Sendable, Hashable { case selectBackup(SelectBackup.State) case importMnemonicsFlow(ImportMnemonicsFlowCoordinator.State) } + @CasePathable public enum Action: Sendable, Equatable { case selectBackup(SelectBackup.Action) case importMnemonicsFlow(ImportMnemonicsFlowCoordinator.Action) } public var body: some ReducerOf { - Scope(state: /State.selectBackup, action: /Action.selectBackup) { + Scope(state: \.selectBackup, action: \.selectBackup) { SelectBackup() } - - Scope(state: /State.importMnemonicsFlow, action: /Action.importMnemonicsFlow) { + Scope(state: \.importMnemonicsFlow, action: \.importMnemonicsFlow) { ImportMnemonicsFlowCoordinator() } } @@ -47,6 +48,7 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { case delayedAppendToPath(RestoreProfileFromBackupCoordinator.Path.State) } + @CasePathable public enum ChildAction: Sendable, Equatable { case root(Path.Action) case path(StackActionOf) @@ -68,12 +70,12 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { public init() {} public var body: some ReducerOf { - Scope(state: \.root, action: /Action.child .. ChildAction.root) { + Scope(state: \.root, action: \.child.root) { Path() } Reduce(core) - .forEach(\.path, action: /Action.child .. ChildAction.path) { + .forEach(\.path, action: \.child.path) { Path() } } From 5d4d434f7f9d1a2ca6697a3dc38b5fdd445e8d9f Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 4 Jun 2024 13:16:06 +0200 Subject: [PATCH 03/68] wip --- .../Clients/ResetWalletClient/ResetWalletClient+Live.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift index 6deb9b97b2..c489ea4350 100644 --- a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift +++ b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift @@ -13,6 +13,7 @@ extension ResetWalletClient: DependencyKey { return Self( resetWallet: { do { + print("•• resetWallet") // TODO: Is this the best order? try await appPreferencesClient.deleteProfileAndFactorSources(true) cacheClient.removeAll() From 945aca8ae9f18cf664a0d880ab60a1ef4d2c80b8 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Wed, 5 Jun 2024 12:10:06 +0200 Subject: [PATCH 04/68] wip --- .../CloudBackupClient+Live.swift | 9 ++- .../LedgerHardwareWalletClient+Live.swift | 2 +- .../OverlayWindowClient+Interface.swift | 38 +++++++----- .../OverlayWindowClient+Live.swift | 19 +++--- .../OverlayWindowClient+Test.swift | 7 ++- .../Clients/ProfileStore/ProfileStore.swift | 2 +- .../ResetWalletClient+Live.swift | 1 + .../SecureStorageClient+Live.swift | 2 +- .../Features/AppFeature/App+Reducer.swift | 62 ++++++++++++++----- .../Features/AppFeature/App+View.swift | 5 +- ...FullScreenOverlayCoordinator+Reducer.swift | 18 ++++-- .../AppFeature/Overlay/Overlay+Reducer.swift | 31 ++++------ .../ClaimWallet/ClaimWallet+Reducer.swift | 10 ++- .../Features/MainFeature/Main+Reducer.swift | 35 +++-------- .../Features/MainFeature/Main+View.swift | 11 ++-- 15 files changed, 143 insertions(+), 109 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index d11bfc8d1c..d0258b60cf 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -163,8 +163,13 @@ extension CloudBackupClient { if true /* let backedUpHeader, await !profileStore.isThisDevice(deviceID: backedUpHeader.lastUsedOnDevice.id) */ { print("•• different IDs") - // TODO: Cancel/pause task? - overlayWindowClient.scheduleFullScreenIgnoreAction(.init(root: .claimWallet(.init()))) + + print("•• show fullscreen") + + let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) + + print("•• got fullscreen action \(action)") + } else { print("•• same IDs") } diff --git a/RadixWallet/Clients/LedgerHardwareWalletClient/LedgerHardwareWalletClient+Live.swift b/RadixWallet/Clients/LedgerHardwareWalletClient/LedgerHardwareWalletClient+Live.swift index 89fbe37346..02cc310273 100644 --- a/RadixWallet/Clients/LedgerHardwareWalletClient/LedgerHardwareWalletClient+Live.swift +++ b/RadixWallet/Clients/LedgerHardwareWalletClient/LedgerHardwareWalletClient+Live.swift @@ -50,7 +50,7 @@ extension LedgerHardwareWalletClient: DependencyKey { switch errorFromConnectorExtension.code { case .generic: break case .blindSigningNotEnabledButRequired: - overlayWindowClient.scheduleAlertIgnoreAction( + overlayWindowClient.scheduleAlertAndIgnoreAction( .init( title: { TextState(L10n.LedgerHardwareDevices.CouldNotSign.title) diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift index 212643a354..a3fc636853 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift @@ -6,8 +6,10 @@ public struct OverlayWindowClient: Sendable { /// Schedule an Alert to be shown in the Overlay Window. /// Usually to be called from the Main Window. - public var scheduleAlertIgnoreAction: ScheduleAlertIgnoreAction - public var scheduleAlertAwaitAction: ScheduleAlertAwaitAction + public var scheduleAlert: ScheduleAlert + + /// Schedule an Alert to be shown in the Overlay Window, but don't want for any action + public var scheduleAlertAndIgnoreAction: ScheduleAlertAndIgnoreAction /// Schedule a HUD to be shown in the Overlay Window. /// Usually to be called from the Main Window. @@ -15,42 +17,50 @@ public struct OverlayWindowClient: Sendable { /// Schedule a FullScreen to be shown in the Overlay Window. /// Usually to be called from the Main Window. - public var scheduleFullScreenIgnoreAction: ScheduleFullScreenIgnoreAction + public var scheduleFullScreen: ScheduleFullScreen - /// This is meant to be used by the Overlay Window to send - /// back the actions from an Alert to the Main Window. + /// Used by the Overlay Window to send actions from an Alert back to the client public var sendAlertAction: SendAlertAction + /// Used by the Overlay Window to send actions from an FullScreenOverlay back to the client + public var sendFullScreenAction: SendFullScreenAction + public var setIsUserIteractionEnabled: SetIsUserIteractionEnabled public var isUserInteractionEnabled: IsUserInteractionEnabled public init( scheduledItems: @escaping ScheduledItems, - scheduleAlertIgnoreAction: @escaping ScheduleAlertIgnoreAction, - scheduleAlertAwaitAction: @escaping ScheduleAlertAwaitAction, + scheduleAlert: @escaping ScheduleAlert, + scheduleAlertAndIgnoreAction: @escaping ScheduleAlertAndIgnoreAction, scheduleHUD: @escaping ScheduleHUD, - scheduleFullScreenIgnoreAction: @escaping ScheduleFullScreenIgnoreAction, + scheduleFullScreen: @escaping ScheduleFullScreen, sendAlertAction: @escaping SendAlertAction, + sendFullScreenAction: @escaping SendFullScreenAction, setIsUserIteractionEnabled: @escaping SetIsUserIteractionEnabled, isUserInteractionEnabled: @escaping IsUserInteractionEnabled ) { self.scheduledItems = scheduledItems - self.scheduleAlertIgnoreAction = scheduleAlertIgnoreAction - self.scheduleAlertAwaitAction = scheduleAlertAwaitAction + self.scheduleAlert = scheduleAlert + self.scheduleAlertAndIgnoreAction = scheduleAlertAndIgnoreAction self.scheduleHUD = scheduleHUD - self.scheduleFullScreenIgnoreAction = scheduleFullScreenIgnoreAction + self.scheduleFullScreen = scheduleFullScreen self.sendAlertAction = sendAlertAction + self.sendFullScreenAction = sendFullScreenAction self.setIsUserIteractionEnabled = setIsUserIteractionEnabled self.isUserInteractionEnabled = isUserInteractionEnabled } } extension OverlayWindowClient { - public typealias ScheduleAlertIgnoreAction = @Sendable (Item.AlertState) -> Void - public typealias ScheduleAlertAwaitAction = @Sendable (Item.AlertState) async -> Item.AlertAction + public typealias FullScreenAction = FullScreenOverlayCoordinator.Root.Action + public typealias FullScreenID = FullScreenOverlayCoordinator.State.ID + + public typealias ScheduleAlert = @Sendable (Item.AlertState) async -> Item.AlertAction + public typealias ScheduleAlertAndIgnoreAction = @Sendable (Item.AlertState) -> Void public typealias ScheduleHUD = @Sendable (Item.HUD) -> Void - public typealias ScheduleFullScreenIgnoreAction = @Sendable (FullScreenOverlayCoordinator.State) -> Void + public typealias ScheduleFullScreen = @Sendable (FullScreenOverlayCoordinator.State) async -> FullScreenAction public typealias SendAlertAction = @Sendable (Item.AlertAction, Item.AlertState.ID) -> Void + public typealias SendFullScreenAction = @Sendable (FullScreenAction, FullScreenID) -> Void public typealias ScheduledItems = @Sendable () -> AnyAsyncSequence public typealias SetIsUserIteractionEnabled = @Sendable (Bool) -> Void diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift index c2d8e700ab..2202bd829e 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift @@ -3,6 +3,7 @@ extension OverlayWindowClient: DependencyKey { public static let liveValue: Self = { let items = AsyncPassthroughSubject() let alertActions = AsyncPassthroughSubject<(action: Item.AlertAction, id: Item.AlertState.ID)>() + let fullScreenActions = AsyncPassthroughSubject<(action: FullScreenAction, id: FullScreenID)>() let isUserInteractionEnabled = AsyncPassthroughSubject() @Dependency(\.errorQueue) var errorQueue @@ -18,24 +19,24 @@ extension OverlayWindowClient: DependencyKey { pasteBoardClient.copyEvents().map { _ in Item.hud(.copied) }.subscribe(items) - let scheduleAlertIgnoreAction: ScheduleAlertIgnoreAction = { alert in + let scheduleAlertAndIgnoreAction: ScheduleAlertAndIgnoreAction = { alert in items.send(.alert(alert)) } - let scheduleFullScreenIgnoreAction: ScheduleFullScreenIgnoreAction = { fullScreen in - items.send(.fullScreen(fullScreen)) - } - return .init( scheduledItems: { items.eraseToAnyAsyncSequence() }, - scheduleAlertIgnoreAction: scheduleAlertIgnoreAction, - scheduleAlertAwaitAction: { alert in - scheduleAlertIgnoreAction(alert) + scheduleAlert: { alert in + scheduleAlertAndIgnoreAction(alert) return await alertActions.first { $0.id == alert.id }?.action ?? .dismissed }, + scheduleAlertAndIgnoreAction: scheduleAlertAndIgnoreAction, scheduleHUD: { items.send(.hud($0)) }, - scheduleFullScreenIgnoreAction: scheduleFullScreenIgnoreAction, + scheduleFullScreen: { fullScreen in + items.send(.fullScreen(fullScreen)) + return await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss + }, sendAlertAction: { action, id in alertActions.send((action, id)) }, + sendFullScreenAction: { action, id in fullScreenActions.send((action, id)) }, setIsUserIteractionEnabled: { isUserInteractionEnabled.send($0) }, isUserInteractionEnabled: { isUserInteractionEnabled.eraseToAnyAsyncSequence() } ) diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Test.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Test.swift index 86217b0091..ac84a191bb 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Test.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Test.swift @@ -3,11 +3,12 @@ extension OverlayWindowClient: TestDependencyKey { public static let testValue = Self( scheduledItems: unimplemented("\(Self.self).scheduledItems"), - scheduleAlertIgnoreAction: unimplemented("\(Self.self).scheduleAlertIgnoreAction"), - scheduleAlertAwaitAction: unimplemented("\(Self.self).scheduleAlertAwaitAction"), + scheduleAlert: unimplemented("\(Self.self).scheduleAlert"), + scheduleAlertAndIgnoreAction: unimplemented("\(Self.self).scheduleAlertAndIgnoreAction"), scheduleHUD: unimplemented("\(Self.self).scheduleHUD"), - scheduleFullScreenIgnoreAction: unimplemented("\(Self.self).scheduleFullScreenIgnoreAction"), + scheduleFullScreen: unimplemented("\(Self.self).scheduleFullScreen"), sendAlertAction: unimplemented("\(Self.self).sendAlertAction"), + sendFullScreenAction: unimplemented("\(Self.self).sendFullScreenAction"), setIsUserIteractionEnabled: unimplemented("\(Self.self).setIsUserIteractionEnabled"), isUserInteractionEnabled: unimplemented("\(Self.self).isUserInteractionEnabled") ) diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index fe25f0747d..255c6aeaa4 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -480,7 +480,7 @@ extension ProfileStore { // We present an alert to user where they must choice if they wanna keep using Profile // on this device or delete it. If they delete a new one will be created and we will // onboard user... - let choiceByUser = await overlayWindowClient.scheduleAlertAwaitAction(.profileUsedOnAnotherDeviceAlert( + let choiceByUser = await overlayWindowClient.scheduleAlert(.profileUsedOnAnotherDeviceAlert( conflictingOwners: conflictingOwners )) diff --git a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift index c489ea4350..a3a0e4289a 100644 --- a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift +++ b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift @@ -19,6 +19,7 @@ extension ResetWalletClient: DependencyKey { cacheClient.removeAll() await radixConnectClient.disconnectAll() userDefaults.removeAll() + print("•• send walletDidResetSubject") walletDidResetSubject.send(()) } catch { loggerGlobal.error("Failed to delete profile: \(error)") diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift index 14d235401d..fe675e6150 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift @@ -203,7 +203,7 @@ extension SecureStorageClient: DependencyKey { authenticationPrompt: authenticationPrompt ) else { if notifyIfMissing { - overlayWindowClient.scheduleAlertIgnoreAction(.missingMnemonicAlert) + overlayWindowClient.scheduleAlertAndIgnoreAction(.missingMnemonicAlert) } return nil } diff --git a/RadixWallet/Features/AppFeature/App+Reducer.swift b/RadixWallet/Features/AppFeature/App+Reducer.swift index 21dec694a8..90ccdaa691 100644 --- a/RadixWallet/Features/AppFeature/App+Reducer.swift +++ b/RadixWallet/Features/AppFeature/App+Reducer.swift @@ -4,6 +4,7 @@ import SwiftUI // MARK: - App public struct App: Sendable, FeatureReducer { public struct State: Hashable { + @CasePathable public enum Root: Hashable { case main(Main.State) case onboardingCoordinator(OnboardingCoordinator.State) @@ -22,12 +23,20 @@ public struct App: Sendable, FeatureReducer { } } + @CasePathable + public enum ViewAction: Sendable, Equatable { + case task + } + + @CasePathable public enum InternalAction: Sendable, Equatable { case incompatibleProfileDeleted case toMain(isAccountRecoveryNeeded: Bool) case toOnboarding + case didResetWallet } + @CasePathable public enum ChildAction: Sendable, Equatable { case main(Main.Action) case onboardingCoordinator(OnboardingCoordinator.Action) @@ -37,25 +46,32 @@ public struct App: Sendable, FeatureReducer { @Dependency(\.continuousClock) var clock @Dependency(\.errorQueue) var errorQueue @Dependency(\.appPreferencesClient) var appPreferencesClient + @Dependency(\.resetWalletClient) var resetWalletClient public init() {} public var body: some ReducerOf { - Scope(state: \.root, action: /Action.child) { - EmptyReducer() - .ifCaseLet(/State.Root.main, action: /ChildAction.main) { - Main() - } - .ifCaseLet(/State.Root.onboardingCoordinator, action: /ChildAction.onboardingCoordinator) { - OnboardingCoordinator() - } - .ifCaseLet(/State.Root.splash, action: /ChildAction.splash) { - Splash() - } + Scope(state: \.root, action: \.child) { + Scope(state: \.main, action: \.main) { + Main() + } + Scope(state: \.onboardingCoordinator, action: \.onboardingCoordinator) { + OnboardingCoordinator() + } + Scope(state: \.splash, action: \.splash) { + Splash() + } } Reduce(core) } + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { + switch viewAction { + case .task: + didResetWalletEffect() + } + } + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { switch internalAction { case .incompatibleProfileDeleted: @@ -64,16 +80,13 @@ public struct App: Sendable, FeatureReducer { case .toMain: goToMain(state: &state) - case .toOnboarding: + case .toOnboarding, .didResetWallet: goToOnboarding(state: &state) } } public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { - case .main(.delegate(.removedWallet)): - goToOnboarding(state: &state) - case .onboardingCoordinator(.delegate(.completed)): goToMain(state: &state) @@ -88,17 +101,32 @@ public struct App: Sendable, FeatureReducer { } } - func goToMain(state: inout State) -> Effect { + private func goToMain(state: inout State) -> Effect { state.root = .main(.init( home: .init()) ) return .none } - func goToOnboarding(state: inout State) -> Effect { + private func goToOnboarding(state: inout State) -> Effect { state.root = .onboardingCoordinator(.init()) return .none } + + private func didResetWalletEffect() -> Effect { + .run { send in + do { + print("•• STARTED didResetWalletEffect") + for try await _ in resetWalletClient.walletDidReset() { + guard !Task.isCancelled else { return } + await send(.internal(.didResetWallet)) + } + print("•• ENDED didResetWalletEffect") + } catch { + loggerGlobal.error("Failed to iterate over walletDidReset: \(error)") + } + } + } } // MARK: App.UserFacingError diff --git a/RadixWallet/Features/AppFeature/App+View.swift b/RadixWallet/Features/AppFeature/App+View.swift index d20b3484cb..00b8c29284 100644 --- a/RadixWallet/Features/AppFeature/App+View.swift +++ b/RadixWallet/Features/AppFeature/App+View.swift @@ -12,7 +12,7 @@ extension App { } public var body: some SwiftUI.View { - SwitchStore(store.scope(state: \.root, action: Action.child)) { state in + SwitchStore(store.scope(state: \.root, action: \.child)) { state in switch state { case .main: CaseLet( @@ -38,6 +38,9 @@ extension App { } .tint(.app.gray1) .presentsLoadingViewOverlay() + .task { @MainActor in + await store.send(.view(.task)).finish() + } } } } diff --git a/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift index 769ae25679..1021040629 100644 --- a/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift @@ -2,7 +2,8 @@ import ComposableArchitecture import SwiftUI public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { - public struct State: Sendable, Hashable { + public struct State: Sendable, Hashable, Identifiable { + public let id: UUID = .init() public var root: Root.State public init(root: Root.State) { @@ -27,6 +28,7 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { @CasePathable public enum Action: Sendable, Equatable { + case dismiss case claimWallet(ClaimWallet.Action) } @@ -48,11 +50,19 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { - case .root(.claimWallet(.delegate)): - .send(.delegate(.dismiss)) + case .root(.dismiss): + print("•• FULLSCREEN COORD root(.dismiss)") + return .send(.delegate(.dismiss)) + + case .root(.claimWallet(.delegate(.dismiss))): + print("•• FULLSCREEN COORD claimWallet(.delegate(.dismiss))") + return .send(.delegate(.dismiss)) + +// case .root(.dismiss), .root(.claimWallet(.delegate(.dismiss))): +// return .send(.delegate(.dismiss)) default: - .none + return .none } } } diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift index 742f90f08c..021ed880b8 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift @@ -87,14 +87,16 @@ struct OverlayReducer: Sendable, FeatureReducer { overlayWindowClient.sendAlertAction(action, state.id) } return dismiss(&state) + case .hud(.delegate(.dismiss)): return dismiss(&state) case let .fullScreen(.child(.root(fullScreenAction))): - print("•• fullScreenAction: \(fullScreenAction)") + print("•• OverlayReducer: fullScreenAction: \(fullScreenAction)") return .none case .fullScreen(.delegate(.dismiss)): + print("•• OverlayReducer: .fullScreen(.delegate(.dismiss))") return dismiss(&state) default: @@ -103,19 +105,21 @@ struct OverlayReducer: Sendable, FeatureReducer { } func reduceDismissedDestination(into state: inout State) -> Effect { - dismissAlert(state: &state, withAction: .dismissed) + print("•• OverlayReducer: .reduceDismissedDestination \(state.destination)") + + if let item = state.itemsQueue.first, case let .alert(state) = item { + overlayWindowClient.sendAlertAction(.dismissed, state.id) + } + + return dismiss(&state) } private func showItemIfPossible(state: inout State) -> Effect { - guard !state.itemsQueue.isEmpty else { + guard let presentedItem = state.itemsQueue.first else { return .none } if state.isPresenting { - guard let presentedItem = state.itemsQueue.first else { - return .none - } - if case .hud = presentedItem { // A HUD is force dismissed when next item comes in, AKA it is a lower priority. state.destination = nil @@ -130,9 +134,7 @@ struct OverlayReducer: Sendable, FeatureReducer { } } - let nextItem = state.itemsQueue[0] - - switch nextItem { + switch presentedItem { case let .hud(hud): state.destination = .hud(.init(content: hud)) return .none @@ -145,15 +147,6 @@ struct OverlayReducer: Sendable, FeatureReducer { } } - private func dismissAlert(state: inout State, withAction action: OverlayWindowClient.Item.AlertAction) -> Effect { - let item = state.itemsQueue[0] - if case let .alert(state) = item { - overlayWindowClient.sendAlertAction(action, state.id) - } - - return dismiss(&state) - } - private func dismiss(_ state: inout State) -> Effect { state.destination = nil state.itemsQueue.removeFirst() diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index 181d282898..25330f7b2f 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -25,6 +25,7 @@ public struct ClaimWallet: Sendable, FeatureReducer { public enum DelegateAction: Sendable, Equatable { case didClearWallet case didTransferBack + case dismiss } @Dependency(\.resetWalletClient) var resetWalletClient @@ -34,13 +35,18 @@ public struct ClaimWallet: Sendable, FeatureReducer { public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { switch viewAction { case .clearWalletButtonTapped: - .run { send in + return .run { send in + print("•• CLAIMWALLET will do resetWalletClient.resetWallet") await resetWalletClient.resetWallet() +// await send(.delegate(.dismiss)) + print("•• CLAIMWALLET will send delegate(.didClearWallet)") await send(.delegate(.didClearWallet)) } case .transferBackButtonTapped: // TODO: transfer back - .send(.delegate(.didTransferBack)) + print("•• CLAIMWALLET will send delegate(.dismiss)") + return .send(.delegate(.dismiss)) +// .send(.delegate(.didTransferBack)) } } } diff --git a/RadixWallet/Features/MainFeature/Main+Reducer.swift b/RadixWallet/Features/MainFeature/Main+Reducer.swift index c150fc5ddd..352e7081d3 100644 --- a/RadixWallet/Features/MainFeature/Main+Reducer.swift +++ b/RadixWallet/Features/MainFeature/Main+Reducer.swift @@ -17,33 +17,34 @@ public struct Main: Sendable, FeatureReducer { } } + @CasePathable public enum ViewAction: Sendable, Equatable { case task } + @CasePathable public enum ChildAction: Sendable, Equatable { case home(Home.Action) } - public enum DelegateAction: Sendable, Equatable { - case removedWallet - } - + @CasePathable public enum InternalAction: Sendable, Equatable { case currentGatewayChanged(to: Gateway) } public struct Destination: DestinationReducer { + @CasePathable public enum State: Sendable, Hashable { case settings(Settings.State) } + @CasePathable public enum Action: Sendable, Equatable { case settings(Settings.Action) } public var body: some ReducerOf { - Scope(state: /State.settings, action: /Action.settings) { + Scope(state: \.settings, action: \.settings) { Settings() } } @@ -52,13 +53,11 @@ public struct Main: Sendable, FeatureReducer { @Dependency(\.gatewaysClient) var gatewaysClient @Dependency(\.personasClient) var personasClient @Dependency(\.cloudBackupClient) var cloudBackupClient - @Dependency(\.resetWalletClient) var resetWalletClient - @Dependency(\.overlayWindowClient) var overlayWindowClient public init() {} public var body: some ReducerOf { - Scope(state: \.home, action: /Action.child .. ChildAction.home) { + Scope(state: \.home, action: \.child.home) { Home() } Reduce(core) @@ -74,13 +73,6 @@ public struct Main: Sendable, FeatureReducer { case .task: startAutomaticBackupsEffect() .merge(with: gatewayValuesEffect()) - .merge(with: didResetWalletEffect()) - .merge(with: overlayActionEffect()) - } - } - - private func overlayActionEffect() -> Effect { - .run { _ in } } @@ -104,19 +96,6 @@ public struct Main: Sendable, FeatureReducer { } } - private func didResetWalletEffect() -> Effect { - .run { send in - do { - for try await _ in resetWalletClient.walletDidReset() { - guard !Task.isCancelled else { return } - await send(.delegate(.removedWallet)) - } - } catch { - loggerGlobal.error("Failed to iterate over walletDidReset: \(error)") - } - } - } - public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { case .home(.delegate(.displaySettings)): diff --git a/RadixWallet/Features/MainFeature/Main+View.swift b/RadixWallet/Features/MainFeature/Main+View.swift index 4d65b533fb..38716684b6 100644 --- a/RadixWallet/Features/MainFeature/Main+View.swift +++ b/RadixWallet/Features/MainFeature/Main+View.swift @@ -44,7 +44,7 @@ private extension StoreOf
{ } var home: StoreOf { - scope(state: \.home) { .child(.home($0)) } + scope(state: \.home, action: \.child.home) } } @@ -52,12 +52,9 @@ private extension StoreOf
{ private extension View { func destinations(with store: StoreOf
) -> some View { let destinationStore = store.destination - return navigationDestination( - store: destinationStore, - state: /Main.Destination.State.settings, - action: Main.Destination.Action.settings, - destination: { Settings.View(store: $0) } - ) + return navigationDestination(store: destinationStore.scope(state: \.settings, action: \.settings)) { + Settings.View(store: $0) + } } } From 41effd94cdff8cd7bb24a62aaca723387214c6bd Mon Sep 17 00:00:00 2001 From: kugel3 Date: Wed, 5 Jun 2024 12:39:28 +0200 Subject: [PATCH 05/68] wip --- .../CloudBackupClient+Live.swift | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index d0258b60cf..24882c46e2 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -124,7 +124,7 @@ extension CloudBackupClient { } @Sendable - func backupProfileAndSaveResult(_ profile: Profile) async { + func periodic(_ profile: Profile, checkIfClaimed: Bool) async { let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) let result: BackupResult.Result do { @@ -154,35 +154,60 @@ extension CloudBackupClient { guard !Task.isCancelled else { print("•• CANCEL automaticBackups"); return } print("•• tick") - + var timeToCheckClaim = false if tick.timeIntervalSince(lastClaimCheck) > 15 { print("•• time to check claims") lastClaimCheck = tick + timeToCheckClaim = true + + await periodic(profile, checkIfClaimed: true) + } else { + await periodic(profile, checkIfClaimed: false) + } - let backedUpHeader = try? await getProfileHeader(fetchProfileRecord(.init(recordName: profile.id.uuidString))) + let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty + let lastBackup = userDefaults.getLastCloudBackups[profile.id] + let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.profileHash == profile.hashValue - if true /* let backedUpHeader, await !profileStore.isThisDevice(deviceID: backedUpHeader.lastUsedOnDevice.id) */ { - print("•• different IDs") + let shouldBackUp = needsBackUp && !lastBackupSucceeded + let shouldCheckClaim = shouldBackUp || timeToCheckClaim - print("•• show fullscreen") + guard shouldBackUp || shouldCheckClaim else { continue } - let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) + let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) - print("•• got fullscreen action \(action)") + let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } - } else { - print("•• same IDs") - } + if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { + print("•• CloudBackupClient: different IDs, show claims screen") + let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) + print("•• CloudBackupClient: got fullscreen action \(action)") } - guard profile.appPreferences.security.isCloudProfileSyncEnabled else { continue } - guard profile.header.isNonEmpty else { continue } - - let last = userDefaults.getLastCloudBackups[profile.id] - if let last, last.result == .success, last.profileHash == profile.hashValue { continue } + guard shouldBackUp else { + print("•• CloudBackupClient: no need to back up") + continue + } + print("•• CloudBackupClient: going to back up") + + let result: BackupResult.Result + do { + let json = profile.toJSONString() + try await uploadProfileToICloud(.right(json), header: profile.header, existingRecord: existingRecord) + result = .success + } catch CKError.accountTemporarilyUnavailable { + result = .temporarilyUnavailable + } catch CKError.notAuthenticated { + result = .notAuthenticated + } catch { + loggerGlobal.error("Automatic cloud backup failed with error \(error)") + result = .failure + } - await backupProfileAndSaveResult(profile) + try? userDefaults.setLastCloudBackup(result, of: profile) } + + print("•• FINISH automaticBackups") }, loadDeviceID: { try? secureStorageClient.loadDeviceInfo()?.id From 84f2e9946b1e6b10100fba8163b3a365486f2cff Mon Sep 17 00:00:00 2001 From: kugel3 Date: Wed, 5 Jun 2024 12:43:44 +0200 Subject: [PATCH 06/68] wip --- .../CloudBackupClient+Live.swift | 80 ++++++++----------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 24882c46e2..a4242d5cd0 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -124,8 +124,7 @@ extension CloudBackupClient { } @Sendable - func periodic(_ profile: Profile, checkIfClaimed: Bool) async { - let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) + func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async { let result: BackupResult.Result do { let json = profile.toJSONString() @@ -143,6 +142,35 @@ extension CloudBackupClient { try? userDefaults.setLastCloudBackup(result, of: profile) } + @Sendable + func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async { + let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty + let lastBackup = userDefaults.getLastCloudBackups[profile.id] + let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.profileHash == profile.hashValue + + let shouldBackUp = needsBackUp && !lastBackupSucceeded + let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed + + guard shouldBackUp || shouldCheckClaim else { return } + + let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) + + let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } + + if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { + print("•• CloudBackupClient: different IDs, show claims screen") + let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) + print("•• CloudBackupClient: got fullscreen action \(action)") + } + + guard shouldBackUp else { + print("•• CloudBackupClient: no need to back up") + return + } + print("•• CloudBackupClient: going to back up") + await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + } + return .init( startAutomaticBackups: { let ticks = AsyncTimerSequence(every: .seconds(1)) @@ -154,57 +182,13 @@ extension CloudBackupClient { guard !Task.isCancelled else { print("•• CANCEL automaticBackups"); return } print("•• tick") - var timeToCheckClaim = false if tick.timeIntervalSince(lastClaimCheck) > 15 { print("•• time to check claims") lastClaimCheck = tick - timeToCheckClaim = true - - await periodic(profile, checkIfClaimed: true) + await performAutomaticBackup(profile, timeToCheckIfClaimed: true) } else { - await periodic(profile, checkIfClaimed: false) - } - - let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty - let lastBackup = userDefaults.getLastCloudBackups[profile.id] - let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.profileHash == profile.hashValue - - let shouldBackUp = needsBackUp && !lastBackupSucceeded - let shouldCheckClaim = shouldBackUp || timeToCheckClaim - - guard shouldBackUp || shouldCheckClaim else { continue } - - let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) - - let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } - - if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { - print("•• CloudBackupClient: different IDs, show claims screen") - let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) - print("•• CloudBackupClient: got fullscreen action \(action)") - } - - guard shouldBackUp else { - print("•• CloudBackupClient: no need to back up") - continue - } - print("•• CloudBackupClient: going to back up") - - let result: BackupResult.Result - do { - let json = profile.toJSONString() - try await uploadProfileToICloud(.right(json), header: profile.header, existingRecord: existingRecord) - result = .success - } catch CKError.accountTemporarilyUnavailable { - result = .temporarilyUnavailable - } catch CKError.notAuthenticated { - result = .notAuthenticated - } catch { - loggerGlobal.error("Automatic cloud backup failed with error \(error)") - result = .failure + await performAutomaticBackup(profile, timeToCheckIfClaimed: false) } - - try? userDefaults.setLastCloudBackup(result, of: profile) } print("•• FINISH automaticBackups") From e13d9956d34e84c09e2da927fbb2cece63a134ae Mon Sep 17 00:00:00 2001 From: kugel3 Date: Wed, 5 Jun 2024 16:30:41 +0200 Subject: [PATCH 07/68] wip --- .../CloudBackupClient+Live.swift | 19 ++++++++++++++----- .../OverlayWindowClient+Interface.swift | 2 +- .../OverlayWindowClient+Live.swift | 5 ++++- .../ResetWalletClient+Live.swift | 3 --- .../Features/AppFeature/App+Reducer.swift | 2 -- ...FullScreenOverlayCoordinator+Reducer.swift | 17 +++++------------ .../AppFeature/Overlay/Overlay+Reducer.swift | 18 ++++++++---------- .../ClaimWallet/ClaimWallet+Reducer.swift | 14 ++++++++------ .../ClaimWallet/ClaimWallet+View.swift | 7 +++++++ 9 files changed, 47 insertions(+), 40 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index a4242d5cd0..79976fe3e7 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -130,13 +130,18 @@ extension CloudBackupClient { let json = profile.toJSONString() try await uploadProfileToICloud(.right(json), header: profile.header, existingRecord: existingRecord) result = .success + print("•• CloudBackupClient: backup success") } catch CKError.accountTemporarilyUnavailable { result = .temporarilyUnavailable + print("•• CloudBackupClient: backup failed temporarilyUnavailable") + } catch CKError.notAuthenticated { result = .notAuthenticated + print("•• CloudBackupClient: backup failed notAuthenticated") } catch { loggerGlobal.error("Automatic cloud backup failed with error \(error)") result = .failure + print("•• CloudBackupClient: backup failed \(error)") } try? userDefaults.setLastCloudBackup(result, of: profile) @@ -148,16 +153,18 @@ extension CloudBackupClient { let lastBackup = userDefaults.getLastCloudBackups[profile.id] let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.profileHash == profile.hashValue - let shouldBackUp = needsBackUp && !lastBackupSucceeded + let shouldBackUp = false && needsBackUp && !lastBackupSucceeded let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed guard shouldBackUp || shouldCheckClaim else { return } + print("•• performAutomaticBackup, shouldBackUp: \(shouldBackUp), timeToCheckIfClaimed, \(timeToCheckIfClaimed)") + let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } - if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { + if shouldCheckClaim { // }, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { print("•• CloudBackupClient: different IDs, show claims screen") let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) print("•• CloudBackupClient: got fullscreen action \(action)") @@ -180,12 +187,14 @@ extension CloudBackupClient { print("•• START automaticBackups") for try await (profile, tick) in combineLatest(profiles, ticks) { guard !Task.isCancelled else { print("•• CANCEL automaticBackups"); return } + guard tick > lastClaimCheck else { continue } - print("•• tick") - if tick.timeIntervalSince(lastClaimCheck) > 15 { + print("•• tick \(tick.formatted(date: .omitted, time: .standard)) \(tick.timeIntervalSince(lastClaimCheck))") + if tick.timeIntervalSince(lastClaimCheck) > 10 { print("•• time to check claims") - lastClaimCheck = tick await performAutomaticBackup(profile, timeToCheckIfClaimed: true) + lastClaimCheck = .now + print("•• did backup and check claims") } else { await performAutomaticBackup(profile, timeToCheckIfClaimed: false) } diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift index a3fc636853..806e285792 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift @@ -52,7 +52,7 @@ public struct OverlayWindowClient: Sendable { } extension OverlayWindowClient { - public typealias FullScreenAction = FullScreenOverlayCoordinator.Root.Action + public typealias FullScreenAction = FullScreenOverlayCoordinator.DelegateAction public typealias FullScreenID = FullScreenOverlayCoordinator.State.ID public typealias ScheduleAlert = @Sendable (Item.AlertState) async -> Item.AlertAction diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift index 2202bd829e..f99713f72f 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift @@ -33,7 +33,10 @@ extension OverlayWindowClient: DependencyKey { scheduleHUD: { items.send(.hud($0)) }, scheduleFullScreen: { fullScreen in items.send(.fullScreen(fullScreen)) - return await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss + let action = await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss + print("•• OverlayWindowClient got fullscreen action") + return action +// return await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss }, sendAlertAction: { action, id in alertActions.send((action, id)) }, sendFullScreenAction: { action, id in fullScreenActions.send((action, id)) }, diff --git a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift index a3a0e4289a..1bb1b608e4 100644 --- a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift +++ b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift @@ -13,13 +13,10 @@ extension ResetWalletClient: DependencyKey { return Self( resetWallet: { do { - print("•• resetWallet") - // TODO: Is this the best order? try await appPreferencesClient.deleteProfileAndFactorSources(true) cacheClient.removeAll() await radixConnectClient.disconnectAll() userDefaults.removeAll() - print("•• send walletDidResetSubject") walletDidResetSubject.send(()) } catch { loggerGlobal.error("Failed to delete profile: \(error)") diff --git a/RadixWallet/Features/AppFeature/App+Reducer.swift b/RadixWallet/Features/AppFeature/App+Reducer.swift index 90ccdaa691..30392e9e98 100644 --- a/RadixWallet/Features/AppFeature/App+Reducer.swift +++ b/RadixWallet/Features/AppFeature/App+Reducer.swift @@ -116,12 +116,10 @@ public struct App: Sendable, FeatureReducer { private func didResetWalletEffect() -> Effect { .run { send in do { - print("•• STARTED didResetWalletEffect") for try await _ in resetWalletClient.walletDidReset() { guard !Task.isCancelled else { return } await send(.internal(.didResetWallet)) } - print("•• ENDED didResetWalletEffect") } catch { loggerGlobal.error("Failed to iterate over walletDidReset: \(error)") } diff --git a/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift index 1021040629..8a651c4f70 100644 --- a/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/FullScreenOverlayCoordinator+Reducer.swift @@ -17,6 +17,7 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { } public enum DelegateAction: Sendable, Equatable { + case claimWallet(ClaimWallet.DelegateAction) case dismiss } @@ -28,7 +29,6 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { @CasePathable public enum Action: Sendable, Equatable { - case dismiss case claimWallet(ClaimWallet.Action) } @@ -50,19 +50,12 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { - case .root(.dismiss): - print("•• FULLSCREEN COORD root(.dismiss)") - return .send(.delegate(.dismiss)) - - case .root(.claimWallet(.delegate(.dismiss))): - print("•• FULLSCREEN COORD claimWallet(.delegate(.dismiss))") - return .send(.delegate(.dismiss)) - -// case .root(.dismiss), .root(.claimWallet(.delegate(.dismiss))): -// return .send(.delegate(.dismiss)) + // Forward all delegate actions, re-wrapped + case let .root(.claimWallet(.delegate(action))): + .send(.delegate(.claimWallet(action))) default: - return .none + .none } } } diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift index 021ed880b8..ac7b153a27 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift @@ -83,7 +83,7 @@ struct OverlayReducer: Sendable, FeatureReducer { func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { switch presentedAction { case let .alert(action): - if let item = state.itemsQueue.first, case let .alert(state) = item { + if case let .alert(state) = state.itemsQueue.first { overlayWindowClient.sendAlertAction(action, state.id) } return dismiss(&state) @@ -91,12 +91,11 @@ struct OverlayReducer: Sendable, FeatureReducer { case .hud(.delegate(.dismiss)): return dismiss(&state) - case let .fullScreen(.child(.root(fullScreenAction))): - print("•• OverlayReducer: fullScreenAction: \(fullScreenAction)") - return .none - - case .fullScreen(.delegate(.dismiss)): - print("•• OverlayReducer: .fullScreen(.delegate(.dismiss))") + case let .fullScreen(.delegate(action)): + print("•• OverlayReducer: fullScreenAction: \(action)") + if case let .fullScreen(state) = state.itemsQueue.first { + overlayWindowClient.sendFullScreenAction(action, state.id) + } return dismiss(&state) default: @@ -105,9 +104,7 @@ struct OverlayReducer: Sendable, FeatureReducer { } func reduceDismissedDestination(into state: inout State) -> Effect { - print("•• OverlayReducer: .reduceDismissedDestination \(state.destination)") - - if let item = state.itemsQueue.first, case let .alert(state) = item { + if case let .alert(state) = state.itemsQueue.first { overlayWindowClient.sendAlertAction(.dismissed, state.id) } @@ -148,6 +145,7 @@ struct OverlayReducer: Sendable, FeatureReducer { } private func dismiss(_ state: inout State) -> Effect { + print("•• OverlayReducer: dismiss") state.destination = nil state.itemsQueue.removeFirst() return setIsUserInteractionEnabled(&state, isEnabled: false) diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index 25330f7b2f..c1ff3edf4e 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -36,17 +36,19 @@ public struct ClaimWallet: Sendable, FeatureReducer { switch viewAction { case .clearWalletButtonTapped: return .run { send in - print("•• CLAIMWALLET will do resetWalletClient.resetWallet") + print("•• CLAIMWALLET clearWalletButtonTapped") await resetWalletClient.resetWallet() -// await send(.delegate(.dismiss)) print("•• CLAIMWALLET will send delegate(.didClearWallet)") await send(.delegate(.didClearWallet)) } case .transferBackButtonTapped: - // TODO: transfer back - print("•• CLAIMWALLET will send delegate(.dismiss)") - return .send(.delegate(.dismiss)) -// .send(.delegate(.didTransferBack)) + state.isLoading = true + return .run { send in + print("•• CLAIMWALLET transferBackButtonTapped") + try await Task.sleep(for: .seconds(2)) + print("•• CLAIMWALLET will send delegate(.didTransferBack)") + await send(.delegate(.didTransferBack)) + } } } } diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift index 6e9a695dab..7af801a71e 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift @@ -48,6 +48,13 @@ extension ClaimWallet { store.send(.view(.transferBackButtonTapped)) } .buttonStyle(.primaryText()) + .overlay { + if viewStore.isLoading { + Color.red.opacity(0.2) + + ProgressView() + } + } } } .padding(.horizontal, .large1) From 58bac33fcb7d626dc8650e374d3f4a8b667602d9 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Wed, 5 Jun 2024 20:44:46 +0200 Subject: [PATCH 08/68] wip, with debug code --- .../CloudBackupClient+Interface.swift | 6 ++- .../CloudBackupClient+Live.swift | 53 +++++++++---------- .../CloudBackupClient+Test.swift | 6 ++- .../OverlayWindowClient+Live.swift | 5 +- .../Clients/ProfileStore/ProfileStore.swift | 2 +- .../SecurityCenterClient+Live.swift | 2 +- .../UserDefaults+Dependency+Extension.swift | 12 +++-- .../AppFeature/Overlay/Overlay+Reducer.swift | 2 - .../ClaimWallet/ClaimWallet+Reducer.swift | 36 ++++++++----- .../ClaimWallet/ClaimWallet+View.swift | 8 ++- 10 files changed, 77 insertions(+), 55 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift index 77220ee534..cfc72b7f1f 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift @@ -13,6 +13,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { public let lastBackup: LastBackup public let loadProfile: LoadProfile public let loadProfileHeaders: LoadProfileHeaders + public let reclaimProfile: ReclaimProfile public init( startAutomaticBackups: @escaping StartAutomaticBackups, @@ -22,7 +23,8 @@ public struct CloudBackupClient: DependencyKey, Sendable { checkAccountStatus: @escaping CheckAccountStatus, lastBackup: @escaping LastBackup, loadProfile: @escaping LoadProfile, - loadProfileHeaders: @escaping LoadProfileHeaders + loadProfileHeaders: @escaping LoadProfileHeaders, + reclaimProfile: @escaping ReclaimProfile ) { self.startAutomaticBackups = startAutomaticBackups self.loadDeviceID = loadDeviceID @@ -32,6 +34,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { self.lastBackup = lastBackup self.loadProfile = loadProfile self.loadProfileHeaders = loadProfileHeaders + self.reclaimProfile = reclaimProfile } } @@ -44,6 +47,7 @@ extension CloudBackupClient { public typealias LastBackup = @Sendable (ProfileID) -> AnyAsyncSequence public typealias LoadProfile = @Sendable (ProfileID) async throws -> BackedUpProfile public typealias LoadProfileHeaders = @Sendable () async throws -> [Profile.Header] + public typealias ReclaimProfile = @Sendable () async throws -> Void } // MARK: CloudBackupClient.BackedUpProfile diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 79976fe3e7..37d9fb4c63 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -124,26 +124,24 @@ extension CloudBackupClient { } @Sendable - func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async { + func backupProfileAndSaveResult(existingRecord: CKRecord?) async { + let profile = await profileStore.profile let result: BackupResult.Result do { let json = profile.toJSONString() try await uploadProfileToICloud(.right(json), header: profile.header, existingRecord: existingRecord) result = .success - print("•• CloudBackupClient: backup success") } catch CKError.accountTemporarilyUnavailable { result = .temporarilyUnavailable - print("•• CloudBackupClient: backup failed temporarilyUnavailable") - } catch CKError.notAuthenticated { result = .notAuthenticated - print("•• CloudBackupClient: backup failed notAuthenticated") } catch { loggerGlobal.error("Automatic cloud backup failed with error \(error)") result = .failure - print("•• CloudBackupClient: backup failed \(error)") } + print("•• CloudBackupClient: backup result \(result == .success ? "OK" : "fail")") + try? userDefaults.setLastCloudBackup(result, of: profile) } @@ -151,56 +149,54 @@ extension CloudBackupClient { func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async { let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty let lastBackup = userDefaults.getLastCloudBackups[profile.id] - let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.profileHash == profile.hashValue + let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.saveHash == profile.saveHash - let shouldBackUp = false && needsBackUp && !lastBackupSucceeded + let shouldBackUp = needsBackUp && !lastBackupSucceeded let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed - guard shouldBackUp || shouldCheckClaim else { return } + print("•• perform, lastOK: \(lastBackupSucceeded), shouldBackUp: \(shouldBackUp), timeToCheck, \(timeToCheckIfClaimed)") - print("•• performAutomaticBackup, shouldBackUp: \(shouldBackUp), timeToCheckIfClaimed, \(timeToCheckIfClaimed)") + guard shouldBackUp || shouldCheckClaim else { return } let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } - if shouldCheckClaim { // }, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { - print("•• CloudBackupClient: different IDs, show claims screen") + let didTransferBackProfile: Bool + if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) - print("•• CloudBackupClient: got fullscreen action \(action)") + didTransferBackProfile = action == .claimWallet(.didTransferBack) + } else { + didTransferBackProfile = false } - guard shouldBackUp else { - print("•• CloudBackupClient: no need to back up") - return - } - print("•• CloudBackupClient: going to back up") - await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + guard shouldBackUp || didTransferBackProfile else { return } + + await backupProfileAndSaveResult(existingRecord: existingRecord) } return .init( startAutomaticBackups: { - let ticks = AsyncTimerSequence(every: .seconds(1)) + let ticks = AsyncTimerSequence(every: .seconds(2)) let profiles = await profileStore.values() + .map { + print("•••••• profile changed LM: \($0.header.lastModified.formatted(date: .omitted, time: .standard)) LU: \($0.header.lastUsedOnDevice.date.formatted(date: .omitted, time: .standard))") + return $0 + } var lastClaimCheck: Date = .now // .distantPast - print("•• START automaticBackups") for try await (profile, tick) in combineLatest(profiles, ticks) { - guard !Task.isCancelled else { print("•• CANCEL automaticBackups"); return } + guard !Task.isCancelled else { return } guard tick > lastClaimCheck else { continue } - print("•• tick \(tick.formatted(date: .omitted, time: .standard)) \(tick.timeIntervalSince(lastClaimCheck))") + await print("•• tick \(Int(tick.timeIntervalSince(lastClaimCheck)))") if tick.timeIntervalSince(lastClaimCheck) > 10 { - print("•• time to check claims") await performAutomaticBackup(profile, timeToCheckIfClaimed: true) lastClaimCheck = .now - print("•• did backup and check claims") } else { await performAutomaticBackup(profile, timeToCheckIfClaimed: false) } } - - print("•• FINISH automaticBackups") }, loadDeviceID: { try? secureStorageClient.loadDeviceInfo()?.id @@ -263,6 +259,9 @@ extension CloudBackupClient { try await fetchAllProfileRecords(headerOnly: true) .map(getProfileHeader) .filter(\.isNonEmpty) + }, + reclaimProfile: { + try await profileStore.claimOwnershipOfProfile() } ) } diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift index 02be1e332b..624458ff08 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift @@ -22,7 +22,8 @@ extension CloudBackupClient: TestDependencyKey { checkAccountStatus: { throw NoopError() }, lastBackup: { _ in AsyncLazySequence([]).eraseToAnyAsyncSequence() }, loadProfile: { _ in throw NoopError() }, - loadProfileHeaders: { throw NoopError() } + loadProfileHeaders: { throw NoopError() }, + reclaimProfile: { throw NoopError() } ) public static let testValue = Self( @@ -33,6 +34,7 @@ extension CloudBackupClient: TestDependencyKey { checkAccountStatus: unimplemented("\(Self.self).checkAccountStatus"), lastBackup: unimplemented("\(Self.self).lastBackup"), loadProfile: unimplemented("\(Self.self).loadProfile"), - loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders") + loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders"), + reclaimProfile: unimplemented("\(Self.self).reclaimProfile") ) } diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift index f99713f72f..2202bd829e 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Live.swift @@ -33,10 +33,7 @@ extension OverlayWindowClient: DependencyKey { scheduleHUD: { items.send(.hud($0)) }, scheduleFullScreen: { fullScreen in items.send(.fullScreen(fullScreen)) - let action = await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss - print("•• OverlayWindowClient got fullscreen action") - return action -// return await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss + return await fullScreenActions.first { $0.id == fullScreen.id }?.action ?? .dismiss }, sendAlertAction: { action, id in alertActions.send((action, id)) }, sendFullScreenAction: { action, id in fullScreenActions.send((action, id)) }, diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 255c6aeaa4..9eb7dc005b 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -392,7 +392,7 @@ extension ProfileStore { /// then saves this profile and emits an update. /// - Parameter profile: Profile to update `lastUsedOnDevice` of and /// save on this device. - private func claimOwnershipOfProfile() throws { + public func claimOwnershipOfProfile() throws { var copy = profile try _claimOwnership(of: ©) } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 66766fed32..ce28712c6e 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -45,7 +45,7 @@ extension SecurityCenterClient { func statusValues(results: AnyAsyncSequence) async -> AnyAsyncSequence { await combineLatest(profileStore.values(), results.prepend(nil)).map { profile, backup in guard let backup else { return nil } - let upToDate = backup.profileHash == profile.hashValue + let upToDate = backup.saveHash == profile.saveHash let success = backup.result == .success return .init(backupDate: backup.backupDate, upToDate: upToDate, success: success) } diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 74f004bc13..87aeac58af 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -169,7 +169,7 @@ extension UserDefaults.Dependency { var backups: [UUID: BackupResult] = getLastCloudBackups backups[profile.id] = .init( backupDate: .now, - profileHash: profile.hashValue, + saveHash: profile.saveHash, result: result ) @@ -189,7 +189,7 @@ extension UserDefaults.Dependency { var backups: [ProfileID: BackupResult] = getLastManualBackups backups[profile.id] = .init( backupDate: .now, - profileHash: profile.hashValue, + saveHash: profile.saveHash, result: .success ) @@ -234,7 +234,7 @@ extension UserDefaults.Dependency { // MARK: - BackupResult public struct BackupResult: Codable, Sendable { public let backupDate: Date - public let profileHash: Int + public let saveHash: String public let result: Result public enum Result: Codable, Sendable { @@ -244,3 +244,9 @@ public struct BackupResult: Codable, Sendable { case failure } } + +extension Profile { + public var saveHash: String { + "\(header.lastModified.timeIntervalSince1970)-\(header.lastUsedOnDevice.id.uuidString)" + } +} diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift index ac7b153a27..bdf9ed37c1 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift @@ -92,7 +92,6 @@ struct OverlayReducer: Sendable, FeatureReducer { return dismiss(&state) case let .fullScreen(.delegate(action)): - print("•• OverlayReducer: fullScreenAction: \(action)") if case let .fullScreen(state) = state.itemsQueue.first { overlayWindowClient.sendFullScreenAction(action, state.id) } @@ -145,7 +144,6 @@ struct OverlayReducer: Sendable, FeatureReducer { } private func dismiss(_ state: inout State) -> Effect { - print("•• OverlayReducer: dismiss") state.destination = nil state.itemsQueue.removeFirst() return setIsUserInteractionEnabled(&state, isEnabled: false) diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index c1ff3edf4e..f67631ab36 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -4,17 +4,14 @@ import SwiftUI // MARK: - NewConnectionApproval public struct ClaimWallet: Sendable, FeatureReducer { public struct State: Sendable, Hashable { - public var isLoading: Bool + public var isLoading: Bool = false + public var failedToReclaim: Bool = false public var screenState: ControlState { isLoading ? .loading(.global(text: nil)) : .enabled } - public init( - isLoading: Bool = false - ) { - self.isLoading = isLoading - } + public init() {} } public enum ViewAction: Sendable, Equatable { @@ -22,6 +19,10 @@ public struct ClaimWallet: Sendable, FeatureReducer { case transferBackButtonTapped } + public enum InternalAction: Sendable, Equatable { + case failedToReclaim + } + public enum DelegateAction: Sendable, Equatable { case didClearWallet case didTransferBack @@ -29,6 +30,7 @@ public struct ClaimWallet: Sendable, FeatureReducer { } @Dependency(\.resetWalletClient) var resetWalletClient + @Dependency(\.cloudBackupClient) var cloudBackupClient public init() {} @@ -36,19 +38,29 @@ public struct ClaimWallet: Sendable, FeatureReducer { switch viewAction { case .clearWalletButtonTapped: return .run { send in - print("•• CLAIMWALLET clearWalletButtonTapped") await resetWalletClient.resetWallet() - print("•• CLAIMWALLET will send delegate(.didClearWallet)") await send(.delegate(.didClearWallet)) } case .transferBackButtonTapped: state.isLoading = true + state.failedToReclaim = false return .run { send in - print("•• CLAIMWALLET transferBackButtonTapped") - try await Task.sleep(for: .seconds(2)) - print("•• CLAIMWALLET will send delegate(.didTransferBack)") - await send(.delegate(.didTransferBack)) + do { + try await cloudBackupClient.reclaimProfile() + await send(.delegate(.didTransferBack)) + } catch { + await send(.internal(.failedToReclaim)) + } } } } + + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { + switch internalAction { + case .failedToReclaim: + state.isLoading = false + state.failedToReclaim = true + return .none + } + } } diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift index 7af801a71e..084f78a4f9 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift @@ -38,6 +38,12 @@ extension ClaimWallet { Spacer() VStack { + if viewStore.failedToReclaim { + Text("Failed to transfer ownership") + .foregroundColor(.app.error) + .textStyle(.secondaryHeader) + } + Button(L10n.ConfigurationBackup.Automated.walletTransferredClearButton) { store.send(.view(.clearWalletButtonTapped)) } @@ -50,8 +56,6 @@ extension ClaimWallet { .buttonStyle(.primaryText()) .overlay { if viewStore.isLoading { - Color.red.opacity(0.2) - ProgressView() } } From 790f57b47bd8e55d9e47810916e77caff6e5adf9 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 00:52:38 +0200 Subject: [PATCH 09/68] cleanup --- .../CloudBackupClient+Live.swift | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 37d9fb4c63..4c97565152 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -140,8 +140,6 @@ extension CloudBackupClient { result = .failure } - print("•• CloudBackupClient: backup result \(result == .success ? "OK" : "fail")") - try? userDefaults.setLastCloudBackup(result, of: profile) } @@ -154,8 +152,6 @@ extension CloudBackupClient { let shouldBackUp = needsBackUp && !lastBackupSucceeded let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed - print("•• perform, lastOK: \(lastBackupSucceeded), shouldBackUp: \(shouldBackUp), timeToCheck, \(timeToCheckIfClaimed)") - guard shouldBackUp || shouldCheckClaim else { return } let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) @@ -175,22 +171,22 @@ extension CloudBackupClient { await backupProfileAndSaveResult(existingRecord: existingRecord) } + let retryBackupInterval: DispatchTimeInterval = .seconds(60) + let checkClaimedProfileInterval: TimeInterval = 15 * 60 + return .init( startAutomaticBackups: { - let ticks = AsyncTimerSequence(every: .seconds(2)) + let ticks = AsyncTimerSequence(every: retryBackupInterval) let profiles = await profileStore.values() - .map { - print("•••••• profile changed LM: \($0.header.lastModified.formatted(date: .omitted, time: .standard)) LU: \($0.header.lastUsedOnDevice.date.formatted(date: .omitted, time: .standard))") - return $0 - } - var lastClaimCheck: Date = .now // .distantPast + var lastClaimCheck: Date = .distantPast for try await (profile, tick) in combineLatest(profiles, ticks) { guard !Task.isCancelled else { return } - guard tick > lastClaimCheck else { continue } + try secureStorageClient.updateIsCloudProfileSyncEnabled(profile.id, .disable) - await print("•• tick \(Int(tick.timeIntervalSince(lastClaimCheck)))") - if tick.timeIntervalSince(lastClaimCheck) > 10 { + // This will skip the ticks that get backed up while we are awaiting performAutomaticBackup + guard tick > lastClaimCheck else { continue } + if tick.timeIntervalSince(lastClaimCheck) > checkClaimedProfileInterval { await performAutomaticBackup(profile, timeToCheckIfClaimed: true) lastClaimCheck = .now } else { @@ -210,7 +206,6 @@ extension CloudBackupClient { let migratable = try headerList.compactMap { header -> (Data, Profile.Header)? in let id = header.id - guard !previouslyMigrated.contains(id), header.id != activeProfile else { return nil } guard let profileData = try secureStorageClient.loadProfileSnapshotData(id) else { @@ -231,10 +226,7 @@ extension CloudBackupClient { return backedUpRecord } - let uploadedRecord = try await uploadProfileToICloud(.left(profileData), header: header, existingRecord: backedUpRecord) - try secureStorageClient.updateIsCloudProfileSyncEnabled(header.id, .disable) - - return uploadedRecord + return try await uploadProfileToICloud(.left(profileData), header: header, existingRecord: backedUpRecord) } let migratedIDs = migrated.compactMap { ProfileID(uuidString: $0.recordID.recordName) } From f3ef1c0902cb86c889f65b5b5ab32729c4957694 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 4 Jun 2024 03:26:25 +0200 Subject: [PATCH 10/68] Fix TCA error --- .../DesignSystem/Extensions/View+Extra.swift | 25 ++++++++++------- .../ImportMnemonic/ImportMnemonic+View.swift | 25 +++++------------ .../ImportMnemonic/ImportMnemonic.swift | 6 ++-- ...portMnemonicControllingAccounts+View.swift | 28 ++++++------------- .../ImportMnemonicControllingAccounts.swift | 6 ++-- ...ManualAccountRecoverySeedPhrase+View.swift | 9 ++---- 6 files changed, 42 insertions(+), 57 deletions(-) diff --git a/RadixWallet/Core/DesignSystem/Extensions/View+Extra.swift b/RadixWallet/Core/DesignSystem/Extensions/View+Extra.swift index b6383e8b21..36e6358286 100644 --- a/RadixWallet/Core/DesignSystem/Extensions/View+Extra.swift +++ b/RadixWallet/Core/DesignSystem/Extensions/View+Extra.swift @@ -9,18 +9,23 @@ extension View { } } - func radixToolbar(title: String, alwaysVisible: Bool = true) -> some View { - self - .toolbar { - ToolbarItem(placement: .principal) { - Text(title) - .foregroundColor(.app.gray1) - .textStyle(.body1Header) + func radixToolbar(title: String, alwaysVisible: Bool = true, closeAction: (() -> Void)? = nil) -> some View { + toolbar { + ToolbarItem(placement: .principal) { + Text(title) + .foregroundColor(.app.gray1) + .textStyle(.body1Header) + } + + if let closeAction { + ToolbarItem(placement: .navigationBarLeading) { + CloseButton(action: closeAction) } } - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.app.background, for: .navigationBar) - .toolbarBackground(alwaysVisible ? .visible : .automatic, for: .navigationBar) + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.app.background, for: .navigationBar) + .toolbarBackground(alwaysVisible ? .visible : .automatic, for: .navigationBar) } func eraseToAnyView() -> AnyView { diff --git a/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift b/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift index 29c591253f..e0e2a76ed8 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift @@ -243,28 +243,17 @@ private extension View { } private func backupConfirmation(with destinationStore: PresentationStoreOf) -> some View { - alert( - store: destinationStore, - state: /ImportMnemonic.Destination.State.backupConfirmation, - action: ImportMnemonic.Destination.Action.backupConfirmation - ) + alert(store: destinationStore.scope(state: \.backupConfirmation, action: \.backupConfirmation)) } - private func verifyMnemonic(with destinationStore: PresentationStoreOf) -> some View { - navigationDestination( - store: destinationStore, - state: /ImportMnemonic.Destination.State.verifyMnemonic, - action: ImportMnemonic.Destination.Action.verifyMnemonic, - destination: { VerifyMnemonic.View(store: $0) } - ) + private func onContinueWarning(with destinationStore: PresentationStoreOf) -> some View { + alert(store: destinationStore.scope(state: \.onContinueWarning, action: \.onContinueWarning)) } - private func onContinueWarning(with destinationStore: PresentationStoreOf) -> some View { - alert( - store: destinationStore, - state: /ImportMnemonic.Destination.State.onContinueWarning, - action: ImportMnemonic.Destination.Action.onContinueWarning - ) + private func verifyMnemonic(with destinationStore: PresentationStoreOf) -> some View { + navigationDestination(store: destinationStore.scope(state: \.verifyMnemonic, action: \.verifyMnemonic)) { + VerifyMnemonic.View(store: $0) + } } } diff --git a/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift b/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift index 29ed5de334..c480cef284 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift @@ -299,16 +299,18 @@ public struct ImportMnemonic: Sendable, FeatureReducer { } public struct Destination: DestinationReducer { + @CasePathable public enum State: Sendable, Hashable { case backupConfirmation(AlertState) case onContinueWarning(AlertState) case verifyMnemonic(VerifyMnemonic.State) } + @CasePathable public enum Action: Sendable, Equatable { case backupConfirmation(BackupConfirmation) - case verifyMnemonic(VerifyMnemonic.Action) case onContinueWarning(OnContinueWarning) + case verifyMnemonic(VerifyMnemonic.Action) public enum BackupConfirmation: Sendable, Hashable { case userHasBackedUp @@ -321,7 +323,7 @@ public struct ImportMnemonic: Sendable, FeatureReducer { } public var body: some Reducer { - Scope(state: /State.verifyMnemonic, action: /Action.verifyMnemonic) { + Scope(state: \.verifyMnemonic, action: \.verifyMnemonic) { VerifyMnemonic() } } diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift index 7faab1fd8b..9118b0c050 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift @@ -107,28 +107,18 @@ private extension View { } private func importMnemonic(with destinationStore: PresentationStoreOf) -> some View { - sheet( - store: destinationStore, - state: /ImportMnemonicControllingAccounts.Destination.State.importMnemonic, - action: ImportMnemonicControllingAccounts.Destination.Action.importMnemonic, - content: { - ImportMnemonic.View(store: $0) - .radixToolbar(title: L10n.EnterSeedPhrase.Header.title, alwaysVisible: false) - .inNavigationView - } - ) + sheet(store: destinationStore.scope(state: \.importMnemonic, action: \.importMnemonic)) { store in + ImportMnemonic.View(store: store) + .radixToolbar(title: L10n.EnterSeedPhrase.Header.title, alwaysVisible: false) + .inNavigationStack + } } private func confirmSkippingBDFS(with destinationStore: PresentationStoreOf) -> some View { - sheet( - store: destinationStore, - state: /ImportMnemonicControllingAccounts.Destination.State.confirmSkippingBDFS, - action: ImportMnemonicControllingAccounts.Destination.Action.confirmSkippingBDFS, - content: { - ConfirmSkippingBDFS.View(store: $0) - .inNavigationStack - } - ) + sheet(store: destinationStore.scope(state: \.confirmSkippingBDFS, action: \.confirmSkippingBDFS)) { + ConfirmSkippingBDFS.View(store: $0) + .inNavigationStack + } } } diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift index b504da0c1f..9150c5f514 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift @@ -80,11 +80,13 @@ public struct ImportMnemonicControllingAccounts: Sendable, FeatureReducer { // MARK: - Destination public struct Destination: DestinationReducer { + @CasePathable public enum State: Sendable, Hashable { case importMnemonic(ImportMnemonic.State) case confirmSkippingBDFS(ConfirmSkippingBDFS.State) } + @CasePathable public enum Action: Sendable, Equatable { case importMnemonic(ImportMnemonic.Action) /// **B**abylon **D**evice **F**actor **S**ource @@ -92,10 +94,10 @@ public struct ImportMnemonicControllingAccounts: Sendable, FeatureReducer { } public var body: some ReducerOf { - Scope(state: /State.importMnemonic, action: /Action.importMnemonic) { + Scope(state: \.importMnemonic, action: \.importMnemonic) { ImportMnemonic() } - Scope(state: /State.confirmSkippingBDFS, action: /Action.confirmSkippingBDFS) { + Scope(state: \.confirmSkippingBDFS, action: \.confirmSkippingBDFS) { ConfirmSkippingBDFS() } } diff --git a/RadixWallet/Features/SettingsFeature/Troubleshooting/ManualAccountRecoveryScan/ManualAccountRecoverySeedPhrase+View.swift b/RadixWallet/Features/SettingsFeature/Troubleshooting/ManualAccountRecoveryScan/ManualAccountRecoverySeedPhrase+View.swift index 18c5b46f10..69abe8a94b 100644 --- a/RadixWallet/Features/SettingsFeature/Troubleshooting/ManualAccountRecoveryScan/ManualAccountRecoverySeedPhrase+View.swift +++ b/RadixWallet/Features/SettingsFeature/Troubleshooting/ManualAccountRecoveryScan/ManualAccountRecoverySeedPhrase+View.swift @@ -89,12 +89,9 @@ private extension StoreOf { private extension View { func destinations(with store: StoreOf) -> some View { let destinationStore = store.destination - return navigationDestination( - store: destinationStore, - state: /ManualAccountRecoverySeedPhrase.Destination.State.importMnemonic, - action: ManualAccountRecoverySeedPhrase.Destination.Action.importMnemonic, - destination: { ImportMnemonic.View(store: $0) } - ) + return navigationDestination(store: destinationStore.scope(state: \.importMnemonic, action: \.importMnemonic)) { + ImportMnemonic.View(store: $0) + } } } From baafd24457ecb76190496e2990c792d90fa64496 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 01:33:02 +0200 Subject: [PATCH 11/68] make tests build --- .../Clients/ProfileStoreTests/ProfileStoreTests.swift | 6 +++--- .../Features/AppFeatureTests/AppFeatureTests.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift index 90f0c9c32f..56dc77d33f 100644 --- a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift +++ b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift @@ -804,7 +804,7 @@ final class ProfileStoreExistingProfileTests: TestCase { } func when(_ d: inout DependencyValues) { - d.overlayWindowClient.scheduleAlertAwaitAction = { _ in + d.overlayWindowClient.scheduleAlert = { _ in // THEN NO alert is displayed alertNotScheduled.fulfill() return .dismissed // irrelevant, should not happen @@ -998,7 +998,7 @@ final class ProfileStoreExistingProfileTests: TestCase { } func then(_ d: inout DependencyValues) { - d.overlayWindowClient.scheduleAlertAwaitAction = { alert in + d.overlayWindowClient.scheduleAlert = { alert in XCTAssertNoDifference( alert.message, TextState(overlayClientProfileStoreOwnershipConflictTextState) ) @@ -1127,7 +1127,7 @@ extension ProfileStoreExistingProfileTests { } func when(_ d: inout DependencyValues) { - d.overlayWindowClient.scheduleAlertAwaitAction = { alert in + d.overlayWindowClient.scheduleAlert = { alert in XCTAssertNoDifference( alert.message, TextState(overlayClientProfileStoreOwnershipConflictTextState) ) diff --git a/RadixWalletTests/Features/AppFeatureTests/AppFeatureTests.swift b/RadixWalletTests/Features/AppFeatureTests/AppFeatureTests.swift index 27324bc60a..52e031d728 100644 --- a/RadixWalletTests/Features/AppFeatureTests/AppFeatureTests.swift +++ b/RadixWalletTests/Features/AppFeatureTests/AppFeatureTests.swift @@ -21,7 +21,7 @@ final class AppFeatureTests: TestCase { $0.gatewaysClient.gatewaysValues = { AsyncLazySequence([.init(current: .mainnet)]).eraseToAnyAsyncSequence() } } // when - await store.send(.child(.main(.delegate(.removedWallet)))) { + await store.send(.internal(.didResetWallet)) { $0.root = .onboardingCoordinator(.init()) } } From 77f562625b967256a130023d1758f71b5a768a2d Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 10:04:20 +0200 Subject: [PATCH 12/68] Change when to show yellow items --- .../ConfigurationBackup+Reducer.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index aca022f9e8..7c36cb3716 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -29,8 +29,16 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { return !cloudBackupsEnabled && !lastCloudBackup.upToDate } + public var showLastBackupLabel: Bool { + if let lastCloudBackup, lastCloudBackup.success, !lastCloudBackup.upToDate { + true + } else { + false + } + } + public var actionsRequired: [Item] { - problems.isEmpty ? [] : Item.allCases + showLastBackupLabel ? Item.allCases : [] } public init() {} From ac0c7e7f9b52fceb4925046370dee8b01fdfe973 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 13:02:28 +0200 Subject: [PATCH 13/68] Remove unneeded reclaimProfile --- .../CloudBackupClient+Interface.swift | 6 +---- .../CloudBackupClient+Live.swift | 11 +++----- .../CloudBackupClient+Test.swift | 6 ++--- .../Clients/ProfileStore/ProfileStore.swift | 2 +- .../ClaimWallet/ClaimWallet+Reducer.swift | 27 ++----------------- .../ClaimWallet/ClaimWallet+View.swift | 11 -------- 6 files changed, 10 insertions(+), 53 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift index cfc72b7f1f..77220ee534 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift @@ -13,7 +13,6 @@ public struct CloudBackupClient: DependencyKey, Sendable { public let lastBackup: LastBackup public let loadProfile: LoadProfile public let loadProfileHeaders: LoadProfileHeaders - public let reclaimProfile: ReclaimProfile public init( startAutomaticBackups: @escaping StartAutomaticBackups, @@ -23,8 +22,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { checkAccountStatus: @escaping CheckAccountStatus, lastBackup: @escaping LastBackup, loadProfile: @escaping LoadProfile, - loadProfileHeaders: @escaping LoadProfileHeaders, - reclaimProfile: @escaping ReclaimProfile + loadProfileHeaders: @escaping LoadProfileHeaders ) { self.startAutomaticBackups = startAutomaticBackups self.loadDeviceID = loadDeviceID @@ -34,7 +32,6 @@ public struct CloudBackupClient: DependencyKey, Sendable { self.lastBackup = lastBackup self.loadProfile = loadProfile self.loadProfileHeaders = loadProfileHeaders - self.reclaimProfile = reclaimProfile } } @@ -47,7 +44,6 @@ extension CloudBackupClient { public typealias LastBackup = @Sendable (ProfileID) -> AnyAsyncSequence public typealias LoadProfile = @Sendable (ProfileID) async throws -> BackedUpProfile public typealias LoadProfileHeaders = @Sendable () async throws -> [Profile.Header] - public typealias ReclaimProfile = @Sendable () async throws -> Void } // MARK: CloudBackupClient.BackedUpProfile diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 4c97565152..6bfc09af8d 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -158,15 +158,15 @@ extension CloudBackupClient { let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } - let didTransferBackProfile: Bool + let shouldReclaim: Bool if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) - didTransferBackProfile = action == .claimWallet(.didTransferBack) + shouldReclaim = action == .claimWallet(.transferBack) } else { - didTransferBackProfile = false + shouldReclaim = false } - guard shouldBackUp || didTransferBackProfile else { return } + guard shouldBackUp || shouldReclaim else { return } await backupProfileAndSaveResult(existingRecord: existingRecord) } @@ -251,9 +251,6 @@ extension CloudBackupClient { try await fetchAllProfileRecords(headerOnly: true) .map(getProfileHeader) .filter(\.isNonEmpty) - }, - reclaimProfile: { - try await profileStore.claimOwnershipOfProfile() } ) } diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift index 624458ff08..02be1e332b 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift @@ -22,8 +22,7 @@ extension CloudBackupClient: TestDependencyKey { checkAccountStatus: { throw NoopError() }, lastBackup: { _ in AsyncLazySequence([]).eraseToAnyAsyncSequence() }, loadProfile: { _ in throw NoopError() }, - loadProfileHeaders: { throw NoopError() }, - reclaimProfile: { throw NoopError() } + loadProfileHeaders: { throw NoopError() } ) public static let testValue = Self( @@ -34,7 +33,6 @@ extension CloudBackupClient: TestDependencyKey { checkAccountStatus: unimplemented("\(Self.self).checkAccountStatus"), lastBackup: unimplemented("\(Self.self).lastBackup"), loadProfile: unimplemented("\(Self.self).loadProfile"), - loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders"), - reclaimProfile: unimplemented("\(Self.self).reclaimProfile") + loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders") ) } diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 9eb7dc005b..255c6aeaa4 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -392,7 +392,7 @@ extension ProfileStore { /// then saves this profile and emits an update. /// - Parameter profile: Profile to update `lastUsedOnDevice` of and /// save on this device. - public func claimOwnershipOfProfile() throws { + private func claimOwnershipOfProfile() throws { var copy = profile try _claimOwnership(of: ©) } diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index f67631ab36..58e4bb87b8 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -5,7 +5,6 @@ import SwiftUI public struct ClaimWallet: Sendable, FeatureReducer { public struct State: Sendable, Hashable { public var isLoading: Bool = false - public var failedToReclaim: Bool = false public var screenState: ControlState { isLoading ? .loading(.global(text: nil)) : .enabled @@ -19,18 +18,13 @@ public struct ClaimWallet: Sendable, FeatureReducer { case transferBackButtonTapped } - public enum InternalAction: Sendable, Equatable { - case failedToReclaim - } - public enum DelegateAction: Sendable, Equatable { case didClearWallet - case didTransferBack + case transferBack case dismiss } @Dependency(\.resetWalletClient) var resetWalletClient - @Dependency(\.cloudBackupClient) var cloudBackupClient public init() {} @@ -43,24 +37,7 @@ public struct ClaimWallet: Sendable, FeatureReducer { } case .transferBackButtonTapped: state.isLoading = true - state.failedToReclaim = false - return .run { send in - do { - try await cloudBackupClient.reclaimProfile() - await send(.delegate(.didTransferBack)) - } catch { - await send(.internal(.failedToReclaim)) - } - } - } - } - - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { - switch internalAction { - case .failedToReclaim: - state.isLoading = false - state.failedToReclaim = true - return .none + return .send(.delegate(.transferBack)) } } } diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift index 084f78a4f9..6e9a695dab 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+View.swift @@ -38,12 +38,6 @@ extension ClaimWallet { Spacer() VStack { - if viewStore.failedToReclaim { - Text("Failed to transfer ownership") - .foregroundColor(.app.error) - .textStyle(.secondaryHeader) - } - Button(L10n.ConfigurationBackup.Automated.walletTransferredClearButton) { store.send(.view(.clearWalletButtonTapped)) } @@ -54,11 +48,6 @@ extension ClaimWallet { store.send(.view(.transferBackButtonTapped)) } .buttonStyle(.primaryText()) - .overlay { - if viewStore.isLoading { - ProgressView() - } - } } } .padding(.horizontal, .large1) From b8d86a6f2a85dbb46558c72c64587d7241909c16 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 13:05:06 +0200 Subject: [PATCH 14/68] Comment fix --- .../OverlayWindowClient/OverlayWindowClient+Interface.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift index 806e285792..89da18b1ac 100644 --- a/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift +++ b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift @@ -8,7 +8,7 @@ public struct OverlayWindowClient: Sendable { /// Usually to be called from the Main Window. public var scheduleAlert: ScheduleAlert - /// Schedule an Alert to be shown in the Overlay Window, but don't want for any action + /// Schedule an Alert to be shown in the Overlay Window, but don't wait for any action public var scheduleAlertAndIgnoreAction: ScheduleAlertAndIgnoreAction /// Schedule a HUD to be shown in the Overlay Window. From 86ccf9cd76545a36ae21c07860e84c9613318eba Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 13:15:35 +0200 Subject: [PATCH 15/68] Pass in profile --- .../Clients/CloudBackupClient/CloudBackupClient+Live.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 6bfc09af8d..6aced3c185 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -124,8 +124,7 @@ extension CloudBackupClient { } @Sendable - func backupProfileAndSaveResult(existingRecord: CKRecord?) async { - let profile = await profileStore.profile + func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async { let result: BackupResult.Result do { let json = profile.toJSONString() @@ -168,7 +167,7 @@ extension CloudBackupClient { guard shouldBackUp || shouldReclaim else { return } - await backupProfileAndSaveResult(existingRecord: existingRecord) + await backupProfileAndSaveResult(profile, existingRecord: existingRecord) } let retryBackupInterval: DispatchTimeInterval = .seconds(60) From c5b47c5ad1634cac779d490e312bcbe68db68c15 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 13:15:55 +0200 Subject: [PATCH 16/68] Turn off keychain sync less often --- .../Clients/CloudBackupClient/CloudBackupClient+Live.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 6aced3c185..5e53c01437 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -175,13 +175,16 @@ extension CloudBackupClient { return .init( startAutomaticBackups: { + // The active profile should not be synced to iCloud keychain + let profileID = await profileStore.profile.id + try secureStorageClient.updateIsCloudProfileSyncEnabled(profileID, .disable) + let ticks = AsyncTimerSequence(every: retryBackupInterval) let profiles = await profileStore.values() var lastClaimCheck: Date = .distantPast for try await (profile, tick) in combineLatest(profiles, ticks) { guard !Task.isCancelled else { return } - try secureStorageClient.updateIsCloudProfileSyncEnabled(profile.id, .disable) // This will skip the ticks that get backed up while we are awaiting performAutomaticBackup guard tick > lastClaimCheck else { continue } From b5406c3a6ee1c8058e465451fc1fdf7de92d808f Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 13:17:55 +0200 Subject: [PATCH 17/68] remove unused dismiss --- RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index 58e4bb87b8..a8c1cf9976 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -21,7 +21,6 @@ public struct ClaimWallet: Sendable, FeatureReducer { public enum DelegateAction: Sendable, Equatable { case didClearWallet case transferBack - case dismiss } @Dependency(\.resetWalletClient) var resetWalletClient From b5edba53617b9ab514b387fcac62372f18ee4080 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 13:50:08 +0200 Subject: [PATCH 18/68] add support for TCA dismiss() in fullscreen overlay --- .../Features/AppFeature/Overlay/Overlay+Reducer.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift index bdf9ed37c1..99aa270184 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift @@ -103,8 +103,13 @@ struct OverlayReducer: Sendable, FeatureReducer { } func reduceDismissedDestination(into state: inout State) -> Effect { - if case let .alert(state) = state.itemsQueue.first { + switch state.itemsQueue.first { + case let .alert(state): overlayWindowClient.sendAlertAction(.dismissed, state.id) + case let .fullScreen(state): + overlayWindowClient.sendFullScreenAction(.dismiss, state.id) + default: + break } return dismiss(&state) From 4825e87d61b00185c2f2fdf964e0f6d30560bd2b Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:01:06 +0200 Subject: [PATCH 19/68] PROFILESTORE: stop emitting --- .../Clients/ProfileStore/ProfileStore.swift | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 255c6aeaa4..91a61b6eb3 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -259,21 +259,8 @@ extension ProfileStore { public func unlockedApp() async -> Profile { loggerGlobal.notice("Unlocking app") - let buffered = bufferedOwnershipConflictWhileAppLocked self.mode = .appIsUnlocked - if let buffered { - loggerGlobal.notice("We had a buffered Profile ownership conflict, emitting it now.") - do { - try await doEmit(conflictingOwners: buffered) - return profile // might be a new one! if user selected "delete" - } catch { - logAssertionFailure("Failure during Profile ownership resolution, error: \(error)") - // Not import enough to prevent app from being used - return profile - } - } else { - return profile - } + return profile } } @@ -465,34 +452,11 @@ extension ProfileStore { guard appIsUnlocked else { return buffer(conflictingOwners: conflictingOwners) } - - try await doEmit(conflictingOwners: conflictingOwners) } throw Error.profileUsedOnAnotherDevice } // All good } - - private func doEmit(conflictingOwners: ConflictingOwners) async throws { - @Dependency(\.overlayWindowClient) var overlayWindowClient - assert(appIsUnlocked) - - // We present an alert to user where they must choice if they wanna keep using Profile - // on this device or delete it. If they delete a new one will be created and we will - // onboard user... - let choiceByUser = await overlayWindowClient.scheduleAlert(.profileUsedOnAnotherDeviceAlert( - conflictingOwners: conflictingOwners - )) - - if choiceByUser == .claimAndContinueUseOnThisPhone { - try self.claimOwnershipOfProfile() - } else if choiceByUser == .deleteProfileFromThisPhone { - try self._deleteProfile( - keepInICloudIfPresent: true, // local resolution should not affect iCloud - assertOwnership: false // duh.. we know we had a conflict, ownership check will fail. - ) - } - } } // MARK: Private Static From b7e6a43d3ecdb99165c9ef158b55a3c9e4453494 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:02:19 +0200 Subject: [PATCH 20/68] remove cloud import --- .../BackupsClient+Interface.swift | 21 ------------------ .../BackupsClient/BackupsClient+Live.swift | 6 ----- .../BackupsClient/BackupsClient+Test.swift | 2 -- .../Clients/ProfileStore/ProfileStore.swift | 22 ------------------- .../RestoreProfileFromBackupCoordinator.swift | 8 +++---- 5 files changed, 4 insertions(+), 55 deletions(-) diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift index abdc3c4631..c34f4e1a4a 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift @@ -9,7 +9,6 @@ public struct BackupsClient: Sendable { public var lookupProfileSnapshotByHeader: LookupProfileSnapshotByHeader public var importProfileSnapshot: ImportProfileSnapshot public var didExportProfileSnapshot: DidExportProfileSnapshot - public var importCloudProfile: ImportCloudProfile public var loadDeviceID: LoadDeviceID public init( @@ -18,7 +17,6 @@ public struct BackupsClient: Sendable { lookupProfileSnapshotByHeader: @escaping LookupProfileSnapshotByHeader, importProfileSnapshot: @escaping ImportProfileSnapshot, didExportProfileSnapshot: @escaping DidExportProfileSnapshot, - importCloudProfile: @escaping ImportCloudProfile, loadDeviceID: @escaping LoadDeviceID ) { self.snapshotOfProfileForExport = snapshotOfProfileForExport @@ -26,7 +24,6 @@ public struct BackupsClient: Sendable { self.lookupProfileSnapshotByHeader = lookupProfileSnapshotByHeader self.importProfileSnapshot = importProfileSnapshot self.didExportProfileSnapshot = didExportProfileSnapshot - self.importCloudProfile = importCloudProfile self.loadDeviceID = loadDeviceID } } @@ -37,23 +34,5 @@ extension BackupsClient { public typealias LookupProfileSnapshotByHeader = @Sendable (Profile.Header) async throws -> (Profile?, Bool) public typealias ImportProfileSnapshot = @Sendable (Profile, Set, Bool) async throws -> Void public typealias DidExportProfileSnapshot = @Sendable (Profile) throws -> Void - public typealias ImportCloudProfile = @Sendable (Profile.Header, Set, Bool) async throws -> Void public typealias LoadDeviceID = @Sendable () async -> UUID? } - -extension BackupsClient { - public func importSnapshot( - _ snapshot: Profile, - fromCloud: Bool, - containsP2PLinks: Bool - ) async throws { - let factorSourceIDs: Set = .init( - snapshot.factorSources.compactMap { $0.extract(DeviceFactorSource.self) }.map(\.id) - ) - if fromCloud { - try await importCloudProfile(snapshot.header, factorSourceIDs, containsP2PLinks) - } else { - try await importProfileSnapshot(snapshot, factorSourceIDs, containsP2PLinks) - } - } -} diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift index a8c2da36ca..87bd42b298 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift @@ -82,12 +82,6 @@ extension BackupsClient: DependencyKey { didExportProfileSnapshot: { profile in try userDefaults.setLastManualBackup(of: profile) }, - importCloudProfile: { header, factorSourceIDs, containsP2PLinks in - try await importFor(factorSourceIDs: factorSourceIDs) { - try await profileStore.importCloudProfileSnapshot(header) - userDefaults.setShowRelinkConnectorsAfterProfileRestore(containsP2PLinks) - } - }, loadDeviceID: { try? secureStorageClient.loadDeviceInfo()?.id } diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift index 15a251d5b2..aae1ecf04f 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift @@ -16,7 +16,6 @@ extension BackupsClient: TestDependencyKey { lookupProfileSnapshotByHeader: unimplemented("\(Self.self).lookupProfileSnapshotByHeader"), importProfileSnapshot: unimplemented("\(Self.self).importProfileSnapshot"), didExportProfileSnapshot: unimplemented("\(Self.self).didExportProfileSnapshot"), - importCloudProfile: unimplemented("\(Self.self).importCloudProfile"), loadDeviceID: unimplemented("\(Self.self).loadDeviceID") ) @@ -26,7 +25,6 @@ extension BackupsClient: TestDependencyKey { lookupProfileSnapshotByHeader: { _ in throw NoopError() }, importProfileSnapshot: { _, _, _ in throw NoopError() }, didExportProfileSnapshot: { _ in throw NoopError() }, - importCloudProfile: { _, _, _ in throw NoopError() }, loadDeviceID: { nil } ) } diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 91a61b6eb3..60402d773b 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -22,7 +22,6 @@ import Sargon /// func unlockedApp() async -> Profile /// func finishedOnboarding() async /// func finishOnboarding(with _: AccountsRecoveredFromScanningUsingMnemonic) async throws -/// func importCloudProfileSnapshot(_ h: Profile.Header) throws /// func importProfileSnapshot(_ s: Profile) throws /// func deleteProfile(keepInICloudIfPresent: Bool) throws /// func updating(_ t: (inout Profile) async throws -> T) async throws -> T @@ -119,27 +118,6 @@ extension ProfileStore { } } - /// Looks up a Profile for the given `header` and tries to import it, - /// updates `headerList` (Keychain), `activeProfileID` (UserDefaults) - /// and saves the snapshot of the profile into Keychain. - /// - Parameter profile: Imported Profile to use and save. - public func importCloudProfileSnapshot( - _ header: Profile.Header - ) throws { - do { - // Load the snapshot, also this will validate if the snapshot actually exist - let profileSnapshot = try secureStorageClient.loadProfileSnapshot(header.id) - guard let profileSnapshot else { - struct FailedToLoadProfile: Swift.Error {} - throw FailedToLoadProfile() - } - try importProfileSnapshot(profileSnapshot) - } catch { - logAssertionFailure("Critical failure, unable to save imported profile snapshot: \(String(describing: error))", severity: .critical) - throw error - } - } - /// Change current profile to new imported profle snapshot and saves it, by /// updates `headerList` (Keychain), `activeProfileID` (UserDefaults) /// and saves the snapshot of the profile into Keychain. diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift index a4f54497ad..7b1c2a67e3 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift @@ -114,11 +114,11 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { } return .run { send in loggerGlobal.notice("Importing snapshot...") - try await backupsClient.importSnapshot( - profileSelection.profile, - fromCloud: profileSelection.isInCloud, - containsP2PLinks: profileSelection.containsP2PLinks + + let factorSourceIDs: Set = .init( + profileSelection.profile.factorSources.compactMap { $0.extract(DeviceFactorSource.self) }.map(\.id) ) + try await backupsClient.importProfileSnapshot(profileSelection.profile, factorSourceIDs, profileSelection.containsP2PLinks) if let notYetSavedNewMainBDFS { try await factorSourcesClient.saveNewMainBDFS(notYetSavedNewMainBDFS) From fcd84e99c56a2843294aa65271e3b16acbfe6eb8 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:02:39 +0200 Subject: [PATCH 21/68] don't sync new profiles to icloud keychain --- .../Clients/SecureStorageClient/SecureStorageClient+Live.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift index fe675e6150..46401bc00b 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift @@ -274,7 +274,7 @@ extension SecureStorageClient: DependencyKey { try saveProfile( snapshotData: profile.profileSnapshot(), key: profile.header.id.keychainKey, - iCloudSyncEnabled: profile.appPreferences.security.isCloudProfileSyncEnabled + iCloudSyncEnabled: false ) } From 531ff3fa9e95a6d0eaa6cc86e1048abfbbff4f30 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:04:44 +0200 Subject: [PATCH 22/68] Remove isInCloud --- .../Children/SelectBackup/SelectBackup+Reducer.swift | 8 ++++---- .../Coordinator/RestoreProfileFromBackupCoordinator.swift | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift index b9130a471d..bbb0655afe 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift @@ -76,7 +76,7 @@ public struct SelectBackup: Sendable, FeatureReducer { } public enum DelegateAction: Sendable, Equatable { - case selectedProfile(Profile, isInCloud: Bool, containsLegacyP2PLinks: Bool) + case selectedProfile(Profile, containsLegacyP2PLinks: Bool) case backToStartOfOnboarding case profileCreatedFromImportedBDFS } @@ -123,7 +123,7 @@ public struct SelectBackup: Sendable, FeatureReducer { return .run { send in do { let backedUpProfile = try await cloudBackupClient.loadProfile(profileID) - await send(.delegate(.selectedProfile(backedUpProfile.profile, isInCloud: true, containsLegacyP2PLinks: backedUpProfile.containsLegacyP2PLinks))) + await send(.delegate(.selectedProfile(backedUpProfile.profile, containsLegacyP2PLinks: backedUpProfile.containsLegacyP2PLinks))) } catch { errorQueue.schedule(error) } @@ -152,7 +152,7 @@ public struct SelectBackup: Sendable, FeatureReducer { case let .plaintext(profile): let containsP2PLinks = Profile.checkIfProfileJsonContainsLegacyP2PLinks(contents: data) - return .send(.delegate(.selectedProfile(profile, isInCloud: false, containsLegacyP2PLinks: containsP2PLinks))) + return .send(.delegate(.selectedProfile(profile, containsLegacyP2PLinks: containsP2PLinks))) } } catch { errorQueue.schedule(error) @@ -206,7 +206,7 @@ public struct SelectBackup: Sendable, FeatureReducer { case let .inputEncryptionPassword(.delegate(.successfullyDecrypted(_, decrypted, containsP2PLinks))): state.destination = nil overlayWindowClient.scheduleHUD(.decryptedProfile) - return .send(.delegate(.selectedProfile(decrypted, isInCloud: false, containsLegacyP2PLinks: containsP2PLinks))) + return .send(.delegate(.selectedProfile(decrypted, containsLegacyP2PLinks: containsP2PLinks))) case .inputEncryptionPassword(.delegate(.successfullyEncrypted)): preconditionFailure("Incorrect implementation, expected decryption") diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift index 7b1c2a67e3..7aad59aaf2 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift @@ -5,7 +5,6 @@ import SwiftUI // MARK: - ProfileSelection public struct ProfileSelection: Sendable, Hashable { public let profile: Profile - public let isInCloud: Bool public let containsP2PLinks: Bool } @@ -90,8 +89,8 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { - case let .root(.selectBackup(.delegate(.selectedProfile(profile, isInCloud, containsLegacyP2PLinks)))): - state.profileSelection = .init(profile: profile, isInCloud: isInCloud, containsP2PLinks: containsLegacyP2PLinks) + case let .root(.selectBackup(.delegate(.selectedProfile(profile, containsLegacyP2PLinks)))): + state.profileSelection = .init(profile: profile, containsP2PLinks: containsLegacyP2PLinks) return .run { send in try? await clock.sleep(for: .milliseconds(300)) From f37750be77fa70ca71857acdfd8a2d3cf5385c2f Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:11:10 +0200 Subject: [PATCH 23/68] simplify --- .../BackupsClient/BackupsClient+Live.swift | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift index 87bd42b298..be791a1357 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift @@ -11,22 +11,6 @@ extension BackupsClient: DependencyKey { @Dependency(\.secureStorageClient) var secureStorageClient @Dependency(\.factorSourcesClient) var factorSourcesClient - @Sendable - func importFor( - factorSourceIDs: Set, - operation: () async throws -> Void - ) async throws { - do { - try await operation() - } catch { - // revert the saved mnemonic - for factorSourceID in factorSourceIDs { - try? secureStorageClient.deleteMnemonicByFactorSourceID(factorSourceID) - } - throw error - } - } - return Self( snapshotOfProfileForExport: { await profileStore.profile @@ -74,9 +58,15 @@ extension BackupsClient: DependencyKey { return (profileSnapshot, containsP2PLinks) }, importProfileSnapshot: { snapshot, factorSourceIDs, containsP2PLinks in - try await importFor(factorSourceIDs: factorSourceIDs) { + do { try await profileStore.importProfileSnapshot(snapshot) userDefaults.setShowRelinkConnectorsAfterProfileRestore(containsP2PLinks) + } catch { + // Revert the saved mnemonic + for factorSourceID in factorSourceIDs { + try? secureStorageClient.deleteMnemonicByFactorSourceID(factorSourceID) + } + throw error } }, didExportProfileSnapshot: { profile in From c406537a8eccadc079f74993b46f34fb77b8f443 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:13:17 +0200 Subject: [PATCH 24/68] remove lookupProfileSnapshotByHeader --- .../BackupsClient/BackupsClient+Interface.swift | 4 ---- .../Clients/BackupsClient/BackupsClient+Live.swift | 10 ---------- .../Clients/BackupsClient/BackupsClient+Test.swift | 2 -- 3 files changed, 16 deletions(-) diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift index c34f4e1a4a..ee0edb9835 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift @@ -6,7 +6,6 @@ public typealias DeviceID = UUID public struct BackupsClient: Sendable { public var snapshotOfProfileForExport: SnapshotOfProfileForExport public var loadProfileBackups: LoadProfileBackups - public var lookupProfileSnapshotByHeader: LookupProfileSnapshotByHeader public var importProfileSnapshot: ImportProfileSnapshot public var didExportProfileSnapshot: DidExportProfileSnapshot public var loadDeviceID: LoadDeviceID @@ -14,14 +13,12 @@ public struct BackupsClient: Sendable { public init( snapshotOfProfileForExport: @escaping SnapshotOfProfileForExport, loadProfileBackups: @escaping LoadProfileBackups, - lookupProfileSnapshotByHeader: @escaping LookupProfileSnapshotByHeader, importProfileSnapshot: @escaping ImportProfileSnapshot, didExportProfileSnapshot: @escaping DidExportProfileSnapshot, loadDeviceID: @escaping LoadDeviceID ) { self.snapshotOfProfileForExport = snapshotOfProfileForExport self.loadProfileBackups = loadProfileBackups - self.lookupProfileSnapshotByHeader = lookupProfileSnapshotByHeader self.importProfileSnapshot = importProfileSnapshot self.didExportProfileSnapshot = didExportProfileSnapshot self.loadDeviceID = loadDeviceID @@ -31,7 +28,6 @@ public struct BackupsClient: Sendable { extension BackupsClient { public typealias SnapshotOfProfileForExport = @Sendable () async throws -> Profile public typealias LoadProfileBackups = @Sendable () async -> Profile.HeaderList? - public typealias LookupProfileSnapshotByHeader = @Sendable (Profile.Header) async throws -> (Profile?, Bool) public typealias ImportProfileSnapshot = @Sendable (Profile, Set, Bool) async throws -> Void public typealias DidExportProfileSnapshot = @Sendable (Profile) throws -> Void public typealias LoadDeviceID = @Sendable () async -> UUID? diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift index be791a1357..a8ddd15fa9 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift @@ -47,16 +47,6 @@ extension BackupsClient: DependencyKey { return nil } }, - lookupProfileSnapshotByHeader: { header in - let containsP2PLinks = if let profileSnapshotData = try? secureStorageClient.loadProfileSnapshotData(header.id) { - Profile.checkIfProfileJsonContainsLegacyP2PLinks(contents: profileSnapshotData) - } else { - false - } - let profileSnapshot = try secureStorageClient.loadProfileSnapshot(header.id) - - return (profileSnapshot, containsP2PLinks) - }, importProfileSnapshot: { snapshot, factorSourceIDs, containsP2PLinks in do { try await profileStore.importProfileSnapshot(snapshot) diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift index aae1ecf04f..6c3889aac1 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift @@ -13,7 +13,6 @@ extension BackupsClient: TestDependencyKey { public static let testValue = Self( snapshotOfProfileForExport: unimplemented("\(Self.self).snapshotOfProfileForExport"), loadProfileBackups: unimplemented("\(Self.self).loadProfile"), - lookupProfileSnapshotByHeader: unimplemented("\(Self.self).lookupProfileSnapshotByHeader"), importProfileSnapshot: unimplemented("\(Self.self).importProfileSnapshot"), didExportProfileSnapshot: unimplemented("\(Self.self).didExportProfileSnapshot"), loadDeviceID: unimplemented("\(Self.self).loadDeviceID") @@ -22,7 +21,6 @@ extension BackupsClient: TestDependencyKey { public static let noop = Self( snapshotOfProfileForExport: { throw NoopError() }, loadProfileBackups: { nil }, - lookupProfileSnapshotByHeader: { _ in throw NoopError() }, importProfileSnapshot: { _, _, _ in throw NoopError() }, didExportProfileSnapshot: { _ in throw NoopError() }, loadDeviceID: { nil } From d5316cce7f42dac2c812ea2f98735b7d1dece3bc Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:20:11 +0200 Subject: [PATCH 25/68] Delete ProfileBackupSettings --- RadixWallet.xcodeproj/project.pbxproj | 16 - .../ProfileBackupSettings+Reducer.swift | 320 ------------------ .../ProfileBackupSettings+View.swift | 213 ------------ .../SelectBackup/SelectBackup+Reducer.swift | 1 + 4 files changed, 1 insertion(+), 549 deletions(-) delete mode 100644 RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+Reducer.swift delete mode 100644 RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+View.swift diff --git a/RadixWallet.xcodeproj/project.pbxproj b/RadixWallet.xcodeproj/project.pbxproj index 9027e14ce0..ebb1c39528 100644 --- a/RadixWallet.xcodeproj/project.pbxproj +++ b/RadixWallet.xcodeproj/project.pbxproj @@ -166,8 +166,6 @@ 48CFC2AC2ADC10D900E77A5C /* Home+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD112ADC10D800E77A5C /* Home+View.swift */; }; 48CFC2B12ADC10D900E77A5C /* DerivePublicKeys+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD1A2ADC10D800E77A5C /* DerivePublicKeys+View.swift */; }; 48CFC2B22ADC10D900E77A5C /* DerivePublicKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD1B2ADC10D800E77A5C /* DerivePublicKeys.swift */; }; - 48CFC2B32ADC10D900E77A5C /* ProfileBackupSettings+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD1E2ADC10D800E77A5C /* ProfileBackupSettings+View.swift */; }; - 48CFC2B42ADC10D900E77A5C /* ProfileBackupSettings+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD1F2ADC10D800E77A5C /* ProfileBackupSettings+Reducer.swift */; }; 48CFC2B52ADC10D900E77A5C /* EncryptOrDecryptProfile+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD212ADC10D800E77A5C /* EncryptOrDecryptProfile+View.swift */; }; 48CFC2B62ADC10D900E77A5C /* EncryptOrDecryptProfile+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD222ADC10D800E77A5C /* EncryptOrDecryptProfile+Reducer.swift */; }; 48CFC2B72ADC10D900E77A5C /* ExportableProfileFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD232ADC10D800E77A5C /* ExportableProfileFile.swift */; }; @@ -1337,8 +1335,6 @@ 48CFBD112ADC10D800E77A5C /* Home+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Home+View.swift"; sourceTree = ""; }; 48CFBD1A2ADC10D800E77A5C /* DerivePublicKeys+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DerivePublicKeys+View.swift"; sourceTree = ""; }; 48CFBD1B2ADC10D800E77A5C /* DerivePublicKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DerivePublicKeys.swift; sourceTree = ""; }; - 48CFBD1E2ADC10D800E77A5C /* ProfileBackupSettings+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ProfileBackupSettings+View.swift"; sourceTree = ""; }; - 48CFBD1F2ADC10D800E77A5C /* ProfileBackupSettings+Reducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ProfileBackupSettings+Reducer.swift"; sourceTree = ""; }; 48CFBD212ADC10D800E77A5C /* EncryptOrDecryptProfile+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EncryptOrDecryptProfile+View.swift"; sourceTree = ""; }; 48CFBD222ADC10D800E77A5C /* EncryptOrDecryptProfile+Reducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EncryptOrDecryptProfile+Reducer.swift"; sourceTree = ""; }; 48CFBD232ADC10D800E77A5C /* ExportableProfileFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExportableProfileFile.swift; sourceTree = ""; }; @@ -3080,22 +3076,12 @@ 48CFBD1C2ADC10D800E77A5C /* ProfileBackupsFeature */ = { isa = PBXGroup; children = ( - 48CFBD1D2ADC10D800E77A5C /* ProfileBackupSettings */, 48CFBD202ADC10D800E77A5C /* Shared */, 48CFBD242ADC10D800E77A5C /* RestoreProfileFromBackup */, ); path = ProfileBackupsFeature; sourceTree = ""; }; - 48CFBD1D2ADC10D800E77A5C /* ProfileBackupSettings */ = { - isa = PBXGroup; - children = ( - 48CFBD1E2ADC10D800E77A5C /* ProfileBackupSettings+View.swift */, - 48CFBD1F2ADC10D800E77A5C /* ProfileBackupSettings+Reducer.swift */, - ); - path = ProfileBackupSettings; - sourceTree = ""; - }; 48CFBD202ADC10D800E77A5C /* Shared */ = { isa = PBXGroup; children = ( @@ -7371,7 +7357,6 @@ 48CFC2842ADC10D900E77A5C /* TransactionReview.swift in Sources */, 48CFC5142ADC10DA00E77A5C /* MetadataNonFungibleLocalIdArrayValue.swift in Sources */, 48CFC4782ADC10DA00E77A5C /* LocalAuthenticationConfig.swift in Sources */, - 48CFC2B32ADC10D900E77A5C /* ProfileBackupSettings+View.swift in Sources */, 48CFC2962ADC10D900E77A5C /* NormalFeesCustomization.swift in Sources */, 48CFC3172ADC10D900E77A5C /* Settings+Reducer.swift in Sources */, E7A5AC982C09F44C006CB6EC /* ResetWalletClient+Live.swift in Sources */, @@ -7714,7 +7699,6 @@ 48CFC4E22ADC10DA00E77A5C /* InvalidEntityError.swift in Sources */, A41557552B7645F70040AD4E /* TransactionHistory+View.swift in Sources */, 48CFC47D2ADC10DA00E77A5C /* BackupsClient+Interface.swift in Sources */, - 48CFC2B42ADC10D900E77A5C /* ProfileBackupSettings+Reducer.swift in Sources */, 48CFC5792ADC10DA00E77A5C /* FungibleResourcesCollectionItem.swift in Sources */, 48CFC4AF2ADC10DA00E77A5C /* NonFungibleResourcesCollectionItemGloballyAggregated.swift in Sources */, 48CFC54D2ADC10DA00E77A5C /* StateEntityDetailsResponseNonFungibleResourceDetails.swift in Sources */, diff --git a/RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+Reducer.swift deleted file mode 100644 index bd00779d0b..0000000000 --- a/RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+Reducer.swift +++ /dev/null @@ -1,320 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -// MARK: - ProfileBackupSettings -public struct ProfileBackupSettings: Sendable, FeatureReducer { - public struct State: Sendable, Hashable { - public var preferences: AppPreferences? - public var backupProfileHeaders: Profile.HeaderList? - public var selectedProfileHeader: Profile.Header? - - public var thisDeviceID: UUID? - - var isCloudProfileSyncEnabled: Bool { - preferences?.security.isCloudProfileSyncEnabled == true - } - - @PresentationState - public var destination: Destination.State? - - /// An exportable Profile file, either encrypted or plaintext. - public var profileFile: ExportableProfileFile? - - public init( - backupProfileHeaders: Profile.HeaderList? = nil, - selectedProfileHeader: Profile.Header? = nil, - thisDeviceID: UUID? = nil - ) { - self.backupProfileHeaders = backupProfileHeaders - self.selectedProfileHeader = selectedProfileHeader - self.thisDeviceID = thisDeviceID - } - } - - public enum ViewAction: Sendable, Equatable { - case task - case cloudProfileSyncToggled(Bool) - case exportProfileButtonTapped - case dismissFileExporter - case profileExportResult(Result) - - case deleteProfileAndFactorSourcesButtonTapped - } - - public struct Destination: DestinationReducer { - static let confirmCloudSyncDisableAlert: Self.State = .confirmCloudSyncDisable(.init( - title: { - TextState(L10n.IOSProfileBackup.ConfirmCloudSyncDisableAlert.title) - }, - actions: { - ButtonState(role: .destructive, action: .confirm) { - TextState(L10n.Common.confirm) - } - } - )) - - static let optionallyEncryptProfileBeforeExportingAlert: Self.State = .optionallyEncryptProfileBeforeExporting(.init( - title: { - TextState(L10n.ProfileBackup.ManualBackups.encryptBackupDialogTitle) - }, - actions: { - ButtonState(action: .encrypt) { - TextState(L10n.ProfileBackup.ManualBackups.encryptBackupDialogConfirm) - } - ButtonState(action: .doNotEncrypt) { - TextState(L10n.ProfileBackup.ManualBackups.encryptBackupDialogDeny) - } - } - )) - - public enum State: Sendable, Hashable { - case confirmCloudSyncDisable(AlertState) - case syncTakesLongTimeAlert(AlertState) - case optionallyEncryptProfileBeforeExporting(AlertState) - case deleteProfileConfirmationDialog(ConfirmationDialogState) - - case inputEncryptionPassword(EncryptOrDecryptProfile.State) - } - - public enum Action: Sendable, Equatable { - case confirmCloudSyncDisable(ConfirmCloudSyncDisable) - case optionallyEncryptProfileBeforeExporting(SelectEncryptOrNot) - - case inputEncryptionPassword(EncryptOrDecryptProfile.Action) - case syncTakesLongTimeAlert(SyncTakesLongTimeAlert) - - public enum ConfirmCloudSyncDisable: Sendable, Hashable { - case confirm - } - - public enum SyncTakesLongTimeAlert: Sendable, Hashable { - case ok - } - - public enum SelectEncryptOrNot: Sendable, Hashable { - case encrypt - case doNotEncrypt - } - - case deleteProfileConfirmationDialog(DeleteProfileConfirmationDialogAction) - - public enum DeleteProfileConfirmationDialogAction: Sendable, Hashable { - case deleteProfile - case deleteProfileLocalKeepInICloudIfPresent - case cancel - } - } - - public var body: some Reducer { - Scope(state: /State.optionallyEncryptProfileBeforeExporting, action: /Action.optionallyEncryptProfileBeforeExporting) { - EmptyReducer() - } - Scope(state: /State.inputEncryptionPassword, action: /Action.inputEncryptionPassword) { - EncryptOrDecryptProfile() - } - } - } - - public enum InternalAction: Sendable, Equatable { - case loadedProfileSnapshotToExportAsPlaintext(Profile) - case loadPreferences(AppPreferences) - } - - public enum DelegateAction: Sendable, Equatable { - case deleteProfileAndFactorSources(keepInICloudIfPresent: Bool) - } - - @Dependency(\.errorQueue) var errorQueue - @Dependency(\.cacheClient) var cacheClient - @Dependency(\.backupsClient) var backupsClient - @Dependency(\.appPreferencesClient) var appPreferencesClient - @Dependency(\.radixConnectClient) var radixConnectClient - @Dependency(\.overlayWindowClient) var overlayWindowClient - @Dependency(\.userDefaults) var userDefaults - - public init() {} - - public var body: some ReducerOf { - Reduce(core) - .ifLet(destinationPath, action: /Action.destination) { - Destination() - } - } - - private let destinationPath: WritableKeyPath> = \.$destination - - public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { - switch viewAction { - case .deleteProfileAndFactorSourcesButtonTapped: - state.destination = .deleteProfileConfirmationDialog(.deleteProfileConfirmationDialog) - return .none - - case let .cloudProfileSyncToggled(isEnabled): - if !isEnabled { - state.destination = Destination.confirmCloudSyncDisableAlert - return .none - } else { - return updateCloudSync(state: &state, isEnabled: true) - } - - case .exportProfileButtonTapped: - state.destination = Destination.optionallyEncryptProfileBeforeExportingAlert - return .none - - case .task: - return .run { send in - await send(.internal(.loadPreferences( - appPreferencesClient.getPreferences() - ))) - } - - case .dismissFileExporter: - state.profileFile = nil - return .none - - case let .profileExportResult(.success(exportedProfileURL)): - let didEncryptIt = exportedProfileURL.absoluteString.contains(.profileFileEncryptedPart) - overlayWindowClient.scheduleHUD(.exportedProfile(encrypted: didEncryptIt)) - loggerGlobal.notice("Profile successfully exported to: \(exportedProfileURL)") - return .none - - case let .profileExportResult(.failure(error)): - loggerGlobal.error("Failed to export profile, error: \(error)") - errorQueue.schedule(error) - return .none - } - } - - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { - switch internalAction { - case let .loadPreferences(preferences): - state.preferences = preferences - return .none - - case let .loadedProfileSnapshotToExportAsPlaintext(snapshot): - return showFileExporter(with: .plaintext(snapshot), &state) - } - } - - public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { - switch presentedAction { - case let .deleteProfileConfirmationDialog(confirmationAction): - switch confirmationAction { - case .deleteProfile: - return deleteProfile(keepInICloudIfPresent: false) - - case .deleteProfileLocalKeepInICloudIfPresent: - return deleteProfile(keepInICloudIfPresent: true) - - case .cancel: - return .none - } - - case .syncTakesLongTimeAlert(.ok): - state.destination = nil - return .none - - case .optionallyEncryptProfileBeforeExporting(.doNotEncrypt): - return exportProfile(encrypt: false, state: &state) - - case .optionallyEncryptProfileBeforeExporting(.encrypt): - return exportProfile(encrypt: true, state: &state) - - case .confirmCloudSyncDisable(.confirm): - state.destination = nil - return updateCloudSync(state: &state, isEnabled: false) - - case .inputEncryptionPassword(.delegate(.dismiss)): - state.destination = nil - return .none - - case .inputEncryptionPassword(.delegate(.successfullyDecrypted)): - preconditionFailure("What? Decrypted? Expected to only have ENCRYPTED. Incorrect implementation somewhere...") - - case let .inputEncryptionPassword(.delegate(.successfullyEncrypted(_, encrypted: encrypted))): - state.destination = nil - return showFileExporter(with: .encrypted(encrypted), &state) - - default: - return .none - } - } - - public func reduceDismissedDestination(into state: inout State) -> Effect { - state.destination = nil - return .none - } - - private func showFileExporter(with file: ExportableProfileFile, _ state: inout State) -> Effect { - // This will trigger `fileExporter` to be shown - state.profileFile = file - return .none - } - - private func exportProfile(encrypt: Bool, state: inout State) -> Effect { - if encrypt { - state.destination = .inputEncryptionPassword(.init(mode: .loadThenEncrypt)) - return .none - } else { - return .run { send in - do { - let snapshot = try await backupsClient.snapshotOfProfileForExport() - await send(.internal(.loadedProfileSnapshotToExportAsPlaintext(snapshot))) - } catch { - loggerGlobal.error("Failed to encrypt profile snapshot, error: \(error)") - errorQueue.schedule(error) - } - } - } - } - - private func updateCloudSync(state: inout State, isEnabled: Bool) -> Effect { - state.preferences?.security.isCloudProfileSyncEnabled = isEnabled - if isEnabled { - state.destination = .cloudSyncTakesLongTimeAlert - } - return .run { _ in - try await appPreferencesClient.setIsCloudProfileSyncEnabled(isEnabled) - } - } - - private func deleteProfile(keepInICloudIfPresent: Bool) -> Effect { - .run { send in - cacheClient.removeAll() - await radixConnectClient.disconnectAll() - userDefaults.removeAll() - await send(.delegate(.deleteProfileAndFactorSources(keepInICloudIfPresent: keepInICloudIfPresent))) - } - } -} - -// MARK: - LackedPermissionToAccessSecurityScopedResource -struct LackedPermissionToAccessSecurityScopedResource: Error {} - -extension ConfirmationDialogState { - static let deleteProfileConfirmationDialog = ConfirmationDialogState { - TextState(L10n.ProfileBackup.ResetWalletDialog.title) - } actions: { - ButtonState(role: .destructive, action: .deleteProfileLocalKeepInICloudIfPresent) { - TextState(L10n.ProfileBackup.ResetWalletDialog.resetButtonTitle) - } - ButtonState(role: .destructive, action: .deleteProfile) { - TextState(L10n.ProfileBackup.ResetWalletDialog.resetAndDeleteBackupButtonTitle) - } - ButtonState(role: .cancel, action: .cancel) { - TextState(L10n.Common.cancel) - } - } message: { - TextState(L10n.ProfileBackup.ResetWalletDialog.message) - } -} - -extension ProfileBackupSettings.Destination.State { - fileprivate static let cloudSyncTakesLongTimeAlert = Self.syncTakesLongTimeAlert(.init( - title: { TextState(L10n.IOSProfileBackup.ICloudSyncEnabledAlert.title) }, - actions: { - ButtonState(action: .ok, label: { TextState(L10n.Common.ok) }) - }, - message: { TextState(L10n.IOSProfileBackup.ICloudSyncEnabledAlert.message) } - )) -} diff --git a/RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+View.swift b/RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+View.swift deleted file mode 100644 index e0aa39d548..0000000000 --- a/RadixWallet/Features/ProfileBackupsFeature/ProfileBackupSettings/ProfileBackupSettings+View.swift +++ /dev/null @@ -1,213 +0,0 @@ -import ComposableArchitecture -import SwiftUI -import UniformTypeIdentifiers - -extension ProfileBackupSettings.State { - var viewState: ProfileBackupSettings.ViewState { - .init( - isCloudProfileSyncEnabled: isCloudProfileSyncEnabled, - profileFile: profileFile - ) - } -} - -extension ProfileBackupSettings { - public struct ViewState: Equatable { - let isCloudProfileSyncEnabled: Bool - let profileFile: ExportableProfileFile? - - public var isDisplayingFileExporter: Bool { - profileFile != nil - } - } - - @MainActor - public struct View: SwiftUI.View { - private let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some SwiftUI.View { - WithViewStore(store, observe: \.viewState, send: { .view($0) }) { viewStore in - coreView(with: viewStore) - .destinations(with: store) - .exportFileSheet(with: viewStore) - } - .task { @MainActor in - await store.send(.view(.task)).finish() - } - .radixToolbar(title: L10n.AccountSecuritySettings.Backups.title) - } - } -} - -extension ProfileBackupSettings.View { - @MainActor - @ViewBuilder - private func coreView(with viewStore: ViewStoreOf) -> some SwiftUI.View { - ScrollView { - VStack(alignment: .leading, spacing: .large3) { - // Contains bold text segments. - Text(LocalizedStringKey(L10n.ProfileBackup.headerTitle)) - .padding(.horizontal, .medium2) - .padding(.vertical, .small1) - - section(L10n.ProfileBackup.AutomaticBackups.title) { - isCloudProfileSyncEnabled(with: viewStore) - } - - section(L10n.ProfileBackup.ManualBackups.title) { - VStack(alignment: .leading, spacing: .medium1) { - // Contains bold text segments. - Text(LocalizedStringKey(L10n.ProfileBackup.ManualBackups.subtitle)) - Button(L10n.ProfileBackup.ManualBackups.exportButtonTitle) { - viewStore.send(.exportProfileButtonTapped) - } - .buttonStyle(.secondaryRectangular(shouldExpand: true)) - } - } - - section(L10n.ProfileBackup.DeleteWallet.buttonTitle) { - VStack(alignment: .leading, spacing: .medium1) { - // Contains bold text segments. - Text(LocalizedStringKey(L10n.IOSProfileBackup.DeleteWallet.subtitle)) - - Button(L10n.IOSProfileBackup.DeleteWallet.confirmButton) { - viewStore.send(.deleteProfileAndFactorSourcesButtonTapped) - } - .foregroundColor(.app.white) - .font(.app.body1Header) - .frame(height: .standardButtonHeight) - .frame(maxWidth: .infinity) - .padding(.horizontal, .medium1) - .background(.app.red1) - .cornerRadius(.small2) - } - } - } - } - .background(Color.app.gray5) - .foregroundColor(.app.gray2) - .textStyle(.body1HighImportance) - .multilineTextAlignment(.leading) - } - - @MainActor - @ViewBuilder - private func section( - _ title: String, - @ViewBuilder content: () -> some SwiftUI.View - ) -> some SwiftUI.View { - VStack(alignment: .leading, spacing: .small1) { - Text(title) - .padding(.horizontal, .medium2) - .background(Color.app.gray5) - - content() - .padding(.horizontal, .medium2) - .padding(.vertical, .small1) - .background(Color.app.white) - } - } - - @MainActor - private func isCloudProfileSyncEnabled(with viewStore: ViewStoreOf) -> some SwiftUI.View { - HStack { - Image(asset: AssetResource.backups) - - ToggleView( - title: L10n.IOSProfileBackup.ProfileSync.title, - subtitle: L10n.IOSProfileBackup.ProfileSync.subtitle, - isOn: viewStore.binding( - get: \.isCloudProfileSyncEnabled, - send: { .cloudProfileSyncToggled($0) } - ) - ) - } - } -} - -private extension StoreOf { - var destination: PresentationStoreOf { - func scopeState(state: State) -> PresentationState { - state.$destination - } - return scope(state: scopeState, action: Action.destination) - } -} - -@MainActor -private extension View { - func destinations(with store: StoreOf) -> some View { - let destinationStore = store.destination - return cloudSyncTakesLongTimeAlert(with: destinationStore) - .disableCloudSyncConfirmationAlert(with: destinationStore) - .encryptBeforeExportChoiceAlert(with: destinationStore) - .encryptBeforeExportSheet(with: destinationStore) - .deleteProfileConfirmationDialog(with: destinationStore) - } - - private func deleteProfileConfirmationDialog(with destinationStore: PresentationStoreOf) -> some View { - confirmationDialog( - store: destinationStore, - state: /ProfileBackupSettings.Destination.State.deleteProfileConfirmationDialog, - action: ProfileBackupSettings.Destination.Action.deleteProfileConfirmationDialog - ) - } - - private func cloudSyncTakesLongTimeAlert(with destinationStore: PresentationStoreOf) -> some View { - alert( - store: destinationStore, - state: /ProfileBackupSettings.Destination.State.syncTakesLongTimeAlert, - action: ProfileBackupSettings.Destination.Action.syncTakesLongTimeAlert - ) - } - - private func disableCloudSyncConfirmationAlert(with destinationStore: PresentationStoreOf) -> some View { - alert( - store: destinationStore, - state: /ProfileBackupSettings.Destination.State.confirmCloudSyncDisable, - action: ProfileBackupSettings.Destination.Action.confirmCloudSyncDisable - ) - } - - private func encryptBeforeExportChoiceAlert(with destinationStore: PresentationStoreOf) -> some View { - alert( - store: destinationStore, - state: /ProfileBackupSettings.Destination.State.optionallyEncryptProfileBeforeExporting, - action: ProfileBackupSettings.Destination.Action.optionallyEncryptProfileBeforeExporting - ) - } - - private func encryptBeforeExportSheet(with destinationStore: PresentationStoreOf) -> some View { - sheet( - store: destinationStore, - state: /ProfileBackupSettings.Destination.State.inputEncryptionPassword, - action: ProfileBackupSettings.Destination.Action.inputEncryptionPassword, - content: { EncryptOrDecryptProfile.View(store: $0).inNavigationView } - ) - } - - func exportFileSheet(with viewStore: ViewStoreOf) -> some View { - fileExporter( - isPresented: viewStore.binding( - get: \.isDisplayingFileExporter, - send: .dismissFileExporter - ), - document: viewStore.profileFile, - contentType: .profile, - // Need to disable, since broken in swiftformat 0.52.7 - // swiftformat:disable redundantClosure - defaultFilename: { - switch viewStore.profileFile { - case .plaintext, .none: String.filenameProfileNotEncrypted - case .encrypted: String.filenameProfileEncrypted - } - }(), - // swiftformat:enable redundantClosure - onCompletion: { viewStore.send(.profileExportResult($0.mapError { $0 as NSError })) } - ) - } -} diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift index bbb0655afe..9bec2974ad 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift @@ -140,6 +140,7 @@ public struct SelectBackup: Sendable, FeatureReducer { case let .profileImportResult(.success(profileURL)): do { guard profileURL.startAccessingSecurityScopedResource() else { + struct LackedPermissionToAccessSecurityScopedResource: Error {} throw LackedPermissionToAccessSecurityScopedResource() } defer { profileURL.stopAccessingSecurityScopedResource() } From 895d9fdc01132477dc8071fe69a7224ec90de19e Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 22:27:13 +0200 Subject: [PATCH 26/68] Remove loadProfileBackups --- .../BackupsClient+Interface.swift | 4 --- .../BackupsClient/BackupsClient+Live.swift | 35 ------------------- .../BackupsClient/BackupsClient+Test.swift | 2 -- 3 files changed, 41 deletions(-) diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift index ee0edb9835..e4fddc510a 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift @@ -5,20 +5,17 @@ public typealias DeviceID = UUID // MARK: - BackupsClient public struct BackupsClient: Sendable { public var snapshotOfProfileForExport: SnapshotOfProfileForExport - public var loadProfileBackups: LoadProfileBackups public var importProfileSnapshot: ImportProfileSnapshot public var didExportProfileSnapshot: DidExportProfileSnapshot public var loadDeviceID: LoadDeviceID public init( snapshotOfProfileForExport: @escaping SnapshotOfProfileForExport, - loadProfileBackups: @escaping LoadProfileBackups, importProfileSnapshot: @escaping ImportProfileSnapshot, didExportProfileSnapshot: @escaping DidExportProfileSnapshot, loadDeviceID: @escaping LoadDeviceID ) { self.snapshotOfProfileForExport = snapshotOfProfileForExport - self.loadProfileBackups = loadProfileBackups self.importProfileSnapshot = importProfileSnapshot self.didExportProfileSnapshot = didExportProfileSnapshot self.loadDeviceID = loadDeviceID @@ -27,7 +24,6 @@ public struct BackupsClient: Sendable { extension BackupsClient { public typealias SnapshotOfProfileForExport = @Sendable () async throws -> Profile - public typealias LoadProfileBackups = @Sendable () async -> Profile.HeaderList? public typealias ImportProfileSnapshot = @Sendable (Profile, Set, Bool) async throws -> Void public typealias DidExportProfileSnapshot = @Sendable (Profile) throws -> Void public typealias LoadDeviceID = @Sendable () async -> UUID? diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift index a8ddd15fa9..b90b31ae4a 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift @@ -1,7 +1,5 @@ extension BackupsClient: DependencyKey { - public typealias Value = BackupsClient - public static let liveValue = Self.live() public static func live( @@ -9,44 +7,11 @@ extension BackupsClient: DependencyKey { ) -> Self { @Dependency(\.userDefaults) var userDefaults @Dependency(\.secureStorageClient) var secureStorageClient - @Dependency(\.factorSourcesClient) var factorSourcesClient return Self( snapshotOfProfileForExport: { await profileStore.profile }, - loadProfileBackups: { () -> Profile.HeaderList? in - do { - let headers = try secureStorageClient.loadProfileHeaderList() - guard let headers else { - return nil - } - // filter out header for which the related profile is not present in the keychain: - var filteredHeaders = [Profile.Header]() - for header in headers { - guard - let snapshot = try? secureStorageClient.loadProfileSnapshot(header.id), - // A profile will be empty (no network) if you start app and go to RESTORE. - // We will delete this empty profile in ProfileStore once user finished import. - !snapshot.networks.isEmpty - else { - continue - } - filteredHeaders.append(header) - } - guard !filteredHeaders.isEmpty else { - return nil - } - - return .init(rawValue: filteredHeaders.asIdentified()) - } catch { - assertionFailure("Corrupt Profile headers") - loggerGlobal.critical("Corrupt Profile header: \(error.legibleLocalizedDescription)") - // Corrupt Profile Headers, delete - _ = try? secureStorageClient.deleteProfileHeaderList() - return nil - } - }, importProfileSnapshot: { snapshot, factorSourceIDs, containsP2PLinks in do { try await profileStore.importProfileSnapshot(snapshot) diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift index 6c3889aac1..a8f7f0c8cb 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift +++ b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift @@ -12,7 +12,6 @@ extension BackupsClient: TestDependencyKey { public static let testValue = Self( snapshotOfProfileForExport: unimplemented("\(Self.self).snapshotOfProfileForExport"), - loadProfileBackups: unimplemented("\(Self.self).loadProfile"), importProfileSnapshot: unimplemented("\(Self.self).importProfileSnapshot"), didExportProfileSnapshot: unimplemented("\(Self.self).didExportProfileSnapshot"), loadDeviceID: unimplemented("\(Self.self).loadDeviceID") @@ -20,7 +19,6 @@ extension BackupsClient: TestDependencyKey { public static let noop = Self( snapshotOfProfileForExport: { throw NoopError() }, - loadProfileBackups: { nil }, importProfileSnapshot: { _, _, _ in throw NoopError() }, didExportProfileSnapshot: { _ in throw NoopError() }, loadDeviceID: { nil } From 82875d9f9a3d056816633abefedc6ad2d48f02c4 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 23:18:14 +0200 Subject: [PATCH 27/68] Rename and refactor backupsclient --- RadixWallet.xcodeproj/project.pbxproj | 30 +++++++++---------- .../BackupsClient+Interface.swift | 30 ------------------- .../BackupsClient/BackupsClient+Test.swift | 26 ---------------- .../TransportProfileClient+Interface.swift | 30 +++++++++++++++++++ .../TransportProfileClient+Live.swift} | 12 ++++---- .../TransportProfileClient+Test.swift | 26 ++++++++++++++++ .../ImportMnemonicsFlowCoordinator.swift | 4 +-- .../RestoreProfileFromBackupCoordinator.swift | 4 +-- .../EncryptOrDecryptProfile+Reducer.swift | 4 +-- .../ConfigurationBackup+Reducer.swift | 4 +-- .../Coordinator/DisplayMnemonics.swift | 1 - 11 files changed, 85 insertions(+), 86 deletions(-) delete mode 100644 RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift delete mode 100644 RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift create mode 100644 RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift rename RadixWallet/Clients/{BackupsClient/BackupsClient+Live.swift => TransportProfileClient/TransportProfileClient+Live.swift} (80%) create mode 100644 RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift diff --git a/RadixWallet.xcodeproj/project.pbxproj b/RadixWallet.xcodeproj/project.pbxproj index ebb1c39528..b182b55d8e 100644 --- a/RadixWallet.xcodeproj/project.pbxproj +++ b/RadixWallet.xcodeproj/project.pbxproj @@ -472,8 +472,8 @@ 48CFC4792ADC10DA00E77A5C /* LocalAuthenticationClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFAB2ADC10D900E77A5C /* LocalAuthenticationClient+Live.swift */; }; 48CFC47A2ADC10DA00E77A5C /* LocalAuthenticationClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFAC2ADC10D900E77A5C /* LocalAuthenticationClient+Interface.swift */; }; 48CFC47B2ADC10DA00E77A5C /* LocalAuthenticationClient+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFAD2ADC10D900E77A5C /* LocalAuthenticationClient+Test.swift */; }; - 48CFC47C2ADC10DA00E77A5C /* BackupsClient+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFAF2ADC10D900E77A5C /* BackupsClient+Test.swift */; }; - 48CFC47D2ADC10DA00E77A5C /* BackupsClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFB02ADC10D900E77A5C /* BackupsClient+Interface.swift */; }; + 48CFC47C2ADC10DA00E77A5C /* TransportProfileClient+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFAF2ADC10D900E77A5C /* TransportProfileClient+Test.swift */; }; + 48CFC47D2ADC10DA00E77A5C /* TransportProfileClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFB02ADC10D900E77A5C /* TransportProfileClient+Interface.swift */; }; 48CFC47E2ADC10DA00E77A5C /* OverlayWindowClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFB22ADC10D900E77A5C /* OverlayWindowClient+Live.swift */; }; 48CFC47F2ADC10DA00E77A5C /* ROLAClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFB42ADC10D900E77A5C /* ROLAClient+Interface.swift */; }; 48CFC4802ADC10DA00E77A5C /* ROLAClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBFB52ADC10D900E77A5C /* ROLAClient+Live.swift */; }; @@ -665,7 +665,7 @@ 48CFC57D2ADC10DA00E77A5C /* StateEntityDetailsResponseItemDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C02ADC10D900E77A5C /* StateEntityDetailsResponseItemDetails.swift */; }; 48CFC57E2ADC10DA00E77A5C /* ValidatorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C12ADC10D900E77A5C /* ValidatorState.swift */; }; 48CFC57F2ADC10DA00E77A5C /* GatewayAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C22ADC10D900E77A5C /* GatewayAPI.swift */; }; - 48CFC5802ADC10DA00E77A5C /* BackupsClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C42ADC10D900E77A5C /* BackupsClient+Live.swift */; }; + 48CFC5802ADC10DA00E77A5C /* TransportProfileClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C42ADC10D900E77A5C /* TransportProfileClient+Live.swift */; }; 48CFC5812ADC10DA00E77A5C /* URLFormatterClient+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C62ADC10D900E77A5C /* URLFormatterClient+Test.swift */; }; 48CFC5822ADC10DA00E77A5C /* URLFormatterClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C72ADC10D900E77A5C /* URLFormatterClient+Interface.swift */; }; 48CFC5832ADC10DA00E77A5C /* SecureStorageClient+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFC0C92ADC10D900E77A5C /* SecureStorageClient+Test.swift */; }; @@ -1641,8 +1641,8 @@ 48CFBFAB2ADC10D900E77A5C /* LocalAuthenticationClient+Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LocalAuthenticationClient+Live.swift"; sourceTree = ""; }; 48CFBFAC2ADC10D900E77A5C /* LocalAuthenticationClient+Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LocalAuthenticationClient+Interface.swift"; sourceTree = ""; }; 48CFBFAD2ADC10D900E77A5C /* LocalAuthenticationClient+Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LocalAuthenticationClient+Test.swift"; sourceTree = ""; }; - 48CFBFAF2ADC10D900E77A5C /* BackupsClient+Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BackupsClient+Test.swift"; sourceTree = ""; }; - 48CFBFB02ADC10D900E77A5C /* BackupsClient+Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BackupsClient+Interface.swift"; sourceTree = ""; }; + 48CFBFAF2ADC10D900E77A5C /* TransportProfileClient+Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TransportProfileClient+Test.swift"; sourceTree = ""; }; + 48CFBFB02ADC10D900E77A5C /* TransportProfileClient+Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TransportProfileClient+Interface.swift"; sourceTree = ""; }; 48CFBFB22ADC10D900E77A5C /* OverlayWindowClient+Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OverlayWindowClient+Live.swift"; sourceTree = ""; }; 48CFBFB42ADC10D900E77A5C /* ROLAClient+Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ROLAClient+Interface.swift"; sourceTree = ""; }; 48CFBFB52ADC10D900E77A5C /* ROLAClient+Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ROLAClient+Live.swift"; sourceTree = ""; }; @@ -1834,7 +1834,7 @@ 48CFC0C02ADC10D900E77A5C /* StateEntityDetailsResponseItemDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateEntityDetailsResponseItemDetails.swift; sourceTree = ""; }; 48CFC0C12ADC10D900E77A5C /* ValidatorState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatorState.swift; sourceTree = ""; }; 48CFC0C22ADC10D900E77A5C /* GatewayAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GatewayAPI.swift; sourceTree = ""; }; - 48CFC0C42ADC10D900E77A5C /* BackupsClient+Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BackupsClient+Live.swift"; sourceTree = ""; }; + 48CFC0C42ADC10D900E77A5C /* TransportProfileClient+Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TransportProfileClient+Live.swift"; sourceTree = ""; }; 48CFC0C62ADC10D900E77A5C /* URLFormatterClient+Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLFormatterClient+Test.swift"; sourceTree = ""; }; 48CFC0C72ADC10D900E77A5C /* URLFormatterClient+Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLFormatterClient+Interface.swift"; sourceTree = ""; }; 48CFC0C92ADC10D900E77A5C /* SecureStorageClient+Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SecureStorageClient+Test.swift"; sourceTree = ""; }; @@ -4284,7 +4284,7 @@ 48CFBF622ADC10D900E77A5C /* AccountsClient */, 48CFBF6C2ADC10D900E77A5C /* AppPreferencesClient */, 48CFBF652ADC10D900E77A5C /* AuthorizedDappsClient */, - 48CFBFAE2ADC10D900E77A5C /* BackupsClient */, + 48CFBFAE2ADC10D900E77A5C /* TransportProfileClient */, 48CFBF772ADC10D900E77A5C /* CacheClient */, 48CFC0D62ADC10D900E77A5C /* CameraPermissionClient */, 48CFBF562ADC10D900E77A5C /* DappInteractionClient */, @@ -4536,14 +4536,14 @@ path = LocalAuthenticationClient; sourceTree = ""; }; - 48CFBFAE2ADC10D900E77A5C /* BackupsClient */ = { + 48CFBFAE2ADC10D900E77A5C /* TransportProfileClient */ = { isa = PBXGroup; children = ( - 48CFBFAF2ADC10D900E77A5C /* BackupsClient+Test.swift */, - 48CFC0C42ADC10D900E77A5C /* BackupsClient+Live.swift */, - 48CFBFB02ADC10D900E77A5C /* BackupsClient+Interface.swift */, + 48CFBFAF2ADC10D900E77A5C /* TransportProfileClient+Test.swift */, + 48CFC0C42ADC10D900E77A5C /* TransportProfileClient+Live.swift */, + 48CFBFB02ADC10D900E77A5C /* TransportProfileClient+Interface.swift */, ); - path = BackupsClient; + path = TransportProfileClient; sourceTree = ""; }; 48CFBFB32ADC10D900E77A5C /* ROLAClient */ = { @@ -7073,7 +7073,7 @@ 48CFC3142ADC10D900E77A5C /* CompletionMigrateOlympiaAccountsToBabylon.swift in Sources */, 8308184E2B9F16AD002D8351 /* TokenPriceClient+Mock.swift in Sources */, 48CFC2852ADC10D900E77A5C /* SubmitTransaction+View.swift in Sources */, - 48CFC47C2ADC10DA00E77A5C /* BackupsClient+Test.swift in Sources */, + 48CFC47C2ADC10DA00E77A5C /* TransportProfileClient+Test.swift in Sources */, 48CFC28D2ADC10D900E77A5C /* TransactionReviewRawTransaction+View.swift in Sources */, 83EE47882AF0EE3C00155F03 /* ProgrammaticScryptoSborValueBytes.swift in Sources */, 48CFC2C92ADC10D900E77A5C /* FungibleResourceAsset+View.swift in Sources */, @@ -7673,7 +7673,7 @@ 48CFC2A22ADC10D900E77A5C /* Persona+View.swift in Sources */, 48CFC5582ADC10DA00E77A5C /* NonFungibleIdType.swift in Sources */, 48CFC57D2ADC10DA00E77A5C /* StateEntityDetailsResponseItemDetails.swift in Sources */, - 48CFC5802ADC10DA00E77A5C /* BackupsClient+Live.swift in Sources */, + 48CFC5802ADC10DA00E77A5C /* TransportProfileClient+Live.swift in Sources */, 48CFC6082ADC10DA00E77A5C /* AccountsRequestResponseItem.swift in Sources */, 48AE39E82B0CBEA800813CF3 /* SelectInactiveAccountsToAdd.swift in Sources */, 48CFC59A2ADC10DA00E77A5C /* HitTargetSize.swift in Sources */, @@ -7698,7 +7698,7 @@ 48CFC5352ADC10DA00E77A5C /* StateEntityDetailsResponseItemAncestorIdentities.swift in Sources */, 48CFC4E22ADC10DA00E77A5C /* InvalidEntityError.swift in Sources */, A41557552B7645F70040AD4E /* TransactionHistory+View.swift in Sources */, - 48CFC47D2ADC10DA00E77A5C /* BackupsClient+Interface.swift in Sources */, + 48CFC47D2ADC10DA00E77A5C /* TransportProfileClient+Interface.swift in Sources */, 48CFC5792ADC10DA00E77A5C /* FungibleResourcesCollectionItem.swift in Sources */, 48CFC4AF2ADC10DA00E77A5C /* NonFungibleResourcesCollectionItemGloballyAggregated.swift in Sources */, 48CFC54D2ADC10DA00E77A5C /* StateEntityDetailsResponseNonFungibleResourceDetails.swift in Sources */, diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift deleted file mode 100644 index e4fddc510a..0000000000 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Sargon - -public typealias DeviceID = UUID - -// MARK: - BackupsClient -public struct BackupsClient: Sendable { - public var snapshotOfProfileForExport: SnapshotOfProfileForExport - public var importProfileSnapshot: ImportProfileSnapshot - public var didExportProfileSnapshot: DidExportProfileSnapshot - public var loadDeviceID: LoadDeviceID - - public init( - snapshotOfProfileForExport: @escaping SnapshotOfProfileForExport, - importProfileSnapshot: @escaping ImportProfileSnapshot, - didExportProfileSnapshot: @escaping DidExportProfileSnapshot, - loadDeviceID: @escaping LoadDeviceID - ) { - self.snapshotOfProfileForExport = snapshotOfProfileForExport - self.importProfileSnapshot = importProfileSnapshot - self.didExportProfileSnapshot = didExportProfileSnapshot - self.loadDeviceID = loadDeviceID - } -} - -extension BackupsClient { - public typealias SnapshotOfProfileForExport = @Sendable () async throws -> Profile - public typealias ImportProfileSnapshot = @Sendable (Profile, Set, Bool) async throws -> Void - public typealias DidExportProfileSnapshot = @Sendable (Profile) throws -> Void - public typealias LoadDeviceID = @Sendable () async -> UUID? -} diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift deleted file mode 100644 index a8f7f0c8cb..0000000000 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift +++ /dev/null @@ -1,26 +0,0 @@ - -extension DependencyValues { - public var backupsClient: BackupsClient { - get { self[BackupsClient.self] } - set { self[BackupsClient.self] = newValue } - } -} - -// MARK: - BackupsClient + TestDependencyKey -extension BackupsClient: TestDependencyKey { - public static let previewValue = Self.noop - - public static let testValue = Self( - snapshotOfProfileForExport: unimplemented("\(Self.self).snapshotOfProfileForExport"), - importProfileSnapshot: unimplemented("\(Self.self).importProfileSnapshot"), - didExportProfileSnapshot: unimplemented("\(Self.self).didExportProfileSnapshot"), - loadDeviceID: unimplemented("\(Self.self).loadDeviceID") - ) - - public static let noop = Self( - snapshotOfProfileForExport: { throw NoopError() }, - importProfileSnapshot: { _, _, _ in throw NoopError() }, - didExportProfileSnapshot: { _ in throw NoopError() }, - loadDeviceID: { nil } - ) -} diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift new file mode 100644 index 0000000000..c8bbd19a06 --- /dev/null +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift @@ -0,0 +1,30 @@ +import Sargon + +public typealias DeviceID = UUID + +// MARK: - TransportProfileClient +public struct TransportProfileClient: Sendable { + public var importProfile: ImportProfile + public var profileForExport: ProfileForExport + public var didExportProfile: DidExportProfile + public var loadDeviceID: LoadDeviceID + + public init( + importProfile: @escaping ImportProfile, + profileForExport: @escaping ProfileForExport, + didExportProfile: @escaping DidExportProfile, + loadDeviceID: @escaping LoadDeviceID + ) { + self.importProfile = importProfile + self.profileForExport = profileForExport + self.didExportProfile = didExportProfile + self.loadDeviceID = loadDeviceID + } +} + +extension TransportProfileClient { + public typealias ImportProfile = @Sendable (Profile, Set, Bool) async throws -> Void + public typealias ProfileForExport = @Sendable () async throws -> Profile + public typealias DidExportProfile = @Sendable (Profile) throws -> Void + public typealias LoadDeviceID = @Sendable () async -> UUID? +} diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift similarity index 80% rename from RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift rename to RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift index b90b31ae4a..94cab2e25e 100644 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift @@ -1,5 +1,5 @@ -extension BackupsClient: DependencyKey { +extension TransportProfileClient: DependencyKey { public static let liveValue = Self.live() public static func live( @@ -9,10 +9,7 @@ extension BackupsClient: DependencyKey { @Dependency(\.secureStorageClient) var secureStorageClient return Self( - snapshotOfProfileForExport: { - await profileStore.profile - }, - importProfileSnapshot: { snapshot, factorSourceIDs, containsP2PLinks in + importProfile: { snapshot, factorSourceIDs, containsP2PLinks in do { try await profileStore.importProfileSnapshot(snapshot) userDefaults.setShowRelinkConnectorsAfterProfileRestore(containsP2PLinks) @@ -24,7 +21,10 @@ extension BackupsClient: DependencyKey { throw error } }, - didExportProfileSnapshot: { profile in + profileForExport: { + await profileStore.profile + }, + didExportProfile: { profile in try userDefaults.setLastManualBackup(of: profile) }, loadDeviceID: { diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift new file mode 100644 index 0000000000..3c91be4138 --- /dev/null +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift @@ -0,0 +1,26 @@ + +extension DependencyValues { + public var transportProfileClient: TransportProfileClient { + get { self[TransportProfileClient.self] } + set { self[TransportProfileClient.self] = newValue } + } +} + +// MARK: - TransportProfileClient + TestDependencyKey +extension TransportProfileClient: TestDependencyKey { + public static let previewValue = Self.noop + + public static let testValue = Self( + importProfile: unimplemented("\(Self.self).importProfile"), + profileForExport: unimplemented("\(Self.self).profileForExport"), + didExportProfile: unimplemented("\(Self.self).didExportProfile"), + loadDeviceID: unimplemented("\(Self.self).loadDeviceID") + ) + + public static let noop = Self( + importProfile: { _, _, _ in throw NoopError() }, + profileForExport: { throw NoopError() }, + didExportProfile: { _ in throw NoopError() }, + loadDeviceID: { nil } + ) +} diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicsFlowCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicsFlowCoordinator.swift index c022ef7c3e..a73257aa7e 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicsFlowCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicsFlowCoordinator.swift @@ -112,7 +112,7 @@ public struct ImportMnemonicsFlowCoordinator: Sendable, FeatureReducer { @Dependency(\.continuousClock) var clock @Dependency(\.secureStorageClient) var secureStorageClient @Dependency(\.overlayWindowClient) var overlayWindowClient - @Dependency(\.backupsClient) var backupsClient + @Dependency(\.transportProfileClient) var transportProfileClient public init() {} @@ -133,7 +133,7 @@ public struct ImportMnemonicsFlowCoordinator: Sendable, FeatureReducer { let snapshot = if let fromOnboarding = context.profileSnapshotFromOnboarding { fromOnboarding } else { - try await backupsClient.snapshotOfProfileForExport() + try await transportProfileClient.profileForExport() } let ents = try await deviceFactorSourceClient.controlledEntities(snapshot) let hasAnyBDFSExplicitlyMarkedMain = ents.contains(where: \.isExplicitMain) diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift index 7aad59aaf2..2008a972ad 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift @@ -60,7 +60,7 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { case profileCreatedFromImportedBDFS } - @Dependency(\.backupsClient) var backupsClient + @Dependency(\.transportProfileClient) var transportProfileClient @Dependency(\.factorSourcesClient) var factorSourcesClient @Dependency(\.errorQueue) var errorQueue @Dependency(\.continuousClock) var clock @@ -117,7 +117,7 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { let factorSourceIDs: Set = .init( profileSelection.profile.factorSources.compactMap { $0.extract(DeviceFactorSource.self) }.map(\.id) ) - try await backupsClient.importProfileSnapshot(profileSelection.profile, factorSourceIDs, profileSelection.containsP2PLinks) + try await transportProfileClient.importProfile(profileSelection.profile, factorSourceIDs, profileSelection.containsP2PLinks) if let notYetSavedNewMainBDFS { try await factorSourcesClient.saveNewMainBDFS(notYetSavedNewMainBDFS) diff --git a/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift index dda12109aa..8877cdfbbc 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift @@ -94,7 +94,7 @@ public struct EncryptOrDecryptProfile: Sendable, FeatureReducer { @Dependency(\.jsonEncoder) var jsonEncoder @Dependency(\.errorQueue) var errorQueue - @Dependency(\.backupsClient) var backupsClient + @Dependency(\.transportProfileClient) var transportProfileClient public var body: some ReducerOf { Reduce(core) @@ -112,7 +112,7 @@ public struct EncryptOrDecryptProfile: Sendable, FeatureReducer { await send(.internal(.focusTextField(.encryptionPassword))) switch mode { case .loadThenEncrypt: - let result = await TaskResult { try await backupsClient.snapshotOfProfileForExport() } + let result = await TaskResult { try await transportProfileClient.profileForExport() } await send(.internal(.loadProfileSnapshotToEncryptResult( result diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index 7c36cb3716..21bbfa0e36 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -108,7 +108,7 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { @Dependency(\.overlayWindowClient) var overlayWindowClient @Dependency(\.appPreferencesClient) var appPreferencesClient @Dependency(\.cloudBackupClient) var cloudBackupClient - @Dependency(\.backupsClient) var backupsClient + @Dependency(\.transportProfileClient) var transportProfileClient @Dependency(\.securityCenterClient) var securityCenterClient @Dependency(\.userDefaults) var userDefaults @@ -150,7 +150,7 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { overlayWindowClient.scheduleHUD(.exportedProfile(encrypted: didEncryptIt)) loggerGlobal.notice("Profile successfully exported to: \(exportedProfileURL)") if let profile { - try? backupsClient.didExportProfileSnapshot(profile) + try? transportProfileClient.didExportProfile(profile) } return .none diff --git a/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift b/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift index 13aabddff1..23aabcbbe0 100644 --- a/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift +++ b/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift @@ -58,7 +58,6 @@ public struct DisplayMnemonics: Sendable, FeatureReducer { @Dependency(\.errorQueue) var errorQueue @Dependency(\.deviceFactorSourceClient) var deviceFactorSourceClient @Dependency(\.keychainClient) var keychainClient - @Dependency(\.backupsClient) var backupsClient @Dependency(\.securityCenterClient) var securityCenterClient public init() {} From 631f155e1af87e58d59160224e234a91435964eb Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 23:38:29 +0200 Subject: [PATCH 28/68] Remove unused --- RadixWallet.xcodeproj/project.pbxproj | 4 -- ...WindowClient+Alert+OwnershipConflict.swift | 47 ------------------- .../Clients/ProfileStore/ProfileStore.swift | 8 ---- .../TransportProfileClient+Interface.swift | 6 +-- .../TransportProfileClient+Live.swift | 7 +-- .../TransportProfileClient+Test.swift | 6 +-- 6 files changed, 5 insertions(+), 73 deletions(-) delete mode 100644 RadixWallet/Clients/ProfileStore/ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift diff --git a/RadixWallet.xcodeproj/project.pbxproj b/RadixWallet.xcodeproj/project.pbxproj index b182b55d8e..f92b256abb 100644 --- a/RadixWallet.xcodeproj/project.pbxproj +++ b/RadixWallet.xcodeproj/project.pbxproj @@ -1099,7 +1099,6 @@ E63257652BB314F600952051 /* ExecutionSummary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63257642BB314F600952051 /* ExecutionSummary+Extensions.swift */; }; E634CA2F2AFD25B100C43DB7 /* DebugKeychainContents+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E634CA2D2AFD25B100C43DB7 /* DebugKeychainContents+View.swift */; }; E634CA302AFD25B100C43DB7 /* DebugKeychainContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E634CA2E2AFD25B100C43DB7 /* DebugKeychainContents.swift */; }; - E6390FB32AE6C3E200B4DEE2 /* ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6390FB22AE6C3E200B4DEE2 /* ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift */; }; E63D123D2ADD1FC00001CBB1 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = E63D123C2ADD1FC00001CBB1 /* SwiftUIIntrospect */; }; E64463FE2B75304C0006CAF8 /* Dictionary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64463FD2B75304C0006CAF8 /* Dictionary+Extensions.swift */; }; E657773C2B0BAB35002DB237 /* RecoverWalletControlWithBDFSOnly+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E657773A2B0BAB35002DB237 /* RecoverWalletControlWithBDFSOnly+View.swift */; }; @@ -2240,7 +2239,6 @@ E63257642BB314F600952051 /* ExecutionSummary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExecutionSummary+Extensions.swift"; sourceTree = ""; }; E634CA2D2AFD25B100C43DB7 /* DebugKeychainContents+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DebugKeychainContents+View.swift"; sourceTree = ""; }; E634CA2E2AFD25B100C43DB7 /* DebugKeychainContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugKeychainContents.swift; sourceTree = ""; }; - E6390FB22AE6C3E200B4DEE2 /* ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift"; sourceTree = ""; }; E64463FD2B75304C0006CAF8 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = ""; }; E657773A2B0BAB35002DB237 /* RecoverWalletControlWithBDFSOnly+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecoverWalletControlWithBDFSOnly+View.swift"; sourceTree = ""; }; E657773B2B0BAB35002DB237 /* RecoverWalletControlWithBDFSOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoverWalletControlWithBDFSOnly.swift; sourceTree = ""; }; @@ -4992,7 +4990,6 @@ children = ( 48CFC0DD2ADC10D900E77A5C /* ProfileStore.swift */, 48AB4E832AE19F5B001B238E /* ProfileStore+AsyncSequence+Updates.swift */, - E6390FB22AE6C3E200B4DEE2 /* ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift */, ); path = ProfileStore; sourceTree = ""; @@ -7223,7 +7220,6 @@ 48CFC57A2ADC10DA00E77A5C /* ResourcePoolState.swift in Sources */, 48CFC2B52ADC10D900E77A5C /* EncryptOrDecryptProfile+View.swift in Sources */, 48CFC4282ADC10DA00E77A5C /* Data_Extensions.swift in Sources */, - E6390FB32AE6C3E200B4DEE2 /* ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift in Sources */, 83EE47A22AF0EECD00155F03 /* TransactionFungibleBalanceChanges.swift in Sources */, 48CFC3162ADC10D900E77A5C /* ScanMultipleOlympiaQRCodes.swift in Sources */, 48CFC2CC2ADC10D900E77A5C /* FungibleResourceAsset+Reducer.swift in Sources */, diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift b/RadixWallet/Clients/ProfileStore/ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift deleted file mode 100644 index c3ad63612f..0000000000 --- a/RadixWallet/Clients/ProfileStore/ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift +++ /dev/null @@ -1,47 +0,0 @@ -extension OverlayWindowClient.Item.AlertState { - public static func profileUsedOnAnotherDeviceAlert( - conflictingOwners: ConflictingOwners - ) -> Self { - .init( - title: { TextState(L10n.Splash.ProfileOnAnotherDeviceAlert.title) }, - actions: { - ButtonState( - role: .none, - action: .claimAndContinueUseOnThisPhone, - label: { - TextState(L10n.Splash.ProfileOnAnotherDeviceAlert.claimExisting) - } - ) - ButtonState( - role: .destructive, - action: .deleteProfileFromThisPhone, - label: { - TextState(L10n.Splash.ProfileOnAnotherDeviceAlert.claimHere) - } - ) - ButtonState( - role: .cancel, - action: .dismissed, - label: { - TextState(L10n.Splash.ProfileOnAnotherDeviceAlert.askLater) - } - ) - }, - message: { - TextState(overlayClientProfileStoreOwnershipConflictTextState) - } - ) - } -} - -let overlayClientProfileStoreOwnershipConflictTextState = L10n.Splash.ProfileOnAnotherDeviceAlert.message - -extension OverlayWindowClient.Item.AlertAction { - static var claimAndContinueUseOnThisPhone: Self { - .primaryButtonTapped - } - - static var deleteProfileFromThisPhone: Self { - .secondaryButtonTapped - } -} diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 60402d773b..567ecbd20f 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -118,14 +118,6 @@ extension ProfileStore { } } - /// Change current profile to new imported profle snapshot and saves it, by - /// updates `headerList` (Keychain), `activeProfileID` (UserDefaults) - /// and saves the snapshot of the profile into Keychain. - /// - Parameter profile: Imported Profile to use and save. - public func importProfileSnapshot(_ snapshot: Profile) throws { - try importProfile(snapshot) - } - /// Change current profile to new importedProfile and saves it, by /// updates `headerList` (Keychain), `activeProfileID` (UserDefaults) /// and saves a snapshot of the profile into Keychain. diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift index c8bbd19a06..003554649e 100644 --- a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift @@ -7,18 +7,15 @@ public struct TransportProfileClient: Sendable { public var importProfile: ImportProfile public var profileForExport: ProfileForExport public var didExportProfile: DidExportProfile - public var loadDeviceID: LoadDeviceID public init( importProfile: @escaping ImportProfile, profileForExport: @escaping ProfileForExport, - didExportProfile: @escaping DidExportProfile, - loadDeviceID: @escaping LoadDeviceID + didExportProfile: @escaping DidExportProfile ) { self.importProfile = importProfile self.profileForExport = profileForExport self.didExportProfile = didExportProfile - self.loadDeviceID = loadDeviceID } } @@ -26,5 +23,4 @@ extension TransportProfileClient { public typealias ImportProfile = @Sendable (Profile, Set, Bool) async throws -> Void public typealias ProfileForExport = @Sendable () async throws -> Profile public typealias DidExportProfile = @Sendable (Profile) throws -> Void - public typealias LoadDeviceID = @Sendable () async -> UUID? } diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift index 94cab2e25e..686222f9c1 100644 --- a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift @@ -9,9 +9,9 @@ extension TransportProfileClient: DependencyKey { @Dependency(\.secureStorageClient) var secureStorageClient return Self( - importProfile: { snapshot, factorSourceIDs, containsP2PLinks in + importProfile: { profile, factorSourceIDs, containsP2PLinks in do { - try await profileStore.importProfileSnapshot(snapshot) + try await profileStore.importProfile(profile) userDefaults.setShowRelinkConnectorsAfterProfileRestore(containsP2PLinks) } catch { // Revert the saved mnemonic @@ -26,9 +26,6 @@ extension TransportProfileClient: DependencyKey { }, didExportProfile: { profile in try userDefaults.setLastManualBackup(of: profile) - }, - loadDeviceID: { - try? secureStorageClient.loadDeviceInfo()?.id } ) } diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift index 3c91be4138..5b2681521b 100644 --- a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift @@ -13,14 +13,12 @@ extension TransportProfileClient: TestDependencyKey { public static let testValue = Self( importProfile: unimplemented("\(Self.self).importProfile"), profileForExport: unimplemented("\(Self.self).profileForExport"), - didExportProfile: unimplemented("\(Self.self).didExportProfile"), - loadDeviceID: unimplemented("\(Self.self).loadDeviceID") + didExportProfile: unimplemented("\(Self.self).didExportProfile") ) public static let noop = Self( importProfile: { _, _, _ in throw NoopError() }, profileForExport: { throw NoopError() }, - didExportProfile: { _ in throw NoopError() }, - loadDeviceID: { nil } + didExportProfile: { _ in throw NoopError() } ) } From 142b97b476f2cdad7a693984e22ae8c08c17474a Mon Sep 17 00:00:00 2001 From: kugel3 Date: Thu, 6 Jun 2024 23:55:03 +0200 Subject: [PATCH 29/68] saveHash -> saveIdentifier --- .../CloudBackupClient/CloudBackupClient+Live.swift | 2 +- .../SecurityCenterClient/SecurityCenterClient+Live.swift | 2 +- .../UserDefaults+Dependency+Extension.swift | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 5e53c01437..3b8d9b8d20 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -146,7 +146,7 @@ extension CloudBackupClient { func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async { let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty let lastBackup = userDefaults.getLastCloudBackups[profile.id] - let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.saveHash == profile.saveHash + let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.saveIdentifier == profile.saveIdentifier let shouldBackUp = needsBackUp && !lastBackupSucceeded let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index ce28712c6e..4a9d250e44 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -45,7 +45,7 @@ extension SecurityCenterClient { func statusValues(results: AnyAsyncSequence) async -> AnyAsyncSequence { await combineLatest(profileStore.values(), results.prepend(nil)).map { profile, backup in guard let backup else { return nil } - let upToDate = backup.saveHash == profile.saveHash + let upToDate = backup.saveIdentifier == profile.saveIdentifier let success = backup.result == .success return .init(backupDate: backup.backupDate, upToDate: upToDate, success: success) } diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 87aeac58af..19b1d45d68 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -169,7 +169,7 @@ extension UserDefaults.Dependency { var backups: [UUID: BackupResult] = getLastCloudBackups backups[profile.id] = .init( backupDate: .now, - saveHash: profile.saveHash, + saveIdentifier: profile.saveIdentifier, result: result ) @@ -189,7 +189,7 @@ extension UserDefaults.Dependency { var backups: [ProfileID: BackupResult] = getLastManualBackups backups[profile.id] = .init( backupDate: .now, - saveHash: profile.saveHash, + saveIdentifier: profile.saveIdentifier, result: .success ) @@ -234,7 +234,7 @@ extension UserDefaults.Dependency { // MARK: - BackupResult public struct BackupResult: Codable, Sendable { public let backupDate: Date - public let saveHash: String + public let saveIdentifier: String public let result: Result public enum Result: Codable, Sendable { @@ -246,7 +246,7 @@ public struct BackupResult: Codable, Sendable { } extension Profile { - public var saveHash: String { + public var saveIdentifier: String { "\(header.lastModified.timeIntervalSince1970)-\(header.lastUsedOnDevice.id.uuidString)" } } From 30b68b734e13c927317502a972e18ce1fa4caf3f Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 00:13:44 +0200 Subject: [PATCH 30/68] simplify SecureStorageClient --- .../AppPreferencesClient+Live.swift | 4 -- .../CloudBackupClient+Live.swift | 2 +- .../SecureStorageClient+Interface.swift | 21 +++------ .../SecureStorageClient+Live.swift | 43 +++++-------------- .../SecureStorageClient+Test.swift | 8 ++-- 5 files changed, 22 insertions(+), 56 deletions(-) diff --git a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift index c1213aff82..ec3574e290 100644 --- a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift +++ b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift @@ -29,10 +29,6 @@ extension AppPreferencesClient: DependencyKey { try await profileStore.updating { profile in profile.appPreferences.security.isCloudProfileSyncEnabled = isEnabled } - try secureStorageClient.updateIsCloudProfileSyncEnabled( - profile.id, - isEnabled ? .enable : .disable - ) }, setIsCloudBackupEnabled: { isEnabled in let profile = await profileStore.profile diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 3b8d9b8d20..d65d599cf9 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -177,7 +177,7 @@ extension CloudBackupClient { startAutomaticBackups: { // The active profile should not be synced to iCloud keychain let profileID = await profileStore.profile.id - try secureStorageClient.updateIsCloudProfileSyncEnabled(profileID, .disable) + try secureStorageClient.disableCloudProfileSync(profileID) let ticks = AsyncTimerSequence(every: retryBackupInterval) let profiles = await profileStore.values() diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift index 3502857f9d..7faae198b8 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift @@ -20,7 +20,7 @@ public struct SecureStorageClient: Sendable { public var deleteMnemonicByFactorSourceID: DeleteMnemonicByFactorSourceID public var deleteProfileAndMnemonicsByFactorSourceIDs: DeleteProfileAndMnemonicsByFactorSourceIDs - public var updateIsCloudProfileSyncEnabled: UpdateIsCloudProfileSyncEnabled + public var disableCloudProfileSync: DisableCloudProfileSync public var loadProfileHeaderList: LoadProfileHeaderList public var saveProfileHeaderList: SaveProfileHeaderList @@ -56,7 +56,7 @@ public struct SecureStorageClient: Sendable { containsMnemonicIdentifiedByFactorSourceID: @escaping ContainsMnemonicIdentifiedByFactorSourceID, deleteMnemonicByFactorSourceID: @escaping DeleteMnemonicByFactorSourceID, deleteProfileAndMnemonicsByFactorSourceIDs: @escaping DeleteProfileAndMnemonicsByFactorSourceIDs, - updateIsCloudProfileSyncEnabled: @escaping UpdateIsCloudProfileSyncEnabled, + disableCloudProfileSync: @escaping DisableCloudProfileSync, loadProfileHeaderList: @escaping LoadProfileHeaderList, saveProfileHeaderList: @escaping SaveProfileHeaderList, deleteProfileHeaderList: @escaping DeleteProfileHeaderList, @@ -80,7 +80,7 @@ public struct SecureStorageClient: Sendable { self.containsMnemonicIdentifiedByFactorSourceID = containsMnemonicIdentifiedByFactorSourceID self.deleteMnemonicByFactorSourceID = deleteMnemonicByFactorSourceID self.deleteProfileAndMnemonicsByFactorSourceIDs = deleteProfileAndMnemonicsByFactorSourceIDs - self.updateIsCloudProfileSyncEnabled = updateIsCloudProfileSyncEnabled + self.disableCloudProfileSync = disableCloudProfileSync self.loadProfileHeaderList = loadProfileHeaderList self.saveProfileHeaderList = saveProfileHeaderList self.deleteProfileHeaderList = deleteProfileHeaderList @@ -107,7 +107,7 @@ public struct SecureStorageClient: Sendable { containsMnemonicIdentifiedByFactorSourceID: @escaping ContainsMnemonicIdentifiedByFactorSourceID, deleteMnemonicByFactorSourceID: @escaping DeleteMnemonicByFactorSourceID, deleteProfileAndMnemonicsByFactorSourceIDs: @escaping DeleteProfileAndMnemonicsByFactorSourceIDs, - updateIsCloudProfileSyncEnabled: @escaping UpdateIsCloudProfileSyncEnabled, + disableCloudProfileSync: @escaping DisableCloudProfileSync, loadProfileHeaderList: @escaping LoadProfileHeaderList, saveProfileHeaderList: @escaping SaveProfileHeaderList, deleteProfileHeaderList: @escaping DeleteProfileHeaderList, @@ -130,7 +130,7 @@ public struct SecureStorageClient: Sendable { self.containsMnemonicIdentifiedByFactorSourceID = containsMnemonicIdentifiedByFactorSourceID self.deleteMnemonicByFactorSourceID = deleteMnemonicByFactorSourceID self.deleteProfileAndMnemonicsByFactorSourceIDs = deleteProfileAndMnemonicsByFactorSourceIDs - self.updateIsCloudProfileSyncEnabled = updateIsCloudProfileSyncEnabled + self.disableCloudProfileSync = disableCloudProfileSync self.loadProfileHeaderList = loadProfileHeaderList self.saveProfileHeaderList = saveProfileHeaderList self.deleteProfileHeaderList = deleteProfileHeaderList @@ -153,7 +153,7 @@ public struct LoadMnemonicByFactorSourceIDRequest: Sendable, Hashable { } extension SecureStorageClient { - public typealias UpdateIsCloudProfileSyncEnabled = @Sendable (ProfileID, CloudProfileSyncActivation) throws -> Void + public typealias DisableCloudProfileSync = @Sendable (ProfileID) throws -> Void public typealias SaveProfileSnapshot = @Sendable (Profile) throws -> Void public typealias LoadProfileSnapshotData = @Sendable (ProfileID) throws -> Data? public typealias LoadProfileSnapshot = @Sendable (ProfileID) throws -> Profile? @@ -246,15 +246,6 @@ extension SecureStorageClient { } } -// MARK: - CloudProfileSyncActivation -public enum CloudProfileSyncActivation: Sendable, Hashable { - /// iCloud sync was enabled, user request to disable it. - case disable - - /// iCloud sync was disabled, user request to enable it. - case enable -} - #if DEBUG // MARK: - KeyedMnemonicWithPassphrase diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift index 46401bc00b..6ec685a55b 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift @@ -77,14 +77,13 @@ extension SecureStorageClient: DependencyKey { @Sendable func saveProfile( snapshotData data: Data, - key: KeychainClient.Key, - iCloudSyncEnabled: Bool + key: KeychainClient.Key ) throws { try keychainClient.setDataWithoutAuth( data, forKey: key, attributes: .init( - iCloudSyncEnabled: iCloudSyncEnabled, + iCloudSyncEnabled: false, accessibility: .whenUnlocked, // do not delete the Profile if passcode gets deleted. label: importantKeychainIdentifier("Radix Wallet Data"), comment: "Contains your accounts, personas, authorizedDapps, linked connector extensions and wallet app preferences." @@ -92,14 +91,6 @@ extension SecureStorageClient: DependencyKey { ) } - @Sendable func saveProfile( - snapshot profile: Profile, - iCloudSyncEnabled: Bool - ) throws { - let data = profile.profileSnapshot() - try saveProfile(snapshotData: data, key: profile.header.id.keychainKey, iCloudSyncEnabled: iCloudSyncEnabled) - } - @Sendable func loadProfileHeaderList() throws -> Profile.HeaderList? { try keychainClient .getDataWithoutAuth(forKey: profileHeaderListKeychainKey) @@ -273,8 +264,7 @@ extension SecureStorageClient: DependencyKey { let saveProfileSnapshot: SaveProfileSnapshot = { profile in try saveProfile( snapshotData: profile.profileSnapshot(), - key: profile.header.id.keychainKey, - iCloudSyncEnabled: false + key: profile.header.id.keychainKey ) } @@ -321,25 +311,14 @@ extension SecureStorageClient: DependencyKey { } } - let updateIsCloudProfileSyncEnabled: UpdateIsCloudProfileSyncEnabled = { profileId, change in + let disableCloudProfileSync: DisableCloudProfileSync = { profileId in guard let profileSnapshotData = try loadProfileSnapshotData(profileId) else { return } - switch change { - case .disable: - loggerGlobal.notice("Disabling iCloud sync of Profile snapshot (which should also delete it from iCloud)") - try saveProfile( - snapshotData: profileSnapshotData, - key: profileId.keychainKey, - iCloudSyncEnabled: false - ) - case .enable: - loggerGlobal.notice("Enabling iCloud sync of Profile snapshot") - try saveProfile( - snapshotData: profileSnapshotData, - key: profileId.keychainKey, - iCloudSyncEnabled: true - ) - } + loggerGlobal.notice("Disabling iCloud sync of Profile snapshot (which should also delete it from iCloud)") + try saveProfile( + snapshotData: profileSnapshotData, + key: profileId.keychainKey + ) } let deprecatedLoadDeviceID: DeprecatedLoadDeviceID = { @@ -424,7 +403,7 @@ extension SecureStorageClient: DependencyKey { containsMnemonicIdentifiedByFactorSourceID: containsMnemonicIdentifiedByFactorSourceID, deleteMnemonicByFactorSourceID: deleteMnemonicByFactorSourceID, deleteProfileAndMnemonicsByFactorSourceIDs: deleteProfileAndMnemonicsByFactorSourceIDs, - updateIsCloudProfileSyncEnabled: updateIsCloudProfileSyncEnabled, + disableCloudProfileSync: disableCloudProfileSync, loadProfileHeaderList: loadProfileHeaderList, saveProfileHeaderList: saveProfileHeaderList, deleteProfileHeaderList: deleteProfileHeaderList, @@ -450,7 +429,7 @@ extension SecureStorageClient: DependencyKey { containsMnemonicIdentifiedByFactorSourceID: containsMnemonicIdentifiedByFactorSourceID, deleteMnemonicByFactorSourceID: deleteMnemonicByFactorSourceID, deleteProfileAndMnemonicsByFactorSourceIDs: deleteProfileAndMnemonicsByFactorSourceIDs, - updateIsCloudProfileSyncEnabled: updateIsCloudProfileSyncEnabled, + disableCloudProfileSync: disableCloudProfileSync, loadProfileHeaderList: loadProfileHeaderList, saveProfileHeaderList: saveProfileHeaderList, deleteProfileHeaderList: deleteProfileHeaderList, diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift index cb8929786d..f0d472f4ff 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift @@ -20,7 +20,7 @@ extension SecureStorageClient: TestDependencyKey { containsMnemonicIdentifiedByFactorSourceID: { _ in false }, deleteMnemonicByFactorSourceID: { _ in }, deleteProfileAndMnemonicsByFactorSourceIDs: { _, _ in }, - updateIsCloudProfileSyncEnabled: { _, _ in }, + disableCloudProfileSync: { _ in }, loadProfileHeaderList: { nil }, saveProfileHeaderList: { _ in }, deleteProfileHeaderList: {}, @@ -46,7 +46,7 @@ extension SecureStorageClient: TestDependencyKey { containsMnemonicIdentifiedByFactorSourceID: { _ in false }, deleteMnemonicByFactorSourceID: { _ in }, deleteProfileAndMnemonicsByFactorSourceIDs: { _, _ in }, - updateIsCloudProfileSyncEnabled: { _, _ in }, + disableCloudProfileSync: { _ in }, loadProfileHeaderList: { nil }, saveProfileHeaderList: { _ in }, deleteProfileHeaderList: {}, @@ -75,7 +75,7 @@ extension SecureStorageClient: TestDependencyKey { containsMnemonicIdentifiedByFactorSourceID: unimplemented("\(Self.self).containsMnemonicIdentifiedByFactorSourceID"), deleteMnemonicByFactorSourceID: unimplemented("\(Self.self).deleteMnemonicByFactorSourceID"), deleteProfileAndMnemonicsByFactorSourceIDs: unimplemented("\(Self.self).deleteProfileMnemonicsByFactorSourceIDs"), - updateIsCloudProfileSyncEnabled: unimplemented("\(Self.self).updateIsCloudProfileSyncEnabled"), + disableCloudProfileSync: unimplemented("\(Self.self).disableCloudProfileSync"), loadProfileHeaderList: unimplemented("\(Self.self).loadProfileHeaderList"), saveProfileHeaderList: unimplemented("\(Self.self).saveProfileHeaderList"), deleteProfileHeaderList: unimplemented("\(Self.self).deleteProfileHeaderList"), @@ -101,7 +101,7 @@ extension SecureStorageClient: TestDependencyKey { containsMnemonicIdentifiedByFactorSourceID: unimplemented("\(Self.self).containsMnemonicIdentifiedByFactorSourceID"), deleteMnemonicByFactorSourceID: unimplemented("\(Self.self).deleteMnemonicByFactorSourceID"), deleteProfileAndMnemonicsByFactorSourceIDs: unimplemented("\(Self.self).deleteProfileMnemonicsByFactorSourceIDs"), - updateIsCloudProfileSyncEnabled: unimplemented("\(Self.self).updateIsCloudProfileSyncEnabled"), + disableCloudProfileSync: unimplemented("\(Self.self).disableCloudProfileSync"), loadProfileHeaderList: unimplemented("\(Self.self).loadProfileHeaderList"), saveProfileHeaderList: unimplemented("\(Self.self).saveProfileHeaderList"), deleteProfileHeaderList: unimplemented("\(Self.self).deleteProfileHeaderList"), From 68d80d549056239c3246371b60456878d4a3ba8b Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 00:17:54 +0200 Subject: [PATCH 31/68] Simplify AppPreferencesClient --- .../AppPreferencesClient+Interface.swift | 6 ------ .../AppPreferencesClient+Live.swift | 11 ----------- .../AppPreferencesClient+Test.swift | 2 -- 3 files changed, 19 deletions(-) diff --git a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Interface.swift b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Interface.swift index 7e41fa0f52..5edfb39cb7 100644 --- a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Interface.swift +++ b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Interface.swift @@ -4,9 +4,6 @@ public struct AppPreferencesClient: Sendable { public var getPreferences: GetPreferences public var updatePreferences: UpdatePreferences - /// Needs special treatment since this setting involves Keychain and iCloud - public var setIsCloudProfileSyncEnabled: SetIsCloudProfileSyncEnabled - /// Sets the flag on the profile, does not delete old backups public var setIsCloudBackupEnabled: SetIsCloudBackupEnabled @@ -21,7 +18,6 @@ public struct AppPreferencesClient: Sendable { updatePreferences: @escaping UpdatePreferences, extractProfile: @escaping ExtractProfile, deleteProfileAndFactorSources: @escaping DeleteProfile, - setIsCloudProfileSyncEnabled: @escaping SetIsCloudProfileSyncEnabled, setIsCloudBackupEnabled: @escaping SetIsCloudBackupEnabled ) { self.appPreferenceUpdates = appPreferenceUpdates @@ -29,7 +25,6 @@ public struct AppPreferencesClient: Sendable { self.updatePreferences = updatePreferences self.extractProfile = extractProfile self.deleteProfileAndFactorSources = deleteProfileAndFactorSources - self.setIsCloudProfileSyncEnabled = setIsCloudProfileSyncEnabled self.setIsCloudBackupEnabled = setIsCloudBackupEnabled } } @@ -37,7 +32,6 @@ public struct AppPreferencesClient: Sendable { // MARK: - Typealias extension AppPreferencesClient { public typealias AppPreferenceUpdates = @Sendable () async -> AnyAsyncSequence - public typealias SetIsCloudProfileSyncEnabled = @Sendable (Bool) async throws -> Void public typealias SetIsCloudBackupEnabled = @Sendable (Bool) async throws -> Void public typealias GetPreferences = @Sendable () async -> AppPreferences public typealias UpdatePreferences = @Sendable (AppPreferences) async throws -> Void diff --git a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift index ec3574e290..25d12bff5f 100644 --- a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift +++ b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift @@ -20,16 +20,6 @@ extension AppPreferencesClient: DependencyKey { deleteProfileAndFactorSources: { keepInICloudIfPresent in try await profileStore.deleteProfile(keepInICloudIfPresent: keepInICloudIfPresent) }, - setIsCloudProfileSyncEnabled: { isEnabled in - @Dependency(\.secureStorageClient) var secureStorageClient - let profile = await profileStore.profile - let wasEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled - guard wasEnabled != isEnabled else { return } - - try await profileStore.updating { profile in - profile.appPreferences.security.isCloudProfileSyncEnabled = isEnabled - } - }, setIsCloudBackupEnabled: { isEnabled in let profile = await profileStore.profile let wasEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled @@ -42,6 +32,5 @@ extension AppPreferencesClient: DependencyKey { ) } - public typealias Value = AppPreferencesClient public static let liveValue: Self = .live() } diff --git a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Test.swift b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Test.swift index 4f494b42f6..e8ee7eb42f 100644 --- a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Test.swift +++ b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Test.swift @@ -8,7 +8,6 @@ extension AppPreferencesClient: TestDependencyKey { updatePreferences: unimplemented("\(Self.self).updatePreferences"), extractProfile: unimplemented("\(Self.self).extractProfile"), deleteProfileAndFactorSources: unimplemented("\(Self.self).deleteProfileAndFactorSources"), - setIsCloudProfileSyncEnabled: unimplemented("\(Self.self).setIsCloudProfileSyncEnabled"), setIsCloudBackupEnabled: unimplemented("\(Self.self).setIsCloudBackupEnabled") ) } @@ -20,7 +19,6 @@ extension AppPreferencesClient { updatePreferences: { _ in }, extractProfile: { fatalError() }, deleteProfileAndFactorSources: { _ in }, - setIsCloudProfileSyncEnabled: { _ in }, setIsCloudBackupEnabled: { _ in } ) } From bea783e0cfdec76c7d02bac0d0f7058e752e9ec7 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 00:18:16 +0200 Subject: [PATCH 32/68] Simplify cloudbackupsclient --- .../CloudBackupClient/CloudBackupClient+Interface.swift | 4 ---- .../Clients/CloudBackupClient/CloudBackupClient+Live.swift | 3 --- .../Clients/CloudBackupClient/CloudBackupClient+Test.swift | 2 -- .../Children/SelectBackup/SelectBackup+Reducer.swift | 4 ++-- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift index 77220ee534..5d672cdc67 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift @@ -6,7 +6,6 @@ import os // MARK: - CloudBackupClient public struct CloudBackupClient: DependencyKey, Sendable { public let startAutomaticBackups: StartAutomaticBackups - public let loadDeviceID: LoadDeviceID public let migrateProfilesFromKeychain: MigrateProfilesFromKeychain public let deleteProfileBackup: DeleteProfileBackup public let checkAccountStatus: CheckAccountStatus @@ -16,7 +15,6 @@ public struct CloudBackupClient: DependencyKey, Sendable { public init( startAutomaticBackups: @escaping StartAutomaticBackups, - loadDeviceID: @escaping LoadDeviceID, migrateProfilesFromKeychain: @escaping MigrateProfilesFromKeychain, deleteProfileBackup: @escaping DeleteProfileBackup, checkAccountStatus: @escaping CheckAccountStatus, @@ -25,7 +23,6 @@ public struct CloudBackupClient: DependencyKey, Sendable { loadProfileHeaders: @escaping LoadProfileHeaders ) { self.startAutomaticBackups = startAutomaticBackups - self.loadDeviceID = loadDeviceID self.migrateProfilesFromKeychain = migrateProfilesFromKeychain self.deleteProfileBackup = deleteProfileBackup self.checkAccountStatus = checkAccountStatus @@ -37,7 +34,6 @@ public struct CloudBackupClient: DependencyKey, Sendable { extension CloudBackupClient { public typealias StartAutomaticBackups = @Sendable () async throws -> Void - public typealias LoadDeviceID = @Sendable () async -> UUID? public typealias MigrateProfilesFromKeychain = @Sendable () async throws -> [CKRecord] public typealias DeleteProfileBackup = @Sendable (ProfileID) async throws -> Void public typealias CheckAccountStatus = @Sendable () async throws -> CKAccountStatus diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index d65d599cf9..371ecaf9ad 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -196,9 +196,6 @@ extension CloudBackupClient { } } }, - loadDeviceID: { - try? secureStorageClient.loadDeviceInfo()?.id - }, migrateProfilesFromKeychain: { let activeProfile = await profileStore.profile.id let backedUpRecords = try await fetchAllProfileRecords() diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift index 02be1e332b..b186658d87 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift @@ -16,7 +16,6 @@ extension CloudBackupClient: TestDependencyKey { public static let noop = Self( startAutomaticBackups: {}, - loadDeviceID: { nil }, migrateProfilesFromKeychain: { throw NoopError() }, deleteProfileBackup: { _ in }, checkAccountStatus: { throw NoopError() }, @@ -27,7 +26,6 @@ extension CloudBackupClient: TestDependencyKey { public static let testValue = Self( startAutomaticBackups: unimplemented("\(Self.self).startAutomaticBackups"), - loadDeviceID: unimplemented("\(Self.self).loadDeviceID"), migrateProfilesFromKeychain: unimplemented("\(Self.self).migrateProfilesFromKeychain"), deleteProfileBackup: unimplemented("\(Self.self).deleteProfileBackup"), checkAccountStatus: unimplemented("\(Self.self).checkAccountStatus"), diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift index 9bec2974ad..9d8dd406ed 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift @@ -228,8 +228,8 @@ public struct SelectBackup: Sendable, FeatureReducer { await send(.internal(.setStatus(.migrating))) _ = try await cloudBackupClient.migrateProfilesFromKeychain() - await send(.internal(.loadedThisDeviceID( - cloudBackupClient.loadDeviceID() + try await send(.internal(.loadedThisDeviceID( + secureStorageClient.loadDeviceInfo()?.id ))) await send(.internal(.setStatus(.loading))) From 829590e782c9cee6cd6310392a0f6b362ed885ea Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 00:53:39 +0200 Subject: [PATCH 33/68] wip claim profile --- .../CloudBackupClient+Interface.swift | 6 ++- .../CloudBackupClient+Live.swift | 46 +++++++++++-------- .../CloudBackupClient+Test.swift | 6 ++- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift index 5d672cdc67..bfc63024a8 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift @@ -12,6 +12,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { public let lastBackup: LastBackup public let loadProfile: LoadProfile public let loadProfileHeaders: LoadProfileHeaders + public let claimProfile: ClaimProfile public init( startAutomaticBackups: @escaping StartAutomaticBackups, @@ -20,7 +21,8 @@ public struct CloudBackupClient: DependencyKey, Sendable { checkAccountStatus: @escaping CheckAccountStatus, lastBackup: @escaping LastBackup, loadProfile: @escaping LoadProfile, - loadProfileHeaders: @escaping LoadProfileHeaders + loadProfileHeaders: @escaping LoadProfileHeaders, + claimProfile: @escaping ClaimProfile ) { self.startAutomaticBackups = startAutomaticBackups self.migrateProfilesFromKeychain = migrateProfilesFromKeychain @@ -29,6 +31,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { self.lastBackup = lastBackup self.loadProfile = loadProfile self.loadProfileHeaders = loadProfileHeaders + self.claimProfile = claimProfile } } @@ -40,6 +43,7 @@ extension CloudBackupClient { public typealias LastBackup = @Sendable (ProfileID) -> AnyAsyncSequence public typealias LoadProfile = @Sendable (ProfileID) async throws -> BackedUpProfile public typealias LoadProfileHeaders = @Sendable () async throws -> [Profile.Header] + public typealias ClaimProfile = @Sendable (Profile) async throws -> Void } // MARK: CloudBackupClient.BackedUpProfile diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 371ecaf9ad..fba9913182 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -62,8 +62,9 @@ extension CloudBackupClient { let container: CKContainer = .default() @Sendable - func fetchProfileRecord(_ id: CKRecord.ID) async throws -> CKRecord { - let record = try await container.privateCloudDatabase.record(for: id) + func fetchProfileRecord(_ id: ProfileID) async throws -> CKRecord { + let recordID = CKRecord.ID(recordName: id.uuidString) + let record = try await container.privateCloudDatabase.record(for: recordID) guard record.recordType == .profile else { throw WrongRecordTypeError(type: record.recordType) } @@ -102,7 +103,7 @@ extension CloudBackupClient { @discardableResult @Sendable - func uploadProfileToICloud(_ profile: Either, header: Profile.Header, existingRecord: CKRecord?) async throws -> CKRecord { + func backupProfile(_ profile: Either, header: Profile.Header, existingRecord: CKRecord?) async throws -> CKRecord { let fileManager = FileManager.default let tempDirectoryURL = fileManager.temporaryDirectory let fileURL = tempDirectoryURL.appendingPathComponent(UUID().uuidString) @@ -124,22 +125,27 @@ extension CloudBackupClient { } @Sendable - func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async { - let result: BackupResult.Result + func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async throws { do { let json = profile.toJSONString() - try await uploadProfileToICloud(.right(json), header: profile.header, existingRecord: existingRecord) - result = .success - } catch CKError.accountTemporarilyUnavailable { - result = .temporarilyUnavailable - } catch CKError.notAuthenticated { - result = .notAuthenticated + try await backupProfile(.right(json), header: profile.header, existingRecord: existingRecord) } catch { - loggerGlobal.error("Automatic cloud backup failed with error \(error)") - result = .failure + let result: BackupResult.Result + switch error { + case CKError.accountTemporarilyUnavailable: + result = .temporarilyUnavailable + case CKError.notAuthenticated: + result = .notAuthenticated + default: + loggerGlobal.error("Automatic cloud backup failed with error \(error)") + result = .failure + } + + try? userDefaults.setLastCloudBackup(result, of: profile) + throw error } - try? userDefaults.setLastCloudBackup(result, of: profile) + try? userDefaults.setLastCloudBackup(.success, of: profile) } @Sendable @@ -153,7 +159,7 @@ extension CloudBackupClient { guard shouldBackUp || shouldCheckClaim else { return } - let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) + let existingRecord = try? await fetchProfileRecord(profile.id) let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } @@ -167,7 +173,7 @@ extension CloudBackupClient { guard shouldBackUp || shouldReclaim else { return } - await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + try? await backupProfileAndSaveResult(profile, existingRecord: existingRecord) } let retryBackupInterval: DispatchTimeInterval = .seconds(60) @@ -225,7 +231,7 @@ extension CloudBackupClient { return backedUpRecord } - return try await uploadProfileToICloud(.left(profileData), header: header, existingRecord: backedUpRecord) + return try await backupProfile(.left(profileData), header: header, existingRecord: backedUpRecord) } let migratedIDs = migrated.compactMap { ProfileID(uuidString: $0.recordID.recordName) } @@ -244,12 +250,16 @@ extension CloudBackupClient { userDefaults.lastCloudBackupValues(for: id) }, loadProfile: { id in - try await getProfile(fetchProfileRecord(.init(recordName: id.uuidString))) + try await getProfile(fetchProfileRecord(id)) }, loadProfileHeaders: { try await fetchAllProfileRecords(headerOnly: true) .map(getProfileHeader) .filter(\.isNonEmpty) + }, + claimProfile: { profile in + let existingRecord = try? await fetchProfileRecord(profile.id) + try await backupProfileAndSaveResult(profile, existingRecord: existingRecord) } ) } diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift index b186658d87..ea9146b3fc 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift @@ -21,7 +21,8 @@ extension CloudBackupClient: TestDependencyKey { checkAccountStatus: { throw NoopError() }, lastBackup: { _ in AsyncLazySequence([]).eraseToAnyAsyncSequence() }, loadProfile: { _ in throw NoopError() }, - loadProfileHeaders: { throw NoopError() } + loadProfileHeaders: { throw NoopError() }, + claimProfile: { _ in throw NoopError() } ) public static let testValue = Self( @@ -31,6 +32,7 @@ extension CloudBackupClient: TestDependencyKey { checkAccountStatus: unimplemented("\(Self.self).checkAccountStatus"), lastBackup: unimplemented("\(Self.self).lastBackup"), loadProfile: unimplemented("\(Self.self).loadProfile"), - loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders") + loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders"), + claimProfile: unimplemented("\(Self.self).claimProfile") ) } From f2baedd4436cd43f2ad059a25535f5303256918d Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 02:13:47 +0200 Subject: [PATCH 34/68] Make import and claiming work correctly --- .../CloudBackupClient+Interface.swift | 8 +++---- .../CloudBackupClient+Live.swift | 13 ++++++---- .../CloudBackupClient+Test.swift | 4 ++-- .../Clients/ProfileStore/ProfileStore.swift | 24 ++++++------------- .../TransportProfileClient+Live.swift | 4 ++++ 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift index bfc63024a8..56221658b7 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift @@ -12,7 +12,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { public let lastBackup: LastBackup public let loadProfile: LoadProfile public let loadProfileHeaders: LoadProfileHeaders - public let claimProfile: ClaimProfile + public let claimProfileOnICloud: ClaimProfileOnICloud public init( startAutomaticBackups: @escaping StartAutomaticBackups, @@ -22,7 +22,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { lastBackup: @escaping LastBackup, loadProfile: @escaping LoadProfile, loadProfileHeaders: @escaping LoadProfileHeaders, - claimProfile: @escaping ClaimProfile + claimProfileOnICloud: @escaping ClaimProfileOnICloud ) { self.startAutomaticBackups = startAutomaticBackups self.migrateProfilesFromKeychain = migrateProfilesFromKeychain @@ -31,7 +31,7 @@ public struct CloudBackupClient: DependencyKey, Sendable { self.lastBackup = lastBackup self.loadProfile = loadProfile self.loadProfileHeaders = loadProfileHeaders - self.claimProfile = claimProfile + self.claimProfileOnICloud = claimProfileOnICloud } } @@ -43,7 +43,7 @@ extension CloudBackupClient { public typealias LastBackup = @Sendable (ProfileID) -> AnyAsyncSequence public typealias LoadProfile = @Sendable (ProfileID) async throws -> BackedUpProfile public typealias LoadProfileHeaders = @Sendable () async throws -> [Profile.Header] - public typealias ClaimProfile = @Sendable (Profile) async throws -> Void + public typealias ClaimProfileOnICloud = @Sendable (Profile) async throws -> Void } // MARK: CloudBackupClient.BackedUpProfile diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index fba9913182..537fc78b24 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -49,6 +49,7 @@ extension CloudBackupClient { struct HeaderAndMetadataMismatchError: Error {} struct WrongRecordTypeError: Error { let type: CKRecord.RecordType } struct ProfileMissingFromKeychainError: Error { let id: ProfileID } + struct FailedToClaimProfileError: Error { let error: Error } public static let liveValue: Self = .live() @@ -161,10 +162,10 @@ extension CloudBackupClient { let existingRecord = try? await fetchProfileRecord(profile.id) - let backedUpID = { try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id } + let backedUpID = try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id let shouldReclaim: Bool - if shouldCheckClaim, let id = backedUpID(), await !profileStore.isThisDevice(deviceID: id) { + if shouldCheckClaim, let backedUpID, await !profileStore.isThisDevice(deviceID: backedUpID) { let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) shouldReclaim = action == .claimWallet(.transferBack) } else { @@ -257,9 +258,13 @@ extension CloudBackupClient { .map(getProfileHeader) .filter(\.isNonEmpty) }, - claimProfile: { profile in + claimProfileOnICloud: { profile in let existingRecord = try? await fetchProfileRecord(profile.id) - try await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + do { + try await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + } catch { + throw FailedToClaimProfileError(error: error) + } } ) } diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift index ea9146b3fc..b79d42193e 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift @@ -22,7 +22,7 @@ extension CloudBackupClient: TestDependencyKey { lastBackup: { _ in AsyncLazySequence([]).eraseToAnyAsyncSequence() }, loadProfile: { _ in throw NoopError() }, loadProfileHeaders: { throw NoopError() }, - claimProfile: { _ in throw NoopError() } + claimProfileOnICloud: { _ in throw NoopError() } ) public static let testValue = Self( @@ -33,6 +33,6 @@ extension CloudBackupClient: TestDependencyKey { lastBackup: unimplemented("\(Self.self).lastBackup"), loadProfile: unimplemented("\(Self.self).loadProfile"), loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders"), - claimProfile: unimplemented("\(Self.self).claimProfile") + claimProfileOnICloud: unimplemented("\(Self.self).claimProfileOnICloud") ) } diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 567ecbd20f..7d66375747 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -121,6 +121,8 @@ extension ProfileStore { /// Change current profile to new importedProfile and saves it, by /// updates `headerList` (Keychain), `activeProfileID` (UserDefaults) /// and saves a snapshot of the profile into Keychain. + /// + /// NB: The profile should be claimed locally before calling this function /// - Parameter profile: Imported Profile to use and save. public func importProfile(_ profileToImport: Profile) throws { // The software design of ProfileStore is to always have a profile at end @@ -134,8 +136,8 @@ extension ProfileStore { var profileToImport = profileToImport - // Before saving it we must claim ownership of it! - try _claimOwnership(of: &profileToImport) + // We need to save before calling `updateHeaderOfThenSave` + try _saveProfileAndEmitUpdate(profileToImport) profileToImport.changeCurrentToMainnetIfNeeded() @@ -345,24 +347,12 @@ extension ProfileStore { // MARK: Helpers extension ProfileStore { - /// Updates the `lastUsedOnDevice` to use this device, on `profile`, - /// then saves this profile and emits an update. - /// - Parameter profile: Profile to update `lastUsedOnDevice` of and - /// save on this device. - private func claimOwnershipOfProfile() throws { - var copy = profile - try _claimOwnership(of: ©) - } - - /// Updates the `lastUsedOnDevice` to use this device, on `profile`, - /// then saves this profile and emits an update. - /// - Parameter profile: Profile to update `lastUsedOnDevice` of and - /// save on this device. - private func _claimOwnership(of profile: inout Profile) throws { + /// Updates the `lastUsedOnDevice` to use this device, on `profile` + /// - Parameter profile: Profile to update `lastUsedOnDevice` of + public func claimOwnership(of profile: inout Profile) { @Dependency(\.date) var date profile.header.lastUsedOnDevice = deviceInfo profile.header.lastUsedOnDevice.date = date() - try _saveProfileAndEmitUpdate(profile) } /// Updates the header of a Profile, lastModified date, contentHint etc. diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift index 686222f9c1..dec6d082eb 100644 --- a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift @@ -7,10 +7,14 @@ extension TransportProfileClient: DependencyKey { ) -> Self { @Dependency(\.userDefaults) var userDefaults @Dependency(\.secureStorageClient) var secureStorageClient + @Dependency(\.cloudBackupClient) var cloudBackupClient return Self( importProfile: { profile, factorSourceIDs, containsP2PLinks in do { + var profile = profile + await profileStore.claimOwnership(of: &profile) + try await cloudBackupClient.claimProfileOnICloud(profile) try await profileStore.importProfile(profile) userDefaults.setShowRelinkConnectorsAfterProfileRestore(containsP2PLinks) } catch { From 60b440650e0576756abd76382e82e746575df74a Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 09:56:36 +0200 Subject: [PATCH 35/68] Move loading --- RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index a8c1cf9976..301c4fb103 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -30,12 +30,12 @@ public struct ClaimWallet: Sendable, FeatureReducer { public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { switch viewAction { case .clearWalletButtonTapped: + state.isLoading = true return .run { send in await resetWalletClient.resetWallet() await send(.delegate(.didClearWallet)) } case .transferBackButtonTapped: - state.isLoading = true return .send(.delegate(.transferBack)) } } From e73e9ab4abb5a58eedc59d9d54b93980074c40cc Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 10:48:13 +0200 Subject: [PATCH 36/68] Don't stop migration if profile is missing from keychain --- .../Clients/CloudBackupClient/CloudBackupClient+Live.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 537fc78b24..d95c1661da 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -48,7 +48,6 @@ extension CloudBackupClient { struct MissingMetadataError: Error {} struct HeaderAndMetadataMismatchError: Error {} struct WrongRecordTypeError: Error { let type: CKRecord.RecordType } - struct ProfileMissingFromKeychainError: Error { let id: ProfileID } struct FailedToClaimProfileError: Error { let error: Error } public static let liveValue: Self = .live() @@ -214,9 +213,7 @@ extension CloudBackupClient { let id = header.id guard !previouslyMigrated.contains(id), header.id != activeProfile else { return nil } - guard let profileData = try secureStorageClient.loadProfileSnapshotData(id) else { - throw ProfileMissingFromKeychainError(id: id) - } + guard let profileData = try? secureStorageClient.loadProfileSnapshotData(id) else { return nil } let profile = try Profile(jsonData: profileData) guard !profile.networks.isEmpty else { return nil } From 91443f009e9a7566e5477a56fe899fbb5e37745d Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 10:48:36 +0200 Subject: [PATCH 37/68] PROFILESTORE temp --- .../ProfileStoreTests/ProfileStoreTests.swift | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift index 56dc77d33f..679f80684b 100644 --- a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift +++ b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift @@ -294,7 +294,7 @@ final class ProfileStoreNewProfileTests: TestCase { } operation: { let sut = ProfileStore() // WHEN import profile - try await sut.importCloudProfileSnapshot(profileSnapshotInIcloud.header) + try await sut.importProfile(profileSnapshotInIcloud) return await sut.profile } @@ -303,33 +303,33 @@ final class ProfileStoreNewProfileTests: TestCase { } } - func test__GIVEN__no_profile__WHEN__import_profile_from_icloud_not_exists__THEN__error_is_thrown() async throws { - let icloudHeader: Header = .testValueProfileID_DEAD_deviceID_ABBA - try await withTimeLimit { - let assertionFailureIsCalled = self.expectation(description: "assertionFailure is called") - try await withTestClients { - // GIVEN no profile - $0.noProfile() - $0.secureStorageClient.loadProfileSnapshot = { headerId in - XCTAssertEqual(headerId, icloudHeader.id) - return nil - } - $0.assertionFailure = AssertionFailureAction.init(action: { _, _, _ in - // THEN identity is checked - assertionFailureIsCalled.fulfill() - }) - } operation: { - let sut = ProfileStore() - // WHEN import profile - do { - try await sut.importCloudProfileSnapshot(icloudHeader) - return XCTFail("expected error") - } catch {} - } - - await self.nearFutureFulfillment(of: assertionFailureIsCalled) - } - } +// func test__GIVEN__no_profile__WHEN__import_profile_from_icloud_not_exists__THEN__error_is_thrown() async throws { +// let icloudHeader: Header = .testValueProfileID_DEAD_deviceID_ABBA +// try await withTimeLimit { +// let assertionFailureIsCalled = self.expectation(description: "assertionFailure is called") +// try await withTestClients { +// // GIVEN no profile +// $0.noProfile() +// $0.secureStorageClient.loadProfileSnapshot = { headerId in +// XCTAssertEqual(headerId, icloudHeader.id) +// return nil +// } +// $0.assertionFailure = AssertionFailureAction.init(action: { _, _, _ in +// // THEN identity is checked +// assertionFailureIsCalled.fulfill() +// }) +// } operation: { +// let sut = ProfileStore() +// // WHEN import profile +// do { +// try await sut.importCloudProfileSnapshot(icloudHeader) +// return XCTFail("expected error") +// } catch {} +// } +// +// await self.nearFutureFulfillment(of: assertionFailureIsCalled) +// } +// } func test__GIVEN__no_profile__WHEN__import_profile__THEN__ownership_has_changed() async throws { let deviceInfo = DeviceInfo.testValueABBA @@ -746,39 +746,39 @@ final class ProfileStoreExistingProfileTests: TestCase { } } - func test__GIVEN__saved_profile_mismatch_deviceID__WHEN__claimAndContinueUseOnThisPhone__THEN__profile_uses_claimed_device() async throws { - try await doTestMismatch( - savedProfile: Profile.withOneAccount, - action: .claimAndContinueUseOnThisPhone - ) { claimed in - // THEN profile uses claimed device - XCTAssertNoDifference( - claimed.header.lastUsedOnDevice.id, - DeviceInfo.testValueBEEF.id - ) - } - } - - func test__GIVEN__saved_profile_mismatch_deviceID__WHEN__deleteProfile__THEN__profile_got_deleted() async throws { - let uuidOfNewProfile = UUID() - let savedProfile = Profile.withOneAccount - let userDefaults = UserDefaults.Dependency.ephemeral() - try await doTestMismatch( - savedProfile: savedProfile, - userDefaults: userDefaults, - action: .deleteProfileFromThisPhone, - then: { - $0.uuid = .constant(uuidOfNewProfile) - XCTAssertNoDifference(userDefaults.string(key: .activeProfileID), savedProfile.header.id.uuidString) - $0.secureStorageClient.deleteProfileAndMnemonicsByFactorSourceIDs = { idToDelete, _ in - XCTAssertNoDifference(idToDelete, savedProfile.header.id) - } - } - ) - - // New active profile - XCTAssertNoDifference(userDefaults.string(key: .activeProfileID), uuidOfNewProfile.uuidString) - } +// func test__GIVEN__saved_profile_mismatch_deviceID__WHEN__claimAndContinueUseOnThisPhone__THEN__profile_uses_claimed_device() async throws { +// try await doTestMismatch( +// savedProfile: Profile.withOneAccount, +// action: .claimAndContinueUseOnThisPhone +// ) { claimed in +// // THEN profile uses claimed device +// XCTAssertNoDifference( +// claimed.header.lastUsedOnDevice.id, +// DeviceInfo.testValueBEEF.id +// ) +// } +// } + +// func test__GIVEN__saved_profile_mismatch_deviceID__WHEN__deleteProfile__THEN__profile_got_deleted() async throws { +// let uuidOfNewProfile = UUID() +// let savedProfile = Profile.withOneAccount +// let userDefaults = UserDefaults.Dependency.ephemeral() +// try await doTestMismatch( +// savedProfile: savedProfile, +// userDefaults: userDefaults, +// action: .deleteProfileFromThisPhone, +// then: { +// $0.uuid = .constant(uuidOfNewProfile) +// XCTAssertNoDifference(userDefaults.string(key: .activeProfileID), savedProfile.header.id.uuidString) +// $0.secureStorageClient.deleteProfileAndMnemonicsByFactorSourceIDs = { idToDelete, _ in +// XCTAssertNoDifference(idToDelete, savedProfile.header.id) +// } +// } +// ) +// +// // New active profile +// XCTAssertNoDifference(userDefaults.string(key: .activeProfileID), uuidOfNewProfile.uuidString) +// } func test__GIVEN__mismatch__WHEN__app_is_not_yet_unlocked__THEN__no_alert_is_displayed() async throws { let alertNotScheduled = expectation( From d0758d3bea9c5aabe7e2a08a197d0f0f27b6dea3 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 10:51:34 +0200 Subject: [PATCH 38/68] don't migrate if we have newer backed up --- .../Clients/CloudBackupClient/CloudBackupClient+Live.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index d95c1661da..ee061b1b30 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -222,11 +222,11 @@ extension CloudBackupClient { return (profileData, header) } - let migrated = try await migratable.asyncMap { profileData, header in + let migrated: [CKRecord] = try await migratable.asyncCompactMap { profileData, header in let backedUpRecord = backedUpRecords.first { $0.recordID.recordName == header.id.uuidString } if let backedUpRecord, try getProfileHeader(backedUpRecord).lastModified >= header.lastModified { - // We already have a more recent version backed up on iCloud, so we return that - return backedUpRecord + // We already have a more recent version backed up on iCloud + return nil } return try await backupProfile(.left(profileData), header: header, existingRecord: backedUpRecord) From d9458f5efaa719402483ab0df9aaa7388793d7e7 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 13:50:23 +0200 Subject: [PATCH 39/68] fix problems 3 and 9 # Conflicts: # RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift --- .../DeviceFactorSourceClient+Live.swift | 23 ++++++++++++++++--- .../KeychainClient+Interface.swift | 11 ++++++++- .../KeychainClient/KeychainClient+Live.swift | 6 ++++- .../KeychainClient+Mocked.swift | 6 +++-- .../KeychainClient/KeychainHolder.swift | 9 ++++++++ .../SecureStorageClient+Interface.swift | 9 +++++++- .../SecureStorageClient+Live.swift | 6 ++++- .../SecureStorageClient+Test.swift | 8 +++++-- 8 files changed, 67 insertions(+), 11 deletions(-) diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift index 071efb0c24..acce8022a3 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift @@ -71,6 +71,21 @@ extension DeviceFactorSourceClient: DependencyKey { ) } + let profileStore = ProfileStore.shared + + @Sendable + func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[(FactorSourceIDFromHash, present: Bool)]> { + await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged()) + .map { factorSources, _ in + factorSources + .compactMap { $0.extract(DeviceFactorSource.self)?.id } + .map { id in + (id, secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) + } + } + .eraseToAnyAsyncSequence() + } + let problematicEntities: @Sendable () async throws -> (mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses) = { let factorSources = try await factorSourcesClient.getFactorSources(type: DeviceFactorSource.self) let accounts = try await accountsClient.getAccountsOnCurrentNetwork().elements @@ -85,10 +100,12 @@ extension DeviceFactorSourceClient: DependencyKey { let mnemonicPresentFactorSources = factorSources.filter { secureStorageClient.containsMnemonicIdentifiedByFactorSourceID($0.id) } + .map(\.id) - let unrecoverableFactorSources = mnemonicPresentFactorSources.filter { - !userDefaults.getFactorSourceIDOfBackedUpMnemonics().contains($0.id) - }.map(\.id) + let unrecoverableFactorSources = mnemonicPresentFactorSources + .filter { + !userDefaults.getFactorSourceIDOfBackedUpMnemonics().contains($0) + } func mnemonicMissing(_ account: Account) -> Bool { switch account.securityState { diff --git a/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift b/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift index f9692adfbd..b76dfdd31c 100644 --- a/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift +++ b/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift @@ -16,6 +16,7 @@ public struct KeychainClient: Sendable { public var _removeDataForKey: RemoveDataForKey public var _removeAllItems: RemoveAllItems public var _getAllKeysMatchingAttributes: GetAllKeysMatchingAttributes + public var _keychainChanged: KeychainChanged public init( getServiceAndAccessGroup: @escaping GetServiceAndAccessGroup, @@ -28,7 +29,8 @@ public struct KeychainClient: Sendable { getDataWithAuthForKey: @escaping GetDataWithAuthForKey, removeDataForKey: @escaping RemoveDataForKey, removeAllItems: @escaping RemoveAllItems, - getAllKeysMatchingAttributes: @escaping GetAllKeysMatchingAttributes + getAllKeysMatchingAttributes: @escaping GetAllKeysMatchingAttributes, + keychainChanged: @escaping KeychainChanged ) { self._getServiceAndAccessGroup = getServiceAndAccessGroup self._containsDataForKey = containsDataForKey @@ -41,6 +43,7 @@ public struct KeychainClient: Sendable { self._removeDataForKey = removeDataForKey self._removeAllItems = removeAllItems self._getAllKeysMatchingAttributes = getAllKeysMatchingAttributes + self._keychainChanged = keychainChanged } } @@ -86,6 +89,8 @@ extension KeychainClient { (synchronizable: Bool?, accessibility: KeychainAccess.Accessibility?) ) -> [Key] + + public typealias KeychainChanged = @Sendable () -> AnyAsyncSequence } // MARK: - KeychainAttributes @@ -140,6 +145,10 @@ extension KeychainClient { } extension KeychainClient { + public func keychainChanged() -> AnyAsyncSequence { + _keychainChanged() + } + public func serviceAndAccessGroup() -> KeychainServiceAndAccessGroup { _getServiceAndAccessGroup() } diff --git a/RadixWallet/Clients/KeychainClient/KeychainClient+Live.swift b/RadixWallet/Clients/KeychainClient/KeychainClient+Live.swift index 8a6f984f1a..3d9544b0ba 100644 --- a/RadixWallet/Clients/KeychainClient/KeychainClient+Live.swift +++ b/RadixWallet/Clients/KeychainClient/KeychainClient+Live.swift @@ -63,9 +63,13 @@ extension KeychainClient: DependencyKey { ) .compactMap { NonEmptyString(rawValue: $0) - }.compactMap { + } + .compactMap { Key(rawValue: $0) } + }, + keychainChanged: { + keychainHolder.keychainChanged } ) } diff --git a/RadixWallet/Clients/KeychainClient/KeychainClient+Mocked.swift b/RadixWallet/Clients/KeychainClient/KeychainClient+Mocked.swift index e3a2f69b73..772a5c6d3b 100644 --- a/RadixWallet/Clients/KeychainClient/KeychainClient+Mocked.swift +++ b/RadixWallet/Clients/KeychainClient/KeychainClient+Mocked.swift @@ -21,7 +21,8 @@ extension KeychainClient: TestDependencyKey { getDataWithAuthForKey: unimplemented("\(Self.self).getDataWithAuthForKey"), removeDataForKey: unimplemented("\(Self.self).removeDataForKey"), removeAllItems: unimplemented("\(Self.self).removeAllItems"), - getAllKeysMatchingAttributes: unimplemented("\(Self.self).getAllKeysMatchingAttributes") + getAllKeysMatchingAttributes: unimplemented("\(Self.self).getAllKeysMatchingAttributes"), + keychainChanged: unimplemented("\(Self.self).keychainChanged") ) public static let noop: Self = .init( @@ -35,6 +36,7 @@ extension KeychainClient: TestDependencyKey { getDataWithAuthForKey: { _, _ in throw NoopError() }, removeDataForKey: { _ in throw NoopError() }, removeAllItems: {}, - getAllKeysMatchingAttributes: { _ in [] } + getAllKeysMatchingAttributes: { _ in [] }, + keychainChanged: { AsyncLazySequence([]).eraseToAnyAsyncSequence() } ) } diff --git a/RadixWallet/Clients/KeychainClient/KeychainHolder.swift b/RadixWallet/Clients/KeychainClient/KeychainHolder.swift index 2d4cedfe56..e050681e6d 100644 --- a/RadixWallet/Clients/KeychainClient/KeychainHolder.swift +++ b/RadixWallet/Clients/KeychainClient/KeychainHolder.swift @@ -12,6 +12,7 @@ final class KeychainHolder: @unchecked Sendable { private let keychain: Keychain private let service: String private let accessGroup: String? + private let keychainChangedSubject = AsyncPassthroughSubject() private init() { self.keychain = Keychain(service: keychainService) @@ -26,6 +27,10 @@ extension KeychainHolder { typealias Comment = KeychainClient.Comment typealias AuthenticationPrompt = KeychainClient.AuthenticationPrompt + public var keychainChanged: AnyAsyncSequence { + keychainChangedSubject.eraseToAnyAsyncSequence() + } + public func getServiceAndAccessGroup() -> (service: String, accessGroup: String?) { (service, accessGroup) } @@ -70,6 +75,7 @@ extension KeychainHolder { ) throws { try withAttributes(of: attributes) .set(data, key: key.rawValue.rawValue) + keychainChangedSubject.send(()) } func setDataWithAuth( @@ -79,6 +85,7 @@ extension KeychainHolder { ) throws { try withAttributes(of: attributes) .set(data, key: key.rawValue.rawValue) + keychainChangedSubject.send(()) } func getDataWithoutAuth( @@ -130,10 +137,12 @@ extension KeychainHolder { forKey key: Key ) throws { try keychain.remove(key.rawValue.rawValue) + keychainChangedSubject.send(()) } func removeAllItems() throws { try keychain.removeAll() + keychainChangedSubject.send(()) } } diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift index 7faae198b8..63bbf4e55e 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift @@ -39,6 +39,7 @@ public struct SecureStorageClient: Sendable { public var loadP2PLinksPrivateKey: LoadP2PLinksPrivateKey public var saveP2PLinksPrivateKey: SaveP2PLinksPrivateKey + public var keychainChanged: KeychainChanged #if DEBUG public var getAllMnemonics: GetAllMnemonics @@ -68,6 +69,7 @@ public struct SecureStorageClient: Sendable { saveP2PLinks: @escaping SaveP2PLinks, loadP2PLinksPrivateKey: @escaping LoadP2PLinksPrivateKey, saveP2PLinksPrivateKey: @escaping SaveP2PLinksPrivateKey, + keychainChanged: @escaping KeychainChanged, getAllMnemonics: @escaping GetAllMnemonics ) { self.saveProfileSnapshot = saveProfileSnapshot @@ -93,6 +95,7 @@ public struct SecureStorageClient: Sendable { self.loadP2PLinksPrivateKey = loadP2PLinksPrivateKey self.saveP2PLinksPrivateKey = saveP2PLinksPrivateKey self.getAllMnemonics = getAllMnemonics + self.keychainChanged = keychainChanged } #else @@ -118,7 +121,8 @@ public struct SecureStorageClient: Sendable { loadP2PLinks: @escaping LoadP2PLinks, saveP2PLinks: @escaping SaveP2PLinks, loadP2PLinksPrivateKey: @escaping LoadP2PLinksPrivateKey, - saveP2PLinksPrivateKey: @escaping SaveP2PLinksPrivateKey + saveP2PLinksPrivateKey: @escaping SaveP2PLinksPrivateKey, + keychainChanged: @escaping KeychainChanged ) { self.saveProfileSnapshot = saveProfileSnapshot self.loadProfileSnapshotData = loadProfileSnapshotData @@ -142,6 +146,7 @@ public struct SecureStorageClient: Sendable { self.saveP2PLinks = saveP2PLinks self.loadP2PLinksPrivateKey = loadP2PLinksPrivateKey self.saveP2PLinksPrivateKey = saveP2PLinksPrivateKey + self.keychainChanged = keychainChanged } #endif // DEBUG } @@ -189,6 +194,8 @@ extension SecureStorageClient { public typealias LoadP2PLinksPrivateKey = @Sendable () throws -> Curve25519.PrivateKey? public typealias SaveP2PLinksPrivateKey = @Sendable (Curve25519.PrivateKey) throws -> Void + public typealias KeychainChanged = @Sendable () -> AnyAsyncSequence + public enum LoadMnemonicPurpose: Sendable, Hashable, CustomStringConvertible { case signTransaction case signAuthChallenge diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift index 6ec685a55b..e3cf55ac0d 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Live.swift @@ -391,6 +391,8 @@ extension SecureStorageClient: DependencyKey { loggerGlobal.notice("Saved p2pLinksPrivateKeyKey") } + let keychainChanged = keychainClient.keychainChanged + #if DEBUG return Self( saveProfileSnapshot: saveProfileSnapshot, @@ -415,6 +417,7 @@ extension SecureStorageClient: DependencyKey { saveP2PLinks: saveP2PLinks, loadP2PLinksPrivateKey: loadP2PLinksPrivateKey, saveP2PLinksPrivateKey: saveP2PLinksPrivateKey, + keychainChanged: keychainChanged, getAllMnemonics: getAllMnemonics ) #else @@ -440,7 +443,8 @@ extension SecureStorageClient: DependencyKey { loadP2PLinks: loadP2PLinks, saveP2PLinks: saveP2PLinks, loadP2PLinksPrivateKey: loadP2PLinksPrivateKey, - saveP2PLinksPrivateKey: saveP2PLinksPrivateKey + saveP2PLinksPrivateKey: saveP2PLinksPrivateKey, + keychainChanged: keychainChanged ) #endif }() diff --git a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift index f0d472f4ff..7df191ab50 100644 --- a/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift +++ b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Test.swift @@ -32,6 +32,7 @@ extension SecureStorageClient: TestDependencyKey { saveP2PLinks: { _ in }, loadP2PLinksPrivateKey: { nil }, saveP2PLinksPrivateKey: { _ in }, + keychainChanged: { AsyncLazySequence([]).eraseToAnyAsyncSequence() }, getAllMnemonics: { [] } ) #else @@ -57,7 +58,8 @@ extension SecureStorageClient: TestDependencyKey { loadP2PLinks: { nil }, saveP2PLinks: { _ in }, loadP2PLinksPrivateKey: { nil }, - saveP2PLinksPrivateKey: { _ in } + saveP2PLinksPrivateKey: { _ in }, + keychainChanged: { AsyncLazySequence([]).eraseToAnyAsyncSequence() } ) #endif // DEBUG @@ -87,6 +89,7 @@ extension SecureStorageClient: TestDependencyKey { saveP2PLinks: unimplemented("\(Self.self).saveP2PLinks"), loadP2PLinksPrivateKey: unimplemented("\(Self.self).loadP2PLinksPrivateKey"), saveP2PLinksPrivateKey: unimplemented("\(Self.self).saveP2PLinksPrivateKey"), + keychainChanged: unimplemented("\(Self.self).keychainChanged"), getAllMnemonics: unimplemented("\(Self.self).getAllMnemonics") ) #else @@ -112,7 +115,8 @@ extension SecureStorageClient: TestDependencyKey { loadP2PLinks: unimplemented("\(Self.self).loadP2PLinks"), saveP2PLinks: unimplemented("\(Self.self).saveP2PLinks"), loadP2PLinksPrivateKey: unimplemented("\(Self.self).loadP2PLinksPrivateKey"), - saveP2PLinksPrivateKey: unimplemented("\(Self.self).saveP2PLinksPrivateKey") + saveP2PLinksPrivateKey: unimplemented("\(Self.self).saveP2PLinksPrivateKey"), + keychainChanged: unimplemented("\(Self.self).keychainChanged") ) #endif } From e1017f588ca85c87a4fc6db0193f370a9a35b364 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 14:52:43 +0200 Subject: [PATCH 40/68] remove duplicates --- .../DeviceFactorSourceClient+Live.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift index acce8022a3..7e33ef45ea 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift @@ -73,16 +73,22 @@ extension DeviceFactorSourceClient: DependencyKey { let profileStore = ProfileStore.shared + struct FactorSourceHasMnemonic: Sendable, Equatable { + let id: FactorSourceIDFromHash + let present: Bool + } + @Sendable - func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[(FactorSourceIDFromHash, present: Bool)]> { + func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[FactorSourceHasMnemonic]> { await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged()) .map { factorSources, _ in factorSources .compactMap { $0.extract(DeviceFactorSource.self)?.id } .map { id in - (id, secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) + FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) } } + .removeDuplicates() .eraseToAnyAsyncSequence() } From eede0cb2017812f8ede302022a3d93b1bb8c9319 Mon Sep 17 00:00:00 2001 From: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:50:50 +0200 Subject: [PATCH 41/68] SecurityCenterClient improvements (#1167) --- .../DeviceFactorSourceClient+Interface.swift | 2 +- .../DeviceFactorSourceClient+Live.swift | 118 +++++++++--------- .../DeviceFactorSourceClient+Test.swift | 2 +- .../SecurityCenterClient+Interface.swift | 2 + .../SecurityCenterClient+Live.swift | 101 ++++++++------- .../SecurityCenterClient+Test.swift | 2 + .../UserDefaultsClient+AccountRecovery.swift | 6 + .../Features/MainFeature/Main+Reducer.swift | 12 ++ 8 files changed, 138 insertions(+), 107 deletions(-) diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift index 24e0da4a35..e5549b5ddb 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift @@ -42,7 +42,7 @@ extension DeviceFactorSourceClient { public typealias PublicKeysFromOnDeviceHD = @Sendable (PublicKeysFromOnDeviceHDRequest) async throws -> [HierarchicalDeterministicPublicKey] public typealias SignatureFromOnDeviceHD = @Sendable (SignatureFromOnDeviceHDRequest) async throws -> SignatureWithPublicKey public typealias IsAccountRecoveryNeeded = @Sendable () async throws -> Bool - public typealias ProblematicEntities = @Sendable () async throws -> (mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses) + public typealias ProblematicEntities = @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> } // MARK: - DiscrepancyUnsupportedCurve diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift index 7e33ef45ea..8cfb8dc501 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift @@ -5,7 +5,9 @@ struct FailedToFindFactorSource: Swift.Error {} extension DeviceFactorSourceClient: DependencyKey { public typealias Value = Self - public static let liveValue: Self = { + public static let liveValue: Self = .liveValue() + + public static func liveValue(profileStore: ProfileStore = .shared) -> DeviceFactorSourceClient { @Dependency(\.secureStorageClient) var secureStorageClient @Dependency(\.accountsClient) var accountsClient @Dependency(\.personasClient) var personasClient @@ -71,8 +73,6 @@ extension DeviceFactorSourceClient: DependencyKey { ) } - let profileStore = ProfileStore.shared - struct FactorSourceHasMnemonic: Sendable, Equatable { let id: FactorSourceIDFromHash let present: Bool @@ -92,70 +92,70 @@ extension DeviceFactorSourceClient: DependencyKey { .eraseToAnyAsyncSequence() } - let problematicEntities: @Sendable () async throws -> (mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses) = { - let factorSources = try await factorSourcesClient.getFactorSources(type: DeviceFactorSource.self) - let accounts = try await accountsClient.getAccountsOnCurrentNetwork().elements - let hiddenAccounts = try await accountsClient.getHiddenAccountsOnCurrentNetwork().elements - let personas = try await personasClient.getPersonas().elements - let hiddenPersonas = try await personasClient.getHiddenPersonasOnCurrentNetwork().elements - - let mnemonicMissingFactorSources = factorSources.filter { - !secureStorageClient.containsMnemonicIdentifiedByFactorSourceID($0.id) - }.map(\.id) - - let mnemonicPresentFactorSources = factorSources.filter { - secureStorageClient.containsMnemonicIdentifiedByFactorSourceID($0.id) - } - .map(\.id) - - let unrecoverableFactorSources = mnemonicPresentFactorSources - .filter { - !userDefaults.getFactorSourceIDOfBackedUpMnemonics().contains($0) + let problematicEntities: @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> = { + await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { factorSources, backedUpFactorSources, profile in + let mnemonicMissingFactorSources = factorSources + .filter(not(\.present)) + .map(\.id) + + let mnemomincPresentFactorSources = factorSources + .filter(\.present) + .map(\.id) + + let unrecoverableFactorSources = mnemomincPresentFactorSources + .filter { !backedUpFactorSources.contains($0) } + + let network = try profile.network(id: profile.networkID) + let accounts = network.getAccounts() + let hiddenAccounts = network.getHiddenAccounts() + let personas = network.getPersonas() + let hiddenPersonas = network.getHiddenPersonas() + + func mnemonicMissing(_ account: Account) -> Bool { + switch account.securityState { + case let .unsecured(value): + mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) + } } - func mnemonicMissing(_ account: Account) -> Bool { - switch account.securityState { - case let .unsecured(value): - mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) + func mnemonicMissing(_ persona: Persona) -> Bool { + switch persona.securityState { + case let .unsecured(value): + mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) + } } - } - func mnemonicMissing(_ persona: Persona) -> Bool { - switch persona.securityState { - case let .unsecured(value): - mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) + func unrecoverable(_ account: Account) -> Bool { + switch account.securityState { + case let .unsecured(value): + unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) + } } - } - func unrecoverable(_ account: Account) -> Bool { - switch account.securityState { - case let .unsecured(value): - unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) + func unrecoverable(_ persona: Persona) -> Bool { + switch persona.securityState { + case let .unsecured(value): + unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) + } } - } - func unrecoverable(_ persona: Persona) -> Bool { - switch persona.securityState { - case let .unsecured(value): - unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) - } + let mnemonicMissing = ProblematicAddresses( + accounts: accounts.filter(mnemonicMissing(_:)).map(\.address), + hiddenAccounts: hiddenAccounts.filter(mnemonicMissing(_:)).map(\.address), + personas: personas.filter(mnemonicMissing(_:)).map(\.address), + hiddenPersonas: hiddenPersonas.filter(mnemonicMissing(_:)).map(\.address) + ) + + let unrecoverable = ProblematicAddresses( + accounts: accounts.filter(unrecoverable(_:)).map(\.address), + hiddenAccounts: hiddenAccounts.filter(unrecoverable(_:)).map(\.address), + personas: personas.filter(unrecoverable(_:)).map(\.address), + hiddenPersonas: hiddenPersonas.filter(unrecoverable(_:)).map(\.address) + ) + + return (mnemonicMissing: mnemonicMissing, unrecoverable: unrecoverable) } - - let mnemonicMissing = ProblematicAddresses( - accounts: accounts.filter(mnemonicMissing(_:)).map(\.address), - hiddenAccounts: hiddenAccounts.filter(mnemonicMissing(_:)).map(\.address), - personas: personas.filter(mnemonicMissing(_:)).map(\.address), - hiddenPersonas: hiddenPersonas.filter(mnemonicMissing(_:)).map(\.address) - ) - - let unrecoverable = ProblematicAddresses( - accounts: accounts.filter(unrecoverable(_:)).map(\.address), - hiddenAccounts: hiddenAccounts.filter(unrecoverable(_:)).map(\.address), - personas: personas.filter(unrecoverable(_:)).map(\.address), - hiddenPersonas: hiddenPersonas.filter(unrecoverable(_:)).map(\.address) - ) - - return (mnemonicMissing: mnemonicMissing, unrecoverable: unrecoverable) + .eraseToAnyAsyncSequence() } return Self( @@ -220,5 +220,5 @@ extension DeviceFactorSourceClient: DependencyKey { }, problematicEntities: problematicEntities ) - }() + } } diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift index 75af449a23..d15f1d374e 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift @@ -16,7 +16,7 @@ extension DeviceFactorSourceClient: TestDependencyKey { isAccountRecoveryNeeded: { false }, entitiesControlledByFactorSource: { _, _ in throw NoopError() }, controlledEntities: { _ in [] }, - problematicEntities: { (mnemonicMissing: .empty, unrecoverable: .empty) } + problematicEntities: { throw NoopError() } ) public static let testValue = Self( diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index f5ebadecbb..b0ac227180 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -5,6 +5,7 @@ import os // MARK: - SecurityCenterClient public struct SecurityCenterClient: DependencyKey, Sendable { + public let startMonitoring: StartMonitoring public let problems: Problems public let lastManualBackup: LastManualBackup public let lastCloudBackup: LastCloudBackup @@ -12,6 +13,7 @@ public struct SecurityCenterClient: DependencyKey, Sendable { // MARK: SecurityCenterClient.Problems extension SecurityCenterClient { + public typealias StartMonitoring = @Sendable () async throws -> Void public typealias Problems = @Sendable (SecurityProblem.ProblemType?) async -> AnyAsyncSequence<[SecurityProblem]> public typealias LastManualBackup = @Sendable () async -> AnyAsyncSequence public typealias LastCloudBackup = @Sendable () async -> AnyAsyncSequence diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 4a9d250e44..f30080ae6d 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -52,64 +52,73 @@ extension SecurityCenterClient { .eraseToAnyAsyncSequence() } - return .init( - problems: { type in - let profiles = await profileStore.values() - let cloudBackups = await cloudBackups() - let manualBackups = await manualBackups() - - return combineLatest(profiles, cloudBackups, manualBackups).map { profile, cloudBackup, manualBackup in - let isCloudProfileSyncEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled + let problemsSubject = AsyncCurrentValueSubject<[SecurityProblem]>([]) - let problematic = try? await deviceFactorSourceClient.problematicEntities() + @Sendable + func startMonitoring() async throws { + let profileValues = await profileStore.values() + let cloudBackupValues = await cloudBackups() + let manualBackupValues = await manualBackups() + let problematicValues = try await deviceFactorSourceClient.problematicEntities() + + let first = combineLatest(profileValues, problematicValues) + let second = combineLatest(cloudBackupValues, manualBackupValues) + for try await (profileProblematic, backups) in combineLatest(first, second) { + let isCloudProfileSyncEnabled = profileProblematic.0.appPreferences.security.isCloudProfileSyncEnabled + let problematic = profileProblematic.1 + let cloudBackup = backups.0 + let manualBackup = backups.1 + + func hasProblem3() async -> ProblematicAddresses? { + problematic.unrecoverable.isEmpty ? nil : problematic.unrecoverable + } - func hasProblem3() async -> ProblematicAddresses? { - guard let problematic, !problematic.unrecoverable.isEmpty else { return nil } - return problematic.unrecoverable + func hasProblem5() -> Bool { + if isCloudProfileSyncEnabled, let cloudBackup { + !cloudBackup.success + } else { + false // FIXME: GK - is this what we want? } + } - func hasProblem5() -> Bool { - if isCloudProfileSyncEnabled, let cloudBackup { - !cloudBackup.success - } else { - false // FIXME: GK - is this what we want? - } - } + func hasProblem6() -> Bool { + !isCloudProfileSyncEnabled && manualBackup == nil + } - func hasProblem6() -> Bool { - !isCloudProfileSyncEnabled && manualBackup == nil - } + func hasProblem7() -> Bool { + !isCloudProfileSyncEnabled && manualBackup?.upToDate == false + } - func hasProblem7() -> Bool { - !isCloudProfileSyncEnabled && manualBackup?.upToDate == false - } + func hasProblem9() async -> ProblematicAddresses? { + problematic.mnemonicMissing.isEmpty ? nil : problematic.mnemonicMissing + } - func hasProblem9() async -> ProblematicAddresses? { - guard let problematic, !problematic.mnemonicMissing.isEmpty else { return nil } - return problematic.mnemonicMissing - } + var result: [SecurityProblem] = [] - var result: [SecurityProblem] = [] + if let addresses = await hasProblem3() { + result.append(.problem3(addresses: addresses)) + } - if type == nil || type == .securityFactors { - if let addresses = await hasProblem3() { - result.append(.problem3(addresses: addresses)) - } + if let addresses = await hasProblem9() { + result.append(.problem9(addresses: addresses)) + } + if hasProblem5() { result.append(.problem5) } + if hasProblem6() { result.append(.problem6) } + if hasProblem7() { result.append(.problem7) } - if let addresses = await hasProblem9() { - result.append(.problem9(addresses: addresses)) - } - } + print("M- Sending result: \(result.map(\.number))") - if type == nil || type == .configurationBackup { - if hasProblem5() { result.append(.problem5) } - if hasProblem6() { result.append(.problem6) } - if hasProblem7() { result.append(.problem7) } - } + problemsSubject.send(result) + } + } - return result - } - .eraseToAnyAsyncSequence() + return .init( + startMonitoring: startMonitoring, + problems: { type in + problemsSubject + .share() + .map { $0.filter { type == nil || $0.type == type } } + .eraseToAnyAsyncSequence() }, lastManualBackup: manualBackups, lastCloudBackup: cloudBackups diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Test.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Test.swift index 71ad77c2de..8aca60ea02 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Test.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Test.swift @@ -15,12 +15,14 @@ extension SecurityCenterClient: TestDependencyKey { public static let previewValue: Self = .noop public static let noop = Self( + startMonitoring: {}, problems: { _ in AsyncLazySequence([[]]).eraseToAnyAsyncSequence() }, lastManualBackup: { AsyncLazySequence([]).eraseToAnyAsyncSequence() }, lastCloudBackup: { AsyncLazySequence([]).eraseToAnyAsyncSequence() } ) public static let testValue = Self( + startMonitoring: unimplemented("\(Self.self).startMonitoring"), problems: unimplemented("\(Self.self).problems"), lastManualBackup: unimplemented("\(Self.self).lastManualBackup"), lastCloudBackup: unimplemented("\(Self.self).lastCloudBackup") diff --git a/RadixWallet/Core/FeaturePrelude/UserDefaultsClient+AccountRecovery.swift b/RadixWallet/Core/FeaturePrelude/UserDefaultsClient+AccountRecovery.swift index 8459ecb694..cfd8877544 100644 --- a/RadixWallet/Core/FeaturePrelude/UserDefaultsClient+AccountRecovery.swift +++ b/RadixWallet/Core/FeaturePrelude/UserDefaultsClient+AccountRecovery.swift @@ -12,4 +12,10 @@ extension UserDefaults.Dependency { public func removeAllFactorSourceIDsOfBackedUpMnemonics() { remove(.mnemonicsUserClaimsToHaveBackedUp) } + + public func factorSourceIDOfBackedUpMnemonics() -> AnyAsyncSequence> { + codableValues(key: .mnemonicsUserClaimsToHaveBackedUp, codable: OrderedSet.self) + .map { (try? $0.get()) ?? [] } + .eraseToAnyAsyncSequence() + } } diff --git a/RadixWallet/Features/MainFeature/Main+Reducer.swift b/RadixWallet/Features/MainFeature/Main+Reducer.swift index 352e7081d3..98b17969b0 100644 --- a/RadixWallet/Features/MainFeature/Main+Reducer.swift +++ b/RadixWallet/Features/MainFeature/Main+Reducer.swift @@ -53,6 +53,7 @@ public struct Main: Sendable, FeatureReducer { @Dependency(\.gatewaysClient) var gatewaysClient @Dependency(\.personasClient) var personasClient @Dependency(\.cloudBackupClient) var cloudBackupClient + @Dependency(\.securityCenterClient) var securityCenterClient public init() {} @@ -72,6 +73,7 @@ public struct Main: Sendable, FeatureReducer { switch viewAction { case .task: startAutomaticBackupsEffect() + .merge(with: startMonitoringSecurityCenterEffect()) .merge(with: gatewayValuesEffect()) } } @@ -86,6 +88,16 @@ public struct Main: Sendable, FeatureReducer { } } + private func startMonitoringSecurityCenterEffect() -> Effect { + .run { _ in + do { + try await securityCenterClient.startMonitoring() + } catch { + loggerGlobal.notice("securityCenterClient.startMonitoring failed: \(error)") + } + } + } + private func gatewayValuesEffect() -> Effect { .run { send in for try await gateway in await gatewaysClient.currentGatewayValues() { From aac177bcf7a1af57407d8591508463e770205147 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 15:55:17 +0200 Subject: [PATCH 42/68] Stop checking header against profile --- .../CloudBackupClient+Live.swift | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index ee061b1b30..c8717f5a0b 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -46,7 +46,6 @@ extension CloudBackupClient { struct IncorrectRecordTypeError: Error {} struct NoProfileInRecordError: Error {} struct MissingMetadataError: Error {} - struct HeaderAndMetadataMismatchError: Error {} struct WrongRecordTypeError: Error { let type: CKRecord.RecordType } struct FailedToClaimProfileError: Error { let error: Error } @@ -94,10 +93,6 @@ extension CloudBackupClient { let profile = try Profile(jsonString: json) try FileManager.default.removeItem(at: fileURL) - guard try getProfileHeader(record).isEquivalent(to: profile.header) else { - throw HeaderAndMetadataMismatchError() - } - return BackedUpProfile(profile: profile, containsLegacyP2PLinks: containsLegacyP2PLinks) } @@ -271,30 +266,6 @@ private extension Profile.Header { var isNonEmpty: Bool { contentHint.numberOfAccountsOnAllNetworksInTotal + contentHint.numberOfPersonasOnAllNetworksInTotal > 0 } - - func isEquivalent(to other: Self) -> Bool { - snapshotVersion.rawValue == other.snapshotVersion.rawValue && - creatingDevice.isEquivalent(to: other.creatingDevice) && - lastUsedOnDevice.isEquivalent(to: other.lastUsedOnDevice) && - lastModified.isEquivalent(to: other.lastModified) && - contentHint.numberOfAccountsOnAllNetworksInTotal == other.contentHint.numberOfAccountsOnAllNetworksInTotal && - contentHint.numberOfPersonasOnAllNetworksInTotal == other.contentHint.numberOfPersonasOnAllNetworksInTotal && - contentHint.numberOfNetworks == other.contentHint.numberOfNetworks - } -} - -private extension DeviceInfo { - func isEquivalent(to other: Self) -> Bool { - id.uuidString == other.id.uuidString && - date.isEquivalent(to: other.date) && - description == other.description - } -} - -private extension Date { - func isEquivalent(to other: Self) -> Bool { - abs(timeIntervalSince(other)) < 0.001 - } } extension CloudBackupClient { From 0b412dd884d6be9a1329ac8e1dd0fecec9bc1cf4 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 16:01:50 +0200 Subject: [PATCH 43/68] small refactor --- .../SecurityCenterClient+Live.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index f30080ae6d..7ef9d0384c 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -57,24 +57,18 @@ extension SecurityCenterClient { @Sendable func startMonitoring() async throws { let profileValues = await profileStore.values() - let cloudBackupValues = await cloudBackups() - let manualBackupValues = await manualBackups() let problematicValues = try await deviceFactorSourceClient.problematicEntities() + let backupValues = await combineLatest(cloudBackups(), manualBackups()).map { (cloud: $0, manual: $1) } - let first = combineLatest(profileValues, problematicValues) - let second = combineLatest(cloudBackupValues, manualBackupValues) - for try await (profileProblematic, backups) in combineLatest(first, second) { - let isCloudProfileSyncEnabled = profileProblematic.0.appPreferences.security.isCloudProfileSyncEnabled - let problematic = profileProblematic.1 - let cloudBackup = backups.0 - let manualBackup = backups.1 + for try await (profile, problematic, backups) in combineLatest(profileValues, problematicValues, backupValues) { + let isCloudProfileSyncEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled func hasProblem3() async -> ProblematicAddresses? { problematic.unrecoverable.isEmpty ? nil : problematic.unrecoverable } func hasProblem5() -> Bool { - if isCloudProfileSyncEnabled, let cloudBackup { + if isCloudProfileSyncEnabled, let cloudBackup = backups.cloud { !cloudBackup.success } else { false // FIXME: GK - is this what we want? @@ -82,11 +76,11 @@ extension SecurityCenterClient { } func hasProblem6() -> Bool { - !isCloudProfileSyncEnabled && manualBackup == nil + !isCloudProfileSyncEnabled && backups.manual == nil } func hasProblem7() -> Bool { - !isCloudProfileSyncEnabled && manualBackup?.upToDate == false + !isCloudProfileSyncEnabled && backups.manual?.upToDate == false } func hasProblem9() async -> ProblematicAddresses? { From 855098fdd939693e0e7e03dd1bb540d4372fad6c Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 18:01:58 +0200 Subject: [PATCH 44/68] Refine backup status --- .../CloudBackupClient+Live.swift | 10 +++--- .../SecurityCenterClient+Interface.swift | 34 +++++++++++++++---- .../SecurityCenterClient+Live.swift | 20 ++++++----- .../UserDefaults+Dependency+Extension.swift | 12 ++++--- .../ConfigurationBackup+Reducer.swift | 6 ++-- .../ConfigurationBackup+View.swift | 2 +- 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index c8717f5a0b..d1f5c47c38 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -125,18 +125,18 @@ extension CloudBackupClient { let json = profile.toJSONString() try await backupProfile(.right(json), header: profile.header, existingRecord: existingRecord) } catch { - let result: BackupResult.Result + let failure: BackupResult.Result.Failure switch error { case CKError.accountTemporarilyUnavailable: - result = .temporarilyUnavailable + failure = .temporarilyUnavailable case CKError.notAuthenticated: - result = .notAuthenticated + failure = .notAuthenticated default: loggerGlobal.error("Automatic cloud backup failed with error \(error)") - result = .failure + failure = .other } - try? userDefaults.setLastCloudBackup(result, of: profile) + try? userDefaults.setLastCloudBackup(.failure(failure), of: profile) throw error } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index b0ac227180..6aac8b6d49 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -158,12 +158,32 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable { } } -// MARK: - SecurityCenterClient.BackupStatus -extension SecurityCenterClient { - // MARK: - BackupStatus - public struct BackupStatus: Hashable, Codable, Sendable { - public let backupDate: Date - public let upToDate: Bool - public let success: Bool +// MARK: - BackupStatus +public struct BackupStatus: Hashable, Codable, Sendable { + private let timeoutInterval: TimeInterval = 5 * 60 + + public let backupDate: Date + public let upToDate: Bool + public let status: Status + + public var succeeded: Bool { + status == .success + } + + public var failed: Bool { + switch status { + case .failed: + true + case let .started(date): + Date.now.timeIntervalSince(date) > timeoutInterval + case .success: + false + } + } + + public enum Status: Hashable, Codable, Sendable { + case started(Date) + case success + case failed } } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 7ef9d0384c..7da8760505 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -43,13 +43,17 @@ extension SecurityCenterClient { @Sendable func statusValues(results: AnyAsyncSequence) async -> AnyAsyncSequence { - await combineLatest(profileStore.values(), results.prepend(nil)).map { profile, backup in - guard let backup else { return nil } - let upToDate = backup.saveIdentifier == profile.saveIdentifier - let success = backup.result == .success - return .init(backupDate: backup.backupDate, upToDate: upToDate, success: success) - } - .eraseToAnyAsyncSequence() + await combineLatest(profileStore.values(), results.prepend(nil)) + .map { profile, backup -> BackupStatus? in + guard let backup else { return nil } + let upToDate = backup.saveIdentifier == profile.saveIdentifier + return .init( + backupDate: backup.backupDate, + upToDate: upToDate, + status: backup.result == .success ? .success : .failed + ) + } + .eraseToAnyAsyncSequence() } let problemsSubject = AsyncCurrentValueSubject<[SecurityProblem]>([]) @@ -69,7 +73,7 @@ extension SecurityCenterClient { func hasProblem5() -> Bool { if isCloudProfileSyncEnabled, let cloudBackup = backups.cloud { - !cloudBackup.success + cloudBackup.failed } else { false // FIXME: GK - is this what we want? } diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 19b1d45d68..6b35cc0a41 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -237,11 +237,15 @@ public struct BackupResult: Codable, Sendable { public let saveIdentifier: String public let result: Result - public enum Result: Codable, Sendable { + public enum Result: Equatable, Codable, Sendable { case success - case temporarilyUnavailable - case notAuthenticated - case failure + case failure(Failure) + + public enum Failure: Equatable, Codable, Sendable { + case temporarilyUnavailable + case notAuthenticated + case other + } } } diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index 21bbfa0e36..39d0b50dbb 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -3,8 +3,6 @@ import ComposableArchitecture // MARK: - ConfigurationBackup public struct ConfigurationBackup: Sendable, FeatureReducer { - public typealias BackupStatus = SecurityCenterClient.BackupStatus - public struct Exportable: Sendable, Hashable { public let profile: Profile public let file: ExportableProfileFile @@ -25,12 +23,12 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { public var exportable: Exportable? = nil public var outdatedBackupPresent: Bool { - guard let lastCloudBackup, lastCloudBackup.success else { return false } + guard let lastCloudBackup, lastCloudBackup.succeeded else { return false } return !cloudBackupsEnabled && !lastCloudBackup.upToDate } public var showLastBackupLabel: Bool { - if let lastCloudBackup, lastCloudBackup.success, !lastCloudBackup.upToDate { + if let lastCloudBackup, lastCloudBackup.succeeded, !lastCloudBackup.upToDate { true } else { false diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift index 68fd37d653..478f6ed9b4 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift @@ -210,7 +210,7 @@ extension ConfigurationBackup { } private var lastBackedUpString: String? { - guard let lastBackedUp, lastBackedUp.success, !lastBackedUp.upToDate else { return nil } + guard let lastBackedUp, lastBackedUp.succeeded, !lastBackedUp.upToDate else { return nil } return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastBackedUp.backupDate)) } From 671c988e60d8fa6ccd0ad164f702c5e43922b8b3 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 18:23:30 +0200 Subject: [PATCH 45/68] wip --- .../SecurityCenterClient+Interface.swift | 26 +------------------ .../SecurityCenterClient+Live.swift | 14 +++++----- .../UserDefaults+Dependency+Extension.swift | 24 ++++++++++++++--- .../ConfigurationBackup+View.swift | 4 +-- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index 6aac8b6d49..e7d5ad122e 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -160,30 +160,6 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable { // MARK: - BackupStatus public struct BackupStatus: Hashable, Codable, Sendable { - private let timeoutInterval: TimeInterval = 5 * 60 - - public let backupDate: Date + public let result: BackupResult public let upToDate: Bool - public let status: Status - - public var succeeded: Bool { - status == .success - } - - public var failed: Bool { - switch status { - case .failed: - true - case let .started(date): - Date.now.timeIntervalSince(date) > timeoutInterval - case .success: - false - } - } - - public enum Status: Hashable, Codable, Sendable { - case started(Date) - case success - case failed - } } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 7da8760505..37f07d89c0 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -46,16 +46,16 @@ extension SecurityCenterClient { await combineLatest(profileStore.values(), results.prepend(nil)) .map { profile, backup -> BackupStatus? in guard let backup else { return nil } - let upToDate = backup.saveIdentifier == profile.saveIdentifier - return .init( - backupDate: backup.backupDate, - upToDate: upToDate, - status: backup.result == .success ? .success : .failed - ) + return .init(result: backup, upToDate: backup.saveIdentifier == profile.saveIdentifier) } .eraseToAnyAsyncSequence() } + @Sendable + func backupStatus(of result: BackupResult, profile: Profile) async -> BackupStatus { + .init(result: result, upToDate: result.saveIdentifier == profile.saveIdentifier) + } + let problemsSubject = AsyncCurrentValueSubject<[SecurityProblem]>([]) @Sendable @@ -73,7 +73,7 @@ extension SecurityCenterClient { func hasProblem5() -> Bool { if isCloudProfileSyncEnabled, let cloudBackup = backups.cloud { - cloudBackup.failed + cloudBackup.result.failed } else { false // FIXME: GK - is this what we want? } diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 6b35cc0a41..f8b6ebda51 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -232,16 +232,34 @@ extension UserDefaults.Dependency { } // MARK: - BackupResult -public struct BackupResult: Codable, Sendable { +public struct BackupResult: Hashable, Codable, Sendable { + private static let timeoutInterval: TimeInterval = 5 * 60 + public let backupDate: Date public let saveIdentifier: String public let result: Result - public enum Result: Equatable, Codable, Sendable { + public var succeeded: Bool { + result == .success + } + + public var failed: Bool { + switch result { + case .failure: + true + case let .started(date): + Date.now.timeIntervalSince(date) > Self.timeoutInterval + case .success: + false + } + } + + public enum Result: Hashable, Codable, Sendable { + case started(Date) case success case failure(Failure) - public enum Failure: Equatable, Codable, Sendable { + public enum Failure: Hashable, Codable, Sendable { case temporarilyUnavailable case notAuthenticated case other diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift index 478f6ed9b4..816ac2e56f 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift @@ -210,8 +210,8 @@ extension ConfigurationBackup { } private var lastBackedUpString: String? { - guard let lastBackedUp, lastBackedUp.succeeded, !lastBackedUp.upToDate else { return nil } - return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastBackedUp.backupDate)) + guard let lastBackedUp, lastBackedUp.result.succeeded, !lastBackedUp.upToDate else { return nil } + return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastBackedUp.result.backupDate)) } struct ItemView: SwiftUI.View { From 6c0af5fc35f2a3a053b24fd2f32ca9134a4f3f2c Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 18:35:18 +0200 Subject: [PATCH 46/68] wip --- .../SecurityCenterClient+Interface.swift | 6 ++++++ .../SecurityCenterClient+Live.swift | 10 ++-------- .../ConfigurationBackup+Reducer.swift | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index e7d5ad122e..b37333bbd6 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -163,3 +163,9 @@ public struct BackupStatus: Hashable, Codable, Sendable { public let result: BackupResult public let upToDate: Bool } + +extension BackupResult { + public func status(profile: Profile) -> BackupStatus { + .init(result: self, upToDate: saveIdentifier == profile.saveIdentifier) + } +} diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 37f07d89c0..d744c62506 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -44,18 +44,12 @@ extension SecurityCenterClient { @Sendable func statusValues(results: AnyAsyncSequence) async -> AnyAsyncSequence { await combineLatest(profileStore.values(), results.prepend(nil)) - .map { profile, backup -> BackupStatus? in - guard let backup else { return nil } - return .init(result: backup, upToDate: backup.saveIdentifier == profile.saveIdentifier) + .map { profile, backup in + backup.map { BackupStatus(result: $0, upToDate: $0.saveIdentifier == profile.saveIdentifier) } } .eraseToAnyAsyncSequence() } - @Sendable - func backupStatus(of result: BackupResult, profile: Profile) async -> BackupStatus { - .init(result: result, upToDate: result.saveIdentifier == profile.saveIdentifier) - } - let problemsSubject = AsyncCurrentValueSubject<[SecurityProblem]>([]) @Sendable diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index 39d0b50dbb..7dcf2b9a6e 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -23,12 +23,12 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { public var exportable: Exportable? = nil public var outdatedBackupPresent: Bool { - guard let lastCloudBackup, lastCloudBackup.succeeded else { return false } + guard let lastCloudBackup, lastCloudBackup.result.succeeded else { return false } return !cloudBackupsEnabled && !lastCloudBackup.upToDate } public var showLastBackupLabel: Bool { - if let lastCloudBackup, lastCloudBackup.succeeded, !lastCloudBackup.upToDate { + if let lastCloudBackup, lastCloudBackup.result.succeeded, !lastCloudBackup.upToDate { true } else { false @@ -217,7 +217,7 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { .run { send in for try await lastBackup in await securityCenterClient.lastManualBackup() { guard !Task.isCancelled else { return } - await send(.internal(.setLastManualBackup(lastBackup?.backupDate))) + await send(.internal(.setLastManualBackup(lastBackup?.result.backupDate))) } } } From 11de1f172b7fe4ee216676fefa37aa96ea01cd7d Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 18:36:25 +0200 Subject: [PATCH 47/68] wip --- .../SecurityCenterClient+Interface.swift | 7 +++---- .../SecurityCenterClient/SecurityCenterClient+Live.swift | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index b37333bbd6..c645287f1d 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -162,10 +162,9 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable { public struct BackupStatus: Hashable, Codable, Sendable { public let result: BackupResult public let upToDate: Bool -} -extension BackupResult { - public func status(profile: Profile) -> BackupStatus { - .init(result: self, upToDate: saveIdentifier == profile.saveIdentifier) + public init(result: BackupResult, profile: Profile) { + self.result = result + self.upToDate = result.saveIdentifier == profile.saveIdentifier } } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index d744c62506..8b12dc5ff0 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -45,7 +45,7 @@ extension SecurityCenterClient { func statusValues(results: AnyAsyncSequence) async -> AnyAsyncSequence { await combineLatest(profileStore.values(), results.prepend(nil)) .map { profile, backup in - backup.map { BackupStatus(result: $0, upToDate: $0.saveIdentifier == profile.saveIdentifier) } + backup.map { BackupStatus(result: $0, profile: profile) } } .eraseToAnyAsyncSequence() } From b0f1966c72136211544d9d8508c84c56d3f46abe Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 20:23:30 +0200 Subject: [PATCH 48/68] wip --- .../CloudBackupClient+Live.swift | 10 ++++++--- .../DeviceFactorSourceClient+Live.swift | 16 +++++++++----- .../SecurityCenterClient+Live.swift | 20 +++++++++++++++-- .../ConfigurationBackup+Reducer.swift | 22 +++++++++---------- .../ConfigurationBackup+View.swift | 9 ++++---- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index d1f5c47c38..1e59de142d 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -121,6 +121,10 @@ extension CloudBackupClient { @Sendable func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async throws { + try? userDefaults.setLastCloudBackup(.started(.now), of: profile) + print("•• start backup \(Date.now.formatted(date: .omitted, time: .standard))]") + try await Task.sleep(nanoseconds: 5_000_000_000) + do { let json = profile.toJSONString() try await backupProfile(.right(json), header: profile.header, existingRecord: existingRecord) @@ -141,6 +145,7 @@ extension CloudBackupClient { } try? userDefaults.setLastCloudBackup(.success, of: profile) + print("•• saved backup \(Date.now.formatted(date: .omitted, time: .standard))]") } @Sendable @@ -169,6 +174,8 @@ extension CloudBackupClient { guard shouldBackUp || shouldReclaim else { return } try? await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + + print("•• backed up") } let retryBackupInterval: DispatchTimeInterval = .seconds(60) @@ -186,9 +193,6 @@ extension CloudBackupClient { for try await (profile, tick) in combineLatest(profiles, ticks) { guard !Task.isCancelled else { return } - - // This will skip the ticks that get backed up while we are awaiting performAutomaticBackup - guard tick > lastClaimCheck else { continue } if tick.timeIntervalSince(lastClaimCheck) > checkClaimedProfileInterval { await performAutomaticBackup(profile, timeToCheckIfClaimed: true) lastClaimCheck = .now diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift index 8cfb8dc501..8df6bcd8db 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift @@ -80,13 +80,14 @@ extension DeviceFactorSourceClient: DependencyKey { @Sendable func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[FactorSourceHasMnemonic]> { - await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged()) + await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged().prepend(())) .map { factorSources, _ in - factorSources - .compactMap { $0.extract(DeviceFactorSource.self)?.id } - .map { id in - FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) - } + print("•• factorSourcesMnemonicPresence"); return + factorSources + .compactMap { $0.extract(DeviceFactorSource.self)?.id } + .map { id in + FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) + } } .removeDuplicates() .eraseToAnyAsyncSequence() @@ -94,6 +95,9 @@ extension DeviceFactorSourceClient: DependencyKey { let problematicEntities: @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> = { await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { factorSources, backedUpFactorSources, profile in + + print("•• problematicEntities") + let mnemonicMissingFactorSources = factorSources .filter(not(\.present)) .map(\.id) diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 8b12dc5ff0..b911e75976 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -65,6 +65,21 @@ extension SecurityCenterClient { problematic.unrecoverable.isEmpty ? nil : problematic.unrecoverable } + switch backups.cloud?.result.result { + case .success: + if backups.cloud?.upToDate == true { + print("•• CLOUD SUCCESS") + } else { + print("•• CLOUD old but success") + } + case let .started(date): + print("•• CLOUD STARTED \(Date.now.timeIntervalSince(date))") + case .failure: + print("•• CLOUD FAILURE") + case .none: + print("•• CLOUD nil") + } + func hasProblem5() -> Bool { if isCloudProfileSyncEnabled, let cloudBackup = backups.cloud { cloudBackup.result.failed @@ -98,9 +113,9 @@ extension SecurityCenterClient { if hasProblem6() { result.append(.problem6) } if hasProblem7() { result.append(.problem7) } - print("M- Sending result: \(result.map(\.number))") - problemsSubject.send(result) + + print("•• -> \(result.map(\.number))") } } @@ -110,6 +125,7 @@ extension SecurityCenterClient { problemsSubject .share() .map { $0.filter { type == nil || $0.type == type } } + .removeDuplicates() .eraseToAnyAsyncSequence() }, lastManualBackup: manualBackups, diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index 7dcf2b9a6e..768a3a3295 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -27,16 +27,21 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { return !cloudBackupsEnabled && !lastCloudBackup.upToDate } - public var showLastBackupLabel: Bool { - if let lastCloudBackup, lastCloudBackup.result.succeeded, !lastCloudBackup.upToDate { - true + public var showActionsRequiredAndLastBackup: Bool { + guard let lastCloudBackup else { return false } + if lastCloudBackup.upToDate, !lastCloudBackup.result.failed { + return false } else { - false + return true } } public var actionsRequired: [Item] { - showLastBackupLabel ? Item.allCases : [] + showActionsRequiredAndLastBackup ? Item.allCases : [] + } + + public var displayedLastBackup: Date? { + showActionsRequiredAndLastBackup ? lastCloudBackup?.result.backupDate : nil } public init() {} @@ -64,7 +69,6 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { case setProblems([SecurityProblem]) case setLastManualBackup(Date?) case setLastCloudBackup(BackupStatus?) - case didDeleteOutdatedBackup(ProfileID) case exportProfile(Profile) } @@ -133,11 +137,10 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { return .none case .deleteOutdatedTapped: - return .run { send in + return .run { _ in let profile = await ProfileStore.shared.profile do { try await cloudBackupClient.deleteProfileBackup(profile.id) - await send(.internal(.didDeleteOutdatedBackup(profile.id))) } catch { loggerGlobal.error("Failed to delete outdate backup \(profile.id.uuidString): \(error)") } @@ -184,9 +187,6 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { switch internalAction { - case let .didDeleteOutdatedBackup(id): - return .none - case let .setCloudBackupEnabled(isEnabled): state.cloudBackupsEnabled = isEnabled return .none diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift index 816ac2e56f..88bbe01440 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift @@ -30,7 +30,7 @@ extension ConfigurationBackup { let backupsEnabled = viewStore.binding(get: \.cloudBackupsEnabled) { .view(.cloudBackupsToggled($0)) } AutomatedBackupView( backupsEnabled: backupsEnabled, - lastBackedUp: viewStore.lastCloudBackup, + displayedLastBackup: viewStore.displayedLastBackup, actionsRequired: viewStore.actionsRequired, outdatedBackupPresent: viewStore.outdatedBackupPresent, deleteOutdatedAction: { store.send(.view(.deleteOutdatedTapped)) } @@ -138,7 +138,7 @@ extension ConfigurationBackup { struct AutomatedBackupView: SwiftUI.View { @Binding var backupsEnabled: Bool - let lastBackedUp: BackupStatus? + let displayedLastBackup: Date? let actionsRequired: [Item] let outdatedBackupPresent: Bool let deleteOutdatedAction: () -> Void @@ -210,8 +210,9 @@ extension ConfigurationBackup { } private var lastBackedUpString: String? { - guard let lastBackedUp, lastBackedUp.result.succeeded, !lastBackedUp.upToDate else { return nil } - return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastBackedUp.result.backupDate)) + displayedLastBackup.map { date in + L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: date)) + } } struct ItemView: SwiftUI.View { From f470f445d692b1c2faa38b06c0562d82b9253c13 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 20:40:23 +0200 Subject: [PATCH 49/68] fixed --- .../CloudBackupClient+Live.swift | 5 ----- .../DeviceFactorSourceClient+Live.swift | 13 +++++-------- .../UserDefaults+Dependency+Extension.swift | 14 ++++++++++---- .../ConfigurationBackup+Reducer.swift | 19 ++++++++++--------- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 1e59de142d..7b2ba1aa47 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -122,8 +122,6 @@ extension CloudBackupClient { @Sendable func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async throws { try? userDefaults.setLastCloudBackup(.started(.now), of: profile) - print("•• start backup \(Date.now.formatted(date: .omitted, time: .standard))]") - try await Task.sleep(nanoseconds: 5_000_000_000) do { let json = profile.toJSONString() @@ -145,7 +143,6 @@ extension CloudBackupClient { } try? userDefaults.setLastCloudBackup(.success, of: profile) - print("•• saved backup \(Date.now.formatted(date: .omitted, time: .standard))]") } @Sendable @@ -174,8 +171,6 @@ extension CloudBackupClient { guard shouldBackUp || shouldReclaim else { return } try? await backupProfileAndSaveResult(profile, existingRecord: existingRecord) - - print("•• backed up") } let retryBackupInterval: DispatchTimeInterval = .seconds(60) diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift index 8df6bcd8db..e5feeaceb3 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift @@ -82,12 +82,11 @@ extension DeviceFactorSourceClient: DependencyKey { func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[FactorSourceHasMnemonic]> { await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged().prepend(())) .map { factorSources, _ in - print("•• factorSourcesMnemonicPresence"); return - factorSources - .compactMap { $0.extract(DeviceFactorSource.self)?.id } - .map { id in - FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) - } + factorSources + .compactMap { $0.extract(DeviceFactorSource.self)?.id } + .map { id in + FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) + } } .removeDuplicates() .eraseToAnyAsyncSequence() @@ -96,8 +95,6 @@ extension DeviceFactorSourceClient: DependencyKey { let problematicEntities: @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> = { await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { factorSources, backedUpFactorSources, profile in - print("•• problematicEntities") - let mnemonicMissingFactorSources = factorSources .filter(not(\.present)) .map(\.id) diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index f8b6ebda51..08585d3f9c 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -167,10 +167,13 @@ extension UserDefaults.Dependency { public func setLastCloudBackup(_ result: BackupResult.Result, of profile: Profile) throws { var backups: [UUID: BackupResult] = getLastCloudBackups + let now = Date.now + let lastSuccess = result == .success ? now : backups[profile.id]?.lastSuccess backups[profile.id] = .init( - backupDate: .now, + backupDate: now, saveIdentifier: profile.saveIdentifier, - result: result + result: result, + lastSuccess: lastSuccess ) try save(codable: backups, forKey: .lastCloudBackups) @@ -187,10 +190,12 @@ extension UserDefaults.Dependency { /// Only call this on successful manual backups public func setLastManualBackup(of profile: Profile) throws { var backups: [ProfileID: BackupResult] = getLastManualBackups + let now = Date.now backups[profile.id] = .init( - backupDate: .now, + backupDate: now, saveIdentifier: profile.saveIdentifier, - result: .success + result: .success, + lastSuccess: now ) try save(codable: backups, forKey: .lastManualBackups) @@ -238,6 +243,7 @@ public struct BackupResult: Hashable, Codable, Sendable { public let backupDate: Date public let saveIdentifier: String public let result: Result + public let lastSuccess: Date? public var succeeded: Bool { result == .success diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index 768a3a3295..3110f1de7c 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -27,21 +27,22 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { return !cloudBackupsEnabled && !lastCloudBackup.upToDate } - public var showActionsRequiredAndLastBackup: Bool { - guard let lastCloudBackup else { return false } + public var actionsRequired: [Item] { + guard let lastCloudBackup else { return [] } if lastCloudBackup.upToDate, !lastCloudBackup.result.failed { - return false + return [] } else { - return true + return Item.allCases } } - public var actionsRequired: [Item] { - showActionsRequiredAndLastBackup ? Item.allCases : [] - } - public var displayedLastBackup: Date? { - showActionsRequiredAndLastBackup ? lastCloudBackup?.result.backupDate : nil + guard let lastCloudBackup else { return nil } + if lastCloudBackup.result.succeeded { + return lastCloudBackup.upToDate ? nil : lastCloudBackup.result.backupDate + } else { + return lastCloudBackup.result.lastSuccess + } } public init() {} From fe183f962255a80cde072565661c5eae897d1749 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Fri, 7 Jun 2024 20:52:54 +0200 Subject: [PATCH 50/68] matias fix --- .../Personas/Child/List/PersonaList+Reducer.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift b/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift index 36b73f11be..30bad61a3c 100644 --- a/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift +++ b/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift @@ -95,7 +95,11 @@ public struct PersonaList: Sendable, FeatureReducer { public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { switch internalAction { - case let .personasLoaded(personas): + case var .personasLoaded(personas): + personas.mutateAll { persona in + persona.securityProblemsConfig.update(problems: state.problems) + } + state.personas = personas return .none From d9a2d7a8503d080f1a3217abe9362034906c2441 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Sun, 9 Jun 2024 21:10:27 +0200 Subject: [PATCH 51/68] migration speedup --- .../CloudBackupClient/CloudBackupClient+Live.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 7b2ba1aa47..3c1654e6d0 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -71,10 +71,10 @@ extension CloudBackupClient { } @Sendable - func fetchAllProfileRecords(headerOnly: Bool = false) async throws -> [CKRecord] { + func fetchAllProfileHeaders() async throws -> [CKRecord] { let records = try await container.privateCloudDatabase.records( matching: .init(recordType: .profile, predicate: .init(value: true)), - desiredKeys: headerOnly ? .header : nil + desiredKeys: .header ) return try records.matchResults.map { try $0.1.get() } } @@ -198,7 +198,6 @@ extension CloudBackupClient { }, migrateProfilesFromKeychain: { let activeProfile = await profileStore.profile.id - let backedUpRecords = try await fetchAllProfileRecords() guard let headerList = try secureStorageClient.loadProfileHeaderList() else { return [] } let previouslyMigrated = userDefaults.getMigratedKeychainProfiles @@ -216,6 +215,8 @@ extension CloudBackupClient { return (profileData, header) } + let backedUpRecords = try await fetchAllProfileHeaders() + let migrated: [CKRecord] = try await migratable.asyncCompactMap { profileData, header in let backedUpRecord = backedUpRecords.first { $0.recordID.recordName == header.id.uuidString } if let backedUpRecord, try getProfileHeader(backedUpRecord).lastModified >= header.lastModified { @@ -245,7 +246,7 @@ extension CloudBackupClient { try await getProfile(fetchProfileRecord(id)) }, loadProfileHeaders: { - try await fetchAllProfileRecords(headerOnly: true) + try await fetchAllProfileHeaders() .map(getProfileHeader) .filter(\.isNonEmpty) }, From ef04213e0a97c520a3a69bedb2b77f8327d5c748 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Sun, 9 Jun 2024 22:24:08 +0200 Subject: [PATCH 52/68] PROFILESTORE remove ConflictingOwners --- .../Clients/ProfileStore/ProfileStore.swift | 48 ++++--------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 7d66375747..2e465a8406 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -22,9 +22,10 @@ import Sargon /// func unlockedApp() async -> Profile /// func finishedOnboarding() async /// func finishOnboarding(with _: AccountsRecoveredFromScanningUsingMnemonic) async throws -/// func importProfileSnapshot(_ s: Profile) throws +/// func importProfile(_ s: Profile) throws /// func deleteProfile(keepInICloudIfPresent: Bool) throws /// func updating(_ t: (inout Profile) async throws -> T) async throws -> T +/// func claimOwnership(of profile: inout Profile) /// /// The app is suppose to call `unlockedApp` after user has authenticated from `Splash`, which /// will emit any Profile ownership conflict if needed, and returns the newly claimed Profile that had @@ -68,26 +69,20 @@ public final actor ProfileStore { private enum Mode { case appIsUnlocked - case appIsLocked(bufferedProfileOwnershipConflict: ConflictingOwners?) + case appIsLocked } init() { let metaDeviceInfo = Self._deviceInfo() - let (deviceInfo, profile, conflictingOwners) = Self._loadSavedElseNewProfile(metaDeviceInfo: metaDeviceInfo) + let (deviceInfo, profile) = Self._loadSavedElseNewProfile(metaDeviceInfo: metaDeviceInfo) loggerGlobal.info("profile.id: \(profile.id)") loggerGlobal.info("device.id: \(deviceInfo.id)") self.deviceInfo = deviceInfo self.profileSubject = AsyncCurrentValueSubject(profile) - self.mode = .appIsLocked(bufferedProfileOwnershipConflict: conflictingOwners) + self.mode = .appIsLocked } } -// MARK: - ConflictingOwners -public struct ConflictingOwners: Sendable, Hashable { - public let ownerOfCurrentProfile: DeviceInfo - public let thisDevice: DeviceInfo -} - // MARK: Public extension ProfileStore { /// The current value of Profile. Use `updating` method to update it. Also see `values` for an AsyncSequence of Profile. @@ -331,18 +326,6 @@ extension ProfileStore { case .appIsLocked: false } } - - private var bufferedOwnershipConflictWhileAppLocked: ConflictingOwners? { - switch mode { - case .appIsUnlocked: nil - case let .appIsLocked(buffered): buffered - } - } - - private func buffer(conflictingOwners: ConflictingOwners?) { - loggerGlobal.info("App is locked, buffering conflicting profle owner") - self.mode = .appIsLocked(bufferedProfileOwnershipConflict: conflictingOwners) - } } // MARK: Helpers @@ -403,16 +386,6 @@ extension ProfileStore { guard deviceInfo.id == header.lastUsedOnDevice.id else { loggerGlobal.error("Device ID mismatch, profile might have been used on another device. Last used in header was: \(String(describing: header.lastUsedOnDevice)) and info of this device: \(String(describing: deviceInfo))") - Task { - let conflictingOwners = ConflictingOwners( - ownerOfCurrentProfile: header.lastUsedOnDevice, - thisDevice: deviceInfo - ) - - guard appIsUnlocked else { - return buffer(conflictingOwners: conflictingOwners) - } - } throw Error.profileUsedOnAnotherDevice } // All good @@ -421,7 +394,7 @@ extension ProfileStore { // MARK: Private Static extension ProfileStore { - typealias NewProfileTuple = (deviceInfo: DeviceInfo, profile: Profile, conflictingOwners: ConflictingOwners?) + typealias NewProfileTuple = (deviceInfo: DeviceInfo, profile: Profile) private static func _loadSavedElseNewProfile( metaDeviceInfo: MetaDeviceInfo @@ -432,8 +405,7 @@ extension ProfileStore { func newProfile() throws -> NewProfileTuple { try ( deviceInfo: metaDeviceInfo.deviceInfo, - profile: _tryGenerateAndSaveNewProfile(deviceInfo: deviceInfo), - conflictingOwners: nil + profile: _tryGenerateAndSaveNewProfile(deviceInfo: deviceInfo) ) } @@ -470,11 +442,7 @@ extension ProfileStore { } return ( deviceInfo: deviceInfo, - profile: existing, - conflictingOwners: matchingIDs ? nil : .init( - ownerOfCurrentProfile: existing.header.lastUsedOnDevice, - thisDevice: deviceInfo - ) + profile: existing ) } else { return try newProfile() From 1006b549221d4cbb7bf91cf082afea622d75acb4 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Sun, 9 Jun 2024 22:57:42 +0200 Subject: [PATCH 53/68] LastUSedDevice on backup cards --- .../Children/SelectBackup/SelectBackup+View.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+View.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+View.swift index 824d9f0f61..ca133c25d5 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+View.swift @@ -117,13 +117,13 @@ extension SelectBackup.View { ) -> some View { let header = item.value let isVersionCompatible = header.isVersionCompatible() - let creatingDevice = header.creatingDevice.id == viewStore.thisDeviceID ? L10n.IOSProfileBackup.thisDevice : header.creatingDevice.description + let lastDevice = header.lastUsedOnDevice.id == viewStore.thisDeviceID ? L10n.IOSProfileBackup.thisDevice : header.lastUsedOnDevice.description return Card(action: item.action) { HStack { VStack(alignment: .leading, spacing: 0) { Group { // Contains bold text segments. - Text(LocalizedStringKey(L10n.RecoverProfileBackup.backupFrom(creatingDevice))) + Text(LocalizedStringKey(L10n.RecoverProfileBackup.backupFrom(lastDevice))) Text(L10n.IOSProfileBackup.lastModifedDateLabel(formatDate(header.lastModified))) Text(L10n.IOSProfileBackup.totalAccountsNumberLabel(Int(header.contentHint.numberOfAccountsOnAllNetworksInTotal))) From a868f90bfe366067fe2d362de5d30e54cca1b3ad Mon Sep 17 00:00:00 2001 From: kugel3 Date: Sun, 9 Jun 2024 23:03:11 +0200 Subject: [PATCH 54/68] Fix tests --- .../ProfileStoreTests/ProfileStoreTests.swift | 100 ++---------------- .../SecureStorageClientTests.swift | 4 +- .../MainFeatureTests/MainFeatureTests.swift | 1 + 3 files changed, 9 insertions(+), 96 deletions(-) diff --git a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift index 679f80684b..87472ca8e6 100644 --- a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift +++ b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift @@ -341,7 +341,9 @@ final class ProfileStoreNewProfileTests: TestCase { } operation: { let sut = ProfileStore() // WHEN import profile - try await sut.importProfile(Profile.withOneAccountsDeviceInfo_BEEF_mnemonic_ABANDON_ART) + var profile = Profile.withOneAccountsDeviceInfo_BEEF_mnemonic_ABANDON_ART + await sut.claimOwnership(of: &profile) + try await sut.importProfile(profile) return await sut.profile } @@ -746,40 +748,6 @@ final class ProfileStoreExistingProfileTests: TestCase { } } -// func test__GIVEN__saved_profile_mismatch_deviceID__WHEN__claimAndContinueUseOnThisPhone__THEN__profile_uses_claimed_device() async throws { -// try await doTestMismatch( -// savedProfile: Profile.withOneAccount, -// action: .claimAndContinueUseOnThisPhone -// ) { claimed in -// // THEN profile uses claimed device -// XCTAssertNoDifference( -// claimed.header.lastUsedOnDevice.id, -// DeviceInfo.testValueBEEF.id -// ) -// } -// } - -// func test__GIVEN__saved_profile_mismatch_deviceID__WHEN__deleteProfile__THEN__profile_got_deleted() async throws { -// let uuidOfNewProfile = UUID() -// let savedProfile = Profile.withOneAccount -// let userDefaults = UserDefaults.Dependency.ephemeral() -// try await doTestMismatch( -// savedProfile: savedProfile, -// userDefaults: userDefaults, -// action: .deleteProfileFromThisPhone, -// then: { -// $0.uuid = .constant(uuidOfNewProfile) -// XCTAssertNoDifference(userDefaults.string(key: .activeProfileID), savedProfile.header.id.uuidString) -// $0.secureStorageClient.deleteProfileAndMnemonicsByFactorSourceIDs = { idToDelete, _ in -// XCTAssertNoDifference(idToDelete, savedProfile.header.id) -// } -// } -// ) -// -// // New active profile -// XCTAssertNoDifference(userDefaults.string(key: .activeProfileID), uuidOfNewProfile.uuidString) -// } - func test__GIVEN__mismatch__WHEN__app_is_not_yet_unlocked__THEN__no_alert_is_displayed() async throws { let alertNotScheduled = expectation( description: "overlayWindowClient did NOT scheduled alert" @@ -955,62 +923,6 @@ final class ProfileStoreExistingProfileTests: TestCase { } } } - - func test__GIVEN__saved_profile__WHEN__we_update_profile_without_ownership__THEN__ownership_conflict_alert_is_shown() async throws { - try await withTimeLimit(.normal) { - // GIVEN saved profile - let saved = Profile.withOneAccountsDeviceInfo_ABBA_mnemonic_ABANDON_ART - let profileHasBeenUpdated = LockIsolated(false) - let ownership_conflict_alert_is_shown = self.expectation(description: "ownership conflict alert is shown") - - try await withTestClients { - $0.savedProfile(saved) - when(&$0) - then(&$0) - } operation: { - let sut = ProfileStore() - await sut.unlockedApp() - // WHEN we update profile... - do { - try await sut.updating { - $0.header.lastModified = Date() - profileHasBeenUpdated.setValue(true) - } - return XCTFail("Expected to throw") - } catch { - // expected to throw - } - } - - func when(_ d: inout DependencyValues) { - d.secureStorageClient.loadProfileSnapshot = { _ in - profileHasBeenUpdated.withValue { hasBeenUpdated in - if hasBeenUpdated { - var modified = saved - modified.header.lastUsedOnDevice = .testValueBEEF // 0xBEEF != 0xABBA - // WHEN ... without ownership - return modified - } else { - return saved - } - } - } - } - - func then(_ d: inout DependencyValues) { - d.overlayWindowClient.scheduleAlert = { alert in - XCTAssertNoDifference( - alert.message, TextState(overlayClientProfileStoreOwnershipConflictTextState) - ) - // THEN ownership conflict alert is shown - ownership_conflict_alert_is_shown.fulfill() - return .dismissed - } - } - - await self.nearFutureFulfillment(of: ownership_conflict_alert_is_shown) - } - } } // MARK: - ProfileStoreAsyncSequenceTests @@ -1103,7 +1015,7 @@ extension ProfileStoreExistingProfileTests { private func doTestMismatch( savedProfile: Profile, userDefaults: UserDefaults.Dependency = .ephemeral(), - action: OverlayWindowClient.Item.AlertAction, + action: OverlayWindowClient.FullScreenAction, then: @escaping @Sendable (inout DependencyValues) -> Void = { _ in }, result assertResult: @escaping @Sendable (Profile) -> Void = { _ in } ) async throws { @@ -1127,9 +1039,9 @@ extension ProfileStoreExistingProfileTests { } func when(_ d: inout DependencyValues) { - d.overlayWindowClient.scheduleAlert = { alert in + d.overlayWindowClient.scheduleFullScreen = { screen in XCTAssertNoDifference( - alert.message, TextState(overlayClientProfileStoreOwnershipConflictTextState) + screen, .init(root: .claimWallet(.init())) ) alertScheduled.fulfill() return action diff --git a/RadixWalletTests/Clients/SecureStorageClientTests/SecureStorageClientTests.swift b/RadixWalletTests/Clients/SecureStorageClientTests/SecureStorageClientTests.swift index e77ac4ad70..b689a5e275 100644 --- a/RadixWalletTests/Clients/SecureStorageClientTests/SecureStorageClientTests.swift +++ b/RadixWalletTests/Clients/SecureStorageClientTests/SecureStorageClientTests.swift @@ -24,11 +24,11 @@ final class SecureStorageClientTests: TestCase { } } - func test__WHEN__profile_is_saved__THEN__setDataWithoutAuth_called_with_icloud_sync_is_enabled() async throws { + func test__WHEN__profile_is_saved__THEN__setDataWithoutAuth_called_with_icloud_sync_is_disabled() async throws { try await doTest(authConfig: .biometricsAndPasscodeSetUp) { sut, _, profile in try await sut.saveProfileSnapshot(profile) } assertKeychainSetItemWithoutAuthRequest: { _, _, attributes in - XCTAssertTrue(attributes.iCloudSyncEnabled) + XCTAssertFalse(attributes.iCloudSyncEnabled) } } diff --git a/RadixWalletTests/Features/MainFeatureTests/MainFeatureTests.swift b/RadixWalletTests/Features/MainFeatureTests/MainFeatureTests.swift index 9459d7f309..b7b1115b59 100644 --- a/RadixWalletTests/Features/MainFeatureTests/MainFeatureTests.swift +++ b/RadixWalletTests/Features/MainFeatureTests/MainFeatureTests.swift @@ -32,6 +32,7 @@ final class MainFeatureTests: TestCase { .dependency(\.cloudBackupClient, .noop) .dependency(\.gatewaysClient.currentGatewayValues) { AsyncLazySequence([.stokenet]).eraseToAnyAsyncSequence() } .dependency(\.resetWalletClient, .noop) + .dependency(\.securityCenterClient, .noop) } XCTAssertFalse(store.state.showIsUsingTestnetBanner) From a5a2e33124ddfa5adb559527a034e002a2ac6016 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Sun, 9 Jun 2024 23:09:41 +0200 Subject: [PATCH 55/68] PROFILESTORE remove mode --- .../OnboardingClient+Interface.swift | 9 ------- .../OnboardingClient+Live.swift | 3 --- .../OnboardingClient+Test.swift | 2 -- .../Clients/ProfileStore/ProfileStore.swift | 27 ------------------- .../Features/SplashFeature/Splash.swift | 7 ++--- 5 files changed, 2 insertions(+), 46 deletions(-) diff --git a/RadixWallet/Clients/OnboardingClient/OnboardingClient+Interface.swift b/RadixWallet/Clients/OnboardingClient/OnboardingClient+Interface.swift index d18db9a864..01187ea887 100644 --- a/RadixWallet/Clients/OnboardingClient/OnboardingClient+Interface.swift +++ b/RadixWallet/Clients/OnboardingClient/OnboardingClient+Interface.swift @@ -2,20 +2,15 @@ import Sargon // MARK: - OnboardingClient public struct OnboardingClient: Sendable { - /// Call this when user has finished authentication from lock screen (e.g. Splash) - public var unlockApp: UnlockApp // FIXME: Move to a new Lock/Unlock client? - public var loadProfile: LoadProfile public var finishOnboarding: FinishOnboarding public var finishOnboardingWithRecoveredAccountAndBDFS: FinishOnboardingWithRecoveredAccountsAndBDFS public init( - unlockApp: @escaping UnlockApp, loadProfile: @escaping LoadProfile, finishOnboarding: @escaping FinishOnboarding, finishOnboardingWithRecoveredAccountAndBDFS: @escaping FinishOnboardingWithRecoveredAccountsAndBDFS ) { - self.unlockApp = unlockApp self.loadProfile = loadProfile self.finishOnboarding = finishOnboarding self.finishOnboardingWithRecoveredAccountAndBDFS = finishOnboardingWithRecoveredAccountAndBDFS @@ -27,8 +22,4 @@ extension OnboardingClient { public typealias FinishOnboarding = @Sendable () async -> EqVoid public typealias FinishOnboardingWithRecoveredAccountsAndBDFS = @Sendable (AccountsRecoveredFromScanningUsingMnemonic) async throws -> EqVoid - - /// This might return a NEW profile if user did press DELETE conflicting - /// profile during Ownership conflict resultion alert... - public typealias UnlockApp = @Sendable () async -> Profile } diff --git a/RadixWallet/Clients/OnboardingClient/OnboardingClient+Live.swift b/RadixWallet/Clients/OnboardingClient/OnboardingClient+Live.swift index 089f37367a..986a82c571 100644 --- a/RadixWallet/Clients/OnboardingClient/OnboardingClient+Live.swift +++ b/RadixWallet/Clients/OnboardingClient/OnboardingClient+Live.swift @@ -8,9 +8,6 @@ extension OnboardingClient: DependencyKey { profileStore: ProfileStore = .shared ) -> Self { Self( - unlockApp: { - await profileStore.unlockedApp() - }, loadProfile: { await profileStore.profile }, diff --git a/RadixWallet/Clients/OnboardingClient/OnboardingClient+Test.swift b/RadixWallet/Clients/OnboardingClient/OnboardingClient+Test.swift index 2bf3f42f61..ddd7141aa8 100644 --- a/RadixWallet/Clients/OnboardingClient/OnboardingClient+Test.swift +++ b/RadixWallet/Clients/OnboardingClient/OnboardingClient+Test.swift @@ -11,14 +11,12 @@ extension OnboardingClient: TestDependencyKey { public static let previewValue = Self.noop public static let testValue = Self( - unlockApp: unimplemented("\(Self.self).unlockApp"), loadProfile: unimplemented("\(Self.self).loadProfile"), finishOnboarding: unimplemented("\(Self.self).finishOnboarding"), finishOnboardingWithRecoveredAccountAndBDFS: unimplemented("\(Self.self).finishOnboardingWithRecoveredAccountAndBDFS") ) public static let noop = Self( - unlockApp: { fatalError("noop") }, loadProfile: { fatalError("noop") }, finishOnboarding: { EqVoid.instance }, finishOnboardingWithRecoveredAccountAndBDFS: { _ in EqVoid.instance } diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 2e465a8406..61c99e3f77 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -19,7 +19,6 @@ import Sargon /// /// var profile: Profile { get } /// func values() -> AnyAsyncSequence -/// func unlockedApp() async -> Profile /// func finishedOnboarding() async /// func finishOnboarding(with _: AccountsRecoveredFromScanningUsingMnemonic) async throws /// func importProfile(_ s: Profile) throws @@ -60,18 +59,6 @@ public final actor ProfileStore { /// device model and name is async. private var deviceInfo: DeviceInfo - /// After user has pass keychain auth prompt in Splash this becomes - /// `appIsUnlocked`. The idea is that we buffer ownership conflicts until UI - /// is ready to display it, reason being we dont wanna display the - /// OverlayClient UI for ownership conflict simultaneously as - /// unlock app keychain auth prompt. - private var mode: Mode - - private enum Mode { - case appIsUnlocked - case appIsLocked - } - init() { let metaDeviceInfo = Self._deviceInfo() let (deviceInfo, profile) = Self._loadSavedElseNewProfile(metaDeviceInfo: metaDeviceInfo) @@ -79,7 +66,6 @@ public final actor ProfileStore { loggerGlobal.info("device.id: \(deviceInfo.id)") self.deviceInfo = deviceInfo self.profileSubject = AsyncCurrentValueSubject(profile) - self.mode = .appIsLocked } } @@ -223,12 +209,6 @@ extension ProfileStore { public func isThisDevice(deviceID: DeviceID) -> Bool { deviceID == deviceInfo.id } - - public func unlockedApp() async -> Profile { - loggerGlobal.notice("Unlocking app") - self.mode = .appIsUnlocked - return profile - } } extension DeviceInfo { @@ -319,13 +299,6 @@ extension ProfileStore { try _updateHeader(of: &toSave) try _saveProfileAndEmitUpdate(toSave) } - - private var appIsUnlocked: Bool { - switch mode { - case .appIsUnlocked: true - case .appIsLocked: false - } - } } // MARK: Helpers diff --git a/RadixWallet/Features/SplashFeature/Splash.swift b/RadixWallet/Features/SplashFeature/Splash.swift index 757a3461f5..d215866129 100644 --- a/RadixWallet/Features/SplashFeature/Splash.swift +++ b/RadixWallet/Features/SplashFeature/Splash.swift @@ -145,12 +145,9 @@ public struct Splash: Sendable, FeatureReducer { func delegateCompleted() -> Effect { .run { send in - let profile = await onboardingClient.unlockApp() await send(.delegate( - .completed( - profile - )) - ) + .completed(ProfileStore.shared.profile) + )) } } From 4c062115d290809e39ab760354b68067319d6376 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Sun, 9 Jun 2024 23:09:50 +0200 Subject: [PATCH 56/68] fix tests --- .../Clients/ProfileStoreTests/ProfileStoreTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift index 87472ca8e6..b839fd3f6b 100644 --- a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift +++ b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift @@ -1032,7 +1032,6 @@ extension ProfileStoreExistingProfileTests { then(&$0) } operation: { [self] in let sut = ProfileStore.init() - await sut.unlockedApp() // must unlock to allow alert to be displayed // The scheduling of the alert needs some time... await nearFutureFulfillment(of: alertScheduled) return await sut.profile From b051682144bc5f3ebe2490ab9945fadaf906eed7 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Mon, 10 Jun 2024 13:03:15 +0200 Subject: [PATCH 57/68] Avoid a potential claim edge case issue --- .../CloudBackupClient+Live.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 3c1654e6d0..8a4b0cc1c0 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -149,21 +149,28 @@ extension CloudBackupClient { func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async { let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty let lastBackup = userDefaults.getLastCloudBackups[profile.id] - let lastBackupSucceeded = lastBackup?.result == .success && lastBackup?.saveIdentifier == profile.saveIdentifier + let isBackedUp = lastBackup?.result == .success && lastBackup?.saveIdentifier == profile.saveIdentifier - let shouldBackUp = needsBackUp && !lastBackupSucceeded + let shouldBackUp = needsBackUp && !isBackedUp let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed guard shouldBackUp || shouldCheckClaim else { return } let existingRecord = try? await fetchProfileRecord(profile.id) - let backedUpID = try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id let shouldReclaim: Bool if shouldCheckClaim, let backedUpID, await !profileStore.isThisDevice(deviceID: backedUpID) { let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) - shouldReclaim = action == .claimWallet(.transferBack) + switch action { + case .claimWallet(.transferBack): + shouldReclaim = true + case .claimWallet(.didClearWallet): + return + case .dismiss: + assertionFailure(".dismiss should never be sent from claimWallet") + return + } } else { shouldReclaim = false } From 97cbff1f57457b6dd87ac9cf9762e989333085aa Mon Sep 17 00:00:00 2001 From: kugel3 Date: Mon, 10 Jun 2024 16:25:45 +0200 Subject: [PATCH 58/68] Update "Last backup" logic --- .../SecurityCenterClient+Interface.swift | 4 +- .../SecurityCenterClient+Live.swift | 21 +-------- .../UserDefaults+Dependency+Extension.swift | 6 +-- .../ConfigurationBackup+Reducer.swift | 20 +++------ .../ConfigurationBackup+View.swift | 44 +++++++++++-------- 5 files changed, 37 insertions(+), 58 deletions(-) diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index c645287f1d..9e26ad4e8f 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -161,10 +161,10 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable { // MARK: - BackupStatus public struct BackupStatus: Hashable, Codable, Sendable { public let result: BackupResult - public let upToDate: Bool + public let isCurrent: Bool public init(result: BackupResult, profile: Profile) { self.result = result - self.upToDate = result.saveIdentifier == profile.saveIdentifier + self.isCurrent = result.saveIdentifier == profile.saveIdentifier } } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index b911e75976..d17ec5fc18 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -65,26 +65,11 @@ extension SecurityCenterClient { problematic.unrecoverable.isEmpty ? nil : problematic.unrecoverable } - switch backups.cloud?.result.result { - case .success: - if backups.cloud?.upToDate == true { - print("•• CLOUD SUCCESS") - } else { - print("•• CLOUD old but success") - } - case let .started(date): - print("•• CLOUD STARTED \(Date.now.timeIntervalSince(date))") - case .failure: - print("•• CLOUD FAILURE") - case .none: - print("•• CLOUD nil") - } - func hasProblem5() -> Bool { if isCloudProfileSyncEnabled, let cloudBackup = backups.cloud { cloudBackup.result.failed } else { - false // FIXME: GK - is this what we want? + false } } @@ -93,7 +78,7 @@ extension SecurityCenterClient { } func hasProblem7() -> Bool { - !isCloudProfileSyncEnabled && backups.manual?.upToDate == false + !isCloudProfileSyncEnabled && backups.manual?.isCurrent == false } func hasProblem9() async -> ProblematicAddresses? { @@ -114,8 +99,6 @@ extension SecurityCenterClient { if hasProblem7() { result.append(.problem7) } problemsSubject.send(result) - - print("•• -> \(result.map(\.number))") } } diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 08585d3f9c..8dc1aa069f 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -170,7 +170,7 @@ extension UserDefaults.Dependency { let now = Date.now let lastSuccess = result == .success ? now : backups[profile.id]?.lastSuccess backups[profile.id] = .init( - backupDate: now, + date: now, saveIdentifier: profile.saveIdentifier, result: result, lastSuccess: lastSuccess @@ -192,7 +192,7 @@ extension UserDefaults.Dependency { var backups: [ProfileID: BackupResult] = getLastManualBackups let now = Date.now backups[profile.id] = .init( - backupDate: now, + date: now, saveIdentifier: profile.saveIdentifier, result: .success, lastSuccess: now @@ -240,7 +240,7 @@ extension UserDefaults.Dependency { public struct BackupResult: Hashable, Codable, Sendable { private static let timeoutInterval: TimeInterval = 5 * 60 - public let backupDate: Date + public let date: Date public let saveIdentifier: String public let result: Result public let lastSuccess: Date? diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift index 3110f1de7c..a4aefc18f6 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+Reducer.swift @@ -24,24 +24,14 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { public var outdatedBackupPresent: Bool { guard let lastCloudBackup, lastCloudBackup.result.succeeded else { return false } - return !cloudBackupsEnabled && !lastCloudBackup.upToDate + return !cloudBackupsEnabled && !lastCloudBackup.isCurrent } public var actionsRequired: [Item] { - guard let lastCloudBackup else { return [] } - if lastCloudBackup.upToDate, !lastCloudBackup.result.failed { - return [] + if let lastCloudBackup, lastCloudBackup.isCurrent, !lastCloudBackup.result.failed { + [] } else { - return Item.allCases - } - } - - public var displayedLastBackup: Date? { - guard let lastCloudBackup else { return nil } - if lastCloudBackup.result.succeeded { - return lastCloudBackup.upToDate ? nil : lastCloudBackup.result.backupDate - } else { - return lastCloudBackup.result.lastSuccess + Item.allCases } } @@ -218,7 +208,7 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { .run { send in for try await lastBackup in await securityCenterClient.lastManualBackup() { guard !Task.isCancelled else { return } - await send(.internal(.setLastManualBackup(lastBackup?.result.backupDate))) + await send(.internal(.setLastManualBackup(lastBackup?.result.date))) } } } diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift index 88bbe01440..37884dbf9c 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift @@ -1,6 +1,23 @@ import ComposableArchitecture import SwiftUI +extension ConfigurationBackup.State { + var lastCloudBackupString: String? { + if actionsRequired.isEmpty { + nil + } else { + L10n.ConfigurationBackup.Automated.lastBackup( + lastCloudBackup?.result.lastSuccess.map(RadixDateFormatter.string) ?? L10n.Common.none + ) + } + } + + var lastManualBackupString: String? { + guard let lastManualBackup else { return nil } + return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastManualBackup)) + } +} + // MARK: - ConfigurationBackup.View extension ConfigurationBackup { @MainActor @@ -30,7 +47,7 @@ extension ConfigurationBackup { let backupsEnabled = viewStore.binding(get: \.cloudBackupsEnabled) { .view(.cloudBackupsToggled($0)) } AutomatedBackupView( backupsEnabled: backupsEnabled, - displayedLastBackup: viewStore.displayedLastBackup, + lastBackupString: viewStore.lastCloudBackupString, actionsRequired: viewStore.actionsRequired, outdatedBackupPresent: viewStore.outdatedBackupPresent, deleteOutdatedAction: { store.send(.view(.deleteOutdatedTapped)) } @@ -42,7 +59,7 @@ extension ConfigurationBackup { .textStyle(.body1Header) .padding(.bottom, .medium2) - ManualBackupView(lastBackedUp: viewStore.lastManualBackup) { + ManualBackupView(lastBackupString: viewStore.lastManualBackupString) { store.send(.view(.exportTapped)) } } @@ -138,7 +155,7 @@ extension ConfigurationBackup { struct AutomatedBackupView: SwiftUI.View { @Binding var backupsEnabled: Bool - let displayedLastBackup: Date? + let lastBackupString: String? let actionsRequired: [Item] let outdatedBackupPresent: Bool let deleteOutdatedAction: () -> Void @@ -157,8 +174,8 @@ extension ConfigurationBackup { .textStyle(.body1Header) .foregroundStyle(.app.gray1) - if let lastBackedUpString { - Text(lastBackedUpString) + if let lastBackupString { + Text(lastBackupString) .textStyle(.body2Regular) .foregroundStyle(.app.gray2) } @@ -209,12 +226,6 @@ extension ConfigurationBackup { } } - private var lastBackedUpString: String? { - displayedLastBackup.map { date in - L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: date)) - } - } - struct ItemView: SwiftUI.View { @SwiftUI.State private var expanded: Bool = false let item: Item @@ -275,7 +286,7 @@ extension ConfigurationBackup { } struct ManualBackupView: SwiftUI.View { - let lastBackedUp: Date? + let lastBackupString: String? let exportAction: () -> Void var body: some SwiftUI.View { @@ -293,8 +304,8 @@ extension ConfigurationBackup { .buttonStyle(.primaryRectangular(shouldExpand: true)) .padding(.horizontal, .large2) - if let lastBackedUpString { - Text(lastBackedUpString) + if let lastBackupString { + Text(lastBackupString) .textStyle(.body2Regular) .foregroundStyle(.app.gray2) .padding(.horizontal, .medium2) @@ -304,11 +315,6 @@ extension ConfigurationBackup { } } } - - private var lastBackedUpString: String? { - guard let lastBackedUp else { return nil } - return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastBackedUp)) - } } struct WarningView: SwiftUI.View { From 7748ef89170b23ce5b8ae3df7981847ccc559ee5 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Mon, 10 Jun 2024 16:27:19 +0200 Subject: [PATCH 59/68] subtly better --- .../SecurityCenterFeature/ConfigurationBackup+View.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift index 37884dbf9c..1a66440d5c 100644 --- a/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift +++ b/RadixWallet/Features/SecurityCenterFeature/ConfigurationBackup+View.swift @@ -3,7 +3,7 @@ import SwiftUI extension ConfigurationBackup.State { var lastCloudBackupString: String? { - if actionsRequired.isEmpty { + if let lastCloudBackup, lastCloudBackup.isCurrent, lastCloudBackup.result.succeeded { nil } else { L10n.ConfigurationBackup.Automated.lastBackup( From c1d06fdc9209fc971f7ad799b1892b82438b1ac8 Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 11 Jun 2024 04:05:46 +0200 Subject: [PATCH 60/68] Fix elusive problem --- .../CloudBackupClient/CloudBackupClient+Live.swift | 13 ++++++++----- RadixWallet/Clients/ProfileStore/ProfileStore.swift | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 8a4b0cc1c0..87208b99b6 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -165,10 +165,7 @@ extension CloudBackupClient { switch action { case .claimWallet(.transferBack): shouldReclaim = true - case .claimWallet(.didClearWallet): - return - case .dismiss: - assertionFailure(".dismiss should never be sent from claimWallet") + case .claimWallet(.didClearWallet), .dismiss: return } } else { @@ -177,7 +174,13 @@ extension CloudBackupClient { guard shouldBackUp || shouldReclaim else { return } - try? await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + var profileToUpload = profile + if shouldReclaim { + // The profile will already be locally claimed, but we want to update the lastUsedOnDevice date + await profileStore.claimOwnership(of: &profileToUpload) + } + + try? await backupProfileAndSaveResult(profileToUpload, existingRecord: existingRecord) } let retryBackupInterval: DispatchTimeInterval = .seconds(60) diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 61c99e3f77..9dfe9f73bd 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -309,6 +309,7 @@ extension ProfileStore { @Dependency(\.date) var date profile.header.lastUsedOnDevice = deviceInfo profile.header.lastUsedOnDevice.date = date() + profile.header.lastModified = date() } /// Updates the header of a Profile, lastModified date, contentHint etc. From 6d06317b65f62746e9bafd927b60ad0fe56e6ceb Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 11 Jun 2024 05:23:30 +0200 Subject: [PATCH 61/68] check if backup file still exists --- .../CloudBackupClient/CloudBackupClient+Live.swift | 14 +++++--------- .../SecurityCenterClient+Interface.swift | 2 +- .../UserDefaults+Dependency+Extension.swift | 8 ++++---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 87208b99b6..21a794ed1a 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -148,19 +148,15 @@ extension CloudBackupClient { @Sendable func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async { let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty - let lastBackup = userDefaults.getLastCloudBackups[profile.id] - let isBackedUp = lastBackup?.result == .success && lastBackup?.saveIdentifier == profile.saveIdentifier - + let existingRecord = try? await fetchProfileRecord(profile.id) + let backedUpHeader = try? existingRecord.map(getProfileHeader) + let isBackedUp = backedUpHeader?.saveIdentifier == profile.header.saveIdentifier let shouldBackUp = needsBackUp && !isBackedUp - let shouldCheckClaim = shouldBackUp || timeToCheckIfClaimed - guard shouldBackUp || shouldCheckClaim else { return } - - let existingRecord = try? await fetchProfileRecord(profile.id) - let backedUpID = try? existingRecord.map(getProfileHeader)?.lastUsedOnDevice.id + guard shouldBackUp || timeToCheckIfClaimed else { return } let shouldReclaim: Bool - if shouldCheckClaim, let backedUpID, await !profileStore.isThisDevice(deviceID: backedUpID) { + if let backedUpID = backedUpHeader?.lastUsedOnDevice.id, await !profileStore.isThisDevice(deviceID: backedUpID) { let action = await overlayWindowClient.scheduleFullScreen(.init(root: .claimWallet(.init()))) switch action { case .claimWallet(.transferBack): diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index 9e26ad4e8f..3a5b719fd6 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -165,6 +165,6 @@ public struct BackupStatus: Hashable, Codable, Sendable { public init(result: BackupResult, profile: Profile) { self.result = result - self.isCurrent = result.saveIdentifier == profile.saveIdentifier + self.isCurrent = result.saveIdentifier == profile.header.saveIdentifier } } diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 8dc1aa069f..dc20e16f37 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -171,7 +171,7 @@ extension UserDefaults.Dependency { let lastSuccess = result == .success ? now : backups[profile.id]?.lastSuccess backups[profile.id] = .init( date: now, - saveIdentifier: profile.saveIdentifier, + saveIdentifier: profile.header.saveIdentifier, result: result, lastSuccess: lastSuccess ) @@ -193,7 +193,7 @@ extension UserDefaults.Dependency { let now = Date.now backups[profile.id] = .init( date: now, - saveIdentifier: profile.saveIdentifier, + saveIdentifier: profile.header.saveIdentifier, result: .success, lastSuccess: now ) @@ -273,8 +273,8 @@ public struct BackupResult: Hashable, Codable, Sendable { } } -extension Profile { +extension Profile.Header { public var saveIdentifier: String { - "\(header.lastModified.timeIntervalSince1970)-\(header.lastUsedOnDevice.id.uuidString)" + "\(lastModified.timeIntervalSince1970)-\(lastUsedOnDevice.id.uuidString)" } } From b2712029f26f1f3bdbf584779cc30a715151090e Mon Sep 17 00:00:00 2001 From: kugel3 Date: Tue, 11 Jun 2024 05:39:15 +0200 Subject: [PATCH 62/68] bug or on purpose? --- RadixWallet/Clients/ProfileStore/ProfileStore.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/RadixWallet/Clients/ProfileStore/ProfileStore.swift b/RadixWallet/Clients/ProfileStore/ProfileStore.swift index 9dfe9f73bd..1d8b0e018d 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -161,6 +161,7 @@ extension ProfileStore { with accountsRecoveredFromScanningUsingMnemonic: AccountsRecoveredFromScanningUsingMnemonic ) async throws { @Dependency(\.uuid) var uuid + @Dependency(\.date) var date loggerGlobal.notice("Finish onboarding with accounts recovered from scanning using menmonic") let (creatingDevice, model, name) = await updateDeviceInfo() var bdfs = accountsRecoveredFromScanningUsingMnemonic.deviceFactorSource @@ -182,12 +183,15 @@ extension ProfileStore { authorizedDapps: [] ) + var lastUsedOnDevice = creatingDevice + lastUsedOnDevice.date = date() + let profile = Profile( header: Header( snapshotVersion: .v100, id: uuid(), creatingDevice: creatingDevice, - lastUsedOnDevice: creatingDevice, + lastUsedOnDevice: lastUsedOnDevice, lastModified: bdfs.addedOn, contentHint: .init( numberOfAccountsOnAllNetworksInTotal: UInt16( @@ -245,6 +249,7 @@ extension ProfileStore { @discardableResult private func updateDeviceInfo() async -> (info: DeviceInfo, model: String, name: String) { @Dependency(\.device) var device + @Dependency(\.date) var date let model = await device.model let name = await device.name let deviceDescription = DeviceInfo.deviceDescription( @@ -256,6 +261,7 @@ extension ProfileStore { try? secureStorageClient.saveDeviceInfo(lastUsedOnDevice) try? await updating { $0.header.lastUsedOnDevice = lastUsedOnDevice + $0.header.lastUsedOnDevice.date = date() $0.header.creatingDevice.description = deviceDescription } return (info: lastUsedOnDevice, model: model, name: name) @@ -479,12 +485,15 @@ extension ProfileStore { @Dependency(\.date) var date @Dependency(\.mnemonicClient) var mnemonicClient + var lastUsedOnDevice = creatingDevice + lastUsedOnDevice.date = date() + let profileID = uuid() let header = Profile.Header( snapshotVersion: .v100, id: profileID, creatingDevice: creatingDevice, - lastUsedOnDevice: creatingDevice, + lastUsedOnDevice: lastUsedOnDevice, lastModified: date.now, contentHint: .init( numberOfAccountsOnAllNetworksInTotal: 0, From 4bda00a8f16e5083e3ffe719afb42ded475adb67 Mon Sep 17 00:00:00 2001 From: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:36:12 +0200 Subject: [PATCH 63/68] Small improvement to Personas loading (#1169) --- .../Child/List/PersonaList+Reducer.swift | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift b/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift index 30bad61a3c..f5f3b64b0c 100644 --- a/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift +++ b/RadixWallet/Features/DappsAndPersonas/Personas/Child/List/PersonaList+Reducer.swift @@ -50,7 +50,7 @@ public struct PersonaList: Sendable, FeatureReducer { } public enum InternalAction: Sendable, Equatable { - case personasLoaded(IdentifiedArrayOf) + case setPersonas([Persona]) case setSecurityProblems([SecurityProblem]) } @@ -80,27 +80,12 @@ public struct PersonaList: Sendable, FeatureReducer { case personasMissingFromClient(OrderedSet) } - /// Returns the ids of personas to include under the given strategy. nil means that all ids should be included - private func personaIDs(_ strategy: State.ReloadingStrategy) async throws -> OrderedSet? { - switch strategy { - case .all: - return nil - case let .ids(ids): - return ids - case let .dApp(dAppID): - guard let dApp = try? await authorizedDappsClient.getDetailedDapp(dAppID) else { return [] } - return OrderedSet(dApp.detailedAuthorizedPersonas.map(\.id)) - } - } - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { switch internalAction { - case var .personasLoaded(personas): - personas.mutateAll { persona in - persona.securityProblemsConfig.update(problems: state.problems) - } - + case let .setPersonas(personas): state.personas = personas + .map { PersonaFeature.State(persona: $0, problems: state.problems) } + .asIdentified() return .none case let .setSecurityProblems(problems): @@ -133,15 +118,15 @@ public struct PersonaList: Sendable, FeatureReducer { } private func personasEffect(state: State) -> Effect { - .run { [strategy = state.strategy, problems = state.problems] send in + .run { [strategy = state.strategy] send in for try await personas in await personasClient.personas() { guard !Task.isCancelled else { return } let ids = try await personaIDs(strategy) ?? OrderedSet(validating: personas.ids) - let result = ids.compactMap { personas[id: $0] }.map { PersonaFeature.State(persona: $0, problems: problems) } + let result = ids.compactMap { personas[id: $0] } guard result.count == ids.count else { throw UpdatePersonaError.personasMissingFromClient(ids.subtracting(result.map(\.id))) } - await send(.internal(.personasLoaded(result.asIdentified()))) + await send(.internal(.setPersonas(result))) } } catch: { error, _ in loggerGlobal.error("Failed to update personas from client, error: \(error)") @@ -157,4 +142,17 @@ public struct PersonaList: Sendable, FeatureReducer { } } } + + /// Returns the ids of personas to include under the given strategy. nil means that all ids should be included + private func personaIDs(_ strategy: State.ReloadingStrategy) async throws -> OrderedSet? { + switch strategy { + case .all: + return nil + case let .ids(ids): + return ids + case let .dApp(dAppID): + guard let dApp = try? await authorizedDappsClient.getDetailedDapp(dAppID) else { return [] } + return OrderedSet(dApp.detailedAuthorizedPersonas.map(\.id)) + } + } } From 023a1674616b1223b5aaf78323a3c2ab6e5aaf46 Mon Sep 17 00:00:00 2001 From: Matias Bzurovski Date: Mon, 10 Jun 2024 16:37:12 +0200 Subject: [PATCH 64/68] Remove push presentation from RestoreFromBackup flow --- ...oreProfileFromBackupCoordinator+View.swift | 53 ++++++++------- .../RestoreProfileFromBackupCoordinator.swift | 68 +++++++++---------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift index ff4464dda4..d456ce8d4d 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift @@ -12,34 +12,35 @@ extension RestoreProfileFromBackupCoordinator { } public var body: some SwiftUI.View { - NavigationStackStore( - store.scope(state: \.path, action: { .child(.path($0)) }) - ) { - path(for: store.scope(state: \.root, action: { .child(.root($0)) })) - } destination: { - path(for: $0) - } + SelectBackup.View(store: store.selectBackup) + .destinations(with: store) } + } +} + +private extension StoreOf { + var selectBackup: StoreOf { + scope(state: \.selectBackup, action: \.child.selectBackup) + } + + var destination: PresentationStoreOf { + func scopeState(state: State) -> PresentationState { + state.$destination + } + return scope(state: scopeState, action: Action.destination) + } +} + +@MainActor +private extension View { + func destinations(with store: StoreOf) -> some View { + let destinationStore = store.destination + return importMnemonics(with: destinationStore) + } - private func path( - for store: StoreOf - ) -> some SwiftUI.View { - SwitchStore(store) { state in - switch state { - case .selectBackup: - CaseLet( - /RestoreProfileFromBackupCoordinator.Path.State.selectBackup, - action: RestoreProfileFromBackupCoordinator.Path.Action.selectBackup, - then: { SelectBackup.View(store: $0) } - ) - case .importMnemonicsFlow: - CaseLet( - /RestoreProfileFromBackupCoordinator.Path.State.importMnemonicsFlow, - action: RestoreProfileFromBackupCoordinator.Path.Action.importMnemonicsFlow, - then: { ImportMnemonicsFlowCoordinator.View(store: $0) } - ) - } - } + private func importMnemonics(with destinationStore: PresentationStoreOf) -> some View { + sheet(store: destinationStore.scope(state: \.importMnemonicsFlow, action: \.importMnemonicsFlow)) { + ImportMnemonicsFlowCoordinator.View(store: $0) } } } diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift index 2008a972ad..3ae9cae7d8 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift @@ -11,32 +11,25 @@ public struct ProfileSelection: Sendable, Hashable { // MARK: - RestoreProfileFromBackupCoordinator public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { public struct State: Sendable, Hashable { - public var root: Path.State - public var path: StackState = .init() + public var selectBackup = SelectBackup.State() public var profileSelection: ProfileSelection? - public init() { - self.root = .selectBackup(.init()) - } + @PresentationState + public var destination: Destination.State? } - public struct Path: Sendable, Hashable, Reducer { + public struct Destination: DestinationReducer { @CasePathable public enum State: Sendable, Hashable { - case selectBackup(SelectBackup.State) case importMnemonicsFlow(ImportMnemonicsFlowCoordinator.State) } @CasePathable public enum Action: Sendable, Equatable { - case selectBackup(SelectBackup.Action) case importMnemonicsFlow(ImportMnemonicsFlowCoordinator.Action) } public var body: some ReducerOf { - Scope(state: \.selectBackup, action: \.selectBackup) { - SelectBackup() - } Scope(state: \.importMnemonicsFlow, action: \.importMnemonicsFlow) { ImportMnemonicsFlowCoordinator() } @@ -44,13 +37,12 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { } public enum InternalAction: Sendable, Equatable { - case delayedAppendToPath(RestoreProfileFromBackupCoordinator.Path.State) + case startImportMnemonicsFlow(Profile) } @CasePathable public enum ChildAction: Sendable, Equatable { - case root(Path.Action) - case path(StackActionOf) + case selectBackup(SelectBackup.Action) } public enum DelegateAction: Sendable, Equatable { @@ -69,44 +61,50 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { public init() {} public var body: some ReducerOf { - Scope(state: \.root, action: \.child.root) { - Path() + Scope(state: \.selectBackup, action: \.child.selectBackup) { + SelectBackup() } - Reduce(core) - .forEach(\.path, action: \.child.path) { - Path() + .ifLet(destinationPath, action: /Action.destination) { + Destination() } } - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { - switch internalAction { - case let .delayedAppendToPath(destination): - state.path.append(destination) - return .none - } - } + private let destinationPath: WritableKeyPath> = \.$destination public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { - case let .root(.selectBackup(.delegate(.selectedProfile(profile, containsLegacyP2PLinks)))): + case let .selectBackup(.delegate(.selectedProfile(profile, containsLegacyP2PLinks))): state.profileSelection = .init(profile: profile, containsP2PLinks: containsLegacyP2PLinks) return .run { send in try? await clock.sleep(for: .milliseconds(300)) _ = await radixConnectClient.loadP2PLinksAndConnectAll() - await send(.internal(.delayedAppendToPath( - .importMnemonicsFlow(.init(context: .fromOnboarding(profile: profile))) - ))) + await send(.internal(.startImportMnemonicsFlow(profile))) } - case .root(.selectBackup(.delegate(.backToStartOfOnboarding))): + case .selectBackup(.delegate(.backToStartOfOnboarding)): return .send(.delegate(.backToStartOfOnboarding)) - case .root(.selectBackup(.delegate(.profileCreatedFromImportedBDFS))): + case .selectBackup(.delegate(.profileCreatedFromImportedBDFS)): return .send(.delegate(.profileCreatedFromImportedBDFS)) - case let .path(.element(_, action: .importMnemonicsFlow(.delegate(.finishedImportingMnemonics(skipList, _, notYetSavedNewMainBDFS))))): + default: + return .none + } + } + + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { + switch internalAction { + case let .startImportMnemonicsFlow(profile): + state.destination = .importMnemonicsFlow(.init(context: .fromOnboarding(profile: profile))) + return .none + } + } + + public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { + switch presentedAction { + case let .importMnemonicsFlow(.delegate(.finishedImportingMnemonics(skipList, _, notYetSavedNewMainBDFS))): loggerGlobal.notice("Starting import snapshot process...") guard let profileSelection = state.profileSelection else { preconditionFailure("Expected to have a profile") @@ -130,8 +128,8 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { errorQueue.schedule(error) } - case let .path(.element(_, action: .importMnemonicsFlow(.delegate(.finishedEarly(didFail))))): - state.path.removeLast() + case let .importMnemonicsFlow(.delegate(.finishedEarly(didFail))): + state.destination = nil return .run { send in await radixConnectClient.disconnectAll() if didFail { From c06e24eae5fec5b00618e27f317e580fabbabb49 Mon Sep 17 00:00:00 2001 From: Matias Bzurovski Date: Mon, 10 Jun 2024 18:00:39 +0200 Subject: [PATCH 65/68] fix missing X button --- .../Coordinator/RestoreProfileFromBackupCoordinator+View.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift index d456ce8d4d..1007e2f34a 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator+View.swift @@ -13,6 +13,7 @@ extension RestoreProfileFromBackupCoordinator { public var body: some SwiftUI.View { SelectBackup.View(store: store.selectBackup) + .inNavigationStack .destinations(with: store) } } From fb1e1072a98686726cbd6db4a6c0a9602cbf63a2 Mon Sep 17 00:00:00 2001 From: Gustaf Kugelberg <123396602+kugel3@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:35:18 +0200 Subject: [PATCH 66/68] [ABW-3396] Claim Wallet (#1161) Co-authored-by: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com> Co-authored-by: Matias Bzurovski --- .../DeviceFactorSourceClient+Interface.swift | 14 ++--- .../DeviceFactorSourceClient+Live.swift | 52 +++++++------------ .../DeviceFactorSourceClient+Test.swift | 6 +-- .../KeychainClient+Interface.swift | 4 ++ .../SecurityCenterClient+Interface.swift | 6 +-- .../SecurityCenterClient+Live.swift | 14 ++--- .../TransportProfileClient+Interface.swift | 2 +- ...EntitiesControlledByMnemonic+Reducer.swift | 2 +- 8 files changed, 45 insertions(+), 55 deletions(-) diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift index e5549b5ddb..47f7a4d4ba 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift @@ -12,10 +12,10 @@ public struct DeviceFactorSourceClient: Sendable { /// Fetched accounts and personas on current network that are controlled by a device factor source, for every factor source in current profile public var controlledEntities: GetControlledEntities - /// The entities (`Accounts` & `Personas`) that are problematic. This is, that either: + /// The entities (`Accounts` & `Personas`) that are in bad state. This is, that either: /// - their mnmemonic is missing (entity was imported but seed phrase never entered), or /// - their mnmemonic is not backed up (entity was created but seed phrase never written down). - public var problematicEntities: ProblematicEntities + public var entitiesInBadState: EntitiesInBadState public init( publicKeysFromOnDeviceHD: @escaping PublicKeysFromOnDeviceHD, @@ -23,14 +23,14 @@ public struct DeviceFactorSourceClient: Sendable { isAccountRecoveryNeeded: @escaping IsAccountRecoveryNeeded, entitiesControlledByFactorSource: @escaping GetEntitiesControlledByFactorSource, controlledEntities: @escaping GetControlledEntities, - problematicEntities: @escaping ProblematicEntities + entitiesInBadState: @escaping EntitiesInBadState ) { self.publicKeysFromOnDeviceHD = publicKeysFromOnDeviceHD self.signatureFromOnDeviceHD = signatureFromOnDeviceHD self.isAccountRecoveryNeeded = isAccountRecoveryNeeded self.entitiesControlledByFactorSource = entitiesControlledByFactorSource self.controlledEntities = controlledEntities - self.problematicEntities = problematicEntities + self.entitiesInBadState = entitiesInBadState } } @@ -42,7 +42,7 @@ extension DeviceFactorSourceClient { public typealias PublicKeysFromOnDeviceHD = @Sendable (PublicKeysFromOnDeviceHDRequest) async throws -> [HierarchicalDeterministicPublicKey] public typealias SignatureFromOnDeviceHD = @Sendable (SignatureFromOnDeviceHDRequest) async throws -> SignatureWithPublicKey public typealias IsAccountRecoveryNeeded = @Sendable () async throws -> Bool - public typealias ProblematicEntities = @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> + public typealias EntitiesInBadState = @Sendable () async throws -> AnyAsyncSequence<(withoutControl: AddressesOfEntitiesInBadState, unrecoverable: AddressesOfEntitiesInBadState)> } // MARK: - DiscrepancyUnsupportedCurve @@ -241,8 +241,8 @@ extension SigningPurpose { // MARK: - FactorInstanceDoesNotHaveADerivationPathUnableToSign struct FactorInstanceDoesNotHaveADerivationPathUnableToSign: Swift.Error {} -// MARK: - ProblematicAddresses -public struct ProblematicAddresses: Sendable, Hashable { +// MARK: - AddressesOfEntitiesInBadState +public struct AddressesOfEntitiesInBadState: Sendable, Hashable { let accounts: [AccountAddress] let hiddenAccounts: [AccountAddress] let personas: [IdentityAddress] diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift index e5feeaceb3..33edf4b8c2 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Live.swift @@ -73,33 +73,33 @@ extension DeviceFactorSourceClient: DependencyKey { ) } - struct FactorSourceHasMnemonic: Sendable, Equatable { + struct KeychainPresenceOfMnemonic: Sendable, Equatable { let id: FactorSourceIDFromHash let present: Bool } @Sendable - func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[FactorSourceHasMnemonic]> { + func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[KeychainPresenceOfMnemonic]> { await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged().prepend(())) .map { factorSources, _ in factorSources .compactMap { $0.extract(DeviceFactorSource.self)?.id } .map { id in - FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) + KeychainPresenceOfMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) } } .removeDuplicates() .eraseToAnyAsyncSequence() } - let problematicEntities: @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> = { - await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { factorSources, backedUpFactorSources, profile in + let entitiesInBadState: @Sendable () async throws -> AnyAsyncSequence<(withoutControl: AddressesOfEntitiesInBadState, unrecoverable: AddressesOfEntitiesInBadState)> = { + await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { presencesOfMnemonics, backedUpFactorSources, profile in - let mnemonicMissingFactorSources = factorSources + let mnemonicMissingFactorSources = presencesOfMnemonics .filter(not(\.present)) .map(\.id) - let mnemomincPresentFactorSources = factorSources + let mnemomincPresentFactorSources = presencesOfMnemonics .filter(\.present) .map(\.id) @@ -112,49 +112,35 @@ extension DeviceFactorSourceClient: DependencyKey { let personas = network.getPersonas() let hiddenPersonas = network.getHiddenPersonas() - func mnemonicMissing(_ account: Account) -> Bool { - switch account.securityState { + func withoutControl(_ entity: some EntityProtocol) -> Bool { + switch entity.securityState { case let .unsecured(value): mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) } } - func mnemonicMissing(_ persona: Persona) -> Bool { - switch persona.securityState { - case let .unsecured(value): - mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) - } - } - - func unrecoverable(_ account: Account) -> Bool { - switch account.securityState { - case let .unsecured(value): - unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) - } - } - - func unrecoverable(_ persona: Persona) -> Bool { - switch persona.securityState { + func unrecoverable(_ entity: some EntityProtocol) -> Bool { + switch entity.securityState { case let .unsecured(value): unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) } } - let mnemonicMissing = ProblematicAddresses( - accounts: accounts.filter(mnemonicMissing(_:)).map(\.address), - hiddenAccounts: hiddenAccounts.filter(mnemonicMissing(_:)).map(\.address), - personas: personas.filter(mnemonicMissing(_:)).map(\.address), - hiddenPersonas: hiddenPersonas.filter(mnemonicMissing(_:)).map(\.address) + let withoutControl = AddressesOfEntitiesInBadState( + accounts: accounts.filter(withoutControl(_:)).map(\.address), + hiddenAccounts: hiddenAccounts.filter(withoutControl(_:)).map(\.address), + personas: personas.filter(withoutControl(_:)).map(\.address), + hiddenPersonas: hiddenPersonas.filter(withoutControl(_:)).map(\.address) ) - let unrecoverable = ProblematicAddresses( + let unrecoverable = AddressesOfEntitiesInBadState( accounts: accounts.filter(unrecoverable(_:)).map(\.address), hiddenAccounts: hiddenAccounts.filter(unrecoverable(_:)).map(\.address), personas: personas.filter(unrecoverable(_:)).map(\.address), hiddenPersonas: hiddenPersonas.filter(unrecoverable(_:)).map(\.address) ) - return (mnemonicMissing: mnemonicMissing, unrecoverable: unrecoverable) + return (withoutControl: withoutControl, unrecoverable: unrecoverable) } .eraseToAnyAsyncSequence() } @@ -219,7 +205,7 @@ extension DeviceFactorSourceClient: DependencyKey { try await entitiesControlledByFactorSource($0, maybeOverridingSnapshot) }) }, - problematicEntities: problematicEntities + entitiesInBadState: entitiesInBadState ) } } diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift index d15f1d374e..d0544f324e 100644 --- a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift +++ b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Test.swift @@ -16,7 +16,7 @@ extension DeviceFactorSourceClient: TestDependencyKey { isAccountRecoveryNeeded: { false }, entitiesControlledByFactorSource: { _, _ in throw NoopError() }, controlledEntities: { _ in [] }, - problematicEntities: { throw NoopError() } + entitiesInBadState: { throw NoopError() } ) public static let testValue = Self( @@ -25,11 +25,11 @@ extension DeviceFactorSourceClient: TestDependencyKey { isAccountRecoveryNeeded: unimplemented("\(Self.self).isAccountRecoveryNeeded"), entitiesControlledByFactorSource: unimplemented("\(Self.self).entitiesControlledByFactorSource"), controlledEntities: unimplemented("\(Self.self).controlledEntities"), - problematicEntities: unimplemented("\(Self.self).problematicEntities") + entitiesInBadState: unimplemented("\(Self.self).entitiesInBadState") ) } -private extension ProblematicAddresses { +private extension AddressesOfEntitiesInBadState { static var empty: Self { .init(accounts: [], hiddenAccounts: [], personas: [], hiddenPersonas: []) } diff --git a/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift b/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift index b76dfdd31c..8f356477b2 100644 --- a/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift +++ b/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift @@ -18,6 +18,10 @@ public struct KeychainClient: Sendable { public var _getAllKeysMatchingAttributes: GetAllKeysMatchingAttributes public var _keychainChanged: KeychainChanged + /// This a _best effort_ publisher that will emit a change every time the Keychain is changed due to actions inside the Wallet app. + /// However, we cannot detect external changes (e.g. Keychain getting wiped when passcode is deleted). + public var _keychainChanged: KeychainChanged + public init( getServiceAndAccessGroup: @escaping GetServiceAndAccessGroup, containsDataForKey: @escaping ContainsDataForKey, diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index 3a5b719fd6..4d485f70a0 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift @@ -24,7 +24,7 @@ extension SecurityCenterClient { public enum SecurityProblem: Hashable, Sendable, Identifiable { /// The given addresses of `accounts` and `personas` are unrecoverable if the user loses their phone, since their corresponding seed phrase has not been written down. /// NOTE: This definition differs from the one at Confluence since we don't have shields implemented yet. - case problem3(addresses: ProblematicAddresses) + case problem3(addresses: AddressesOfEntitiesInBadState) /// Wallet backups to the cloud aren’t working (wallet tried to do a backup and it didn’t work within, say, 5 minutes.) /// This means that currently all accounts and personas are at risk of being practically unrecoverable if the user loses their phone. /// Also they would lose all of their other non-security wallet settings and data. @@ -38,7 +38,7 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable { case problem7 /// User has gotten a new phone (and restored their wallet from backup) and the wallet sees that there are accounts without shields using a phone key, /// meaning they can only be recovered with the seed phrase. (See problem 2) This would also be the state if a user disabled their PIN (and reenabled it), clearing phone keys. - case problem9(addresses: ProblematicAddresses) + case problem9(addresses: AddressesOfEntitiesInBadState) public var id: Int { number } @@ -76,7 +76,7 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable { } } - private func problem3(addresses: ProblematicAddresses) -> String { + private func problem3(addresses: AddressesOfEntitiesInBadState) -> String { typealias Common = L10n.SecurityProblems.Common typealias Problem = L10n.SecurityProblems.No3 let hasHidden = addresses.hiddenAccounts.count + addresses.hiddenPersonas.count > 0 diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index d17ec5fc18..eda93b1564 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -55,14 +55,14 @@ extension SecurityCenterClient { @Sendable func startMonitoring() async throws { let profileValues = await profileStore.values() - let problematicValues = try await deviceFactorSourceClient.problematicEntities() + let entitiesInBadState = try await deviceFactorSourceClient.entitiesInBadState() let backupValues = await combineLatest(cloudBackups(), manualBackups()).map { (cloud: $0, manual: $1) } - for try await (profile, problematic, backups) in combineLatest(profileValues, problematicValues, backupValues) { + for try await (profile, entitiesInBadState, backups) in combineLatest(profileValues, entitiesInBadState, backupValues) { let isCloudProfileSyncEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled - func hasProblem3() async -> ProblematicAddresses? { - problematic.unrecoverable.isEmpty ? nil : problematic.unrecoverable + func hasProblem3() async -> AddressesOfEntitiesInBadState? { + entitiesInBadState.unrecoverable.isEmpty ? nil : entitiesInBadState.unrecoverable } func hasProblem5() -> Bool { @@ -81,8 +81,8 @@ extension SecurityCenterClient { !isCloudProfileSyncEnabled && backups.manual?.isCurrent == false } - func hasProblem9() async -> ProblematicAddresses? { - problematic.mnemonicMissing.isEmpty ? nil : problematic.mnemonicMissing + func hasProblem9() async -> AddressesOfEntitiesInBadState? { + entitiesInBadState.withoutControl.isEmpty ? nil : entitiesInBadState.withoutControl } var result: [SecurityProblem] = [] @@ -117,7 +117,7 @@ extension SecurityCenterClient { } } -private extension ProblematicAddresses { +private extension AddressesOfEntitiesInBadState { var isEmpty: Bool { accounts.count + hiddenAccounts.count + personas.count == 0 } diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift index 003554649e..b3459e1a01 100644 --- a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift @@ -20,7 +20,7 @@ public struct TransportProfileClient: Sendable { } extension TransportProfileClient { - public typealias ImportProfile = @Sendable (Profile, Set, Bool) async throws -> Void + public typealias ImportProfile = @Sendable (Profile, Set, _ containsP2PLinks: Bool) async throws -> Void public typealias ProfileForExport = @Sendable () async throws -> Profile public typealias DidExportProfile = @Sendable (Profile) throws -> Void } diff --git a/RadixWallet/Features/DisplayEntitiesControlledByMnemonic/DisplayEntitiesControlledByMnemonic+Reducer.swift b/RadixWallet/Features/DisplayEntitiesControlledByMnemonic/DisplayEntitiesControlledByMnemonic+Reducer.swift index 08b5573cbf..72c3bf40ff 100644 --- a/RadixWallet/Features/DisplayEntitiesControlledByMnemonic/DisplayEntitiesControlledByMnemonic+Reducer.swift +++ b/RadixWallet/Features/DisplayEntitiesControlledByMnemonic/DisplayEntitiesControlledByMnemonic+Reducer.swift @@ -134,7 +134,7 @@ private extension [SecurityProblem] { } } -private extension ProblematicAddresses { +private extension AddressesOfEntitiesInBadState { var problematicAccounts: Set { Set(accounts + hiddenAccounts) } From d35d23b09a866044b977c8975bb929541f8c30ff Mon Sep 17 00:00:00 2001 From: Gustaf Kugelberg <123396602+kugel3@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:26:10 +0200 Subject: [PATCH 67/68] Retroactively Update Backup Log (#1172) --- .../CloudBackupClient+Live.swift | 15 +++++++++++---- .../FactorSourcesClient+Live.swift | 1 - .../Models/TransactionFailure.swift | 1 + .../UserDefaults+Dependency+Extension.swift | 11 +++++------ .../Core/DesignSystem/Components/Thumbnails.swift | 1 - .../IncomingMessage/P2P+RTCIncomingMessage.swift | 1 + .../P2P+ConnectorExtension+Response.swift | 1 - .../Children/DevAccountPreferences+Reducer.swift | 1 + .../AccountRecoveryScanCoordinator.swift | 1 + RadixWallet/Features/AppFeature/App+Reducer.swift | 1 + .../AssetTransfer+Reducer.swift | 2 -- .../Details/NonFungibleTokenDetails+View.swift | 1 - .../CreateAccountCoordinator+Models.swift | 1 - .../Coordinator/DappInteractionFlow.swift | 1 + .../Coordinator/DappInteractionModels.swift | 1 - .../Interactor/DappInteractor.swift | 2 ++ .../AuthorizedDApps/AuthorizedDApps.swift | 2 ++ .../Features/HomeFeature/Coordinator/Home.swift | 5 +++++ .../ImportWord/ImportMnemonicWord.swift | 1 + .../ImportMnemonicControllingAccounts.swift | 1 + ...overWalletWithoutProfileCoordinator+View.swift | 1 - .../Shared/EncryptOrDecryptProfile+Reducer.swift | 1 + .../DebugUserDefaultsContents.swift | 3 --- .../Coordinator/DisplayMnemonics.swift | 1 + .../Features/Signing/Coordinator/Signing.swift | 2 ++ .../SubmitTransaction/SubmitTransaction.swift | 1 - .../TransactionReview+Sections.swift | 1 + .../IncomingMessage+Decoding.swift | 3 --- 28 files changed, 38 insertions(+), 26 deletions(-) diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift index 21a794ed1a..8e8a880fc6 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -121,7 +121,7 @@ extension CloudBackupClient { @Sendable func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async throws { - try? userDefaults.setLastCloudBackup(.started(.now), of: profile) + try? userDefaults.setLastCloudBackup(.started(.now), of: profile.header) do { let json = profile.toJSONString() @@ -138,18 +138,25 @@ extension CloudBackupClient { failure = .other } - try? userDefaults.setLastCloudBackup(.failure(failure), of: profile) + try? userDefaults.setLastCloudBackup(.failure(failure), of: profile.header) throw error } - try? userDefaults.setLastCloudBackup(.success, of: profile) + try? userDefaults.setLastCloudBackup(.success, of: profile.header) } @Sendable func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async { - let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty let existingRecord = try? await fetchProfileRecord(profile.id) let backedUpHeader = try? existingRecord.map(getProfileHeader) + + if let backedUpHeader, let backupDate = existingRecord?.modificationDate { + try? userDefaults.setLastCloudBackup(.success, of: backedUpHeader, at: backupDate) + } else { + try? userDefaults.removeLastCloudBackup(for: profile.id) + } + + let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty let isBackedUp = backedUpHeader?.saveIdentifier == profile.header.saveIdentifier let shouldBackUp = needsBackUp && !isBackedUp diff --git a/RadixWallet/Clients/FactorSourcesClient/FactorSourcesClient+Live.swift b/RadixWallet/Clients/FactorSourcesClient/FactorSourcesClient+Live.swift index 0b66a15bda..f0fc339e5c 100644 --- a/RadixWallet/Clients/FactorSourcesClient/FactorSourcesClient+Live.swift +++ b/RadixWallet/Clients/FactorSourcesClient/FactorSourcesClient+Live.swift @@ -421,7 +421,6 @@ extension FactorSourceKind: Comparable { case .offDeviceMnemonic: 1 case .securityQuestions: 2 case .trustedContact: 3 - // we want to sign with device last, since it would allow for us to stop using // ephemeral notary and allow us to implement a AutoPurgingMnemonicCache which // deletes items after 1 sec, thus `device` must come last. diff --git a/RadixWallet/Clients/TransactionClient/Models/TransactionFailure.swift b/RadixWallet/Clients/TransactionClient/Models/TransactionFailure.swift index 154bf0928e..0f0c715f9d 100644 --- a/RadixWallet/Clients/TransactionClient/Models/TransactionFailure.swift +++ b/RadixWallet/Clients/TransactionClient/Models/TransactionFailure.swift @@ -47,6 +47,7 @@ extension TransactionFailure { case .failedToSignIntentWithAccountSigners, .failedToSignSignedCompiledIntentWithNotarySigner, .failedToConvertNotarySignature, .failedToConvertAccountSignatures: (errorKind: .failedToSignTransaction, message: nil) } + case .failedToSubmit: (errorKind: .failedToSubmitTransaction, message: nil) diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index dc20e16f37..3aff79a557 100644 --- a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift +++ b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift @@ -165,13 +165,12 @@ extension UserDefaults.Dependency { try save(codable: backups, forKey: .lastCloudBackups) } - public func setLastCloudBackup(_ result: BackupResult.Result, of profile: Profile) throws { + public func setLastCloudBackup(_ result: BackupResult.Result, of header: Profile.Header, at date: Date = .now) throws { var backups: [UUID: BackupResult] = getLastCloudBackups - let now = Date.now - let lastSuccess = result == .success ? now : backups[profile.id]?.lastSuccess - backups[profile.id] = .init( - date: now, - saveIdentifier: profile.header.saveIdentifier, + let lastSuccess = result == .success ? date : backups[header.id]?.lastSuccess + backups[header.id] = .init( + date: date, + saveIdentifier: header.saveIdentifier, result: result, lastSuccess: lastSuccess ) diff --git a/RadixWallet/Core/DesignSystem/Components/Thumbnails.swift b/RadixWallet/Core/DesignSystem/Components/Thumbnails.swift index f35c91e444..3c71aa3c1a 100644 --- a/RadixWallet/Core/DesignSystem/Components/Thumbnails.swift +++ b/RadixWallet/Core/DesignSystem/Components/Thumbnails.swift @@ -255,7 +255,6 @@ public struct LoadableImage: View { case .shimmer: Color.app.gray4 .shimmer(active: true, config: .accountResourcesLoading) - case let .color(color): color case let .asset(imageAsset): diff --git a/RadixWallet/Core/SharedModels/P2P/Application/IncomingMessage/P2P+RTCIncomingMessage.swift b/RadixWallet/Core/SharedModels/P2P/Application/IncomingMessage/P2P+RTCIncomingMessage.swift index 5351f9f29b..ebd35d66f4 100644 --- a/RadixWallet/Core/SharedModels/P2P/Application/IncomingMessage/P2P+RTCIncomingMessage.swift +++ b/RadixWallet/Core/SharedModels/P2P/Application/IncomingMessage/P2P+RTCIncomingMessage.swift @@ -52,6 +52,7 @@ extension P2P.RTCIncomingMessageContainer { case let (.failure(lhsFailure), .failure(rhsFailure)): // FIXME: strongly type messages? to an Error type which is Hashable? return String(describing: lhsFailure) == String(describing: rhsFailure) + case let (.success(lhsSuccess), .success(rhsSuccess)): return lhsSuccess == rhsSuccess diff --git a/RadixWallet/Core/SharedModels/P2P/ConnectorExtension/P2P+ConnectorExtension+Response.swift b/RadixWallet/Core/SharedModels/P2P/ConnectorExtension/P2P+ConnectorExtension+Response.swift index ae34876b86..620d30f189 100644 --- a/RadixWallet/Core/SharedModels/P2P/ConnectorExtension/P2P+ConnectorExtension+Response.swift +++ b/RadixWallet/Core/SharedModels/P2P/ConnectorExtension/P2P+ConnectorExtension+Response.swift @@ -163,7 +163,6 @@ extension P2P.ConnectorExtension.Response.LedgerHardwareWallet { self.response = try decodeResponse { Success.signChallenge($0) } - case .deriveAndDisplayAddress: self.response = try decodeResponse { Success.deriveAndDisplayAddress($0) diff --git a/RadixWallet/Features/AccountPreferencesFeature/Children/DevAccountPreferences+Reducer.swift b/RadixWallet/Features/AccountPreferencesFeature/Children/DevAccountPreferences+Reducer.swift index 4f09c8abcc..280bd5acbe 100644 --- a/RadixWallet/Features/AccountPreferencesFeature/Children/DevAccountPreferences+Reducer.swift +++ b/RadixWallet/Features/AccountPreferencesFeature/Children/DevAccountPreferences+Reducer.swift @@ -219,6 +219,7 @@ public struct DevAccountPreferences: Sendable, FeatureReducer { case let .canCreateAuthSigningKey(canCreateAuthSigningKey): state.canCreateAuthSigningKey = canCreateAuthSigningKey return .none + case let .canTurnIntoDappDefAccountType(canTurnIntoDappDefAccountType): state.canTurnIntoDappDefinitionAccountType = canTurnIntoDappDefAccountType return .none diff --git a/RadixWallet/Features/AccountRecoveryScan/Coordinator/AccountRecoveryScanCoordinator.swift b/RadixWallet/Features/AccountRecoveryScan/Coordinator/AccountRecoveryScanCoordinator.swift index b8528dc257..a37926bdd2 100644 --- a/RadixWallet/Features/AccountRecoveryScan/Coordinator/AccountRecoveryScanCoordinator.swift +++ b/RadixWallet/Features/AccountRecoveryScan/Coordinator/AccountRecoveryScanCoordinator.swift @@ -132,6 +132,7 @@ public struct AccountRecoveryScanCoordinator: Sendable, FeatureReducer { let childState = state.backTo ?? AccountRecoveryScanCoordinator.State.accountRecoveryScanInProgressState(purpose: state.purpose) state.root = .accountRecoveryScanInProgress(childState) return .none + case let .selectInactiveAccountsToAdd(.delegate(.finished(selectedInactive, active))): return completed(purpose: state.purpose, active: active, inactive: selectedInactive) diff --git a/RadixWallet/Features/AppFeature/App+Reducer.swift b/RadixWallet/Features/AppFeature/App+Reducer.swift index 30392e9e98..d56c5805a2 100644 --- a/RadixWallet/Features/AppFeature/App+Reducer.swift +++ b/RadixWallet/Features/AppFeature/App+Reducer.swift @@ -96,6 +96,7 @@ public struct App: Sendable, FeatureReducer { } else { goToMain(state: &state) } + default: .none } diff --git a/RadixWallet/Features/AssetTransferFeature/AssetTransfer+Reducer.swift b/RadixWallet/Features/AssetTransferFeature/AssetTransfer+Reducer.swift index e6ce6c154c..2a2d1f30ea 100644 --- a/RadixWallet/Features/AssetTransferFeature/AssetTransfer+Reducer.swift +++ b/RadixWallet/Features/AssetTransferFeature/AssetTransfer+Reducer.swift @@ -268,7 +268,6 @@ func needsSignatureForDepositting( return false case (.acceptAll, .deny): return true - // Accept Known case (.acceptKnown, .allow): return false @@ -283,7 +282,6 @@ func needsSignatureForDepositting( return !hasResource case (.acceptKnown, .deny): return true - // DenyAll case (.denyAll, .none): return true diff --git a/RadixWallet/Features/AssetsFeature/Components/NonFungibleAssetList/Components/Details/NonFungibleTokenDetails+View.swift b/RadixWallet/Features/AssetsFeature/Components/NonFungibleAssetList/Components/Details/NonFungibleTokenDetails+View.swift index 86d66b05d6..db042451e9 100644 --- a/RadixWallet/Features/AssetsFeature/Components/NonFungibleAssetList/Components/Details/NonFungibleTokenDetails+View.swift +++ b/RadixWallet/Features/AssetsFeature/Components/NonFungibleAssetList/Components/Details/NonFungibleTokenDetails+View.swift @@ -315,7 +315,6 @@ private extension GatewayAPI.ProgrammaticScryptoSborValue { switch self { case .array, .map, .mapEntry, .tuple: .complex - case let .bool(content): .primitive(String(content.value)) case let .bytes(content): diff --git a/RadixWallet/Features/CreateAccount/Coordinator/CreateAccountCoordinator+Models.swift b/RadixWallet/Features/CreateAccount/Coordinator/CreateAccountCoordinator+Models.swift index 7036b0c11e..0f95b3f375 100644 --- a/RadixWallet/Features/CreateAccount/Coordinator/CreateAccountCoordinator+Models.swift +++ b/RadixWallet/Features/CreateAccount/Coordinator/CreateAccountCoordinator+Models.swift @@ -52,7 +52,6 @@ extension CreateAccountConfig { navigationButtonCTA: .goBackToChooseAccounts, specificNetworkID: nil ) - case .newAccountFromHome: self.init( isFirstAccount: false, diff --git a/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionFlow.swift b/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionFlow.swift index 643f420fca..8a83379fc5 100644 --- a/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionFlow.swift +++ b/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionFlow.swift @@ -839,6 +839,7 @@ extension DappInteractionFlow.Path.State { switch anyItem { case .remote(.auth(.usePersona)): return nil + case let .remote(.auth(.login(loginRequest))): self.state = .login(.init( dappMetadata: dappMetadata, diff --git a/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionModels.swift b/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionModels.swift index c19461914b..edc87aeddf 100644 --- a/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionModels.swift +++ b/RadixWallet/Features/DappInteractionFeature/Coordinator/DappInteractionModels.swift @@ -127,7 +127,6 @@ extension P2P.Dapp.Request { 3 case .oneTimePersonaData: 4 - // transactions case .send: 0 diff --git a/RadixWallet/Features/DappInteractionFeature/Interactor/DappInteractor.swift b/RadixWallet/Features/DappInteractionFeature/Interactor/DappInteractor.swift index 57f802195f..b52cbfb9ff 100644 --- a/RadixWallet/Features/DappInteractionFeature/Interactor/DappInteractor.swift +++ b/RadixWallet/Features/DappInteractionFeature/Interactor/DappInteractor.swift @@ -120,6 +120,7 @@ struct DappInteractor: Sendable, FeatureReducer { switch viewAction { case .task: return handleIncomingRequests() + case let .responseFailureAlert(action): switch action { case .dismiss: @@ -150,6 +151,7 @@ struct DappInteractor: Sendable, FeatureReducer { return .run { _ in await radixConnectClient.disconnectAll() } + case .moveToForeground: return .run { _ in _ = await radixConnectClient.loadP2PLinksAndConnectAll() diff --git a/RadixWallet/Features/DappsAndPersonas/AuthorizedDApps/AuthorizedDApps.swift b/RadixWallet/Features/DappsAndPersonas/AuthorizedDApps/AuthorizedDApps.swift index ecae9949b3..16e52fa406 100644 --- a/RadixWallet/Features/DappsAndPersonas/AuthorizedDApps/AuthorizedDApps.swift +++ b/RadixWallet/Features/DappsAndPersonas/AuthorizedDApps/AuthorizedDApps.swift @@ -113,9 +113,11 @@ public struct AuthorizedDappsFeature: Sendable, FeatureReducer { case let .loadedDapps(.failure(error)): errorQueue.schedule(error) return .none + case let .presentDappDetails(presentedDappState): state.destination = .presentedDapp(presentedDappState) return .none + case let .loadedThumbnail(thumbnail, dApp: id): state.thumbnails[id] = thumbnail return .none diff --git a/RadixWallet/Features/HomeFeature/Coordinator/Home.swift b/RadixWallet/Features/HomeFeature/Coordinator/Home.swift index 53f8ee5dee..a4fee67120 100644 --- a/RadixWallet/Features/HomeFeature/Coordinator/Home.swift +++ b/RadixWallet/Features/HomeFeature/Coordinator/Home.swift @@ -183,6 +183,7 @@ public struct Home: Sendable, FeatureReducer { )) ) return .none + case .pullToRefreshStarted: let accountAddresses = state.accounts.map(\.address) return .run { _ in @@ -190,6 +191,7 @@ public struct Home: Sendable, FeatureReducer { } catch: { error, _ in errorQueue.schedule(error) } + case .radixBannerButtonTapped: return .run { _ in await openURL(Home.radixBannerURL) @@ -247,11 +249,13 @@ public struct Home: Sendable, FeatureReducer { } #endif return .none + case let .shouldShowNPSSurvey(shouldShow): if shouldShow { state.addDestination(.npsSurvey(.init())) } return .none + case let .accountsFiatWorthLoaded(fiatWorths): state.accountRows.mutateAll { if let fiatWorth = fiatWorths[$0.id] { @@ -260,6 +264,7 @@ public struct Home: Sendable, FeatureReducer { } state.totalFiatWorth = state.accountRows.map(\.totalFiatWorth).reduce(+) ?? .loading return .none + case .showLinkConnectorIfNeeded: let purpose: NewConnectionApproval.State.Purpose? = if userDefaults.showRelinkConnectorsAfterProfileRestore { .approveRelinkAfterProfileRestore diff --git a/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift b/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift index 12cadfb73c..3100015fa4 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift @@ -157,6 +157,7 @@ public struct ImportMnemonicWord: Sendable, FeatureReducer { candidate, fromPartial: state.value.text ))) + case let .textFieldFocused(field): state.focusedField = field return field == nil ? .send(.delegate(.lostFocus(displayText: state.value.text))) : .none diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift index 9150c5f514..b9852ef472 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts.swift @@ -124,6 +124,7 @@ public struct ImportMnemonicControllingAccounts: Sendable, FeatureReducer { switch viewAction { case .appeared: return .none + case .inputMnemonicButtonTapped: state.destination = .importMnemonic(.init( warning: L10n.RevealSeedPhrase.warning, diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator+View.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator+View.swift index 05300cf406..abced842bb 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator+View.swift @@ -46,7 +46,6 @@ public extension RecoverWalletWithoutProfileCoordinator { action: RecoverWalletWithoutProfileCoordinator.Path.Action.importMnemonic, then: { ImportMnemonic.View(store: $0) } ) - case .recoveryComplete: CaseLet( /RecoverWalletWithoutProfileCoordinator.Path.State.recoveryComplete, diff --git a/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift index 8877cdfbbc..9f1825e720 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+Reducer.swift @@ -120,6 +120,7 @@ public struct EncryptOrDecryptProfile: Sendable, FeatureReducer { case .encryptSpecific: break + case .decrypt: break } diff --git a/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugUserDefaultsContents/DebugUserDefaultsContents.swift b/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugUserDefaultsContents/DebugUserDefaultsContents.swift index b15697d025..1277a2f3db 100644 --- a/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugUserDefaultsContents/DebugUserDefaultsContents.swift +++ b/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugUserDefaultsContents/DebugUserDefaultsContents.swift @@ -96,13 +96,10 @@ extension UserDefaults.Dependency.Key { return [value] case .epochForWhenLastUsedByAccountAddress: return userDefaults.loadEpochForWhenLastUsedByAccountAddress().epochForAccounts.map { "epoch: \($0.epoch) account: \($0.accountAddress)" } - case .hideMigrateOlympiaButton: return [userDefaults.hideMigrateOlympiaButton].map(String.init(describing:)) - case .showRadixBanner: return [userDefaults.showRadixBanner].map(String.init(describing:)) - case .mnemonicsUserClaimsToHaveBackedUp: return userDefaults.getFactorSourceIDOfBackedUpMnemonics().map(String.init(describing:)) case .transactionsCompletedCounter: diff --git a/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift b/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift index 23aabcbbe0..dc4c939383 100644 --- a/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift +++ b/RadixWallet/Features/SettingsFeature/DisplayMnemonics/Coordinator/DisplayMnemonics.swift @@ -159,6 +159,7 @@ public struct DisplayMnemonics: Sendable, FeatureReducer { return .none } + default: return .none } diff --git a/RadixWallet/Features/Signing/Coordinator/Signing.swift b/RadixWallet/Features/Signing/Coordinator/Signing.swift index 529619d6fa..20d0794994 100644 --- a/RadixWallet/Features/Signing/Coordinator/Signing.swift +++ b/RadixWallet/Features/Signing/Coordinator/Signing.swift @@ -111,6 +111,7 @@ public struct Signing: Sendable, FeatureReducer { loggerGlobal.error("Failed to notarize transaction, error: \(error)") errorQueue.schedule(error) return .none + case let .notarizeResult(.success(notarized)): switch state.signingPurposeWithPayload { case .signAuth: @@ -135,6 +136,7 @@ public struct Signing: Sendable, FeatureReducer { case .signWithFactorSource(.delegate(.cancel)): return .send(.delegate(.cancelSigning)) + default: return .none } diff --git a/RadixWallet/Features/TransactionReviewFeature/SubmitTransaction/SubmitTransaction.swift b/RadixWallet/Features/TransactionReviewFeature/SubmitTransaction/SubmitTransaction.swift index bac354fd2f..c9376437f4 100644 --- a/RadixWallet/Features/TransactionReviewFeature/SubmitTransaction/SubmitTransaction.swift +++ b/RadixWallet/Features/TransactionReviewFeature/SubmitTransaction/SubmitTransaction.swift @@ -106,7 +106,6 @@ public struct SubmitTransaction: Sendable, FeatureReducer { } return .send(.delegate(.manuallyDismiss)) - case .dismissTransactionAlert(.presented(.confirm)): return .concatenate(.cancel(id: CancellableId.transactionStatus), .send(.delegate(.manuallyDismiss))) case .dismissTransactionAlert(.presented(.cancel)): diff --git a/RadixWallet/Features/TransactionReviewFeature/TransactionReview+Sections.swift b/RadixWallet/Features/TransactionReviewFeature/TransactionReview+Sections.swift index 59f70bff53..418aca4bfd 100644 --- a/RadixWallet/Features/TransactionReviewFeature/TransactionReview+Sections.swift +++ b/RadixWallet/Features/TransactionReviewFeature/TransactionReview+Sections.swift @@ -61,6 +61,7 @@ extension TransactionReview { switch summary.detailedManifestClass { case nil: return nil + case .general, .transfer: if summary.detailedManifestClass == .general { guard !summary.deposits.isEmpty || !summary.withdrawals.isEmpty else { return nil } diff --git a/RadixWallet/RadixConnect/RadixConnect/RTC/SignalingClient/IncomingMessage/IncomingMessage+Decoding.swift b/RadixWallet/RadixConnect/RadixConnect/RTC/SignalingClient/IncomingMessage/IncomingMessage+Decoding.swift index 16f30eb68f..beb50a4243 100644 --- a/RadixWallet/RadixConnect/RadixConnect/RTC/SignalingClient/IncomingMessage/IncomingMessage+Decoding.swift +++ b/RadixWallet/RadixConnect/RadixConnect/RTC/SignalingClient/IncomingMessage/IncomingMessage+Decoding.swift @@ -49,7 +49,6 @@ extension SignalingClient.IncomingMessage: Decodable { let message = try container.decode(SignalingClient.ClientMessage.self, forKey: .message) let remoteClientId = try container.decode(RemoteClientID.self, forKey: .remoteClientId) self = .fromRemoteClient(.init(remoteClientId: remoteClientId, message: message)) - case .remoteClientJustConnected: let clientId = try container.decode(RemoteClientID.self, forKey: .remoteClientId) self = .fromSignalingServer(.notification(.remoteClientJustConnected(clientId))) @@ -59,7 +58,6 @@ extension SignalingClient.IncomingMessage: Decodable { case .remoteClientDisconnected: let clientId = try container.decode(RemoteClientID.self, forKey: .remoteClientId) self = .fromSignalingServer(.notification(.remoteClientDisconnected(clientId))) - case .missingRemoteClientError: let requestId = try container.decode(SignalingClient.ClientMessage.RequestID.self, forKey: .requestId) @@ -70,7 +68,6 @@ extension SignalingClient.IncomingMessage: Decodable { ) ) ) - case .validationError: let error = try container.decode(JSONValue.self, forKey: .error) let requestId = try container.decode(SignalingClient.ClientMessage.RequestID.self, forKey: .requestId) From 703050d087f64f9c9c0ff2984f8d3b57a28367fa Mon Sep 17 00:00:00 2001 From: danvleju-rdx <163979791+danvleju-rdx@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:49:16 +0300 Subject: [PATCH 68/68] Fix old QR code error message (#1174) --- .../Coordinator/NewConnection+Reducer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RadixWallet/Features/NewConnectionFeature/Coordinator/NewConnection+Reducer.swift b/RadixWallet/Features/NewConnectionFeature/Coordinator/NewConnection+Reducer.swift index 6b896bb95e..2b7039749a 100644 --- a/RadixWallet/Features/NewConnectionFeature/Coordinator/NewConnection+Reducer.swift +++ b/RadixWallet/Features/NewConnectionFeature/Coordinator/NewConnection+Reducer.swift @@ -318,7 +318,7 @@ extension AlertState { TextState(L10n.Common.dismiss) } } message: { - TextState(L10n.LinkedConnectors.incorrectQrMessage) + TextState(L10n.LinkedConnectors.oldQRErrorMessage) } } }