From f94758d2453b0b0c14e9bc22f114e10113afa3a8 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 1/3] [ABW-3396] Claim Wallet (#1161) Co-authored-by: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com> Co-authored-by: Matias Bzurovski --- RadixWallet.xcodeproj/project.pbxproj | 50 +-- .../AppPreferencesClient+Interface.swift | 6 - .../AppPreferencesClient+Live.swift | 15 - .../AppPreferencesClient+Test.swift | 2 - .../BackupsClient+Interface.swift | 59 ---- .../BackupsClient/BackupsClient+Live.swift | 96 ------ .../BackupsClient/BackupsClient+Test.swift | 32 -- .../CloudBackupClient+Interface.swift | 10 +- .../CloudBackupClient+Live.swift | 166 +++++---- .../CloudBackupClient+Test.swift | 8 +- .../DeviceFactorSourceClient+Interface.swift | 14 +- .../DeviceFactorSourceClient+Live.swift | 120 ++++--- .../DeviceFactorSourceClient+Test.swift | 6 +- .../KeychainClient+Interface.swift | 14 +- .../KeychainClient/KeychainClient+Live.swift | 6 +- .../KeychainClient+Mocked.swift | 6 +- .../KeychainClient/KeychainHolder.swift | 9 + .../LedgerHardwareWalletClient+Live.swift | 2 +- .../OnboardingClient+Interface.swift | 9 - .../OnboardingClient+Live.swift | 3 - .../OnboardingClient+Test.swift | 2 - .../OverlayWindowClient+Interface.swift | 38 ++- .../OverlayWindowClient+Live.swift | 19 +- .../OverlayWindowClient+Test.swift | 7 +- ...WindowClient+Alert+OwnershipConflict.swift | 47 --- .../Clients/ProfileStore/ProfileStore.swift | 175 ++-------- .../ResetWalletClient+Live.swift | 16 +- .../SecureStorageClient+Interface.swift | 30 +- .../SecureStorageClient+Live.swift | 51 +-- .../SecureStorageClient+Test.swift | 16 +- .../SecurityCenterClient+Interface.swift | 23 +- .../SecurityCenterClient+Live.swift | 106 +++--- .../SecurityCenterClient+Test.swift | 2 + .../TransportProfileClient+Interface.swift | 26 ++ .../TransportProfileClient+Live.swift | 36 ++ .../TransportProfileClient+Test.swift | 24 ++ .../UserDefaults+Dependency+Extension.swift | 60 +++- .../DesignSystem/Extensions/View+Extra.swift | 25 +- .../UserDefaultsClient+AccountRecovery.swift | 6 + .../Features/AppFeature/App+Reducer.swift | 60 +++- .../Features/AppFeature/App+View.swift | 5 +- ...FullScreenOverlayCoordinator+Reducer.swift | 9 +- .../AppFeature/Overlay/Overlay+Reducer.swift | 38 +-- .../AppFeature/Overlay/Overlay+View.swift | 9 +- .../ClaimWallet/ClaimWallet+Reducer.swift | 16 +- .../Child/List/PersonaList+Reducer.swift | 6 +- ...EntitiesControlledByMnemonic+Reducer.swift | 2 +- .../ImportMnemonic/ImportMnemonic+View.swift | 25 +- .../ImportMnemonic/ImportMnemonic.swift | 6 +- .../Features/MainFeature/Main+Reducer.swift | 40 ++- .../Features/MainFeature/Main+View.swift | 11 +- .../ProfileBackupSettings+Reducer.swift | 320 ------------------ .../ProfileBackupSettings+View.swift | 213 ------------ ...portMnemonicControllingAccounts+View.swift | 28 +- .../ImportMnemonicControllingAccounts.swift | 6 +- .../ImportMnemonicsFlowCoordinator.swift | 4 +- .../SelectBackup/SelectBackup+Reducer.swift | 13 +- .../SelectBackup/SelectBackup+View.swift | 4 +- .../RestoreProfileFromBackupCoordinator.swift | 27 +- .../EncryptOrDecryptProfile+Reducer.swift | 4 +- .../ConfigurationBackup+Reducer.swift | 25 +- .../ConfigurationBackup+View.swift | 43 ++- .../Coordinator/DisplayMnemonics.swift | 1 - ...ManualAccountRecoverySeedPhrase+View.swift | 9 +- .../Features/SplashFeature/Splash.swift | 7 +- .../ProfileStoreTests/ProfileStoreTests.swift | 159 ++------- .../SecureStorageClientTests.swift | 4 +- .../AppFeatureTests/AppFeatureTests.swift | 2 +- .../MainFeatureTests/MainFeatureTests.swift | 1 + 69 files changed, 804 insertions(+), 1635 deletions(-) delete mode 100644 RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift delete mode 100644 RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift delete mode 100644 RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift delete mode 100644 RadixWallet/Clients/ProfileStore/ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift create mode 100644 RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift create mode 100644 RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift create mode 100644 RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift 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..f92b256abb 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 */; }; @@ -474,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 */; }; @@ -667,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 */; }; @@ -1101,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 */; }; @@ -1337,8 +1334,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 = ""; }; @@ -1645,8 +1640,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 = ""; }; @@ -1838,7 +1833,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 = ""; }; @@ -2244,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 = ""; }; @@ -3080,22 +3074,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 = ( @@ -4298,7 +4282,7 @@ 48CFBF622ADC10D900E77A5C /* AccountsClient */, 48CFBF6C2ADC10D900E77A5C /* AppPreferencesClient */, 48CFBF652ADC10D900E77A5C /* AuthorizedDappsClient */, - 48CFBFAE2ADC10D900E77A5C /* BackupsClient */, + 48CFBFAE2ADC10D900E77A5C /* TransportProfileClient */, 48CFBF772ADC10D900E77A5C /* CacheClient */, 48CFC0D62ADC10D900E77A5C /* CameraPermissionClient */, 48CFBF562ADC10D900E77A5C /* DappInteractionClient */, @@ -4550,14 +4534,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 */ = { @@ -5006,7 +4990,6 @@ children = ( 48CFC0DD2ADC10D900E77A5C /* ProfileStore.swift */, 48AB4E832AE19F5B001B238E /* ProfileStore+AsyncSequence+Updates.swift */, - E6390FB22AE6C3E200B4DEE2 /* ProfileStore+OverlayWindowClient+Alert+OwnershipConflict.swift */, ); path = ProfileStore; sourceTree = ""; @@ -7087,7 +7070,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 */, @@ -7237,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 */, @@ -7371,7 +7353,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 */, @@ -7688,7 +7669,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 */, @@ -7713,8 +7694,7 @@ 48CFC5352ADC10DA00E77A5C /* StateEntityDetailsResponseItemAncestorIdentities.swift in Sources */, 48CFC4E22ADC10DA00E77A5C /* InvalidEntityError.swift in Sources */, A41557552B7645F70040AD4E /* TransactionHistory+View.swift in Sources */, - 48CFC47D2ADC10DA00E77A5C /* BackupsClient+Interface.swift in Sources */, - 48CFC2B42ADC10D900E77A5C /* ProfileBackupSettings+Reducer.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/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 c1213aff82..25d12bff5f 100644 --- a/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift +++ b/RadixWallet/Clients/AppPreferencesClient/AppPreferencesClient+Live.swift @@ -20,20 +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 - } - try secureStorageClient.updateIsCloudProfileSyncEnabled( - profile.id, - isEnabled ? .enable : .disable - ) - }, setIsCloudBackupEnabled: { isEnabled in let profile = await profileStore.profile let wasEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled @@ -46,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 } ) } diff --git a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift b/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift deleted file mode 100644 index abdc3c4631..0000000000 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Interface.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Sargon - -public typealias DeviceID = UUID - -// MARK: - BackupsClient -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 importCloudProfile: ImportCloudProfile - public var loadDeviceID: LoadDeviceID - - public init( - snapshotOfProfileForExport: @escaping SnapshotOfProfileForExport, - loadProfileBackups: @escaping LoadProfileBackups, - lookupProfileSnapshotByHeader: @escaping LookupProfileSnapshotByHeader, - importProfileSnapshot: @escaping ImportProfileSnapshot, - didExportProfileSnapshot: @escaping DidExportProfileSnapshot, - importCloudProfile: @escaping ImportCloudProfile, - loadDeviceID: @escaping LoadDeviceID - ) { - self.snapshotOfProfileForExport = snapshotOfProfileForExport - self.loadProfileBackups = loadProfileBackups - self.lookupProfileSnapshotByHeader = lookupProfileSnapshotByHeader - self.importProfileSnapshot = importProfileSnapshot - self.didExportProfileSnapshot = didExportProfileSnapshot - self.importCloudProfile = importCloudProfile - self.loadDeviceID = loadDeviceID - } -} - -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 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 deleted file mode 100644 index a8c2da36ca..0000000000 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Live.swift +++ /dev/null @@ -1,96 +0,0 @@ - -extension BackupsClient: DependencyKey { - public typealias Value = BackupsClient - - public static let liveValue = Self.live() - - public static func live( - profileStore: ProfileStore = .shared - ) -> Self { - @Dependency(\.userDefaults) var userDefaults - @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 - }, - 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 - } - }, - 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 - try await importFor(factorSourceIDs: factorSourceIDs) { - try await profileStore.importProfileSnapshot(snapshot) - userDefaults.setShowRelinkConnectorsAfterProfileRestore(containsP2PLinks) - } - }, - 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 deleted file mode 100644 index 15a251d5b2..0000000000 --- a/RadixWallet/Clients/BackupsClient/BackupsClient+Test.swift +++ /dev/null @@ -1,32 +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"), - loadProfileBackups: unimplemented("\(Self.self).loadProfile"), - 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") - ) - - public static let noop = Self( - snapshotOfProfileForExport: { throw NoopError() }, - loadProfileBackups: { nil }, - lookupProfileSnapshotByHeader: { _ in throw NoopError() }, - importProfileSnapshot: { _, _, _ in throw NoopError() }, - didExportProfileSnapshot: { _ in throw NoopError() }, - importCloudProfile: { _, _, _ in throw NoopError() }, - loadDeviceID: { nil } - ) -} diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift index 77220ee534..56221658b7 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Interface.swift @@ -6,44 +6,44 @@ 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 public let lastBackup: LastBackup public let loadProfile: LoadProfile public let loadProfileHeaders: LoadProfileHeaders + public let claimProfileOnICloud: ClaimProfileOnICloud public init( startAutomaticBackups: @escaping StartAutomaticBackups, - loadDeviceID: @escaping LoadDeviceID, migrateProfilesFromKeychain: @escaping MigrateProfilesFromKeychain, deleteProfileBackup: @escaping DeleteProfileBackup, checkAccountStatus: @escaping CheckAccountStatus, lastBackup: @escaping LastBackup, loadProfile: @escaping LoadProfile, - loadProfileHeaders: @escaping LoadProfileHeaders + loadProfileHeaders: @escaping LoadProfileHeaders, + claimProfileOnICloud: @escaping ClaimProfileOnICloud ) { self.startAutomaticBackups = startAutomaticBackups - self.loadDeviceID = loadDeviceID self.migrateProfilesFromKeychain = migrateProfilesFromKeychain self.deleteProfileBackup = deleteProfileBackup self.checkAccountStatus = checkAccountStatus self.lastBackup = lastBackup self.loadProfile = loadProfile self.loadProfileHeaders = loadProfileHeaders + self.claimProfileOnICloud = claimProfileOnICloud } } 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 public typealias LastBackup = @Sendable (ProfileID) -> AnyAsyncSequence public typealias LoadProfile = @Sendable (ProfileID) async throws -> BackedUpProfile public typealias LoadProfileHeaders = @Sendable () async throws -> [Profile.Header] + 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 04595acd91..21a794ed1a 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift @@ -46,23 +46,24 @@ extension CloudBackupClient { struct IncorrectRecordTypeError: Error {} struct NoProfileInRecordError: Error {} 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() public static func live( profileStore: ProfileStore = .shared ) -> CloudBackupClient { + @Dependency(\.overlayWindowClient) var overlayWindowClient @Dependency(\.secureStorageClient) var secureStorageClient @Dependency(\.userDefaults) var userDefaults 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) } @@ -70,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() } } @@ -92,16 +93,12 @@ 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) } @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) @@ -123,59 +120,99 @@ extension CloudBackupClient { } @Sendable - func backupProfileAndSaveResult(_ profile: Profile) async { - let existingRecord = try? await fetchProfileRecord(.init(recordName: profile.id.uuidString)) - let result: BackupResult.Result + func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async throws { + try? userDefaults.setLastCloudBackup(.started(.now), of: profile) + 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 failure: BackupResult.Result.Failure + switch error { + case CKError.accountTemporarilyUnavailable: + failure = .temporarilyUnavailable + case CKError.notAuthenticated: + failure = .notAuthenticated + default: + loggerGlobal.error("Automatic cloud backup failed with error \(error)") + failure = .other + } + + try? userDefaults.setLastCloudBackup(.failure(failure), of: profile) + throw error } - try? userDefaults.setLastCloudBackup(result, of: profile) + try? userDefaults.setLastCloudBackup(.success, of: profile) } + @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) + let isBackedUp = backedUpHeader?.saveIdentifier == profile.header.saveIdentifier + let shouldBackUp = needsBackUp && !isBackedUp + + guard shouldBackUp || timeToCheckIfClaimed else { return } + + let shouldReclaim: Bool + 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): + shouldReclaim = true + case .claimWallet(.didClearWallet), .dismiss: + return + } + } else { + shouldReclaim = false + } + + guard shouldBackUp || shouldReclaim else { return } + + 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) + let checkClaimedProfileInterval: TimeInterval = 15 * 60 + return .init( startAutomaticBackups: { - let timer = AsyncTimerSequence(every: .seconds(60)) + // The active profile should not be synced to iCloud keychain + let profileID = await profileStore.profile.id + try secureStorageClient.disableCloudProfileSync(profileID) + + let ticks = AsyncTimerSequence(every: retryBackupInterval) let profiles = await profileStore.values() + var lastClaimCheck: Date = .distantPast - for try await (profile, _) in combineLatest(profiles, timer) { + for try await (profile, tick) in combineLatest(profiles, ticks) { guard !Task.isCancelled else { return } - 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 } - - await backupProfileAndSaveResult(profile) + if tick.timeIntervalSince(lastClaimCheck) > checkClaimedProfileInterval { + await performAutomaticBackup(profile, timeToCheckIfClaimed: true) + lastClaimCheck = .now + } else { + await performAutomaticBackup(profile, timeToCheckIfClaimed: false) + } } }, - loadDeviceID: { - try? secureStorageClient.loadDeviceInfo()?.id - }, migrateProfilesFromKeychain: { let activeProfile = await profileStore.profile.id - let backedUpRecords = try await fetchAllProfileRecords() guard let headerList = try secureStorageClient.loadProfileHeaderList() else { return [] } let previouslyMigrated = userDefaults.getMigratedKeychainProfiles 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 { - 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 } @@ -184,17 +221,16 @@ extension CloudBackupClient { return (profileData, header) } - let migrated = try await migratable.asyncMap { profileData, header in + 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 { - // 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 } - let uploadedRecord = try await uploadProfileToICloud(.left(profileData), header: header, existingRecord: backedUpRecord) - try secureStorageClient.updateIsCloudProfileSyncEnabled(header.id, .disable) - - return uploadedRecord + return try await backupProfile(.left(profileData), header: header, existingRecord: backedUpRecord) } let migratedIDs = migrated.compactMap { ProfileID(uuidString: $0.recordID.recordName) } @@ -213,12 +249,20 @@ 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) + try await fetchAllProfileHeaders() .map(getProfileHeader) .filter(\.isNonEmpty) + }, + claimProfileOnICloud: { profile in + let existingRecord = try? await fetchProfileRecord(profile.id) + do { + try await backupProfileAndSaveResult(profile, existingRecord: existingRecord) + } catch { + throw FailedToClaimProfileError(error: error) + } } ) } @@ -228,30 +272,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 { diff --git a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift index 02be1e332b..b79d42193e 100644 --- a/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift +++ b/RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Test.swift @@ -16,23 +16,23 @@ extension CloudBackupClient: TestDependencyKey { public static let noop = Self( startAutomaticBackups: {}, - loadDeviceID: { nil }, migrateProfilesFromKeychain: { throw NoopError() }, deleteProfileBackup: { _ in }, checkAccountStatus: { throw NoopError() }, lastBackup: { _ in AsyncLazySequence([]).eraseToAnyAsyncSequence() }, loadProfile: { _ in throw NoopError() }, - loadProfileHeaders: { throw NoopError() } + loadProfileHeaders: { throw NoopError() }, + claimProfileOnICloud: { _ in throw NoopError() } ) 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"), lastBackup: unimplemented("\(Self.self).lastBackup"), loadProfile: unimplemented("\(Self.self).loadProfile"), - loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders") + loadProfileHeaders: unimplemented("\(Self.self).loadProfileHeaders"), + claimProfileOnICloud: unimplemented("\(Self.self).claimProfileOnICloud") ) } diff --git a/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift b/RadixWallet/Clients/DeviceFactorSourceClient/DeviceFactorSourceClient+Interface.swift index 24e0da4a35..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 -> (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 071efb0c24..33edf4b8c2 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,68 +73,76 @@ extension DeviceFactorSourceClient: DependencyKey { ) } - 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 + struct KeychainPresenceOfMnemonic: Sendable, Equatable { + let id: FactorSourceIDFromHash + let present: Bool + } - let mnemonicMissingFactorSources = factorSources.filter { - !secureStorageClient.containsMnemonicIdentifiedByFactorSourceID($0.id) - }.map(\.id) + @Sendable + 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 + KeychainPresenceOfMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id)) + } + } + .removeDuplicates() + .eraseToAnyAsyncSequence() + } - let mnemonicPresentFactorSources = factorSources.filter { - secureStorageClient.containsMnemonicIdentifiedByFactorSourceID($0.id) - } + let entitiesInBadState: @Sendable () async throws -> AnyAsyncSequence<(withoutControl: AddressesOfEntitiesInBadState, unrecoverable: AddressesOfEntitiesInBadState)> = { + await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { presencesOfMnemonics, backedUpFactorSources, profile in - let unrecoverableFactorSources = mnemonicPresentFactorSources.filter { - !userDefaults.getFactorSourceIDOfBackedUpMnemonics().contains($0.id) - }.map(\.id) + let mnemonicMissingFactorSources = presencesOfMnemonics + .filter(not(\.present)) + .map(\.id) - func mnemonicMissing(_ account: Account) -> Bool { - switch account.securityState { - case let .unsecured(value): - mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) - } - } + let mnemomincPresentFactorSources = presencesOfMnemonics + .filter(\.present) + .map(\.id) - func mnemonicMissing(_ persona: Persona) -> Bool { - switch persona.securityState { - case let .unsecured(value): - mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId) - } - } + let unrecoverableFactorSources = mnemomincPresentFactorSources + .filter { !backedUpFactorSources.contains($0) } - func unrecoverable(_ account: Account) -> Bool { - switch account.securityState { - case let .unsecured(value): - unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) - } - } + 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 unrecoverable(_ persona: Persona) -> Bool { - switch persona.securityState { - case let .unsecured(value): - unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) + func withoutControl(_ entity: some EntityProtocol) -> Bool { + switch entity.securityState { + case let .unsecured(value): + mnemonicMissingFactorSources.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) - ) + func unrecoverable(_ entity: some EntityProtocol) -> Bool { + switch entity.securityState { + case let .unsecured(value): + unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId) + } + } - return (mnemonicMissing: mnemonicMissing, unrecoverable: unrecoverable) + 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 = 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 (withoutControl: withoutControl, unrecoverable: unrecoverable) + } + .eraseToAnyAsyncSequence() } return Self( @@ -195,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 75af449a23..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: { (mnemonicMissing: .empty, unrecoverable: .empty) } + 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 f9692adfbd..9880f25826 100644 --- a/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift +++ b/RadixWallet/Clients/KeychainClient/KeychainClient+Interface.swift @@ -17,6 +17,10 @@ public struct KeychainClient: Sendable { public var _removeAllItems: RemoveAllItems public var _getAllKeysMatchingAttributes: GetAllKeysMatchingAttributes + /// 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, @@ -28,7 +32,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 +46,7 @@ public struct KeychainClient: Sendable { self._removeDataForKey = removeDataForKey self._removeAllItems = removeAllItems self._getAllKeysMatchingAttributes = getAllKeysMatchingAttributes + self._keychainChanged = keychainChanged } } @@ -86,6 +92,8 @@ extension KeychainClient { (synchronizable: Bool?, accessibility: KeychainAccess.Accessibility?) ) -> [Key] + + public typealias KeychainChanged = @Sendable () -> AnyAsyncSequence } // MARK: - KeychainAttributes @@ -140,6 +148,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/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/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/OverlayWindowClient/OverlayWindowClient+Interface.swift b/RadixWallet/Clients/OverlayWindowClient/OverlayWindowClient+Interface.swift index 212643a354..89da18b1ac 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 wait 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.DelegateAction + 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+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 a8d1fc3d05..1d8b0e018d 100644 --- a/RadixWallet/Clients/ProfileStore/ProfileStore.swift +++ b/RadixWallet/Clients/ProfileStore/ProfileStore.swift @@ -19,13 +19,12 @@ import Sargon /// /// var profile: Profile { get } /// func values() -> AnyAsyncSequence -/// 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 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 @@ -60,35 +59,16 @@ 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(bufferedProfileOwnershipConflict: ConflictingOwners?) - } - 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) } } -// 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. @@ -119,38 +99,11 @@ 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. - /// - 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. + /// + /// 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 @@ -164,8 +117,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() @@ -208,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 @@ -229,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( @@ -253,23 +210,8 @@ extension ProfileStore { try importProfile(profile) } - 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 - } + public func isThisDevice(deviceID: DeviceID) -> Bool { + deviceID == deviceInfo.id } } @@ -307,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( @@ -318,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) @@ -361,47 +305,17 @@ extension ProfileStore { try _updateHeader(of: &toSave) try _saveProfileAndEmitUpdate(toSave) } - - private var appIsUnlocked: Bool { - switch mode { - case .appIsUnlocked: true - 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 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) + profile.header.lastModified = date() } /// Updates the header of a Profile, lastModified date, contentHint etc. @@ -452,48 +366,15 @@ 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) - } - - 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.scheduleAlertAwaitAction(.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 extension ProfileStore { - typealias NewProfileTuple = (deviceInfo: DeviceInfo, profile: Profile, conflictingOwners: ConflictingOwners?) + typealias NewProfileTuple = (deviceInfo: DeviceInfo, profile: Profile) private static func _loadSavedElseNewProfile( metaDeviceInfo: MetaDeviceInfo @@ -504,8 +385,7 @@ extension ProfileStore { func newProfile() throws -> NewProfileTuple { try ( deviceInfo: metaDeviceInfo.deviceInfo, - profile: _tryGenerateAndSaveNewProfile(deviceInfo: deviceInfo), - conflictingOwners: nil + profile: _tryGenerateAndSaveNewProfile(deviceInfo: deviceInfo) ) } @@ -542,11 +422,7 @@ extension ProfileStore { } return ( deviceInfo: deviceInfo, - profile: existing, - conflictingOwners: matchingIDs ? nil : .init( - ownerOfCurrentProfile: existing.header.lastUsedOnDevice, - thisDevice: deviceInfo - ) + profile: existing ) } else { return try newProfile() @@ -609,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, diff --git a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift index ce91386da3..1bb1b608e4 100644 --- a/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift +++ b/RadixWallet/Clients/ResetWalletClient/ResetWalletClient+Live.swift @@ -4,16 +4,24 @@ 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 { + 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/Clients/SecureStorageClient/SecureStorageClient+Interface.swift b/RadixWallet/Clients/SecureStorageClient/SecureStorageClient+Interface.swift index 3502857f9d..63bbf4e55e 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 @@ -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 @@ -56,7 +57,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, @@ -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 @@ -80,7 +82,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 @@ -93,6 +95,7 @@ public struct SecureStorageClient: Sendable { self.loadP2PLinksPrivateKey = loadP2PLinksPrivateKey self.saveP2PLinksPrivateKey = saveP2PLinksPrivateKey self.getAllMnemonics = getAllMnemonics + self.keychainChanged = keychainChanged } #else @@ -107,7 +110,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, @@ -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 @@ -130,7 +134,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 @@ -142,6 +146,7 @@ public struct SecureStorageClient: Sendable { self.saveP2PLinks = saveP2PLinks self.loadP2PLinksPrivateKey = loadP2PLinksPrivateKey self.saveP2PLinksPrivateKey = saveP2PLinksPrivateKey + self.keychainChanged = keychainChanged } #endif // DEBUG } @@ -153,7 +158,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? @@ -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 @@ -246,15 +253,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 14d235401d..e3cf55ac0d 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) @@ -203,7 +194,7 @@ extension SecureStorageClient: DependencyKey { authenticationPrompt: authenticationPrompt ) else { if notifyIfMissing { - overlayWindowClient.scheduleAlertIgnoreAction(.missingMnemonicAlert) + overlayWindowClient.scheduleAlertAndIgnoreAction(.missingMnemonicAlert) } return nil } @@ -273,8 +264,7 @@ extension SecureStorageClient: DependencyKey { let saveProfileSnapshot: SaveProfileSnapshot = { profile in try saveProfile( snapshotData: profile.profileSnapshot(), - key: profile.header.id.keychainKey, - iCloudSyncEnabled: profile.appPreferences.security.isCloudProfileSyncEnabled + 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 = { @@ -412,6 +391,8 @@ extension SecureStorageClient: DependencyKey { loggerGlobal.notice("Saved p2pLinksPrivateKeyKey") } + let keychainChanged = keychainClient.keychainChanged + #if DEBUG return Self( saveProfileSnapshot: saveProfileSnapshot, @@ -424,7 +405,7 @@ extension SecureStorageClient: DependencyKey { containsMnemonicIdentifiedByFactorSourceID: containsMnemonicIdentifiedByFactorSourceID, deleteMnemonicByFactorSourceID: deleteMnemonicByFactorSourceID, deleteProfileAndMnemonicsByFactorSourceIDs: deleteProfileAndMnemonicsByFactorSourceIDs, - updateIsCloudProfileSyncEnabled: updateIsCloudProfileSyncEnabled, + disableCloudProfileSync: disableCloudProfileSync, loadProfileHeaderList: loadProfileHeaderList, saveProfileHeaderList: saveProfileHeaderList, deleteProfileHeaderList: deleteProfileHeaderList, @@ -436,6 +417,7 @@ extension SecureStorageClient: DependencyKey { saveP2PLinks: saveP2PLinks, loadP2PLinksPrivateKey: loadP2PLinksPrivateKey, saveP2PLinksPrivateKey: saveP2PLinksPrivateKey, + keychainChanged: keychainChanged, getAllMnemonics: getAllMnemonics ) #else @@ -450,7 +432,7 @@ extension SecureStorageClient: DependencyKey { containsMnemonicIdentifiedByFactorSourceID: containsMnemonicIdentifiedByFactorSourceID, deleteMnemonicByFactorSourceID: deleteMnemonicByFactorSourceID, deleteProfileAndMnemonicsByFactorSourceIDs: deleteProfileAndMnemonicsByFactorSourceIDs, - updateIsCloudProfileSyncEnabled: updateIsCloudProfileSyncEnabled, + disableCloudProfileSync: disableCloudProfileSync, loadProfileHeaderList: loadProfileHeaderList, saveProfileHeaderList: saveProfileHeaderList, deleteProfileHeaderList: deleteProfileHeaderList, @@ -461,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 cb8929786d..7df191ab50 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: {}, @@ -32,6 +32,7 @@ extension SecureStorageClient: TestDependencyKey { saveP2PLinks: { _ in }, loadP2PLinksPrivateKey: { nil }, saveP2PLinksPrivateKey: { _ in }, + keychainChanged: { AsyncLazySequence([]).eraseToAnyAsyncSequence() }, getAllMnemonics: { [] } ) #else @@ -46,7 +47,7 @@ extension SecureStorageClient: TestDependencyKey { containsMnemonicIdentifiedByFactorSourceID: { _ in false }, deleteMnemonicByFactorSourceID: { _ in }, deleteProfileAndMnemonicsByFactorSourceIDs: { _, _ in }, - updateIsCloudProfileSyncEnabled: { _, _ in }, + disableCloudProfileSync: { _ in }, loadProfileHeaderList: { nil }, saveProfileHeaderList: { _ in }, deleteProfileHeaderList: {}, @@ -57,7 +58,8 @@ extension SecureStorageClient: TestDependencyKey { loadP2PLinks: { nil }, saveP2PLinks: { _ in }, loadP2PLinksPrivateKey: { nil }, - saveP2PLinksPrivateKey: { _ in } + saveP2PLinksPrivateKey: { _ in }, + keychainChanged: { AsyncLazySequence([]).eraseToAnyAsyncSequence() } ) #endif // DEBUG @@ -75,7 +77,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"), @@ -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 @@ -101,7 +104,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"), @@ -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 } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Interface.swift index f5ebadecbb..4d485f70a0 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 @@ -22,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. @@ -36,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 } @@ -74,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 @@ -156,12 +158,13 @@ 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 { + public let result: BackupResult + public let isCurrent: Bool + + public init(result: BackupResult, profile: Profile) { + self.result = result + self.isCurrent = result.saveIdentifier == profile.header.saveIdentifier } } diff --git a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift index 66766fed32..eda93b1564 100644 --- a/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift +++ b/RadixWallet/Clients/SecurityCenterClient/SecurityCenterClient+Live.swift @@ -43,73 +43,73 @@ 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.profileHash == profile.hashValue - 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 in + backup.map { BackupStatus(result: $0, profile: profile) } + } + .eraseToAnyAsyncSequence() } - return .init( - problems: { type in - let profiles = await profileStore.values() - let cloudBackups = await cloudBackups() - let manualBackups = await manualBackups() + let problemsSubject = AsyncCurrentValueSubject<[SecurityProblem]>([]) - return combineLatest(profiles, cloudBackups, manualBackups).map { profile, cloudBackup, manualBackup in - let isCloudProfileSyncEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled + @Sendable + func startMonitoring() async throws { + let profileValues = await profileStore.values() + let entitiesInBadState = try await deviceFactorSourceClient.entitiesInBadState() + let backupValues = await combineLatest(cloudBackups(), manualBackups()).map { (cloud: $0, manual: $1) } - let problematic = try? await deviceFactorSourceClient.problematicEntities() + for try await (profile, entitiesInBadState, backups) in combineLatest(profileValues, entitiesInBadState, backupValues) { + let isCloudProfileSyncEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled - func hasProblem3() async -> ProblematicAddresses? { - guard let problematic, !problematic.unrecoverable.isEmpty else { return nil } - return problematic.unrecoverable - } + func hasProblem3() async -> AddressesOfEntitiesInBadState? { + entitiesInBadState.unrecoverable.isEmpty ? nil : entitiesInBadState.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 = backups.cloud { + cloudBackup.result.failed + } else { + false } + } - func hasProblem6() -> Bool { - !isCloudProfileSyncEnabled && manualBackup == nil - } + func hasProblem6() -> Bool { + !isCloudProfileSyncEnabled && backups.manual == nil + } - func hasProblem7() -> Bool { - !isCloudProfileSyncEnabled && manualBackup?.upToDate == false - } + func hasProblem7() -> Bool { + !isCloudProfileSyncEnabled && backups.manual?.isCurrent == false + } - func hasProblem9() async -> ProblematicAddresses? { - guard let problematic, !problematic.mnemonicMissing.isEmpty else { return nil } - return problematic.mnemonicMissing - } + func hasProblem9() async -> AddressesOfEntitiesInBadState? { + entitiesInBadState.withoutControl.isEmpty ? nil : entitiesInBadState.withoutControl + } - var result: [SecurityProblem] = [] + var result: [SecurityProblem] = [] - if type == nil || type == .securityFactors { - if let addresses = await hasProblem3() { - result.append(.problem3(addresses: addresses)) - } + if let addresses = await hasProblem3() { + result.append(.problem3(addresses: addresses)) + } - if let addresses = await hasProblem9() { - result.append(.problem9(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 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 } } + .removeDuplicates() + .eraseToAnyAsyncSequence() }, lastManualBackup: manualBackups, lastCloudBackup: cloudBackups @@ -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/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/Clients/TransportProfileClient/TransportProfileClient+Interface.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift new file mode 100644 index 0000000000..b3459e1a01 --- /dev/null +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Interface.swift @@ -0,0 +1,26 @@ +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 init( + importProfile: @escaping ImportProfile, + profileForExport: @escaping ProfileForExport, + didExportProfile: @escaping DidExportProfile + ) { + self.importProfile = importProfile + self.profileForExport = profileForExport + self.didExportProfile = didExportProfile + } +} + +extension TransportProfileClient { + 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/Clients/TransportProfileClient/TransportProfileClient+Live.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift new file mode 100644 index 0000000000..dec6d082eb --- /dev/null +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Live.swift @@ -0,0 +1,36 @@ + +extension TransportProfileClient: DependencyKey { + public static let liveValue = Self.live() + + public static func live( + profileStore: ProfileStore = .shared + ) -> 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 { + // Revert the saved mnemonic + for factorSourceID in factorSourceIDs { + try? secureStorageClient.deleteMnemonicByFactorSourceID(factorSourceID) + } + throw error + } + }, + profileForExport: { + await profileStore.profile + }, + didExportProfile: { profile in + try userDefaults.setLastManualBackup(of: profile) + } + ) + } +} diff --git a/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift new file mode 100644 index 0000000000..5b2681521b --- /dev/null +++ b/RadixWallet/Clients/TransportProfileClient/TransportProfileClient+Test.swift @@ -0,0 +1,24 @@ + +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") + ) + + public static let noop = Self( + importProfile: { _, _, _ in throw NoopError() }, + profileForExport: { throw NoopError() }, + didExportProfile: { _ in throw NoopError() } + ) +} diff --git a/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift b/RadixWallet/Clients/UserDefaults+Dependency+Extension/UserDefaults+Dependency+Extension.swift index 74f004bc13..dc20e16f37 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, - profileHash: profile.hashValue, - result: result + date: now, + saveIdentifier: profile.header.saveIdentifier, + 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, - profileHash: profile.hashValue, - result: .success + date: now, + saveIdentifier: profile.header.saveIdentifier, + result: .success, + lastSuccess: now ) try save(codable: backups, forKey: .lastManualBackups) @@ -232,15 +237,44 @@ extension UserDefaults.Dependency { } // MARK: - BackupResult -public struct BackupResult: Codable, Sendable { - public let backupDate: Date - public let profileHash: Int +public struct BackupResult: Hashable, Codable, Sendable { + private static let timeoutInterval: TimeInterval = 5 * 60 + + public let date: Date + public let saveIdentifier: String public let result: Result + public let lastSuccess: Date? + + 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: Codable, Sendable { + public enum Result: Hashable, Codable, Sendable { + case started(Date) case success - case temporarilyUnavailable - case notAuthenticated - case failure + case failure(Failure) + + public enum Failure: Hashable, Codable, Sendable { + case temporarilyUnavailable + case notAuthenticated + case other + } + } +} + +extension Profile.Header { + public var saveIdentifier: String { + "\(lastModified.timeIntervalSince1970)-\(lastUsedOnDevice.id.uuidString)" } } 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/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/AppFeature/App+Reducer.swift b/RadixWallet/Features/AppFeature/App+Reducer.swift index 21dec694a8..30392e9e98 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,30 @@ 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 { + for try await _ in resetWalletClient.walletDidReset() { + guard !Task.isCancelled else { return } + await send(.internal(.didResetWallet)) + } + } 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..8a651c4f70 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) { @@ -16,6 +17,7 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { } public enum DelegateAction: Sendable, Equatable { + case claimWallet(ClaimWallet.DelegateAction) case dismiss } @@ -48,8 +50,9 @@ public struct FullScreenOverlayCoordinator: Sendable, FeatureReducer { public func reduce(into state: inout State, childAction: ChildAction) -> Effect { switch childAction { - case .root(.claimWallet(.delegate)): - .send(.delegate(.dismiss)) + // Forward all delegate actions, re-wrapped + case let .root(.claimWallet(.delegate(action))): + .send(.delegate(.claimWallet(action))) default: .none diff --git a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift index 1c337754c3..99aa270184 100644 --- a/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift +++ b/RadixWallet/Features/AppFeature/Overlay/Overlay+Reducer.swift @@ -83,14 +83,18 @@ 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) + case .hud(.delegate(.dismiss)): return dismiss(&state) - case .fullScreen(.delegate(.dismiss)): + case let .fullScreen(.delegate(action)): + if case let .fullScreen(state) = state.itemsQueue.first { + overlayWindowClient.sendFullScreenAction(action, state.id) + } return dismiss(&state) default: @@ -99,19 +103,24 @@ struct OverlayReducer: Sendable, FeatureReducer { } func reduceDismissedDestination(into state: inout State) -> Effect { - dismissAlert(state: &state, withAction: .dismissed) + 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) } 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 @@ -126,9 +135,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 @@ -141,15 +148,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/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/ClaimWallet/ClaimWallet+Reducer.swift b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift index 181d282898..301c4fb103 100644 --- a/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift +++ b/RadixWallet/Features/ClaimWallet/ClaimWallet+Reducer.swift @@ -4,17 +4,13 @@ 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 screenState: ControlState { isLoading ? .loading(.global(text: nil)) : .enabled } - public init( - isLoading: Bool = false - ) { - self.isLoading = isLoading - } + public init() {} } public enum ViewAction: Sendable, Equatable { @@ -24,7 +20,7 @@ public struct ClaimWallet: Sendable, FeatureReducer { public enum DelegateAction: Sendable, Equatable { case didClearWallet - case didTransferBack + case transferBack } @Dependency(\.resetWalletClient) var resetWalletClient @@ -34,13 +30,13 @@ public struct ClaimWallet: Sendable, FeatureReducer { public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { switch viewAction { case .clearWalletButtonTapped: - .run { send in + state.isLoading = true + return .run { send in await resetWalletClient.resetWallet() await send(.delegate(.didClearWallet)) } case .transferBackButtonTapped: - // TODO: transfer back - .send(.delegate(.didTransferBack)) + return .send(.delegate(.transferBack)) } } } 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 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) } 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/MainFeature/Main+Reducer.swift b/RadixWallet/Features/MainFeature/Main+Reducer.swift index 0cfcf5e1bd..98b17969b0 100644 --- a/RadixWallet/Features/MainFeature/Main+Reducer.swift +++ b/RadixWallet/Features/MainFeature/Main+Reducer.swift @@ -17,48 +17,48 @@ 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() } } } - @Dependency(\.appPreferencesClient) var appPreferencesClient @Dependency(\.gatewaysClient) var gatewaysClient @Dependency(\.personasClient) var personasClient @Dependency(\.cloudBackupClient) var cloudBackupClient - @Dependency(\.resetWalletClient) var resetWalletClient + @Dependency(\.securityCenterClient) var securityCenterClient public init() {} public var body: some ReducerOf { - Scope(state: \.home, action: /Action.child .. ChildAction.home) { + Scope(state: \.home, action: \.child.home) { Home() } Reduce(core) @@ -73,8 +73,8 @@ public struct Main: Sendable, FeatureReducer { switch viewAction { case .task: startAutomaticBackupsEffect() + .merge(with: startMonitoringSecurityCenterEffect()) .merge(with: gatewayValuesEffect()) - .merge(with: didResetWalletEffect()) } } @@ -88,25 +88,23 @@ public struct Main: Sendable, FeatureReducer { } } - private func gatewayValuesEffect() -> Effect { - .run { send in - for try await gateway in await gatewaysClient.currentGatewayValues() { - guard !Task.isCancelled else { return } - loggerGlobal.notice("Changed network to: \(gateway)") - await send(.internal(.currentGatewayChanged(to: gateway))) + private func startMonitoringSecurityCenterEffect() -> Effect { + .run { _ in + do { + try await securityCenterClient.startMonitoring() + } catch { + loggerGlobal.notice("securityCenterClient.startMonitoring failed: \(error)") } } } - private func didResetWalletEffect() -> Effect { + private func gatewayValuesEffect() -> Effect { .run { send in - for try await _ in resetWalletClient.walletDidReset() { + for try await gateway in await gatewaysClient.currentGatewayValues() { guard !Task.isCancelled else { return } - try await appPreferencesClient.deleteProfileAndFactorSources(true) - await send(.delegate(.removedWallet)) + loggerGlobal.notice("Changed network to: \(gateway)") + await send(.internal(.currentGatewayChanged(to: gateway))) } - } catch: { error, _ in - loggerGlobal.error("Failed to delete profile: \(error)") } } 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) + } } } 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/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/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/Children/SelectBackup/SelectBackup+Reducer.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/SelectBackup/SelectBackup+Reducer.swift index b9130a471d..9d8dd406ed 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) } @@ -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() } @@ -152,7 +153,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 +207,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") @@ -227,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))) 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))) diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Coordinator/RestoreProfileFromBackupCoordinator.swift index 7264744ada..2008a972ad 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 } @@ -22,22 +21,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 +47,7 @@ public struct RestoreProfileFromBackupCoordinator: Sendable, FeatureReducer { case delayedAppendToPath(RestoreProfileFromBackupCoordinator.Path.State) } + @CasePathable public enum ChildAction: Sendable, Equatable { case root(Path.Action) case path(StackActionOf) @@ -59,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 @@ -68,12 +69,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() } } @@ -88,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)) @@ -112,11 +113,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 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 aca022f9e8..a4aefc18f6 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,16 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { public var exportable: Exportable? = nil public var outdatedBackupPresent: Bool { - guard let lastCloudBackup, lastCloudBackup.success else { return false } - return !cloudBackupsEnabled && !lastCloudBackup.upToDate + guard let lastCloudBackup, lastCloudBackup.result.succeeded else { return false } + return !cloudBackupsEnabled && !lastCloudBackup.isCurrent } public var actionsRequired: [Item] { - problems.isEmpty ? [] : Item.allCases + if let lastCloudBackup, lastCloudBackup.isCurrent, !lastCloudBackup.result.failed { + [] + } else { + Item.allCases + } } public init() {} @@ -58,7 +60,6 @@ public struct ConfigurationBackup: Sendable, FeatureReducer { case setProblems([SecurityProblem]) case setLastManualBackup(Date?) case setLastCloudBackup(BackupStatus?) - case didDeleteOutdatedBackup(ProfileID) case exportProfile(Profile) } @@ -100,7 +101,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 @@ -127,11 +128,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)") } @@ -142,7 +142,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 @@ -178,9 +178,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 @@ -211,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?.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 68fd37d653..1a66440d5c 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 let lastCloudBackup, lastCloudBackup.isCurrent, lastCloudBackup.result.succeeded { + 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, - lastBackedUp: viewStore.lastCloudBackup, + 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 lastBackedUp: BackupStatus? + 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,11 +226,6 @@ extension ConfigurationBackup { } } - private var lastBackedUpString: String? { - guard let lastBackedUp, lastBackedUp.success, !lastBackedUp.upToDate else { return nil } - return L10n.ConfigurationBackup.Automated.lastBackup(RadixDateFormatter.string(from: lastBackedUp.backupDate)) - } - struct ItemView: SwiftUI.View { @SwiftUI.State private var expanded: Bool = false let item: Item @@ -274,7 +286,7 @@ extension ConfigurationBackup { } struct ManualBackupView: SwiftUI.View { - let lastBackedUp: Date? + let lastBackupString: String? let exportAction: () -> Void var body: some SwiftUI.View { @@ -292,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) @@ -303,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 { 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() {} 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) + } } } 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) + )) } } diff --git a/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift b/RadixWalletTests/Clients/ProfileStoreTests/ProfileStoreTests.swift index 90f0c9c32f..b839fd3f6b 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 @@ -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" @@ -804,7 +772,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 @@ -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.scheduleAlertAwaitAction = { 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 { @@ -1120,16 +1032,15 @@ 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 } func when(_ d: inout DependencyValues) { - d.overlayWindowClient.scheduleAlertAwaitAction = { 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/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()) } } 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 2ba49aeaa85d39d7e20b0166698f04376f793a35 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 2/3] 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 8cd380f4c0e578175ee7fba8992814d3f685ca8e 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 3/3] 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) } } }