diff --git a/docs/src/about/Changelog.md b/docs/src/about/Changelog.md index 7bac212246..4ecbb868e9 100644 --- a/docs/src/about/Changelog.md +++ b/docs/src/about/Changelog.md @@ -39,7 +39,7 @@ Transactions content is shown before signing; no hash signing is allowed, but si #### History feature -The Vault now logs all operations it performs. It it important to remember that this is not log of account operations, but log of device history. This history could be cleared if needed, but not modified by other means. Detected presence of network connection is also logged. +The Vault now logs all operations it performs. It is important to remember that this is not log of account operations, but log of device history. This history could be cleared if needed, but not modified by other means. Detected presence of network connection is also logged. #### N+1 derivation diff --git a/ios/.swift-version b/ios/.swift-version index 95ee81a411..f9ce5a96ef 100644 --- a/ios/.swift-version +++ b/ios/.swift-version @@ -1 +1 @@ -5.9 +5.10 diff --git a/ios/Packages/Blockies/Package.swift b/ios/Packages/Blockies/Package.swift index 022b9464cb..f561d30d2e 100644 --- a/ios/Packages/Blockies/Package.swift +++ b/ios/Packages/Blockies/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 import PackageDescription diff --git a/ios/Packages/Jdenticon/Package.swift b/ios/Packages/Jdenticon/Package.swift index 4a8bd4db8b..e723e7b46d 100644 --- a/ios/Packages/Jdenticon/Package.swift +++ b/ios/Packages/Jdenticon/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.9 import PackageDescription diff --git a/ios/Packages/PolkadotIdenticon/Package.swift b/ios/Packages/PolkadotIdenticon/Package.swift index acaa52a5e4..db8147e0b9 100644 --- a/ios/Packages/PolkadotIdenticon/Package.swift +++ b/ios/Packages/PolkadotIdenticon/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 import PackageDescription diff --git a/ios/PolkadotVault.xcodeproj/project.pbxproj b/ios/PolkadotVault.xcodeproj/project.pbxproj index 8b54876578..df38cc0d2b 100644 --- a/ios/PolkadotVault.xcodeproj/project.pbxproj +++ b/ios/PolkadotVault.xcodeproj/project.pbxproj @@ -104,6 +104,9 @@ 6D476A1B29FC05F500326F74 /* AddKeySetUpNetworksStepTwoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D476A1A29FC05F500326F74 /* AddKeySetUpNetworksStepTwoView.swift */; }; 6D4C0753289AC36100B2EE48 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4C0751289AC36100B2EE48 /* Fonts.swift */; }; 6D4C0758289BD95500B2EE48 /* SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4C0757289BD95500B2EE48 /* SFSymbols.swift */; }; + 6D4CFFBD2BA1686E0061CC1B /* Jdenticon in Frameworks */ = {isa = PBXBuildFile; productRef = 6D4CFFBC2BA1686E0061CC1B /* Jdenticon */; }; + 6D4CFFBF2BA168710061CC1B /* PolkadotIdenticon in Frameworks */ = {isa = PBXBuildFile; productRef = 6D4CFFBE2BA168710061CC1B /* PolkadotIdenticon */; }; + 6D4CFFC12BA16A450061CC1B /* Blockies in Frameworks */ = {isa = PBXBuildFile; productRef = 6D4CFFC02BA16A450061CC1B /* Blockies */; }; 6D4F711929F913FC00729610 /* KeyDetailsPublicKeyViewRenderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4F711829F913FC00729610 /* KeyDetailsPublicKeyViewRenderable.swift */; }; 6D4F711B29F9142D00729610 /* PublicKeyDetailsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4F711A29F9142D00729610 /* PublicKeyDetailsService.swift */; }; 6D4F779929CAECA500374C83 /* SignSpecsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4F779829CAECA500374C83 /* SignSpecsListView.swift */; }; @@ -114,6 +117,11 @@ 6D52E3AA2946F58200AD72F0 /* VerifierCertificateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D52E3A92946F58200AD72F0 /* VerifierCertificateView.swift */; }; 6D52E3AF2946FF5400AD72F0 /* NetworkSelectionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D52E3AE2946FF5400AD72F0 /* NetworkSelectionModal.swift */; }; 6D52E3B12946FF5E00AD72F0 /* NetworkLogoIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D52E3B02946FF5E00AD72F0 /* NetworkLogoIcon.swift */; }; + 6D5338762B7D306700F37EB1 /* BananaSplitModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5338752B7D306700F37EB1 /* BananaSplitModal.swift */; }; + 6D5338782B7D7E9300F37EB1 /* IconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5338772B7D7E9300F37EB1 /* IconButton.swift */; }; + 6D53387A2B7E4CC400F37EB1 /* BananaSplitQRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5338792B7E4CC400F37EB1 /* BananaSplitQRCodeModal.swift */; }; + 6D53387D2B7E512C00F37EB1 /* BananaSplitActionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D53387C2B7E512C00F37EB1 /* BananaSplitActionModal.swift */; }; + 6D5338852B865F4500F37EB1 /* BananaSplitPassphraseModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5338842B865F4500F37EB1 /* BananaSplitPassphraseModal.swift */; }; 6D55F286292CF2F800871896 /* StrokeContainerBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D55F285292CF2F800871896 /* StrokeContainerBackground.swift */; }; 6D57DC4B289D614F00005C63 /* BackendNavigationAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D57DC4A289D614F00005C63 /* BackendNavigationAdapter.swift */; }; 6D57DC4D289D652400005C63 /* Dispatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D57DC4C289D652400005C63 /* Dispatching.swift */; }; @@ -128,11 +136,7 @@ 6D57DC6C289E524800005C63 /* Inter-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 6D57DC61289E524800005C63 /* Inter-Medium.otf */; }; 6D57DC6E289E524800005C63 /* Inter-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 6D57DC63289E524800005C63 /* Inter-SemiBold.otf */; }; 6D5801D12899130E006C41D8 /* PathMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5801D02899130E006C41D8 /* PathMonitorProtocol.swift */; }; - 6D5801D42899133A006C41D8 /* ConnectivityMonitoringAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5801D32899133A006C41D8 /* ConnectivityMonitoringAdapter.swift */; }; 6D5801D728991F11006C41D8 /* RuntimePropertiesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5801D628991F11006C41D8 /* RuntimePropertiesProvider.swift */; }; - 6D5801E1289924AD006C41D8 /* ConnectivityMonitoringAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5801E0289924AD006C41D8 /* ConnectivityMonitoringAdapterTests.swift */; }; - 6D5801E5289937BA006C41D8 /* ConnectivityMonitoringAssemblerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5801E4289937BA006C41D8 /* ConnectivityMonitoringAssemblerTests.swift */; }; - 6D5801E7289937C0006C41D8 /* ConnectivityMonitoringAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5801E6289937C0006C41D8 /* ConnectivityMonitoringAssembler.swift */; }; 6D5DB5E82B3C61A200EF82AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5DB5E72B3C61A200EF82AB /* AppDelegate.swift */; }; 6D5DB5EB2B3D1A9500EF82AB /* DevicePasscodeAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5DB5EA2B3D1A9500EF82AB /* DevicePasscodeAuthenticator.swift */; }; 6D5DCA032A7CFA2E0050B101 /* ExportKeysSelectionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5DCA022A7CFA2E0050B101 /* ExportKeysSelectionModal.swift */; }; @@ -155,11 +159,8 @@ 6D71290E294C2A380048558C /* VerticalActionsBottomModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D71290D294C2A380048558C /* VerticalActionsBottomModal.swift */; }; 6D7129222952B7800048558C /* NetworkSettingsDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D7129212952B7800048558C /* NetworkSettingsDetails.swift */; }; 6D7129252952C3D50048558C /* VerticalRoundedBackgroundContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D7129242952C3D50048558C /* VerticalRoundedBackgroundContainer.swift */; }; - 6D749C5C2A6871BA0064D7E5 /* Blockies in Frameworks */ = {isa = PBXBuildFile; productRef = 6D749C5B2A6871BA0064D7E5 /* Blockies */; }; 6D749C602A69C6020064D7E5 /* PolkadotIdenticonViewPreviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D749C5F2A69C6020064D7E5 /* PolkadotIdenticonViewPreviews.swift */; }; - 6D749C622A69CE2C0064D7E5 /* PolkadotIdenticon in Frameworks */ = {isa = PBXBuildFile; productRef = 6D749C612A69CE2C0064D7E5 /* PolkadotIdenticon */; }; 6D755C092A6FECCD00A73E20 /* JdenticonViewPreviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D755C082A6FECCD00A73E20 /* JdenticonViewPreviews.swift */; }; - 6D755C0B2A6FECE000A73E20 /* Jdenticon in Frameworks */ = {isa = PBXBuildFile; productRef = 6D755C0A2A6FECE000A73E20 /* Jdenticon */; }; 6D77863C2B626FD2009C8E73 /* CreateKeysForNetworksViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D77863B2B626FD2009C8E73 /* CreateKeysForNetworksViewModelTests.swift */; }; 6D77863F2B62FF6F009C8E73 /* EnterKeySetNameViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D77863E2B62FF6F009C8E73 /* EnterKeySetNameViewModelTests.swift */; }; 6D7786412B630116009C8E73 /* MNewSeedBackup+Generate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D7786402B630116009C8E73 /* MNewSeedBackup+Generate.swift */; }; @@ -195,7 +196,7 @@ 6D8973A52A08E18C0046A2F3 /* ScanTabService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8973A42A08E18C0046A2F3 /* ScanTabService.swift */; }; 6D8973A72A08E6550046A2F3 /* GeneralVerifierService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8973A62A08E6550046A2F3 /* GeneralVerifierService.swift */; }; 6D8AF88228BCC4D100CF0AB2 /* AccessControlProvidingAssemblerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8AF88128BCC4D100CF0AB2 /* AccessControlProvidingAssemblerTests.swift */; }; - 6D8AF88A28BCC60600CF0AB2 /* KeychainQueryProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8AF88928BCC60600CF0AB2 /* KeychainQueryProviderTests.swift */; }; + 6D8AF88A28BCC60600CF0AB2 /* KeychainSeedsQueryProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8AF88928BCC60600CF0AB2 /* KeychainSeedsQueryProviderTests.swift */; }; 6D8F813C2994C64E000ED0BA /* ErrorDisplayed+TransactionSigning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F813B2994C64E000ED0BA /* ErrorDisplayed+TransactionSigning.swift */; }; 6D91F3EE28C16163007560F5 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D91F3ED28C16163007560F5 /* CircularProgressView.swift */; }; 6D932CDF292E05CB008AD883 /* InlineButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D932CDE292E05CB008AD883 /* InlineButton.swift */; }; @@ -252,9 +253,8 @@ 6DAB52EF2B5E81BB005FDBA8 /* LogNoteModalViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAB52EE2B5E81BB005FDBA8 /* LogNoteModalViewModelTests.swift */; }; 6DAB52F22B5E832F005FDBA8 /* NoKeySetsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAB52F12B5E832F005FDBA8 /* NoKeySetsViewModelTests.swift */; }; 6DAFCAF82B0A360600DDD165 /* CameraPermissionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCAF72B0A360600DDD165 /* CameraPermissionHandlerTests.swift */; }; - 6DAFCAFA2B0AE5C000DDD165 /* KeychainAccessAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCAF92B0AE5C000DDD165 /* KeychainAccessAdapterTests.swift */; }; + 6DAFCAFA2B0AE5C000DDD165 /* KeychainSeedsAccessAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCAF92B0AE5C000DDD165 /* KeychainSeedsAccessAdapterTests.swift */; }; 6DAFCAFD2B0AE87300DDD165 /* RuntimePropertiesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCAFC2B0AE87300DDD165 /* RuntimePropertiesProviderTests.swift */; }; - 6DAFCB002B0AEB7E00DDD165 /* ConnectivityMediatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCAFF2B0AEB7E00DDD165 /* ConnectivityMediatorTests.swift */; }; 6DAFCB022B0AEE4900DDD165 /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCB012B0AEE4900DDD165 /* ApplicationStatePublisherTests.swift */; }; 6DAFCB042B0AEF6800DDD165 /* PasswordProtectionStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DAFCB032B0AEF6800DDD165 /* PasswordProtectionStatePublisherTests.swift */; }; 6DB2E7C12B4BBAF7002387DE /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DB2E7C02B4BBAF7002387DE /* SettingsViewModelTests.swift */; }; @@ -280,7 +280,6 @@ 6DBD2202289A8E1F005D539B /* ErrorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DBD2201289A8E1F005D539B /* ErrorMock.swift */; }; 6DBF6E2728E44DD700CC959F /* ErrorBottomModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DBF6E2628E44DD700CC959F /* ErrorBottomModal.swift */; }; 6DBF6E2928E47EE000CC959F /* ErrorBottomModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DBF6E2828E47EE000CC959F /* ErrorBottomModalViewModel.swift */; }; - 6DBF6E4328EACB7B00CC959F /* ConnectivityMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DBF6E4228EACB7B00CC959F /* ConnectivityMediator.swift */; }; 6DBF6E4B28EC51D600CC959F /* GenericError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DBF6E4A28EC51D600CC959F /* GenericError.swift */; }; 6DC0FC98296695D000A45883 /* SeedKeysPreview+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC0FC97296695D000A45883 /* SeedKeysPreview+Helpers.swift */; }; 6DC2EDF92B11961800298F00 /* DateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC2EDF82B11961800298F00 /* DateFormatterTests.swift */; }; @@ -293,8 +292,8 @@ 6DC5643328B68FC5003D540B /* AccessControlProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643228B68FC5003D540B /* AccessControlProvider.swift */; }; 6DC5643528B69355003D540B /* AccessControlProvidingAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643428B69355003D540B /* AccessControlProvidingAssembler.swift */; }; 6DC5643728B79EC6003D540B /* SeedsMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643628B79EC6003D540B /* SeedsMediator.swift */; }; - 6DC5643928B7DED8003D540B /* KeychainQueryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643828B7DED8003D540B /* KeychainQueryProvider.swift */; }; - 6DC5643B28B8D189003D540B /* KeychainAccessAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643A28B8D189003D540B /* KeychainAccessAdapter.swift */; }; + 6DC5643928B7DED8003D540B /* KeychainSeedsQueryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643828B7DED8003D540B /* KeychainSeedsQueryProvider.swift */; }; + 6DC5643B28B8D189003D540B /* KeychainSeedsAccessAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643A28B8D189003D540B /* KeychainSeedsAccessAdapter.swift */; }; 6DC5643D28B91EFE003D540B /* NavbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643C28B91EFE003D540B /* NavbarButton.swift */; }; 6DC5644028B929EA003D540B /* KeyDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC5643F28B929EA003D540B /* KeyDetailsView.swift */; }; 6DC909F629C87C8C00AE6BAD /* LogsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC909F529C87C8C00AE6BAD /* LogsService.swift */; }; @@ -307,6 +306,11 @@ 6DD860EA299CED3F0000D81E /* AirgapMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD860E9299CED3F0000D81E /* AirgapMediator.swift */; }; 6DD9FF1628C8B85300FB6195 /* HorizontalActionsBottomModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD9FF1528C8B85300FB6195 /* HorizontalActionsBottomModal.swift */; }; 6DD9FF1928C8C9B000FB6195 /* Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD9FF1828C8C9AF00FB6195 /* Animations.swift */; }; + 6DDD01D32B9504D1000F53B3 /* KeychainBananaSplitQueryProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD01D22B9504D1000F53B3 /* KeychainBananaSplitQueryProviderTests.swift */; }; + 6DDD01D72B958372000F53B3 /* BananaSplitPassphraseModalViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD01D62B958372000F53B3 /* BananaSplitPassphraseModalViewModelTests.swift */; }; + 6DDD01D92B958845000F53B3 /* BananaSplitActionModalViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD01D82B958845000F53B3 /* BananaSplitActionModalViewModelTests.swift */; }; + 6DDD01DB2B9589B5000F53B3 /* BananaSplitQRCodeModalViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD01DA2B9589B5000F53B3 /* BananaSplitQRCodeModalViewModelTests.swift */; }; + 6DDD01DE2B959BF3000F53B3 /* AirgapMediatorAssemblerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD01DD2B959BF3000F53B3 /* AirgapMediatorAssemblerTests.swift */; }; 6DDD38B22B11C3C2000D2B62 /* SeedsMediatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD38B12B11C3C2000D2B62 /* SeedsMediatorTests.swift */; }; 6DDD38B72B1346C8000D2B62 /* KeyDetailsPublicKeyViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD38B62B1346C8000D2B62 /* KeyDetailsPublicKeyViewModelTests.swift */; }; 6DDD38B92B134ADF000D2B62 /* MKeyDetails+Generate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DDD38B82B134ADF000D2B62 /* MKeyDetails+Generate.swift */; }; @@ -369,6 +373,8 @@ 6DF07ADE29C1062300C01DE8 /* SetUpNetworksIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF07ADD29C1062300C01DE8 /* SetUpNetworksIntroView.swift */; }; 6DF07AE229C1140500C01DE8 /* SetUpNetworksStepOneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF07AE129C1140500C01DE8 /* SetUpNetworksStepOneView.swift */; }; 6DF07AE429C1141000C01DE8 /* SetUpNetworksStepTwoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF07AE329C1141000C01DE8 /* SetUpNetworksStepTwoView.swift */; }; + 6DF310A72B8CBC1A00A38205 /* KeychainBananaSplitQueryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF310A62B8CBC1A00A38205 /* KeychainBananaSplitQueryProvider.swift */; }; + 6DF310AB2B8E150100A38205 /* KeychainBananaSplitMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF310AA2B8E150100A38205 /* KeychainBananaSplitMediator.swift */; }; 6DF3CFEC29936F41002DF203 /* EnterKeySetNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF3CFEB29936F41002DF203 /* EnterKeySetNameView.swift */; }; 6DF3CFEE299370F7002DF203 /* CreateKeySetSeedPhraseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF3CFED299370F7002DF203 /* CreateKeySetSeedPhraseView.swift */; }; 6DF3CFF029937E48002DF203 /* AttributedTintInfoBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF3CFEF29937E48002DF203 /* AttributedTintInfoBox.swift */; }; @@ -539,6 +545,11 @@ 6D52E3A92946F58200AD72F0 /* VerifierCertificateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifierCertificateView.swift; sourceTree = ""; }; 6D52E3AE2946FF5400AD72F0 /* NetworkSelectionModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSelectionModal.swift; sourceTree = ""; }; 6D52E3B02946FF5E00AD72F0 /* NetworkLogoIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkLogoIcon.swift; sourceTree = ""; }; + 6D5338752B7D306700F37EB1 /* BananaSplitModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitModal.swift; sourceTree = ""; }; + 6D5338772B7D7E9300F37EB1 /* IconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconButton.swift; sourceTree = ""; }; + 6D5338792B7E4CC400F37EB1 /* BananaSplitQRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitQRCodeModal.swift; sourceTree = ""; }; + 6D53387C2B7E512C00F37EB1 /* BananaSplitActionModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitActionModal.swift; sourceTree = ""; }; + 6D5338842B865F4500F37EB1 /* BananaSplitPassphraseModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitPassphraseModal.swift; sourceTree = ""; }; 6D55F285292CF2F800871896 /* StrokeContainerBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeContainerBackground.swift; sourceTree = ""; }; 6D57DC4A289D614F00005C63 /* BackendNavigationAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendNavigationAdapter.swift; sourceTree = ""; }; 6D57DC4C289D652400005C63 /* Dispatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dispatching.swift; sourceTree = ""; }; @@ -553,12 +564,8 @@ 6D57DC61289E524800005C63 /* Inter-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Medium.otf"; sourceTree = ""; }; 6D57DC63289E524800005C63 /* Inter-SemiBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-SemiBold.otf"; sourceTree = ""; }; 6D5801D02899130E006C41D8 /* PathMonitorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathMonitorProtocol.swift; sourceTree = ""; }; - 6D5801D32899133A006C41D8 /* ConnectivityMonitoringAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMonitoringAdapter.swift; sourceTree = ""; }; 6D5801D628991F11006C41D8 /* RuntimePropertiesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimePropertiesProvider.swift; sourceTree = ""; }; 6D5801DC28992375006C41D8 /* PolkadotVaultTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PolkadotVaultTests-Bridging-Header.h"; sourceTree = ""; }; - 6D5801E0289924AD006C41D8 /* ConnectivityMonitoringAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMonitoringAdapterTests.swift; sourceTree = ""; }; - 6D5801E4289937BA006C41D8 /* ConnectivityMonitoringAssemblerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMonitoringAssemblerTests.swift; sourceTree = ""; }; - 6D5801E6289937C0006C41D8 /* ConnectivityMonitoringAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMonitoringAssembler.swift; sourceTree = ""; }; 6D5DB5E72B3C61A200EF82AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6D5DB5EA2B3D1A9500EF82AB /* DevicePasscodeAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicePasscodeAuthenticator.swift; sourceTree = ""; }; 6D5DCA022A7CFA2E0050B101 /* ExportKeysSelectionModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportKeysSelectionModal.swift; sourceTree = ""; }; @@ -619,7 +626,7 @@ 6D8973A42A08E18C0046A2F3 /* ScanTabService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTabService.swift; sourceTree = ""; }; 6D8973A62A08E6550046A2F3 /* GeneralVerifierService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralVerifierService.swift; sourceTree = ""; }; 6D8AF88128BCC4D100CF0AB2 /* AccessControlProvidingAssemblerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessControlProvidingAssemblerTests.swift; sourceTree = ""; }; - 6D8AF88928BCC60600CF0AB2 /* KeychainQueryProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainQueryProviderTests.swift; sourceTree = ""; }; + 6D8AF88928BCC60600CF0AB2 /* KeychainSeedsQueryProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSeedsQueryProviderTests.swift; sourceTree = ""; }; 6D8F813B2994C64E000ED0BA /* ErrorDisplayed+TransactionSigning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ErrorDisplayed+TransactionSigning.swift"; sourceTree = ""; }; 6D91F3EB28C114D1007560F5 /* ExportPrivateKeyModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportPrivateKeyModal.swift; sourceTree = ""; }; 6D91F3ED28C16163007560F5 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; @@ -675,9 +682,8 @@ 6DAB52EE2B5E81BB005FDBA8 /* LogNoteModalViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogNoteModalViewModelTests.swift; sourceTree = ""; }; 6DAB52F12B5E832F005FDBA8 /* NoKeySetsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoKeySetsViewModelTests.swift; sourceTree = ""; }; 6DAFCAF72B0A360600DDD165 /* CameraPermissionHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPermissionHandlerTests.swift; sourceTree = ""; }; - 6DAFCAF92B0AE5C000DDD165 /* KeychainAccessAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAccessAdapterTests.swift; sourceTree = ""; }; + 6DAFCAF92B0AE5C000DDD165 /* KeychainSeedsAccessAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSeedsAccessAdapterTests.swift; sourceTree = ""; }; 6DAFCAFC2B0AE87300DDD165 /* RuntimePropertiesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimePropertiesProviderTests.swift; sourceTree = ""; }; - 6DAFCAFF2B0AEB7E00DDD165 /* ConnectivityMediatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMediatorTests.swift; sourceTree = ""; }; 6DAFCB012B0AEE4900DDD165 /* ApplicationStatePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationStatePublisherTests.swift; sourceTree = ""; }; 6DAFCB032B0AEF6800DDD165 /* PasswordProtectionStatePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectionStatePublisherTests.swift; sourceTree = ""; }; 6DB2E7C02B4BBAF7002387DE /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; @@ -703,7 +709,6 @@ 6DBD2201289A8E1F005D539B /* ErrorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMock.swift; sourceTree = ""; }; 6DBF6E2628E44DD700CC959F /* ErrorBottomModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorBottomModal.swift; sourceTree = ""; }; 6DBF6E2828E47EE000CC959F /* ErrorBottomModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorBottomModalViewModel.swift; sourceTree = ""; }; - 6DBF6E4228EACB7B00CC959F /* ConnectivityMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMediator.swift; sourceTree = ""; }; 6DBF6E4A28EC51D600CC959F /* GenericError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericError.swift; sourceTree = ""; }; 6DC0FC97296695D000A45883 /* SeedKeysPreview+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SeedKeysPreview+Helpers.swift"; sourceTree = ""; }; 6DC2EDF82B11961800298F00 /* DateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterTests.swift; sourceTree = ""; }; @@ -716,8 +721,8 @@ 6DC5643228B68FC5003D540B /* AccessControlProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessControlProvider.swift; sourceTree = ""; }; 6DC5643428B69355003D540B /* AccessControlProvidingAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessControlProvidingAssembler.swift; sourceTree = ""; }; 6DC5643628B79EC6003D540B /* SeedsMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedsMediator.swift; sourceTree = ""; }; - 6DC5643828B7DED8003D540B /* KeychainQueryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainQueryProvider.swift; sourceTree = ""; }; - 6DC5643A28B8D189003D540B /* KeychainAccessAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAccessAdapter.swift; sourceTree = ""; }; + 6DC5643828B7DED8003D540B /* KeychainSeedsQueryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSeedsQueryProvider.swift; sourceTree = ""; }; + 6DC5643A28B8D189003D540B /* KeychainSeedsAccessAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSeedsAccessAdapter.swift; sourceTree = ""; }; 6DC5643C28B91EFE003D540B /* NavbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavbarButton.swift; sourceTree = ""; }; 6DC5643F28B929EA003D540B /* KeyDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailsView.swift; sourceTree = ""; }; 6DC909F529C87C8C00AE6BAD /* LogsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsService.swift; sourceTree = ""; }; @@ -731,6 +736,11 @@ 6DD860E9299CED3F0000D81E /* AirgapMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirgapMediator.swift; sourceTree = ""; }; 6DD9FF1528C8B85300FB6195 /* HorizontalActionsBottomModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalActionsBottomModal.swift; sourceTree = ""; }; 6DD9FF1828C8C9AF00FB6195 /* Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animations.swift; sourceTree = ""; }; + 6DDD01D22B9504D1000F53B3 /* KeychainBananaSplitQueryProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainBananaSplitQueryProviderTests.swift; sourceTree = ""; }; + 6DDD01D62B958372000F53B3 /* BananaSplitPassphraseModalViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitPassphraseModalViewModelTests.swift; sourceTree = ""; }; + 6DDD01D82B958845000F53B3 /* BananaSplitActionModalViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitActionModalViewModelTests.swift; sourceTree = ""; }; + 6DDD01DA2B9589B5000F53B3 /* BananaSplitQRCodeModalViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BananaSplitQRCodeModalViewModelTests.swift; sourceTree = ""; }; + 6DDD01DD2B959BF3000F53B3 /* AirgapMediatorAssemblerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirgapMediatorAssemblerTests.swift; sourceTree = ""; }; 6DDD38B12B11C3C2000D2B62 /* SeedsMediatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedsMediatorTests.swift; sourceTree = ""; }; 6DDD38B62B1346C8000D2B62 /* KeyDetailsPublicKeyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailsPublicKeyViewModelTests.swift; sourceTree = ""; }; 6DDD38B82B134ADF000D2B62 /* MKeyDetails+Generate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MKeyDetails+Generate.swift"; sourceTree = ""; }; @@ -797,6 +807,8 @@ 6DF07ADD29C1062300C01DE8 /* SetUpNetworksIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetUpNetworksIntroView.swift; sourceTree = ""; }; 6DF07AE129C1140500C01DE8 /* SetUpNetworksStepOneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetUpNetworksStepOneView.swift; sourceTree = ""; }; 6DF07AE329C1141000C01DE8 /* SetUpNetworksStepTwoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetUpNetworksStepTwoView.swift; sourceTree = ""; }; + 6DF310A62B8CBC1A00A38205 /* KeychainBananaSplitQueryProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainBananaSplitQueryProvider.swift; sourceTree = ""; }; + 6DF310AA2B8E150100A38205 /* KeychainBananaSplitMediator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = KeychainBananaSplitMediator.swift; path = ../Seeds/KeychainBananaSplitMediator.swift; sourceTree = ""; }; 6DF3CFEB29936F41002DF203 /* EnterKeySetNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterKeySetNameView.swift; sourceTree = ""; }; 6DF3CFED299370F7002DF203 /* CreateKeySetSeedPhraseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateKeySetSeedPhraseView.swift; sourceTree = ""; }; 6DF3CFEF29937E48002DF203 /* AttributedTintInfoBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedTintInfoBox.swift; sourceTree = ""; }; @@ -824,11 +836,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6D755C0B2A6FECE000A73E20 /* Jdenticon in Frameworks */, - 6D749C5C2A6871BA0064D7E5 /* Blockies in Frameworks */, + 6D4CFFC12BA16A450061CC1B /* Blockies in Frameworks */, 6DEEA87E28AFBF5D00371ECA /* libsigner.a in Frameworks */, 6D971ACA2942E79100121A36 /* QRCode in Frameworks */, - 6D749C622A69CE2C0064D7E5 /* PolkadotIdenticon in Frameworks */, + 6D4CFFBD2BA1686E0061CC1B /* Jdenticon in Frameworks */, + 6D4CFFBF2BA168710061CC1B /* PolkadotIdenticon in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -891,7 +903,6 @@ 6DBF6E2528E44DBA00CC959F /* Errors */, 6DCC572528D8C0410014278A /* Backup */, 6DD9FF1428C8B83C00FB6195 /* Alerts */, - 6D88CFF628C634BC001FB0A1 /* KeySet */, 6DE8466F28C0FA090051346A /* ExportPrivateKey */, ); path = Modals; @@ -1229,6 +1240,7 @@ 6D2D245028CE5F0E00862726 /* PublicKey */ = { isa = PBXGroup; children = ( + 6D2D245628CF5C5200862726 /* PublicKeyActionsModal.swift */, 6D2D245128CE5F2D00862726 /* KeyDetailsPublicKeyView.swift */, 6D4F711829F913FC00729610 /* KeyDetailsPublicKeyViewRenderable.swift */, ); @@ -1320,6 +1332,17 @@ path = NetworkSelection; sourceTree = ""; }; + 6D53387B2B7E512000F37EB1 /* BananaSplit */ = { + isa = PBXGroup; + children = ( + 6D5338752B7D306700F37EB1 /* BananaSplitModal.swift */, + 6D5338792B7E4CC400F37EB1 /* BananaSplitQRCodeModal.swift */, + 6D53387C2B7E512C00F37EB1 /* BananaSplitActionModal.swift */, + 6D5338842B865F4500F37EB1 /* BananaSplitPassphraseModal.swift */, + ); + path = BananaSplit; + sourceTree = ""; + }; 6D57DC4E289D667000005C63 /* Navigation */ = { isa = PBXGroup; children = ( @@ -1383,7 +1406,7 @@ 6DC5643128B68FB2003D540B /* Keychain */, 6DBD21F8289A7A14005D539B /* Database */, 6D5801D528991DA4006C41D8 /* Runtime */, - 6D5801D22899132C006C41D8 /* Connectivity */, + 6DD860E6299CAD030000D81E /* Airgap */, ); path = Core; sourceTree = ""; @@ -1399,16 +1422,6 @@ path = Protocols; sourceTree = ""; }; - 6D5801D22899132C006C41D8 /* Connectivity */ = { - isa = PBXGroup; - children = ( - 6D5801D32899133A006C41D8 /* ConnectivityMonitoringAdapter.swift */, - 6D5801E6289937C0006C41D8 /* ConnectivityMonitoringAssembler.swift */, - 6DBF6E4228EACB7B00CC959F /* ConnectivityMediator.swift */, - ); - path = Connectivity; - sourceTree = ""; - }; 6D5801D528991DA4006C41D8 /* Runtime */ = { isa = PBXGroup; children = ( @@ -1420,12 +1433,12 @@ 6D5801DA2899235C006C41D8 /* Core */ = { isa = PBXGroup; children = ( + 6DDD01DC2B959BE7000F53B3 /* Airgap */, 6D686B9A2B45B35B007B7642 /* Authentication */, 6DAFCAFB2B0AE86C00DDD165 /* Runtime */, 6DAFCAF62B0A360600DDD165 /* Camera */, 6D8AF88028BCC4BF00CF0AB2 /* Keychain */, 6D57DC4E289D667000005C63 /* Navigation */, - 6D5801DF289924A3006C41D8 /* Connectivity */, 6DAFCB012B0AEE4900DDD165 /* ApplicationStatePublisherTests.swift */, 6DAFCB032B0AEF6800DDD165 /* PasswordProtectionStatePublisherTests.swift */, 6DE1B8372B56C79300D299C1 /* JailbreakDetectionPublisherTests.swift */, @@ -1433,16 +1446,6 @@ path = Core; sourceTree = ""; }; - 6D5801DF289924A3006C41D8 /* Connectivity */ = { - isa = PBXGroup; - children = ( - 6D5801E0289924AD006C41D8 /* ConnectivityMonitoringAdapterTests.swift */, - 6D5801E4289937BA006C41D8 /* ConnectivityMonitoringAssemblerTests.swift */, - 6DAFCAFF2B0AEB7E00DDD165 /* ConnectivityMediatorTests.swift */, - ); - path = Connectivity; - sourceTree = ""; - }; 6D5DB5E92B3D1A8700EF82AB /* Authentication */ = { isa = PBXGroup; children = ( @@ -1451,16 +1454,6 @@ path = Authentication; sourceTree = ""; }; - 6D5DCA012A7CFA1E0050B101 /* ExportKeys */ = { - isa = PBXGroup; - children = ( - 6D042AF32901B40400B3F4F7 /* ExportMultipleKeysModal.swift */, - 6D0FA73A2907010E00E45BA6 /* ExportMultipleKeysModal+ViewModel.swift */, - 6D5DCA022A7CFA2E0050B101 /* ExportKeysSelectionModal.swift */, - ); - path = ExportKeys; - sourceTree = ""; - }; 6D6430EA28CB2FD300342E37 /* Animators */ = { isa = PBXGroup; children = ( @@ -1668,22 +1661,23 @@ path = Modifiers; sourceTree = ""; }; - 6D88CFF628C634BC001FB0A1 /* KeySet */ = { + 6D88CFF628C634BC001FB0A1 /* Modals */ = { isa = PBXGroup; children = ( + 6D042AF32901B40400B3F4F7 /* ExportMultipleKeysModal.swift */, + 6D0FA73A2907010E00E45BA6 /* ExportMultipleKeysModal+ViewModel.swift */, + 6D5DCA022A7CFA2E0050B101 /* ExportKeysSelectionModal.swift */, 6D88CFF728C634CA001FB0A1 /* KeyDetailsActionsModal.swift */, - 6D2D245628CF5C5200862726 /* PublicKeyActionsModal.swift */, ); - path = KeySet; + path = Modals; sourceTree = ""; }; 6D8AF88028BCC4BF00CF0AB2 /* Keychain */ = { isa = PBXGroup; children = ( + 6DDD01D12B9504C0000F53B3 /* BananaSplit */, + 6DDD01D02B95049D000F53B3 /* Seeds */, 6D8AF88128BCC4D100CF0AB2 /* AccessControlProvidingAssemblerTests.swift */, - 6D8AF88928BCC60600CF0AB2 /* KeychainQueryProviderTests.swift */, - 6DAFCAF92B0AE5C000DDD165 /* KeychainAccessAdapterTests.swift */, - 6DDD38B12B11C3C2000D2B62 /* SeedsMediatorTests.swift */, ); path = Keychain; sourceTree = ""; @@ -1810,8 +1804,9 @@ 6D9921B3297EC1DA004891B6 /* Onboarding */ = { isa = PBXGroup; children = ( + 6DD860E4299CAAA70000D81E /* NoAirgapView.swift */, 6DF07ADC29C1060D00C01DE8 /* SetUpNetworks */, - 6DD860E6299CAD030000D81E /* Airgap */, + 6DD860E7299CAD140000D81E /* OnboadingAirgapView+Components.swift */, 6D9921B4297EC1E9004891B6 /* OnboardingAgreementsView.swift */, 6D7DF6532987ADEF00A8438E /* OnboardingState.swift */, 6D3A538D299B162F0004DDDD /* OnboardingScreenshotsView.swift */, @@ -2033,11 +2028,10 @@ 6DC5643128B68FB2003D540B /* Keychain */ = { isa = PBXGroup; children = ( + 6DF310A52B8CBC0800A38205 /* BananaSplit */, + 6DF310A42B8CBAB900A38205 /* Seeds */, 6DC5643228B68FC5003D540B /* AccessControlProvider.swift */, 6DC5643428B69355003D540B /* AccessControlProvidingAssembler.swift */, - 6DC5643628B79EC6003D540B /* SeedsMediator.swift */, - 6DC5643828B7DED8003D540B /* KeychainQueryProvider.swift */, - 6DC5643A28B8D189003D540B /* KeychainAccessAdapter.swift */, 6DC2EDFD2B1198FC00298F00 /* KeychainService.swift */, ); path = Keychain; @@ -2046,7 +2040,8 @@ 6DC5643E28B929E0003D540B /* KeyDetails */ = { isa = PBXGroup; children = ( - 6D5DCA012A7CFA1E0050B101 /* ExportKeys */, + 6D53387B2B7E512000F37EB1 /* BananaSplit */, + 6D88CFF628C634BC001FB0A1 /* Modals */, 6DA501CA290A47930096DA4E /* Views */, 6DE8466828BF6E9B0051346A /* Models */, ); @@ -2064,8 +2059,6 @@ 6DD860E6299CAD030000D81E /* Airgap */ = { isa = PBXGroup; children = ( - 6DD860E7299CAD140000D81E /* OnboadingAirgapView+Components.swift */, - 6DD860E4299CAAA70000D81E /* NoAirgapView.swift */, 6DD860E9299CED3F0000D81E /* AirgapMediator.swift */, 6DA317C2299F6FF1005DD060 /* AirgapMediatorAssembler.swift */, ); @@ -2081,9 +2074,54 @@ path = Alerts; sourceTree = ""; }; + 6DDD01D02B95049D000F53B3 /* Seeds */ = { + isa = PBXGroup; + children = ( + 6DAFCAF92B0AE5C000DDD165 /* KeychainSeedsAccessAdapterTests.swift */, + 6DDD38B12B11C3C2000D2B62 /* SeedsMediatorTests.swift */, + 6D8AF88928BCC60600CF0AB2 /* KeychainSeedsQueryProviderTests.swift */, + ); + path = Seeds; + sourceTree = ""; + }; + 6DDD01D12B9504C0000F53B3 /* BananaSplit */ = { + isa = PBXGroup; + children = ( + 6DDD01D22B9504D1000F53B3 /* KeychainBananaSplitQueryProviderTests.swift */, + ); + path = BananaSplit; + sourceTree = ""; + }; + 6DDD01D42B958361000F53B3 /* KeyDetails */ = { + isa = PBXGroup; + children = ( + 6DDD01D52B958365000F53B3 /* BananaSplit */, + ); + path = KeyDetails; + sourceTree = ""; + }; + 6DDD01D52B958365000F53B3 /* BananaSplit */ = { + isa = PBXGroup; + children = ( + 6DDD01D62B958372000F53B3 /* BananaSplitPassphraseModalViewModelTests.swift */, + 6DDD01D82B958845000F53B3 /* BananaSplitActionModalViewModelTests.swift */, + 6DDD01DA2B9589B5000F53B3 /* BananaSplitQRCodeModalViewModelTests.swift */, + ); + path = BananaSplit; + sourceTree = ""; + }; + 6DDD01DC2B959BE7000F53B3 /* Airgap */ = { + isa = PBXGroup; + children = ( + 6DDD01DD2B959BF3000F53B3 /* AirgapMediatorAssemblerTests.swift */, + ); + path = Airgap; + sourceTree = ""; + }; 6DDD38B42B1346BB000D2B62 /* Screens */ = { isa = PBXGroup; children = ( + 6DDD01D42B958361000F53B3 /* KeyDetails */, 6D9856622B715632002358D3 /* DerivedKey */, 6D98564E2B6A6A61002358D3 /* Onboarding */, 6D9856452B6A6227002358D3 /* Errors */, @@ -2129,6 +2167,7 @@ 6DF8316628F9BA4A00CB2BCE /* CapsuleButton.swift */, 6D2A5D102AA607C7009E0C3A /* ActionSheetCircleButton.swift */, 6D699FB42AA9CC7A0043B23A /* QRCodeButton.swift */, + 6D5338772B7D7E9300F37EB1 /* IconButton.swift */, ); path = Buttons; sourceTree = ""; @@ -2273,6 +2312,25 @@ path = SetUpNetworks; sourceTree = ""; }; + 6DF310A42B8CBAB900A38205 /* Seeds */ = { + isa = PBXGroup; + children = ( + 6DC5643828B7DED8003D540B /* KeychainSeedsQueryProvider.swift */, + 6DC5643628B79EC6003D540B /* SeedsMediator.swift */, + 6DC5643A28B8D189003D540B /* KeychainSeedsAccessAdapter.swift */, + ); + path = Seeds; + sourceTree = ""; + }; + 6DF310A52B8CBC0800A38205 /* BananaSplit */ = { + isa = PBXGroup; + children = ( + 6DF310A62B8CBC1A00A38205 /* KeychainBananaSplitQueryProvider.swift */, + 6DF310AA2B8E150100A38205 /* KeychainBananaSplitMediator.swift */, + ); + path = BananaSplit; + sourceTree = ""; + }; 6DF3CFEA29936D33002DF203 /* CreateKey */ = { isa = PBXGroup; children = ( @@ -2348,9 +2406,9 @@ name = PolkadotVault; packageProductDependencies = ( 6D971AC92942E79100121A36 /* QRCode */, - 6D749C5B2A6871BA0064D7E5 /* Blockies */, - 6D755C0A2A6FECE000A73E20 /* Jdenticon */, - 6D749C612A69CE2C0064D7E5 /* PolkadotIdenticon */, + 6D4CFFBC2BA1686E0061CC1B /* Jdenticon */, + 6D4CFFBE2BA168710061CC1B /* PolkadotIdenticon */, + 6D4CFFC02BA16A450061CC1B /* Blockies */, ); productName = PolkadotVault; productReference = 2DE72BBE26A588C7002BB752 /* Vault Dev.app */; @@ -2383,7 +2441,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1530; TargetAttributes = { 2DE72BBD26A588C7002BB752 = { CreatedOnToolsVersion = 12.5.1; @@ -2610,6 +2668,7 @@ 6D88CFF228C60AED001FB0A1 /* FullScreenRoundedModal.swift in Sources */, 6DD9FF1928C8C9B000FB6195 /* Animations.swift in Sources */, 2DA5F85E27566C3600D8DD29 /* TCFieldName.swift in Sources */, + 6D5338762B7D306700F37EB1 /* BananaSplitModal.swift in Sources */, 6DF9A13B2983B0ED00B31B6D /* DevicePincodeRequiredView.swift in Sources */, 6D57DC4D289D652400005C63 /* Dispatching.swift in Sources */, 2D48F35127609CDE004B27BE /* HistoryCard.swift in Sources */, @@ -2630,7 +2689,7 @@ 6D31E7C22A404B4900BF9D9B /* AddKeysForNetworkModal.swift in Sources */, 6DA2ACAA2939E85700AAEADC /* Event+EventTitle.swift in Sources */, 6D01997E289D238700F4C317 /* Localizable.swift in Sources */, - 6DC5643B28B8D189003D540B /* KeychainAccessAdapter.swift in Sources */, + 6DC5643B28B8D189003D540B /* KeychainSeedsAccessAdapter.swift in Sources */, 6D10EACD297114550063FB71 /* DerivationPathComponents.swift in Sources */, 6D33EE332A9F4825005F3827 /* KeyDetailsView+MainList.swift in Sources */, 6DEFB53228FEE42D00762219 /* ExportKeySetService.swift in Sources */, @@ -2720,6 +2779,7 @@ 6D019968289C937600F4C317 /* AuthenticatedScreenContainer.swift in Sources */, 6D16685728F530F4008C664A /* CaptureDeviceConfigurator.swift in Sources */, 6D5DCA032A7CFA2E0050B101 /* ExportKeysSelectionModal.swift in Sources */, + 6D53387D2B7E512C00F37EB1 /* BananaSplitActionModal.swift in Sources */, 6D8045D928D0761E00237F8C /* QRCodeAddressFooterView.swift in Sources */, 2DA5F8332756653B00D8DD29 /* CameraPreview.swift in Sources */, 6DDD737A29404E5000F04CE7 /* Event+EntryType.swift in Sources */, @@ -2728,7 +2788,6 @@ 6DDD737C2940629D00F04CE7 /* LogsMoreActionsModal.swift in Sources */, 6DA08B8E29B615390027CFCB /* RecoverKeySetNameView.swift in Sources */, 2DAA82A727885E73002917C0 /* TCNameValueTemplate.swift in Sources */, - 6D5801E7289937C0006C41D8 /* ConnectivityMonitoringAssembler.swift in Sources */, 6DA08B9229B694A30027CFCB /* BackendConstants.swift in Sources */, 6D8045D728D06E6B00237F8C /* ViewModels+Stubs.swift in Sources */, 6DA317C3299F6FF1005DD060 /* AirgapMediatorAssembler.swift in Sources */, @@ -2747,6 +2806,7 @@ 6D16687D28F84953008C664A /* EnvironmentValues+SafeAreaInsets.swift in Sources */, 6D6DF33429F65A0B00FC06AD /* KeyDetailsActionService.swift in Sources */, 6DA08B8629AC88D50027CFCB /* WrappingHStack.swift in Sources */, + 6D5338852B865F4500F37EB1 /* BananaSplitPassphraseModal.swift in Sources */, 6D6430F728CB662500342E37 /* ExportPrivateKeyWarningModal.swift in Sources */, 6DAA6CB329BF7155002329A8 /* OnboardingMediator.swift in Sources */, 6D52E3AA2946F58200AD72F0 /* VerifierCertificateView.swift in Sources */, @@ -2763,6 +2823,7 @@ 6DF714712A55652900F6A527 /* DynamicDerivationsService.swift in Sources */, 6D7B44FC2979263200111D0E /* TermsOfServiceView.swift in Sources */, 6D52E3A32946C1B500AD72F0 /* SettingsRowView.swift in Sources */, + 6D53387A2B7E4CC400F37EB1 /* BananaSplitQRCodeModal.swift in Sources */, 6D2A5D112AA607C7009E0C3A /* ActionSheetCircleButton.swift in Sources */, 6D16687F28F86DE0008C664A /* CameraButton.swift in Sources */, 6DBBC1F2298CB09C00368638 /* NetworkIdenticon.swift in Sources */, @@ -2778,6 +2839,7 @@ 6D0677AC29BB0C6000D76D90 /* AppLaunchMediator.swift in Sources */, 6D88CFF028C60815001FB0A1 /* CircleButton.swift in Sources */, 6DA501CC290A48190096DA4E /* KeyDetails+ViewModel.swift in Sources */, + 6D5338782B7D7E9300F37EB1 /* IconButton.swift in Sources */, 6DA2ACAE2939EAC300AAEADC /* Event+isImportant.swift in Sources */, 6D8973A52A08E18C0046A2F3 /* ScanTabService.swift in Sources */, 2DA5F84327566BE300D8DD29 /* TransactionCardSelector.swift in Sources */, @@ -2785,7 +2847,6 @@ 6DB99039295E95E9001101DC /* NetworkCapsuleView.swift in Sources */, 6DFE588B297A5F09002BFDBF /* ApplicationStatePublisher.swift in Sources */, 6DA2ACA72939DBCE00AAEADC /* DateFormatter+Utils.swift in Sources */, - 6D5801D42899133A006C41D8 /* ConnectivityMonitoringAdapter.swift in Sources */, 2DA5F86E27566C3600D8DD29 /* TCWarning.swift in Sources */, 6D476A1929FC05ED00326F74 /* AddKeySetUpNetworksStepOneView.swift in Sources */, 2DAA82A927885FCF002917C0 /* TCTXSpecPlain.swift in Sources */, @@ -2832,15 +2893,15 @@ 6D10EAC9296FA8910063FB71 /* Localizable+Formatted.swift in Sources */, 6D17EF8628EEEDDA008626E9 /* CameraPreviewUIView.swift in Sources */, 6DA501D4290BC55A0096DA4E /* KeyDetailsService.swift in Sources */, + 6DF310A72B8CBC1A00A38205 /* KeychainBananaSplitQueryProvider.swift in Sources */, 6D0BF95229F2BBF500F5B569 /* NetworkIconCapsuleView.swift in Sources */, 6D6430F128CB32CC00342E37 /* Snackbar.swift in Sources */, 6DB9903D2962A619001101DC /* MTransaction+ImportDerivedKeys.swift in Sources */, 6DEB18EA2A0B8EEC0013995E /* Stubs+Signer.swift in Sources */, 6D17EF8328EDC951008626E9 /* Encryption+RawRepresentable.swift in Sources */, - 6DBF6E4328EACB7B00CC959F /* ConnectivityMediator.swift in Sources */, 6D36CBBA2A55834B0001BB31 /* CreateDerivedKeyNameService.swift in Sources */, 6D7B44FE2979298700111D0E /* PrivacyPolicyView.swift in Sources */, - 6DC5643928B7DED8003D540B /* KeychainQueryProvider.swift in Sources */, + 6DC5643928B7DED8003D540B /* KeychainSeedsQueryProvider.swift in Sources */, 6D52E3AF2946FF5400AD72F0 /* NetworkSelectionModal.swift in Sources */, 6D5FDDCC2977D08E0076C1C4 /* LogNoteModal.swift in Sources */, 6DF91F3E29C06B5F000A6BB2 /* Verifier+Show.swift in Sources */, @@ -2848,6 +2909,7 @@ 6D77F31F296D0C5600044C7C /* CreateKeyNetworkSelectionView.swift in Sources */, 6DC909F629C87C8C00AE6BAD /* LogsService.swift in Sources */, 6DC5643728B79EC6003D540B /* SeedsMediator.swift in Sources */, + 6DF310AB2B8E150100A38205 /* KeychainBananaSplitMediator.swift in Sources */, 6D042AF22901B3FB00B3F4F7 /* QRCodeImageGenerator.swift in Sources */, 6D95E97528B500EE00E28A11 /* Heights.swift in Sources */, 6D8045DE28D087D400237F8C /* QRCodeRootFooterView.swift in Sources */, @@ -2868,10 +2930,8 @@ 6DE48E822B1F0B96003094D5 /* AutoMockable+J.generated.swift in Sources */, 6DE48E7E2B1F0B95003094D5 /* AutoMockable+X.generated.swift in Sources */, 6D9856752B717F3D002358D3 /* DerivationCheck+Generate.swift in Sources */, - 6D5801E1289924AD006C41D8 /* ConnectivityMonitoringAdapterTests.swift in Sources */, 6D98566F2B716312002358D3 /* AddKeySetUpNetworksStepTwoViewModelTests.swift in Sources */, 6DE48E912B1F0B96003094D5 /* AutoMockable+H.generated.swift in Sources */, - 6DAFCB002B0AEB7E00DDD165 /* ConnectivityMediatorTests.swift in Sources */, 6D9856712B7164CE002358D3 /* DerivationMethodsInfoViewModelTests.swift in Sources */, 6DE48E8E2B1F0B96003094D5 /* AutoMockable+B.generated.swift in Sources */, 6D2C78AC2B56D98F006431E3 /* SignSpecDetailsViewModelTests.swift in Sources */, @@ -2904,7 +2964,8 @@ 6DE48E932B1F0B96003094D5 /* AutoMockable+W.generated.swift in Sources */, 6DE48E2E2B1EB97C003094D5 /* AutoMockableHeader.swift in Sources */, 6DE48E902B1F0B96003094D5 /* AutoMockable+A.generated.swift in Sources */, - 6D8AF88A28BCC60600CF0AB2 /* KeychainQueryProviderTests.swift in Sources */, + 6DDD01DB2B9589B5000F53B3 /* BananaSplitQRCodeModalViewModelTests.swift in Sources */, + 6D8AF88A28BCC60600CF0AB2 /* KeychainSeedsQueryProviderTests.swift in Sources */, 6DB2E7C12B4BBAF7002387DE /* SettingsViewModelTests.swift in Sources */, 6D9856542B6A6B03002358D3 /* AirgapComponentTests.swift in Sources */, 6DDD38B22B11C3C2000D2B62 /* SeedsMediatorTests.swift in Sources */, @@ -2930,16 +2991,18 @@ 6D9856642B715643002358D3 /* CreateKeyNetworkSelectionViewModelTests.swift in Sources */, 6DE48E7F2B1F0B96003094D5 /* AutoMockable+Z.generated.swift in Sources */, 6D80EB572B4EB117009C544B /* SignSpecsListViewModelTests.swift in Sources */, + 6DDD01D32B9504D1000F53B3 /* KeychainBananaSplitQueryProviderTests.swift in Sources */, 6DAFCAF82B0A360600DDD165 /* CameraPermissionHandlerTests.swift in Sources */, 6DE48E952B1F0B96003094D5 /* AutoMockable+Q.generated.swift in Sources */, 6DE48E802B1F0B96003094D5 /* AutoMockable+P.generated.swift in Sources */, 6D9856732B716564002358D3 /* CreateDerivedKeyConfirmationViewModelTests.swift in Sources */, 6D80EB502B4EAD3E009C544B /* VerifierCertificateViewModelTests.swift in Sources */, + 6DDD01D72B958372000F53B3 /* BananaSplitPassphraseModalViewModelTests.swift in Sources */, 6D2C78B02B56EF55006431E3 /* SettingsBackupModalViewModelTests.swift in Sources */, 6D80EB522B4EB0B8009C544B /* MSufficientCryptoReady+Generate.swift in Sources */, 6DE48E8C2B1F0B96003094D5 /* AutoMockable+R.generated.swift in Sources */, + 6DDD01DE2B959BF3000F53B3 /* AirgapMediatorAssemblerTests.swift in Sources */, 6DAB52EF2B5E81BB005FDBA8 /* LogNoteModalViewModelTests.swift in Sources */, - 6D5801E5289937BA006C41D8 /* ConnectivityMonitoringAssemblerTests.swift in Sources */, 6DE07B102B450EB7001AF54C /* OnboardingMediatorTests.swift in Sources */, 6DE48E882B1F0B96003094D5 /* AutoMockable+N.generated.swift in Sources */, 6DAFCB042B0AEF6800DDD165 /* PasswordProtectionStatePublisherTests.swift in Sources */, @@ -2955,7 +3018,8 @@ 6DE48E972B1F0B96003094D5 /* AutoMockable+L.generated.swift in Sources */, 6D98566D2B716112002358D3 /* DerivationPathNameViewModelTests.swift in Sources */, 6DE48E852B1F0B96003094D5 /* AutoMockable+U.generated.swift in Sources */, - 6DAFCAFA2B0AE5C000DDD165 /* KeychainAccessAdapterTests.swift in Sources */, + 6DDD01D92B958845000F53B3 /* BananaSplitActionModalViewModelTests.swift in Sources */, + 6DAFCAFA2B0AE5C000DDD165 /* KeychainSeedsAccessAdapterTests.swift in Sources */, 6DB2E7CE2B4BC7F6002387DE /* NetworkSettingDetailsViewModelTests.swift in Sources */, 6DAB52E92B5E718D005FDBA8 /* LogsListViewModelTests.swift in Sources */, ); @@ -3040,7 +3104,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.8.1; + MACOSX_DEPLOYMENT_TARGET = ""; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3048,6 +3113,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -3098,13 +3164,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.8.1; + MACOSX_DEPLOYMENT_TARGET = ""; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -3126,7 +3194,7 @@ INFOPLIST_FILE = PolkadotVault/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.8.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3140,7 +3208,6 @@ PRODUCT_MODULE_NAME = PolkadotVault; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/PolkadotVault-Bridging-Header.h"; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 arm64e x86_64"; }; @@ -3153,16 +3220,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = accent_pink300; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"PolkadotVault/Preview Content\""; - DEVELOPMENT_TEAM = P2PX3JU8FT; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = PolkadotVault/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.8.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3176,7 +3243,6 @@ PRODUCT_MODULE_NAME = PolkadotVault; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/PolkadotVault-Bridging-Header.h"; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 arm64e x86_64"; }; @@ -3277,13 +3343,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.8.1; + MACOSX_DEPLOYMENT_TARGET = ""; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = QA; @@ -3304,7 +3372,7 @@ INFOPLIST_FILE = PolkadotVault/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.8.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3318,7 +3386,6 @@ PRODUCT_MODULE_NAME = PolkadotVault; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/PolkadotVault-Bridging-Header.h"; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 arm64e x86_64"; }; @@ -3402,22 +3469,22 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 6D5DB5EF2B43F8A700EF82AB /* SwiftLintPlugin */ = { + 6D4CFFBC2BA1686E0061CC1B /* Jdenticon */ = { isa = XCSwiftPackageProductDependency; - package = 6DC1EF8B2AD55E4F009A2777 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; + productName = Jdenticon; }; - 6D749C5B2A6871BA0064D7E5 /* Blockies */ = { + 6D4CFFBE2BA168710061CC1B /* PolkadotIdenticon */ = { isa = XCSwiftPackageProductDependency; - productName = Blockies; + productName = PolkadotIdenticon; }; - 6D749C612A69CE2C0064D7E5 /* PolkadotIdenticon */ = { + 6D4CFFC02BA16A450061CC1B /* Blockies */ = { isa = XCSwiftPackageProductDependency; - productName = PolkadotIdenticon; + productName = Blockies; }; - 6D755C0A2A6FECE000A73E20 /* Jdenticon */ = { + 6D5DB5EF2B43F8A700EF82AB /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; - productName = Jdenticon; + package = 6DC1EF8B2AD55E4F009A2777 /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintPlugin"; }; 6D971AC92942E79100121A36 /* QRCode */ = { isa = XCSwiftPackageProductDependency; diff --git a/ios/PolkadotVault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/PolkadotVault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2a331e5040..152365e837 100644 --- a/ios/PolkadotVault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/PolkadotVault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "db51c407d3be4a051484a141bf0bff36c43d3b1e", - "version" : "1.8.0" + "revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c", + "version" : "1.8.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/dagronf/QRCode", "state" : { - "revision" : "13c605cfa866e257ac9d4ac8a47320a0e11fb6e0", - "version" : "17.0.0" + "revision" : "6e32d9e56b25ab50af62a902d29d119d407d05e1", + "version" : "17.1.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/dagronf/SwiftImageReadWrite", "state" : { - "revision" : "02c141026a0c5d74635a457d2d0964c4cb8935b3", - "version" : "1.4.1" + "revision" : "96361ba7f9dce5184d95aba8ea90d9faa4acabb2", + "version" : "1.6.1" } }, { diff --git a/ios/PolkadotVault.xcodeproj/xcshareddata/xcschemes/PolkadotVault-Dev.xcscheme b/ios/PolkadotVault.xcodeproj/xcshareddata/xcschemes/PolkadotVault-Dev.xcscheme index 5a8186381f..ef39d5f74d 100644 --- a/ios/PolkadotVault.xcodeproj/xcshareddata/xcschemes/PolkadotVault-Dev.xcscheme +++ b/ios/PolkadotVault.xcodeproj/xcshareddata/xcschemes/PolkadotVault-Dev.xcscheme @@ -1,6 +1,6 @@ ) -> Void + _ completion: @escaping (Result) -> Void ) func generatePassphrase( with words: UInt32, @@ -40,16 +44,17 @@ final class BananaSplitService { passphrase: String, totalShards: UInt32, requiredShards: UInt32, - _ completion: @escaping (Result<[QrData], ServiceError>) -> Void + _ completion: @escaping (Result) -> Void ) { backendService.performCall({ - try bsEncrypt( + let qrCodes = try bsEncrypt( secret: secret, title: title, passphrase: passphrase, totalShards: totalShards, requiredShards: requiredShards ) + return BananaSplitBackup(qrCodes: qrCodes.map(\.payload)) }, completion: completion) } diff --git a/ios/PolkadotVault/Components/Buttons/CircleButton.swift b/ios/PolkadotVault/Components/Buttons/CircleButton.swift index 4b8c92d58e..295cede2c4 100644 --- a/ios/PolkadotVault/Components/Buttons/CircleButton.swift +++ b/ios/PolkadotVault/Components/Buttons/CircleButton.swift @@ -7,12 +7,15 @@ import SwiftUI -struct CloseModalButton: View { +struct CircleButton: View { + private let image: ImageResource private let action: () -> Void init( + image: ImageResource = .xmarkButton, action: @escaping () -> Void ) { + self.image = image self.action = action } @@ -23,8 +26,8 @@ struct CloseModalButton: View { ZStack { Circle() .frame(width: Sizes.xmarkButtonDiameter, height: Sizes.xmarkButtonDiameter, alignment: .center) - .foregroundColor(.fill6) - Image(.xmarkButton) + .foregroundColor(.fill18) + Image(image) .foregroundColor(.textAndIconsPrimary) } } diff --git a/ios/PolkadotVault/Components/Buttons/IconButton.swift b/ios/PolkadotVault/Components/Buttons/IconButton.swift new file mode 100644 index 0000000000..20991f765e --- /dev/null +++ b/ios/PolkadotVault/Components/Buttons/IconButton.swift @@ -0,0 +1,58 @@ +// +// IconButton.swift +// PolkadotVault +// +// Created by Krzysztof Rodak on 15/02/2024. +// + +import SwiftUI + +struct IconButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .padding(Spacing.medium) + .foregroundColor(.textAndIconsSecondary) + .frame( + height: Heights.iconButton, + alignment: .center + ) + } +} + +struct IconButton: View { + private let action: () -> Void + private let icon: ImageResource + + init( + action: @escaping () -> Void, + icon: ImageResource + ) { + self.action = action + self.icon = icon + } + + var body: some View { + Button(action: action) { + HStack { + Image(icon) + } + } + .buttonStyle(IconButtonStyle()) + } +} + +#if DEBUG + struct IconButton_Previews: PreviewProvider { + static var previews: some View { + VStack(alignment: .leading, spacing: 10) { + IconButton( + action: {}, + icon: .refreshPassphrase + ) + } + .padding() + .preferredColorScheme(.dark) + .previewLayout(.sizeThatFits) + } + } +#endif diff --git a/ios/PolkadotVault/Components/Text/ActionableInfoBoxView.swift b/ios/PolkadotVault/Components/Text/ActionableInfoBoxView.swift index 81df528f7b..c7024ac588 100644 --- a/ios/PolkadotVault/Components/Text/ActionableInfoBoxView.swift +++ b/ios/PolkadotVault/Components/Text/ActionableInfoBoxView.swift @@ -24,10 +24,11 @@ struct ActionableInfoBoxView: View { VStack(alignment: .leading, spacing: Spacing.medium) { HStack { Text(renderable.text) - .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(nil) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.bodyM.font) - .fixedSize(horizontal: false, vertical: true) Spacer().frame(maxWidth: Spacing.medium) Image(.infoIconBold) .foregroundColor(.accentPink300) @@ -44,8 +45,8 @@ struct ActionableInfoBoxView: View { .onTapGesture { action.action() } } } - .padding(Spacing.medium) + .frame(maxWidth: .infinity) .containerBackground(CornerRadius.small, state: .actionableInfo) } } diff --git a/ios/PolkadotVault/Components/Text/AttributedInfoBoxView.swift b/ios/PolkadotVault/Components/Text/AttributedInfoBoxView.swift index 5cf8a916ca..ee26e44686 100644 --- a/ios/PolkadotVault/Components/Text/AttributedInfoBoxView.swift +++ b/ios/PolkadotVault/Components/Text/AttributedInfoBoxView.swift @@ -13,12 +13,15 @@ struct AttributedInfoBoxView: View { var body: some View { HStack { Text(text) - .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(nil) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) Spacer().frame(maxWidth: Spacing.medium) Image(.helpOutline) .foregroundColor(.accentPink300) } .padding() + .frame(maxWidth: .infinity) .font(PrimaryFont.bodyM.font) .background( RoundedRectangle(cornerRadius: CornerRadius.small) diff --git a/ios/PolkadotVault/Components/Text/AttributedTintInfoBox.swift b/ios/PolkadotVault/Components/Text/AttributedTintInfoBox.swift index a506cd7b49..679a311650 100644 --- a/ios/PolkadotVault/Components/Text/AttributedTintInfoBox.swift +++ b/ios/PolkadotVault/Components/Text/AttributedTintInfoBox.swift @@ -13,12 +13,15 @@ struct AttributedTintInfoBox: View { var body: some View { HStack { Text(text) - .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(nil) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) Spacer().frame(width: Spacing.large) Image(.helpOutline) .foregroundColor(.accentPink300) } .padding(Spacing.medium) + .frame(maxWidth: .infinity) .font(PrimaryFont.bodyM.font) .background( RoundedRectangle(cornerRadius: CornerRadius.medium) diff --git a/ios/PolkadotVault/Components/Text/InfoBoxView.swift b/ios/PolkadotVault/Components/Text/InfoBoxView.swift index 7513655944..9e5c72ed86 100644 --- a/ios/PolkadotVault/Components/Text/InfoBoxView.swift +++ b/ios/PolkadotVault/Components/Text/InfoBoxView.swift @@ -13,7 +13,8 @@ struct InfoBoxView: View { var body: some View { HStack { Text(text) - .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(nil) + .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .foregroundColor(.textAndIconsTertiary) Spacer().frame(maxWidth: Spacing.medium) @@ -21,6 +22,7 @@ struct InfoBoxView: View { .foregroundColor(.accentPink300) } .padding() + .frame(maxWidth: .infinity) .font(PrimaryFont.bodyM.font) .strokeContainerBackground(CornerRadius.small) } diff --git a/ios/PolkadotVault/Components/TextFields/PrimaryTextField.swift b/ios/PolkadotVault/Components/TextFields/PrimaryTextField.swift index 2c2011e263..e9b545362c 100644 --- a/ios/PolkadotVault/Components/TextFields/PrimaryTextField.swift +++ b/ios/PolkadotVault/Components/TextFields/PrimaryTextField.swift @@ -9,6 +9,7 @@ import SwiftUI struct PrimaryTextFieldStyle: ViewModifier { let placeholder: String + let keyboardType: UIKeyboardType @Binding var text: String @Binding var isValid: Bool @@ -19,7 +20,7 @@ struct PrimaryTextFieldStyle: ViewModifier { .font(PrimaryFont.bodyL.font) .autocapitalization(.none) .disableAutocorrection(true) - .keyboardType(.asciiCapable) + .keyboardType(keyboardType) .submitLabel(.return) .frame(height: Heights.textFieldHeight) .padding(.horizontal, Spacing.medium) @@ -35,9 +36,15 @@ struct PrimaryTextFieldStyle: ViewModifier { extension View { func primaryTextFieldStyle( _ placeholder: String, + keyboardType: UIKeyboardType = .asciiCapable, text: Binding, isValid: Binding = Binding.constant(true) ) -> some View { - modifier(PrimaryTextFieldStyle(placeholder: placeholder, text: text, isValid: isValid)) + modifier(PrimaryTextFieldStyle( + placeholder: placeholder, + keyboardType: keyboardType, + text: text, + isValid: isValid + )) } } diff --git a/ios/PolkadotVault/Core/Airgap/AirgapMediator.swift b/ios/PolkadotVault/Core/Airgap/AirgapMediator.swift new file mode 100644 index 0000000000..a06c035f17 --- /dev/null +++ b/ios/PolkadotVault/Core/Airgap/AirgapMediator.swift @@ -0,0 +1,133 @@ +// +// AirgapMediator.swift +// NativeSigner +// +// Created by Krzysztof Rodak on 15/02/2023. +// + +import Combine +import CoreLocation +import Foundation +import Network +import UIKit + +struct AirgapState: Equatable { + let isAirplaneModeOn: Bool + let isWifiOn: Bool + let isLocationServiceEnabled: Bool +} + +// sourcery: AutoMockable +protocol LocationServicesManaging: AnyObject { + static func locationServicesEnabled() -> Bool +} + +extension CLLocationManager: LocationServicesManaging {} + +protocol AirgapMediating: AnyObject { + var isConnectedPublisher: AnyPublisher { get } + var airgapPublisher: AnyPublisher { get } + + func startMonitoringAirgap() +} + +final class AirgapMediator: AirgapMediating { + private let adaptee: PathMonitorProtocol + private let monitoringQueue: DispatchQueue + private let notificationQueue: DispatchQueue + private let locationManager: LocationServicesManaging.Type + private var wifiSubject = CurrentValueSubject(true) + private var airplaneSubject = CurrentValueSubject(false) + private var locationSubject = CurrentValueSubject(true) + + var airgapPublisher: AnyPublisher { + Publishers.CombineLatest3(wifiSubject, airplaneSubject, locationSubject) + .map { wifi, airplane, locationServiceEnabled in + AirgapState( + isAirplaneModeOn: airplane, + isWifiOn: wifi, + isLocationServiceEnabled: locationServiceEnabled + ) + } + .removeDuplicates() + .eraseToAnyPublisher() + } + + var isConnectedPublisher: AnyPublisher { + airgapPublisher + .map { airgapState in + airgapState.isLocationServiceEnabled || airgapState.isWifiOn || !airgapState.isAirplaneModeOn + } + .removeDuplicates() + .eraseToAnyPublisher() + } + + init( + adaptee: PathMonitorProtocol = NWPathMonitor(), + locationManager: LocationServicesManaging.Type = CLLocationManager.self, + monitoringQueue: DispatchQueue = DispatchQueue.global(qos: .userInteractive), + notificationQueue: DispatchQueue = DispatchQueue.main + ) { + self.adaptee = adaptee + self.locationManager = locationManager + self.monitoringQueue = monitoringQueue + self.notificationQueue = notificationQueue + listenToLocationChanges() + } + + func startMonitoringAirgap() { + adaptee.pathUpdateHandler = { [weak self] path in + guard let self else { return } + let isWifiOn: Bool = path.usesInterfaceType(.wifi) + var currentInterfaces = path.availableInterfaces + currentInterfaces.removeAll(where: { $0.type == .wifi }) + notificationQueue.async { + self.airplaneSubject.send(currentInterfaces.isEmpty) + self.wifiSubject.send(isWifiOn) + } + } + adaptee.start(queue: monitoringQueue) + } + + private func listenToLocationChanges() { + NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: nil + ) { [weak self] _ in + guard let self else { return } + updateLocationServicesStatus() + } + updateLocationServicesStatus() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private func updateLocationServicesStatus() { + monitoringQueue.async { + let isEnabled = self.locationManager.locationServicesEnabled() + self.notificationQueue.async { + self.locationSubject.send(isEnabled) + } + } + } +} + +final class AirgapMediatingStub: AirgapMediating { + var stubState = AirgapState(isAirplaneModeOn: true, isWifiOn: false, isLocationServiceEnabled: false) + var stubIsConnected = false + + var isConnectedPublisher: AnyPublisher { + Just(stubIsConnected) + .eraseToAnyPublisher() + } + + var airgapPublisher: AnyPublisher { + Just(stubState) + .eraseToAnyPublisher() + } + + func startMonitoringAirgap() {} +} diff --git a/ios/PolkadotVault/Screens/Onboarding/Airgap/AirgapMediatorAssembler.swift b/ios/PolkadotVault/Core/Airgap/AirgapMediatorAssembler.swift similarity index 100% rename from ios/PolkadotVault/Screens/Onboarding/Airgap/AirgapMediatorAssembler.swift rename to ios/PolkadotVault/Core/Airgap/AirgapMediatorAssembler.swift diff --git a/ios/PolkadotVault/Core/Connectivity/ConnectivityMediator.swift b/ios/PolkadotVault/Core/Connectivity/ConnectivityMediator.swift deleted file mode 100644 index cad128fdaf..0000000000 --- a/ios/PolkadotVault/Core/Connectivity/ConnectivityMediator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ConnectivityMediator.swift -// Polkadot Vault -// -// Created by Krzysztof Rodak on 03/10/2022. -// - -import Foundation - -final class ConnectivityMediator: ObservableObject { - private let connectivityMonitor: ConnectivityMonitoring - - @Published private(set) var isConnectivityOn: Bool = false - - init( - connectivityMonitor: ConnectivityMonitoring = ConnectivityMonitoringAssembler().assemble() - ) { - self.connectivityMonitor = connectivityMonitor - setUpConnectivityMonitoring() - } -} - -private extension ConnectivityMediator { - func setUpConnectivityMonitoring() { - connectivityMonitor.startMonitoring { [weak self] isConnected in - guard let self else { return } - isConnectivityOn = isConnected - } - } -} diff --git a/ios/PolkadotVault/Core/Connectivity/ConnectivityMonitoringAdapter.swift b/ios/PolkadotVault/Core/Connectivity/ConnectivityMonitoringAdapter.swift deleted file mode 100644 index 39aa613ae6..0000000000 --- a/ios/PolkadotVault/Core/Connectivity/ConnectivityMonitoringAdapter.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ConnectivityMonitoringAdapter.swift -// Polkadot Vault -// -// Created by Krzysztof Rodak on 02/08/2022. -// - -import Foundation -import Network - -/// Protocol for monitoring system connectivity -protocol ConnectivityMonitoring: AnyObject { - /// Starts monitoring network connectivity - /// - Parameter update: update callback informing about connectivity change - func startMonitoring(_ update: @escaping (Bool) -> Void) -} - -/// Adapter that monitors for connectivity changes -final class ConnectivityMonitoringAdapter: ObservableObject, ConnectivityMonitoring { - private let adaptee: PathMonitorProtocol - private let monitoringQueue: DispatchQueue - private let notificationQueue: DispatchQueue - private var isConnected: Bool = false - - init( - adaptee: PathMonitorProtocol = NWPathMonitor(), - monitoringQueue: DispatchQueue = DispatchQueue.global(qos: .background), - notificationQueue: DispatchQueue = DispatchQueue.main - ) { - self.adaptee = adaptee - self.monitoringQueue = monitoringQueue - self.notificationQueue = notificationQueue - } - - func startMonitoring(_ update: @escaping (Bool) -> Void) { - adaptee.pathUpdateHandler = { [weak self] path in - guard let self else { return } - let isConnected = !path.availableInterfaces.isEmpty - - // Update only on connectivity changes - guard isConnected != self.isConnected else { return } - self.isConnected = isConnected - notificationQueue.async { - if isConnected { - try? historyDeviceWasOnline() - } - update(isConnected) - } - } - adaptee.start(queue: monitoringQueue) - } -} - -/// Stub that gives control over connectivity changes for testing purposes -final class ConnectivityMonitoringStub: ConnectivityMonitoring { - /// Retained update callback to be used within `triggerUpdateChange` for testing connectivity changes - private var updateCallback: ((Bool) -> Void)? - - /// Stubbed connectivity state to be used in Development Mode - var stubConnectivityState = false - - func startMonitoring(_ update: @escaping (Bool) -> Void) { - updateCallback = update - update(stubConnectivityState) - } - - /// Utility function to trigger updated state callback if needed - /// Should be only used for testing purposes - func triggerUpdateChange() { - updateCallback?(stubConnectivityState) - } -} diff --git a/ios/PolkadotVault/Core/Connectivity/ConnectivityMonitoringAssembler.swift b/ios/PolkadotVault/Core/Connectivity/ConnectivityMonitoringAssembler.swift deleted file mode 100644 index 09356ee923..0000000000 --- a/ios/PolkadotVault/Core/Connectivity/ConnectivityMonitoringAssembler.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ConnectivityMonitoringAssembler.swift -// Polkadot Vault -// -// Created by Krzysztof Rodak on 02/08/2022. -// - -import Foundation - -/// Assembler that prepares dependency for `ConnectivityMonitoring` -final class ConnectivityMonitoringAssembler { - private let runtimePropertiesProvider: RuntimePropertiesProviding - - init(runtimePropertiesProvider: RuntimePropertiesProviding = RuntimePropertiesProvider()) { - self.runtimePropertiesProvider = runtimePropertiesProvider - } - - func assemble() -> ConnectivityMonitoring { - switch runtimePropertiesProvider.runtimeMode { - case .production, - .qa: - ConnectivityMonitoringAdapter() - case .debug: - ConnectivityMonitoringStub() - } - } -} diff --git a/ios/PolkadotVault/Core/Keychain/BananaSplit/KeychainBananaSplitQueryProvider.swift b/ios/PolkadotVault/Core/Keychain/BananaSplit/KeychainBananaSplitQueryProvider.swift new file mode 100644 index 0000000000..d598e9d0eb --- /dev/null +++ b/ios/PolkadotVault/Core/Keychain/BananaSplit/KeychainBananaSplitQueryProvider.swift @@ -0,0 +1,100 @@ +// +// KeychainBananaSplitQueryProvider.swift +// Polkadot Vault +// +// Created by Krzysztof Rodak on 26/02/2024. +// + +import Foundation + +struct BananaSplitPassphrase: Codable, Equatable { + let passphrase: String +} + +enum KeychainBananaSplitQuery { + case fetch(seedName: String) + case check(seedName: String) + case delete(seedName: String) + case save(seedName: String, bananaSplit: BananaSplitBackup) +} + +enum KeychainBananaSplitPassphraseQuery { + case fetch(seedName: String) + case delete(seedName: String) + case save(seedName: String, passphrase: BananaSplitPassphrase, accessControl: SecAccessControl) +} + +// sourcery: AutoMockable +protocol KeychainBananaSplitQueryProviding: AnyObject { + func query(for queryType: KeychainBananaSplitQuery) -> CFDictionary + func passhpraseQuery(for queryType: KeychainBananaSplitPassphraseQuery) -> CFDictionary +} + +final class KeychainBananaSplitQueryProvider: KeychainBananaSplitQueryProviding { + enum Constants { + static let bananaSplitSuffix = "_bananaSplit" + static let passphraseSuffix = "_passphrase" + } + + private let jsonEncoder: JSONEncoder + + init(jsonEncoder: JSONEncoder = JSONEncoder()) { + self.jsonEncoder = jsonEncoder + } + + func query(for queryType: KeychainBananaSplitQuery) -> CFDictionary { + var dictionary: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword + ] + switch queryType { + case let .fetch(seedName): + dictionary[kSecMatchLimit] = kSecMatchLimitOne + dictionary[kSecAttrAccount] = backupName(seedName) + dictionary[kSecReturnData] = true + case let .check(seedName): + dictionary[kSecMatchLimit] = kSecMatchLimitOne + dictionary[kSecAttrAccount] = backupName(seedName) + dictionary[kSecReturnData] = false + case let .delete(seedName): + dictionary[kSecAttrAccount] = backupName(seedName) + case let .save(seedName, bananaSplit): + dictionary[kSecAttrAccount] = backupName(seedName) + if let data = try? jsonEncoder.encode(bananaSplit) { + dictionary[kSecValueData] = data + } + dictionary[kSecReturnData] = false + } + return dictionary as CFDictionary + } + + func passhpraseQuery(for queryType: KeychainBananaSplitPassphraseQuery) -> CFDictionary { + var dictionary: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword + ] + switch queryType { + case let .fetch(seedName): + dictionary[kSecMatchLimit] = kSecMatchLimitOne + dictionary[kSecAttrAccount] = passphraseName(seedName) + dictionary[kSecReturnData] = true + case let .delete(seedName): + dictionary[kSecAttrAccount] = passphraseName(seedName) + case let .save(seedName, passphrase, accessControl): + dictionary[kSecAttrAccessControl] = accessControl + dictionary[kSecAttrAccount] = passphraseName(seedName) + if let data = try? jsonEncoder.encode(passphrase) { + dictionary[kSecValueData] = data + } + + dictionary[kSecReturnData] = false + } + return dictionary as CFDictionary + } + + private func backupName(_ seedName: String) -> String { + seedName + Constants.bananaSplitSuffix + } + + private func passphraseName(_ seedName: String) -> String { + seedName + Constants.passphraseSuffix + } +} diff --git a/ios/PolkadotVault/Core/Keychain/Seeds/KeychainBananaSplitMediator.swift b/ios/PolkadotVault/Core/Keychain/Seeds/KeychainBananaSplitMediator.swift new file mode 100644 index 0000000000..1c3d223304 --- /dev/null +++ b/ios/PolkadotVault/Core/Keychain/Seeds/KeychainBananaSplitMediator.swift @@ -0,0 +1,135 @@ +// +// KeychainBananaSplitMediator.swift +// Polkadot Vault +// +// Created by Krzysztof Rodak on 27/02/2024. +// + +import Foundation + +// sourcery: AutoMockable +protocol KeychainBananaSplitAccessMediating: AnyObject { + func saveBananaSplit( + with seedName: String, + bananaSplitBackup: BananaSplitBackup, + passphrase: BananaSplitPassphrase + ) -> Result + func retrieveBananaSplit(with seedName: String) -> Result + func retrieveBananaSplitPassphrase(with seedName: String) -> Result + func removeBananaSplitBackup(seedName: String) -> Result + func checkIfBananaSplitAlreadyExists(seedName: String) -> Result +} + +final class KeychainBananaSplitMediator: KeychainBananaSplitAccessMediating { + private let keychainService: KeychainServicing + private let queryProvider: KeychainBananaSplitQueryProviding + private let acccessControlProvider: AccessControlProviding + private let jsonDecoder: JSONDecoder + + init( + keychainService: KeychainServicing = KeychainService(), + acccessControlProvider: AccessControlProviding = AccessControlProvidingAssembler().assemble(), + queryProvider: KeychainBananaSplitQueryProviding = KeychainBananaSplitQueryProvider(), + jsonDecoder: JSONDecoder = JSONDecoder() + ) { + self.keychainService = keychainService + self.acccessControlProvider = acccessControlProvider + self.queryProvider = queryProvider + self.jsonDecoder = jsonDecoder + } + + func saveBananaSplit( + with seedName: String, + bananaSplitBackup: BananaSplitBackup, + passphrase: BananaSplitPassphrase + ) -> Result { + do { + let accessControl = try acccessControlProvider.accessControl() + var query = queryProvider.query( + for: .save(seedName: seedName, bananaSplit: bananaSplitBackup) + ) + var osStatus = keychainService.add(query, nil) + if osStatus != errSecSuccess { + let message = SecCopyErrorMessageString(osStatus, nil) as? String ?? "" + return .failure(.saveError(message: message)) + } + + query = queryProvider.passhpraseQuery( + for: KeychainBananaSplitPassphraseQuery.save( + seedName: seedName, + passphrase: passphrase, + accessControl: accessControl + ) + ) + osStatus = keychainService.add(query, nil) + if osStatus != errSecSuccess { + let message = SecCopyErrorMessageString(osStatus, nil) as? String ?? "" + return .failure(.saveError(message: message)) + } + return .success(()) + } catch { + return .failure(.accessControlNotAvailable) + } + } + + func retrieveBananaSplit(with seedName: String) -> Result { + var item: CFTypeRef? + let query = queryProvider.query(for: KeychainBananaSplitQuery.fetch(seedName: seedName)) + let osStatus = keychainService.copyMatching(query, &item) + if osStatus == errSecSuccess, let itemAsData = item as? Data { + do { + let result = try jsonDecoder.decode(BananaSplitBackup.self, from: itemAsData) + return .success(result) + } catch { + return .failure(.dataDecodingError) + } + } + return .failure(.fetchError) + } + + func retrieveBananaSplitPassphrase(with seedName: String) -> Result { + var item: CFTypeRef? + let query = queryProvider.passhpraseQuery(for: KeychainBananaSplitPassphraseQuery.fetch(seedName: seedName)) + let osStatus = keychainService.copyMatching(query, &item) + if osStatus == errSecSuccess, let itemAsData = item as? Data { + do { + let result = try jsonDecoder.decode(BananaSplitPassphrase.self, from: itemAsData) + return .success(result) + } catch { + return .failure(.dataDecodingError) + } + } + return .failure(.fetchError) + } + + func removeBananaSplitBackup(seedName: String) -> Result { + let bananaSplitQuery = queryProvider.query(for: KeychainBananaSplitQuery.delete(seedName: seedName)) + var osStatus = keychainService.delete(bananaSplitQuery) + if osStatus != errSecSuccess { + let errorMessage = SecCopyErrorMessageString(osStatus, nil) as? String ?? "" + return .failure(.deleteError(message: errorMessage)) + } + let passphraseQuery = queryProvider + .passhpraseQuery(for: KeychainBananaSplitPassphraseQuery.delete(seedName: seedName)) + osStatus = keychainService.delete(passphraseQuery) + if osStatus != errSecSuccess { + let errorMessage = SecCopyErrorMessageString(osStatus, nil) as? String ?? "" + return .failure(.deleteError(message: errorMessage)) + } + return .success(()) + } + + func checkIfBananaSplitAlreadyExists(seedName: String) -> Result { + let query = queryProvider.query(for: .check(seedName: seedName)) + var queryResult: AnyObject? + let osStatus = keychainService.copyMatching(query, &queryResult) + switch osStatus { + case errSecItemNotFound: + return .success(false) + case errSecSuccess: + return .success(true) + default: + return .failure(.checkError) + } + } +} diff --git a/ios/PolkadotVault/Core/Keychain/KeychainAccessAdapter.swift b/ios/PolkadotVault/Core/Keychain/Seeds/KeychainSeedsAccessAdapter.swift similarity index 93% rename from ios/PolkadotVault/Core/Keychain/KeychainAccessAdapter.swift rename to ios/PolkadotVault/Core/Keychain/Seeds/KeychainSeedsAccessAdapter.swift index 875224e6a5..bbb411afff 100644 --- a/ios/PolkadotVault/Core/Keychain/KeychainAccessAdapter.swift +++ b/ios/PolkadotVault/Core/Keychain/Seeds/KeychainSeedsAccessAdapter.swift @@ -1,5 +1,5 @@ // -// KeychainAccessAdapter.swift +// KeychainSeedsAccessAdapter.swift // Polkadot Vault // // Created by Krzysztof Rodak on 26/08/2022. @@ -12,7 +12,7 @@ struct FetchSeedsPayload { } /// Protocol that provides access to Keychain's C-like API using modern approach -protocol KeychainAccessAdapting: AnyObject { +protocol KeychainSeedsAccessAdapting: AnyObject { /// Attempts to fetch list of seeds name from Keychain /// - Returns: closure with `.success` and requested `FetchSeedsPayload` /// otherwise `.failure` with `KeychainError` @@ -45,15 +45,15 @@ protocol KeychainAccessAdapting: AnyObject { func removeAllSeeds() -> Bool } -final class KeychainAccessAdapter: KeychainAccessAdapting { +final class KeychainSeedsAccessAdapter: KeychainSeedsAccessAdapting { private let keychainService: KeychainServicing - private let queryProvider: KeychainQueryProviding + private let queryProvider: KeychainSeedsQueryProviding private let acccessControlProvider: AccessControlProviding init( keychainService: KeychainServicing = KeychainService(), acccessControlProvider: AccessControlProviding = AccessControlProvidingAssembler().assemble(), - queryProvider: KeychainQueryProviding = KeychainQueryProvider() + queryProvider: KeychainSeedsQueryProviding = KeychainSeedsQueryProvider() ) { self.keychainService = keychainService self.acccessControlProvider = acccessControlProvider @@ -69,6 +69,10 @@ final class KeychainAccessAdapter: KeychainAccessAdapting { let seedNames = resultAsItems .compactMap { seed in seed[kSecAttrAccount as String] as? String } .sorted() + .filter { + !$0.hasSuffix(KeychainBananaSplitQueryProvider.Constants.bananaSplitSuffix) + && !$0.hasSuffix(KeychainBananaSplitQueryProvider.Constants.passphraseSuffix) + } return .success(FetchSeedsPayload(seeds: seedNames)) } // Keychain returned success but no data diff --git a/ios/PolkadotVault/Core/Keychain/KeychainQueryProvider.swift b/ios/PolkadotVault/Core/Keychain/Seeds/KeychainSeedsQueryProvider.swift similarity index 87% rename from ios/PolkadotVault/Core/Keychain/KeychainQueryProvider.swift rename to ios/PolkadotVault/Core/Keychain/Seeds/KeychainSeedsQueryProvider.swift index e0c47ebbbd..d236e4daf9 100644 --- a/ios/PolkadotVault/Core/Keychain/KeychainQueryProvider.swift +++ b/ios/PolkadotVault/Core/Keychain/Seeds/KeychainSeedsQueryProvider.swift @@ -1,5 +1,5 @@ // -// KeychainQueryProvider.swift +// KeychainSeedsQueryProvider.swift // Polkadot Vault // // Created by Krzysztof Rodak on 26/08/2022. @@ -8,7 +8,7 @@ import Foundation /// Available queries for accessing Keychain -enum KeychainQuery { +enum KeychainSeedsQuery { case fetch case fetchWithData case check @@ -20,15 +20,15 @@ enum KeychainQuery { // sourcery: AutoMockable /// Protocol that provides access to query payload -protocol KeychainQueryProviding: AnyObject { +protocol KeychainSeedsQueryProviding: AnyObject { /// Generates payload query for given query type with given input /// - Parameter queryType: query type and payload if needed /// - Returns: query payload as dictionary that can be used in Keychain querying - func query(for queryType: KeychainQuery) -> CFDictionary + func query(for queryType: KeychainSeedsQuery) -> CFDictionary } -final class KeychainQueryProvider: KeychainQueryProviding { - func query(for queryType: KeychainQuery) -> CFDictionary { +final class KeychainSeedsQueryProvider: KeychainSeedsQueryProviding { + func query(for queryType: KeychainSeedsQuery) -> CFDictionary { var dictionary: [CFString: Any] = [ kSecClass: kSecClassGenericPassword ] diff --git a/ios/PolkadotVault/Core/Keychain/SeedsMediator.swift b/ios/PolkadotVault/Core/Keychain/Seeds/SeedsMediator.swift similarity index 95% rename from ios/PolkadotVault/Core/Keychain/SeedsMediator.swift rename to ios/PolkadotVault/Core/Keychain/Seeds/SeedsMediator.swift index 201fcc9de3..5d02cbfa4f 100644 --- a/ios/PolkadotVault/Core/Keychain/SeedsMediator.swift +++ b/ios/PolkadotVault/Core/Keychain/Seeds/SeedsMediator.swift @@ -11,6 +11,7 @@ import Foundation enum KeychainError: Error, Equatable { case fetchError case checkError + case dataDecodingError case saveError(message: String) case deleteError(message: String) case accessControlNotAvailable @@ -62,8 +63,8 @@ protocol SeedsMediating: AnyObject { /// As this class contains logic related to UI state and data handling, /// it should not interact with Keychain directly, but through injected dependencies final class SeedsMediator: SeedsMediating { - private let queryProvider: KeychainQueryProviding - private let keychainAccessAdapter: KeychainAccessAdapting + private let queryProvider: KeychainSeedsQueryProviding + private let keychainAccessAdapter: KeychainSeedsAccessAdapting private let databaseMediator: DatabaseMediating private let authenticationStateMediator: AuthenticatedStateMediator var seedNamesSubject = CurrentValueSubject<[String], Never>([]) @@ -76,8 +77,8 @@ final class SeedsMediator: SeedsMediating { } init( - queryProvider: KeychainQueryProviding = KeychainQueryProvider(), - keychainAccessAdapter: KeychainAccessAdapting = KeychainAccessAdapter(), + queryProvider: KeychainSeedsQueryProviding = KeychainSeedsQueryProvider(), + keychainAccessAdapter: KeychainSeedsAccessAdapting = KeychainSeedsAccessAdapter(), databaseMediator: DatabaseMediating = DatabaseMediator(), authenticationStateMediator: AuthenticatedStateMediator = ServiceLocator.authenticationStateMediator ) { diff --git a/ios/PolkadotVault/Core/Runtime/RuntimePropertiesProvider.swift b/ios/PolkadotVault/Core/Runtime/RuntimePropertiesProvider.swift index 812c4b683a..88c257301f 100644 --- a/ios/PolkadotVault/Core/Runtime/RuntimePropertiesProvider.swift +++ b/ios/PolkadotVault/Core/Runtime/RuntimePropertiesProvider.swift @@ -22,6 +22,12 @@ protocol RuntimePropertiesProviding: AnyObject { /// Wrapper for accessing `RuntimeProperties` and other application runtime values final class RuntimePropertiesProvider: RuntimePropertiesProviding { + enum Properties: String, CustomStringConvertible { + case testConfiguration = "XCTestConfigurationFilePath" + + var description: String { rawValue } + } + private enum PropertiesValues: String, CustomStringConvertible { case `true` case `false` @@ -30,11 +36,14 @@ final class RuntimePropertiesProvider: RuntimePropertiesProviding { } private let appInformationContainer: ApplicationInformationContaining.Type + private let processInfo: ProcessInfoProtocol init( - appInformationContainer: ApplicationInformationContaining.Type = ApplicationInformation.self + appInformationContainer: ApplicationInformationContaining.Type = ApplicationInformation.self, + processInfo: ProcessInfoProtocol = ProcessInfo.processInfo ) { self.appInformationContainer = appInformationContainer + self.processInfo = processInfo } var runtimeMode: ApplicationRuntimeMode { @@ -44,6 +53,10 @@ final class RuntimePropertiesProvider: RuntimePropertiesProviding { var dynamicDerivationsEnabled: Bool { appInformationContainer.dynamicDerivationsEnabled == PropertiesValues.true.rawValue } + + var isRunningTests: Bool { + processInfo.environment[Properties.testConfiguration.description] != nil + } } extension ApplicationInformation: ApplicationInformationContaining {} diff --git a/ios/PolkadotVault/Core/ServiceLocator.swift b/ios/PolkadotVault/Core/ServiceLocator.swift index 0d48e58a37..fa6e6347ce 100644 --- a/ios/PolkadotVault/Core/ServiceLocator.swift +++ b/ios/PolkadotVault/Core/ServiceLocator.swift @@ -12,7 +12,7 @@ enum ServiceLocator { /// As long as we have `SharedDataModel` as tech debt, we need to have seeds mediator as singleton which is /// unfortunate but necessary for now; to be able to use it outside SwiftUI views it can't be `@EnvironmentalObject` static var seedsMediator: SeedsMediating = SeedsMediator() - static var connectivityMediator: ConnectivityMediator = .init() + static var airgapMediator: AirgapMediating = AirgapMediatorAssembler().assemble() static var authenticationStateMediator: AuthenticatedStateMediator = .init() static var onboardingMediator: OnboardingMediating = OnboardingMediator() diff --git a/ios/PolkadotVault/Design/Heights.swift b/ios/PolkadotVault/Design/Heights.swift index f004278bae..fd9392151c 100644 --- a/ios/PolkadotVault/Design/Heights.swift +++ b/ios/PolkadotVault/Design/Heights.swift @@ -21,6 +21,8 @@ enum Heights { static let navigationBarHeight: CGFloat = 54 /// All variants of `NavbarButton`, 40 pt static let navigationButton: CGFloat = 40 + /// All variants of `IconButton`, 36 pt + static let iconButton: CGFloat = 36 /// All variants of `MenuButton`, 48 pt static let menuButton: CGFloat = 48 /// All variants of `ActionSheetButton`, 44 pt diff --git a/ios/PolkadotVault/Extensions/Localizable+Formatted.swift b/ios/PolkadotVault/Extensions/Localizable+Formatted.swift index 1e421f0cbe..2088a454ba 100644 --- a/ios/PolkadotVault/Extensions/Localizable+Formatted.swift +++ b/ios/PolkadotVault/Extensions/Localizable+Formatted.swift @@ -270,4 +270,21 @@ extension Localizable { } return attributedString } + + static func bananaSplitBackupQRCodeInfo() -> AttributedString { + let mainText = Localizable.BananaSplitBackupQRCode.Label.info.string + let highlightedText = Localizable.BananaSplitBackupQRCode.Label.Info.highlight.string + + let attributedString = NSMutableAttributedString(string: mainText) + attributedString.addAttribute( + .foregroundColor, + value: Color(.textAndIconsTertiary), + range: NSRange(location: 0, length: mainText.count) + ) + + let range = (mainText as NSString).range(of: highlightedText) + attributedString.setAttributes([.foregroundColor: UIColor(.accentPink300)], range: range) + + return AttributedString(attributedString) + } } diff --git a/ios/PolkadotVault/Info.plist b/ios/PolkadotVault/Info.plist index 323f287c56..751389a85f 100644 --- a/ios/PolkadotVault/Info.plist +++ b/ios/PolkadotVault/Info.plist @@ -2,8 +2,8 @@ - LSMinimumSystemVersion - 13.0 + AppRuntimeMode + $(APP_RUNTIME_MODE) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -22,10 +22,14 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + DynamicDerivationsEnabled + $(DYNAMIC_DERIVATIONS_ENABLED) ITSAppUsesNonExemptEncryption LSApplicationCategoryType public.app-category.finance + LSMinimumSystemVersion + 13.0 LSRequiresIPhoneOS NSCameraUsageDescription @@ -68,9 +72,5 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - AppRuntimeMode - $(APP_RUNTIME_MODE) - DynamicDerivationsEnabled - $(DYNAMIC_DERIVATIONS_ENABLED) diff --git a/ios/PolkadotVault/Modals/Alerts/HorizontalActionsBottomModal.swift b/ios/PolkadotVault/Modals/Alerts/HorizontalActionsBottomModal.swift index dc7232f866..4b8d5cd8a6 100644 --- a/ios/PolkadotVault/Modals/Alerts/HorizontalActionsBottomModal.swift +++ b/ios/PolkadotVault/Modals/Alerts/HorizontalActionsBottomModal.swift @@ -15,6 +15,13 @@ struct HorizontalActionsBottomModalViewModel { var mainActionStyle: ActionButtonStyle = .primaryDestructive() var alignment: HorizontalAlignment = .center + static let bananaSplitDeleteBackup = HorizontalActionsBottomModalViewModel( + title: Localizable.BananaSplitDeleteBackup.Label.title.string, + content: Localizable.BananaSplitDeleteBackup.Label.content.string, + dismissActionLabel: Localizable.BananaSplitDeleteBackup.Action.cancel.key, + mainActionLabel: Localizable.BananaSplitDeleteBackup.Action.remove.key + ) + static let forgetKeySet = HorizontalActionsBottomModalViewModel( title: Localizable.KeySetsModal.Confirmation.Label.title.string, content: Localizable.KeySetsModal.Confirmation.Label.content.string, diff --git a/ios/PolkadotVault/Modals/Backup/BackupModal.swift b/ios/PolkadotVault/Modals/Backup/BackupModal.swift index 66ab6d8dae..2ef2cbaefa 100644 --- a/ios/PolkadotVault/Modals/Backup/BackupModal.swift +++ b/ios/PolkadotVault/Modals/Backup/BackupModal.swift @@ -53,7 +53,7 @@ struct BackupModal: View { .font(PrimaryFont.bodyM.font) } Spacer() - CloseModalButton(action: animateDismissal) + CircleButton(action: animateDismissal) } .padding(.leading, Spacing.large) .padding(.trailing, Spacing.medium) diff --git a/ios/PolkadotVault/Modals/ExportPrivateKey/ExportPrivateKeyModal.swift b/ios/PolkadotVault/Modals/ExportPrivateKey/ExportPrivateKeyModal.swift index 6391af2250..407f315eda 100644 --- a/ios/PolkadotVault/Modals/ExportPrivateKey/ExportPrivateKeyModal.swift +++ b/ios/PolkadotVault/Modals/ExportPrivateKey/ExportPrivateKeyModal.swift @@ -34,7 +34,7 @@ struct ExportPrivateKeyModal: View { .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.titleS.font) Spacer() - CloseModalButton(action: animateDismissal) + CircleButton(action: animateDismissal) } .padding([.leading], Spacing.large) .padding([.trailing], Spacing.medium) diff --git a/ios/PolkadotVault/Modals/KeySet/KeyDetailsActionsModal.swift b/ios/PolkadotVault/Modals/KeySet/KeyDetailsActionsModal.swift deleted file mode 100644 index a5046d76d7..0000000000 --- a/ios/PolkadotVault/Modals/KeySet/KeyDetailsActionsModal.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// KeyDetailsActionsModal.swift -// Polkadot Vault -// -// Created by Krzysztof Rodak on 06/09/2022. -// - -import SwiftUI - -struct KeyDetailsActionsModal: View { - @State private var animateBackground: Bool = false - @Binding var isShowingActionSheet: Bool - @Binding var shouldPresentRemoveConfirmationModal: Bool - @Binding var shouldPresentBackupModal: Bool - @Binding var shouldPresentExportKeysSelection: Bool - - var body: some View { - FullScreenRoundedModal( - backgroundTapAction: { animateDismissal() }, - animateBackground: $animateBackground, - content: { - VStack(alignment: .leading, spacing: 0) { - // Export Keys - ActionSheetButton( - action: { animateDismissal { shouldPresentExportKeysSelection.toggle() } }, - icon: Image(.exportKeys), - text: Localizable.KeySetsModal.Action.export.key - ) - ActionSheetButton( - action: { - animateDismissal { shouldPresentBackupModal.toggle() } - }, - icon: Image(.backupKey), - text: Localizable.KeySetsModal.Action.backup.key - ) - ActionSheetButton( - action: { animateDismissal { shouldPresentRemoveConfirmationModal.toggle() } }, - icon: Image(.delete), - text: Localizable.KeySetsModal.Action.delete.key, - style: .destructive - ) - ActionButton( - action: { animateDismissal() }, - text: Localizable.AddKeySet.Button.cancel.key, - style: .emptySecondary() - ) - } - .padding(.horizontal, Spacing.large) - .padding(.top, -Spacing.extraSmall) - .padding(.bottom, Spacing.medium) - } - ) - } - - private func animateDismissal(_ completion: @escaping () -> Void = {}) { - Animations.chainAnimation( - animateBackground.toggle(), - delayedAnimationClosure: { - isShowingActionSheet = false - completion() - }() - ) - } -} diff --git a/ios/PolkadotVault/Modals/NetworkSelection/NetworkSelectionModal.swift b/ios/PolkadotVault/Modals/NetworkSelection/NetworkSelectionModal.swift index 09b510109c..7e30e7844f 100644 --- a/ios/PolkadotVault/Modals/NetworkSelection/NetworkSelectionModal.swift +++ b/ios/PolkadotVault/Modals/NetworkSelection/NetworkSelectionModal.swift @@ -29,7 +29,7 @@ struct NetworkSelectionModal: View { .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.titleS.font) Spacer() - CloseModalButton(action: viewModel.resetAction) + CircleButton(action: viewModel.resetAction) } .padding(.leading, Spacing.large) .padding(.trailing, Spacing.medium) diff --git a/ios/PolkadotVault/PolkadotVaultApp.swift b/ios/PolkadotVault/PolkadotVaultApp.swift index d6bba1d750..f7b2125f27 100644 --- a/ios/PolkadotVault/PolkadotVaultApp.swift +++ b/ios/PolkadotVault/PolkadotVaultApp.swift @@ -9,7 +9,6 @@ import SwiftUI @main struct PolkadotVaultApp: App { - @StateObject var connectivityMediator = ServiceLocator.connectivityMediator @StateObject var navigation = NavigationCoordinator() @StateObject var jailbreakDetectionPublisher = JailbreakDetectionPublisher() @StateObject var applicationStatePublisher = ApplicationStatePublisher() @@ -17,7 +16,9 @@ struct PolkadotVaultApp: App { var body: some Scene { WindowGroup { - if jailbreakDetectionPublisher.isJailbroken { + if RuntimePropertiesProvider().isRunningTests { + EmptyView() + } else if jailbreakDetectionPublisher.isJailbroken { JailbreakDetectedView() } else { MainScreenContainer( @@ -27,7 +28,6 @@ struct PolkadotVaultApp: App { .font(PrimaryFont.bodyL.font) .background(.backgroundPrimary) .environmentObject(navigation) - .environmentObject(connectivityMediator) .environmentObject(jailbreakDetectionPublisher) .environmentObject(applicationStatePublisher) } diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocation.imageset/Contents.json b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocation.imageset/Contents.json new file mode 100644 index 0000000000..16af1b326e --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocation.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "location_off.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocation.imageset/location_off.svg b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocation.imageset/location_off.svg new file mode 100644 index 0000000000..9e1245d047 --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocation.imageset/location_off.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocationError.imageset/Contents.json b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocationError.imageset/Contents.json new file mode 100644 index 0000000000..338d7ea1d1 --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocationError.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "location-on.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocationError.imageset/location-on.svg b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocationError.imageset/location-on.svg new file mode 100644 index 0000000000..515fed819d --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/airgapLocationError.imageset/location-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/key_set/banana_split_backup.imageset/Contents.json b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/banana_split_backup.imageset/Contents.json new file mode 100644 index 0000000000..bd7a54b40c --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/banana_split_backup.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "banana_split_backup.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/key_set/banana_split_backup.imageset/banana_split_backup.svg b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/banana_split_backup.imageset/banana_split_backup.svg new file mode 100644 index 0000000000..702037135d --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/banana_split_backup.imageset/banana_split_backup.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/key_set/show_passphrase.imageset/Contents.json b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/show_passphrase.imageset/Contents.json new file mode 100644 index 0000000000..4e357382f5 --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/show_passphrase.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "show_passphrase.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/key_set/show_passphrase.imageset/show_passphrase.svg b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/show_passphrase.imageset/show_passphrase.svg new file mode 100644 index 0000000000..0cc0f1305b --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/key_set/show_passphrase.imageset/show_passphrase.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/qr_code.imageset/Contents.json b/ios/PolkadotVault/Resources/Assets.xcassets/qr_code.imageset/Contents.json new file mode 100644 index 0000000000..f45b70250d --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/qr_code.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "qr_code.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/qr_code.imageset/qr_code.svg b/ios/PolkadotVault/Resources/Assets.xcassets/qr_code.imageset/qr_code.svg new file mode 100644 index 0000000000..bdee065eb0 --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/qr_code.imageset/qr_code.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/refresh_passphrase.imageset/Contents.json b/ios/PolkadotVault/Resources/Assets.xcassets/refresh_passphrase.imageset/Contents.json new file mode 100644 index 0000000000..190ec8646b --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/refresh_passphrase.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "refreshPassphrase.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/PolkadotVault/Resources/Assets.xcassets/refresh_passphrase.imageset/refreshPassphrase.svg b/ios/PolkadotVault/Resources/Assets.xcassets/refresh_passphrase.imageset/refreshPassphrase.svg new file mode 100644 index 0000000000..bf1a540ebd --- /dev/null +++ b/ios/PolkadotVault/Resources/Assets.xcassets/refresh_passphrase.imageset/refreshPassphrase.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/PolkadotVault/Resources/en.lproj/Localizable.strings b/ios/PolkadotVault/Resources/en.lproj/Localizable.strings index 073948a55c..c89c01f7c3 100644 --- a/ios/PolkadotVault/Resources/en.lproj/Localizable.strings +++ b/ios/PolkadotVault/Resources/en.lproj/Localizable.strings @@ -105,8 +105,8 @@ "KeySets.Label.Empty.Subtitle" = "Add a new Key Set to Store Keys and Sign Transactions"; "KeySetsModal.Action.Export" = "Export Keys"; -"KeySetsModal.Action.Derive" = "Derive from Key"; -"KeySetsModal.Action.Backup" = "Backup Key Set"; +"KeySetsModal.Action.BananaSplit" = "Banana Split Backup"; +"KeySetsModal.Action.Backup" = "Manual Backup"; "KeySetsModal.Action.Delete" = "Delete"; "KeySetsModal.Confirmation.Label.Title" = "Forget this Key Set?"; @@ -503,7 +503,6 @@ "Error.ApplicationUpdateRequired.Label.Info.Highlight" = "paritytech.github.io/parity-signer/tutorials/Upgrading.html"; "Error.ApplicationUpdateRequired.Action.Backup" = "Backup Secret Phrase"; - "Error.LockedDevice.Label.Title" = "Please Unlock the App"; "Error.LockedDevice.Label.Subtitle" = "The device has been locked owing to an authentication failure. Please unlock it to keep using the Polkadot Vault."; "Error.LockedDevice.Action.Unlock" = "Unlock the App"; @@ -518,6 +517,8 @@ "Airgap.Label.Content" = "Please follow these steps to enable Air Gap mode and keep it permanently enabled."; "Airgap.Label.Airplane" = "Enable Airplane Mode"; "Airgap.Label.Wifi" = "Turn off WiFi Connection"; +"Airgap.Label.Location" = "Turn off Location Services"; + "Airgap.Label.Cables" = "Disconnect all cables"; "Airgap.Label.Cables.Confirmation" = "I confirm that all Cables are Disconnected"; "Airgap.Action.Next" = "Next"; @@ -620,3 +621,27 @@ "NoKeySets.Label.Subheader" = "Add a new key set or recover an existing one by entering the secret recovery phrase to get started"; "NoKeySets.Action.Add" = "Add new Key Set"; "NoKeySets.Action.Recover" = "Recover Key Set"; + +"BananaSplitBackup.Label.Title" = "Banana Split Backup"; +"BananaSplitBackup.Label.Header" = "Backup your key set by turning the secret phrase into sharded QR codes with passphrase protection"; +"BananaSplitBackup.Label.Shards.Header" = "Number of QR Code Shards"; +"BananaSplitBackup.Label.Shards.Footer" = "%@ shards out of %@ to reconstruct"; +"BananaSplitBackup.Label.Passphrase.Header" = "Passphrase for the Recovery"; +"BananaSplitBackup.Label.Passphrase.Footer" = "Write down your passphrase. You'll need it to recover from Banana Split."; +"BananaSplitBackup.Label.Passphrase.Info" = "Banana Split backup will recover the key set without derived keys. To back up derived keys, use the manual backup option. Each key will have to be added individually by entering the derivation path name."; +"BananaSplitBackup.Action.Create" = "Create"; +"BananaSplitBackup.Error.CouldNotLoad" = "Banana Split Backup could not be loaded"; + +"BananaSplitBackupQRCode.Label.Info" = "Scan this animated QR code into the bs.parity.io app to print QR code shards."; +"BananaSplitBackupQRCode.Label.Info.Highlight" = "bs.parity.io"; + +"BananaSplitActionModal.Action.Passphrase" = "Show Passphrase"; +"BananaSplitActionModal.Action.Remove" = "Remove Backup"; +"BananaSplitActionModal.Action.Cancel" = "Cancel"; + +"BananaSplitPassphraseModal.Label.Header" = "Passphrase"; +"BananaSplitDeleteBackup.Label.Title" = "Remove Banana Split"; +"BananaSplitDeleteBackup.Label.Content" = "You can still use this backup if you have QR code shards printed and have passphrase written down on paper."; +"BananaSplitDeleteBackup.Action.Cancel" = "Cancel"; +"BananaSplitDeleteBackup.Action.Remove" = "Remove"; + diff --git a/ios/PolkadotVault/Screens/Containers/MainScreenContainer.swift b/ios/PolkadotVault/Screens/Containers/MainScreenContainer.swift index 3af2237cf9..f4ce9e1d80 100644 --- a/ios/PolkadotVault/Screens/Containers/MainScreenContainer.swift +++ b/ios/PolkadotVault/Screens/Containers/MainScreenContainer.swift @@ -59,7 +59,7 @@ extension MainScreenContainer { private let passwordProtectionStatePublisher: PasswordProtectionStatePublisher private let databaseVersionMediator: DatabaseVersionMediator private let appLaunchMediator: AppLaunchMediating - private let connectivityMediator: ConnectivityMediator + private let airgapMediator: AirgapMediating private let cancelBag = CancelBag() @Published var viewState: ViewState = .deviceLocked @@ -73,14 +73,14 @@ extension MainScreenContainer { passwordProtectionStatePublisher: PasswordProtectionStatePublisher = PasswordProtectionStatePublisher(), databaseVersionMediator: DatabaseVersionMediator = DatabaseVersionMediator(), appLaunchMediator: AppLaunchMediating = AppLaunchMediator(), - connectivityMediator: ConnectivityMediator = ServiceLocator.connectivityMediator + airgapMediator: AirgapMediating = ServiceLocator.airgapMediator ) { self.authenticationStateMediator = authenticationStateMediator self.onboardingMediator = onboardingMediator self.passwordProtectionStatePublisher = passwordProtectionStatePublisher self.databaseVersionMediator = databaseVersionMediator self.appLaunchMediator = appLaunchMediator - self.connectivityMediator = connectivityMediator + self.airgapMediator = airgapMediator initialiseAppRun() } @@ -94,15 +94,20 @@ extension MainScreenContainer { private extension MainScreenContainer.ViewModel { func initialiseAppRun() { - appLaunchMediator.finaliseInitialisation(connectivityMediator.isConnectivityOn) { result in - switch result { - case .success: - self.checkInitialState() - case let .failure(error): - self.presentableError = .alertError(message: error.localizedDescription) - self.isPresentingError = true + airgapMediator.isConnectedPublisher + .first() + .sink { [weak self] isConnected in + self?.appLaunchMediator.finaliseInitialisation(isConnected) { result in + switch result { + case .success: + self?.checkInitialState() + case let .failure(error): + self?.presentableError = .alertError(message: error.localizedDescription) + self?.isPresentingError = true + } + } } - } + .store(in: cancelBag) } func checkInitialState() { @@ -144,11 +149,11 @@ private extension MainScreenContainer.ViewModel { } .assign(to: \.viewState, on: self) .store(in: cancelBag) - connectivityMediator.$isConnectivityOn - .sink(receiveValue: { newValue in - guard !self.isPresentingNoAirgap, newValue else { return } - self.isPresentingNoAirgap = newValue - }) + airgapMediator.isConnectedPublisher + .sink { [weak self] isConnected in + guard let self, !self.isPresentingNoAirgap else { return } + isPresentingNoAirgap = isConnected + } .store(in: cancelBag) } } diff --git a/ios/PolkadotVault/Screens/CreateKey/RecoverKeySet/RecoverKeySetSeedPhraseView.swift b/ios/PolkadotVault/Screens/CreateKey/RecoverKeySet/RecoverKeySetSeedPhraseView.swift index 9e5ffa759a..6ecc031af9 100644 --- a/ios/PolkadotVault/Screens/CreateKey/RecoverKeySet/RecoverKeySetSeedPhraseView.swift +++ b/ios/PolkadotVault/Screens/CreateKey/RecoverKeySet/RecoverKeySetSeedPhraseView.swift @@ -7,6 +7,11 @@ import SwiftUI +struct BananaSplitQRCodeRecovery { + let seedName: String + let onRecoveryComplete: (CreateKeysForNetworksView.OnCompletionAction) -> Void +} + struct RecoverKeySetSeedPhraseView: View { private enum Constants { static let capsuleContainerID = "capsuleContainerID" @@ -72,6 +77,11 @@ struct RecoverKeySetSeedPhraseView: View { .padding(.top, Spacing.extraSmall) .padding(.bottom, Spacing.extraExtraSmall) Spacer() + HStack(alignment: .center, spacing: 0) { + Spacer() + CircleButton(image: .qrCode, action: viewModel.didTapQRCode) + .padding(Spacing.extraSmall) + } } .frame(minHeight: 156) .containerBackground(CornerRadius.small) @@ -118,6 +128,23 @@ struct RecoverKeySetSeedPhraseView: View { ) .clearModalBackground() } + .fullScreenModal( + isPresented: $viewModel.isShowingQRScanner, + onDismiss: { + focus = false + viewModel.onCameraDismiss() + } + ) { + CameraView( + viewModel: .init( + isPresented: $viewModel.isShowingQRScanner, + bananaSplitQRCodeRecovery: .init( + seedName: viewModel.seedName, + onRecoveryComplete: viewModel.onCameraComplete(_:) + ) + ) + ) + } } } } @@ -226,9 +253,11 @@ extension RecoverKeySetSeedPhraseView { private var shouldSkipUpdate = false private let service: RecoverKeySetServicing private let onCompletion: (CreateKeysForNetworksView.OnCompletionAction) -> Void + private var flowCompleted: (CreateKeysForNetworksView.OnCompletionAction)? let seedName: String @Binding var isPresented: Bool @Published var isPresentingDetails: Bool = false + @Published var isShowingQRScanner: Bool = false @Published var isValidSeedPhrase: Bool = false @Published var seedPhraseGrid: [GridElement] = [] @Published var userInput: String = Constants.invisibleNonEmptyCharacter @@ -274,6 +303,25 @@ extension RecoverKeySetSeedPhraseView { seedPhraseDraft.append(guess) } + func didTapQRCode() { + isShowingQRScanner = true + } + + func onCameraComplete(_ onComplete: CreateKeysForNetworksView.OnCompletionAction) { + isShowingQRScanner = false + flowCompleted = onComplete + } + + func onCameraDismiss() { + guard let flowCompleted else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + self.isPresented = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + self.onCompletion(flowCompleted) + } + } + } + func onUserInput(_ word: String) { guard !shouldSkipUpdate else { return } defer { shouldSkipUpdate = false } diff --git a/ios/PolkadotVault/Screens/DerivedKey/Subviews/DerivationMethodsInfoView.swift b/ios/PolkadotVault/Screens/DerivedKey/Subviews/DerivationMethodsInfoView.swift index ca4ac409ee..e052ed2ad9 100644 --- a/ios/PolkadotVault/Screens/DerivedKey/Subviews/DerivationMethodsInfoView.swift +++ b/ios/PolkadotVault/Screens/DerivedKey/Subviews/DerivationMethodsInfoView.swift @@ -23,7 +23,7 @@ struct DerivationMethodsInfoView: View { .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.titleS.font) Spacer() - CloseModalButton(action: viewModel.animateDismissal) + CircleButton(action: viewModel.animateDismissal) } .padding(.leading, Spacing.large) .padding(.trailing, Spacing.medium) diff --git a/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitActionModal.swift b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitActionModal.swift new file mode 100644 index 0000000000..90684c1b81 --- /dev/null +++ b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitActionModal.swift @@ -0,0 +1,89 @@ +// +// BananaSplitActionModal.swift +// PolkadotVault +// +// Created by Krzysztof Rodak on 23/02/2024. +// + +import SwiftUI + +struct BananaSplitActionModal: View { + @StateObject var viewModel: ViewModel + + var body: some View { + FullScreenRoundedModal( + backgroundTapAction: { viewModel.dismissActionSheet() }, + animateBackground: $viewModel.animateBackground, + content: { + VStack(alignment: .leading, spacing: 0) { + // Show Passphrase Keys + ActionSheetButton( + action: viewModel.showPassphrase, + icon: Image(.showPassphrase), + text: Localizable.BananaSplitActionModal.Action.passphrase.key + ) + // Remove Keys + ActionSheetButton( + action: viewModel.removeBackup, + icon: Image(.delete), + text: Localizable.BananaSplitActionModal.Action.remove.key, + style: .destructive + ) + // Cancel + ActionButton( + action: viewModel.dismissActionSheet, + text: Localizable.BananaSplitActionModal.Action.cancel.key, + style: .emptySecondary() + ) + } + .padding(.horizontal, Spacing.large) + .padding(.top, -Spacing.extraSmall) + .padding(.bottom, Spacing.medium) + } + ) + } +} + +extension BananaSplitActionModal { + final class ViewModel: ObservableObject { + @Published var animateBackground: Bool = false + @Binding var isPresented: Bool + @Binding var shouldPresentDeleteBackupWarningModal: Bool + @Binding var shouldPresentPassphraseModal: Bool + + init( + isPresented: Binding, + shouldPresentDeleteBackupWarningModal: Binding, + shouldPresentPassphraseModal: Binding + ) { + _isPresented = isPresented + _shouldPresentDeleteBackupWarningModal = shouldPresentDeleteBackupWarningModal + _shouldPresentPassphraseModal = shouldPresentPassphraseModal + } + + func removeBackup() { + shouldPresentDeleteBackupWarningModal = true + dismissActionSheet() + } + + func showPassphrase() { + shouldPresentPassphraseModal = true + dismissActionSheet() + } + + func dismissActionSheet() { + animateDismissal() + } + + func animateDismissal() { + Animations.chainAnimation( + animateBackground.toggle(), + // swiftformat:disable all + delayedAnimationClosure: self.hide() + ) + } + private func hide() { + isPresented = false + } + } +} diff --git a/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitModal.swift b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitModal.swift new file mode 100644 index 0000000000..4aef1eea80 --- /dev/null +++ b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitModal.swift @@ -0,0 +1,309 @@ +// +// BananaSplitModal.swift +// PolkadotVault +// +// Created by Krzysztof Rodak on 21/02/2024. +// + +import SwiftUI + +struct BananaSplitModalView: View { + @StateObject var viewModel: ViewModel + @FocusState private var textFieldFocused: Bool + + var body: some View { + NavigationView { + GeometryReader { geo in + VStack(spacing: 0) { + NavigationBarView( + viewModel: .init( + leftButtons: [.init( + type: .xmark, + action: viewModel.onBackTap + )], + rightButtons: [.init( + type: .activeAction( + Localizable.BananaSplitBackup.Action.create.key, + .constant(!viewModel.isActionAvailable()) + ), + action: { + textFieldFocused = false + viewModel.onCreateTap() + } + )] + ) + ) + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + mainContent() + passphraseView() + infoView() + Spacer() + } + } + } + .frame( + minWidth: geo.size.width, + minHeight: geo.size.height + ) + .background(.backgroundPrimary) + NavigationLink( + destination: + BananaSplitQRCodeModalView( + viewModel: .init( + seedName: viewModel.seedName, + bananaSplitBackup: viewModel.bananaSplitBackup, + onCompletion: viewModel.onQRCodeCompletion + ) + ) + .navigationBarHidden(true), + isActive: $viewModel.isPresentingQRCode + ) { EmptyView() } + } + .navigationBarHidden(true) + .navigationViewStyle(.stack) + .fullScreenModal( + isPresented: $viewModel.isPresentingError + ) { + ErrorBottomModal( + viewModel: viewModel.presentableError, + isShowingBottomAlert: $viewModel.isPresentingError + ) + .clearModalBackground() + } + } + } + + @ViewBuilder + func mainContent() -> some View { + VStack(alignment: .leading, spacing: 0) { + Localizable.BananaSplitBackup.Label.title.text + .foregroundColor(.textAndIconsPrimary) + .font(PrimaryFont.titleL.font) + .padding(.top, Spacing.extraSmall) + .padding(.horizontal, Spacing.extraSmall) + Localizable.BananaSplitBackup.Label.header.text + .foregroundColor(.textAndIconsTertiary) + .font(PrimaryFont.bodyM.font) + .padding(.vertical, Spacing.extraSmall) + .padding(.horizontal, Spacing.extraSmall) + Localizable.BananaSplitBackup.Label.Shards.header.text + .foregroundColor(.textAndIconsPrimary) + .font(PrimaryFont.bodyL.font) + .padding(.vertical, Spacing.extraSmall) + .padding(.horizontal, Spacing.extraSmall) + TextField("", text: $viewModel.totalShards) + .submitLabel(.done) + .primaryTextFieldStyle( + Localizable.NewSeed.Name.Label.placeholder.string, + keyboardType: .asciiCapableNumberPad, + text: $viewModel.totalShards + ) + .focused($textFieldFocused) + .onSubmit { + textFieldFocused = false + viewModel.onSubmitTap() + } + .padding(.vertical, Spacing.medium) + Text(Localizable.BananaSplitBackup.Label.Shards.footer( + viewModel.requiredShardsCount, + viewModel.totalShards + )) + .foregroundColor(.textAndIconsTertiary) + .font(PrimaryFont.captionM.font) + .padding(.horizontal, Spacing.extraSmall) + } + .padding(.horizontal, Spacing.medium) + .padding(.bottom, Spacing.medium) + } + + @ViewBuilder + func passphraseView() -> some View { + VStack(alignment: .leading, spacing: Spacing.medium) { + HStack(alignment: .center, spacing: 0) { + VStack(alignment: .leading, spacing: Spacing.extraExtraSmall) { + Localizable.BananaSplitBackup.Label.Passphrase.header.text + .foregroundColor(.textAndIconsTertiary) + .font(PrimaryFont.bodyM.font) + Text(viewModel.recoveryPassphrase) + .foregroundColor(.textAndIconsPrimary) + .font(PrimaryFont.bodyL.font) + } + Spacer() + IconButton( + action: viewModel.refreshPassphrase, + icon: .refreshPassphrase + ) + } + .padding(.vertical, Spacing.medium) + .padding(.leading, Spacing.medium) + .padding(.trailing, Spacing.extraSmall) + .overlay( + RoundedRectangle(cornerRadius: Spacing.medium) + .stroke(.fill12, lineWidth: 1) + ) + Localizable.BananaSplitBackup.Label.Passphrase.footer.text + .foregroundColor(.textAndIconsTertiary) + .font(PrimaryFont.captionM.font) + .padding(.horizontal, Spacing.extraSmall) + } + .padding(.horizontal, Spacing.medium) + } + + @ViewBuilder + func infoView() -> some View { + HStack(alignment: .center, spacing: Spacing.medium) { + Localizable.BananaSplitBackup.Label.Passphrase.info.text + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.accentPink300) + .font(PrimaryFont.captionM.font) + Image(.infoIconBold) + .foregroundColor(.accentPink300) + } + .padding(Spacing.medium) + .background( + RoundedRectangle(cornerRadius: CornerRadius.medium) + .foregroundColor(.accentPink300Fill8) + ) + .padding(.horizontal, Spacing.medium) + .padding(.top, Spacing.extraExtraLarge) + .padding(.bottom, Spacing.medium) + } +} + +extension BananaSplitModalView { + private enum Constants { + static let passphraseWords: UInt32 = 4 + static let defaultTotalShards: UInt32 = 3 + } + + enum OnCompletionAction: Equatable { + case create([QrData]) + case cancel + case close + } + + final class ViewModel: ObservableObject { + @Published var totalShards: String = .init(Constants.defaultTotalShards) { + didSet { + updateRequiredShards() + } + } + + @Published var requiredShardsCount: UInt32 = 2 + @Published var recoveryPassphrase: String = "" + @Published var isPresentingError: Bool = false + @Published var isPresentingQRCode: Bool = false + @Published var bananaSplitBackup: BananaSplitBackup = .init(qrCodes: []) + @Published var presentableError: ErrorBottomModalViewModel! + @Binding var isPresented: Bool + let onCompletion: (BananaSplitModalView.OnCompletionAction) -> Void + + let seedName: String + private let bananaSplitMediator: KeychainBananaSplitAccessMediating + private let seedsMediator: SeedsMediating + private let service: BananaSplitServicing + private var totalShardsCount: UInt32 { + UInt32(totalShards) ?? Constants.defaultTotalShards + } + + init( + seedName: String, + bananaSplitMediator: KeychainBananaSplitAccessMediating = KeychainBananaSplitMediator(), + seedsMediator: SeedsMediating = ServiceLocator.seedsMediator, + service: BananaSplitServicing = BananaSplitService(), + isPresented: Binding, + onCompletion: @escaping (BananaSplitModalView.OnCompletionAction) -> Void + ) { + self.seedName = seedName + self.bananaSplitMediator = bananaSplitMediator + self.seedsMediator = seedsMediator + self.service = service + self.onCompletion = onCompletion + _isPresented = isPresented + refreshPassphrase() + } + + func onBackTap() { + isPresented = false + } + + func onCreateTap() { + service.encrypt( + secret: seedsMediator.getSeed(seedName: seedName), + title: seedName, + passphrase: recoveryPassphrase, + totalShards: totalShardsCount, + requiredShards: requiredShardsCount + ) { result in + switch result { + case let .success(bananaSplitBackup): + self.bananaSplitBackup = bananaSplitBackup + let result = self.bananaSplitMediator.saveBananaSplit( + with: self.seedName, + bananaSplitBackup: bananaSplitBackup, + passphrase: .init(passphrase: self.recoveryPassphrase) + ) + switch result { + case .success: + self.isPresentingQRCode = true + case let .failure(error): + self.presentableError = .alertError(message: error.localizedDescription) + self.isPresentingError = true + } + case let .failure(error): + self.presentableError = .alertError(message: error.backendDisplayError) + self.isPresentingError = true + } + } + } + + func isActionAvailable() -> Bool { + !totalShards.isEmpty + } + + func refreshPassphrase() { + service.generatePassphrase(with: Constants.passphraseWords) { result in + switch result { + case let .success(newPassphrase): + self.recoveryPassphrase = newPassphrase + case let .failure(error): + self.presentableError = .alertError(message: error.localizedDescription) + self.isPresentingError = true + } + } + } + + func onQRCodeCompletion(_: BananaSplitQRCodeModalView.OnCompletionAction) { + isPresented = false + onCompletion(.close) + } + } +} + +private extension BananaSplitModalView.ViewModel { + func onSubmitTap() { + guard isActionAvailable() else { return } + onCreateTap() + } + + func updateRequiredShards() { + requiredShardsCount = totalShardsCount / 2 + 1 + } +} + +#if DEBUG + struct BananaSplitModalView_Previews: PreviewProvider { + static var previews: some View { + BananaSplitModalView( + viewModel: .init( + seedName: "Key Set", + isPresented: .constant(true), + onCompletion: { _ in } + ) + ) + .previewLayout(.sizeThatFits) + } + } +#endif diff --git a/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitPassphraseModal.swift b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitPassphraseModal.swift new file mode 100644 index 0000000000..4215b98f6e --- /dev/null +++ b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitPassphraseModal.swift @@ -0,0 +1,84 @@ +// +// BananaSplitPassphraseModal.swift +// PolkadotVault +// +// Created by Krzysztof Rodak on 26/02/2024. +// + +import SwiftUI + +struct BananaSplitPassphraseModal: View { + @StateObject var viewModel: ViewModel + + var body: some View { + FullScreenRoundedModal( + backgroundTapAction: { viewModel.dismissActionSheet() }, + animateBackground: $viewModel.animateBackground, + content: { + VStack(alignment: .leading, spacing: Spacing.medium) { + HStack { + Localizable.BananaSplitPassphraseModal.Label.header.text + .foregroundColor(.textAndIconsPrimary) + .font(PrimaryFont.titleS.font) + Spacer() + CircleButton(action: viewModel.dismissActionSheet) + } + Text(viewModel.passphrase) + .multilineTextAlignment(.leading) + .padding(.vertical, Spacing.medium) + } + .padding(.leading, Spacing.large) + .padding(.trailing, Spacing.medium) + .padding(.top, Spacing.small) + .padding(.bottom, Spacing.medium) + } + ) + } +} + +extension BananaSplitPassphraseModal { + final class ViewModel: ObservableObject { + @Published var animateBackground: Bool = false + @Published var passphrase: String = "" + @Binding var isPresented: Bool + private let seedName: String + private let bananaSplitMediator: KeychainBananaSplitAccessMediating + + init( + seedName: String, + isPresented: Binding, + bananaSplitMediator: KeychainBananaSplitAccessMediating = KeychainBananaSplitMediator() + ) { + _isPresented = isPresented + self.seedName = seedName + self.bananaSplitMediator = bananaSplitMediator + loadPassphrase() + } + + func dismissActionSheet() { + animateDismissal() + } + + func animateDismissal() { + Animations.chainAnimation( + animateBackground.toggle(), + // swiftformat:disable all + delayedAnimationClosure: self.hide() + ) + } + + private func hide() { + isPresented = false + } + + private func loadPassphrase() { + switch bananaSplitMediator.retrieveBananaSplitPassphrase(with: seedName) { + case let .success(passphrase): + self.passphrase = passphrase.passphrase + case .failure: + () + } + + } + } +} diff --git a/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitQRCodeModal.swift b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitQRCodeModal.swift new file mode 100644 index 0000000000..afe17de676 --- /dev/null +++ b/ios/PolkadotVault/Screens/KeyDetails/BananaSplit/BananaSplitQRCodeModal.swift @@ -0,0 +1,172 @@ +// +// BananaSplitQRCodeModal.swift +// PolkadotVault +// +// Created by Krzysztof Rodak on 28/02/2024. +// + +import Combine +import SwiftUI + +struct BananaSplitQRCodeModalView: View { + @StateObject var viewModel: ViewModel + + var body: some View { + GeometryReader { geo in + VStack(spacing: 0) { + // Navigation bar + NavigationBarView( + viewModel: .init( + leftButtons: [.init(type: .xmark, action: { viewModel.onCloseTap() })], + rightButtons: [.init(type: .more, action: viewModel.onMoreButtonTap)] + ) + ) + VStack(spacing: 0) { + // QR Code container + Spacer() + VStack(spacing: 0) { + AnimatedQRCodeView( + viewModel: Binding.constant( + .init( + qrCodes: viewModel.bananaSplitBackup.qrCodes + ) + ) + ) + } + .strokeContainerBackground() + // Info + AttributedInfoBoxView(text: Localizable.bananaSplitBackupQRCodeInfo()) + .padding(.vertical, Spacing.extraSmall) + Spacer() + Spacer() + Spacer() + } + .padding(.horizontal, Spacing.large) + .padding(.top, Spacing.extraSmall) + } + .frame( + minWidth: geo.size.width, + minHeight: geo.size.height + ) + .background(.backgroundPrimary) + } + // Action sheet + .fullScreenModal( + isPresented: $viewModel.isPresentingActionSheet, + onDismiss: { + // iOS 15 handling of following .fullscreen presentation after dismissal, we need to dispatch this async + DispatchQueue.main.async { viewModel.checkForActionsPresentation() } + } + ) { + BananaSplitActionModal( + viewModel: .init( + isPresented: $viewModel.isPresentingActionSheet, + shouldPresentDeleteBackupWarningModal: $viewModel.shouldPresentDeleteBackupWarningModal, + shouldPresentPassphraseModal: $viewModel.shouldPresentPassphraseModal + ) + ) + .clearModalBackground() + } + // Passphrase + .fullScreenModal( + isPresented: $viewModel.isPresentingPassphraseModal + ) { + BananaSplitPassphraseModal( + viewModel: .init( + seedName: viewModel.seedName, + isPresented: $viewModel.isPresentingPassphraseModal + ) + ) + .clearModalBackground() + } + .fullScreenModal(isPresented: $viewModel.isPresentingDeleteBackupWarningModal) { + HorizontalActionsBottomModal( + viewModel: .bananaSplitDeleteBackup, + mainAction: viewModel.onDeleteBackupTap(), + isShowingBottomAlert: $viewModel.isPresentingDeleteBackupWarningModal + ) + .clearModalBackground() + } + } +} + +extension BananaSplitQRCodeModalView { + enum OnCompletionAction: Equatable { + case close + case backupDeleted + } + + final class ViewModel: ObservableObject { + let seedName: String + @Published var bananaSplitBackup: BananaSplitBackup + @Published var isPresentingActionSheet = false + @Published var isPresentingDeleteBackupWarningModal = false + @Published var isPresentingPassphraseModal = false + @Published var shouldPresentDeleteBackupWarningModal = false + @Published var shouldPresentPassphraseModal = false + @Published var isPresentingError: Bool = false + @Published var presentableError: ErrorBottomModalViewModel = .alertError(message: "") + private let bananaSplitMediator: KeychainBananaSplitAccessMediating + private let onCompletion: (OnCompletionAction) -> Void + + init( + seedName: String, + bananaSplitBackup: BananaSplitBackup, + bananaSplitMediator: KeychainBananaSplitAccessMediating = KeychainBananaSplitMediator(), + onCompletion: @escaping (OnCompletionAction) -> Void + ) { + _bananaSplitBackup = .init(initialValue: bananaSplitBackup) + self.seedName = seedName + self.bananaSplitMediator = bananaSplitMediator + self.onCompletion = onCompletion + } + + func onMoreButtonTap() { + isPresentingActionSheet = true + } + + func onCloseTap() { + onCompletion(.close) + } + + func checkForActionsPresentation() { + if shouldPresentPassphraseModal { + shouldPresentPassphraseModal.toggle() + isPresentingPassphraseModal = true + } + if shouldPresentDeleteBackupWarningModal { + shouldPresentDeleteBackupWarningModal.toggle() + isPresentingDeleteBackupWarningModal = true + } + } + + func onDeleteBackupTap() { + switch bananaSplitMediator.removeBananaSplitBackup(seedName: seedName) { + case .success: + onCompletion(.backupDeleted) + case let .failure(error): + presentableError = .alertError(message: error.localizedDescription) + isPresentingError = true + } + } + } +} + +#if DEBUG + struct BananaSplitQRCodeModalView_Previews: PreviewProvider { + static var previews: some View { + Group { + BananaSplitQRCodeModalView( + viewModel: .init( + seedName: "seed name", + bananaSplitBackup: .init(qrCodes: [[]]), + onCompletion: { _ in + } + ) + ) + } + .previewLayout(.sizeThatFits) + .preferredColorScheme(.dark) + } + } +#endif diff --git a/ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportKeysSelectionModal.swift b/ios/PolkadotVault/Screens/KeyDetails/Modals/ExportKeysSelectionModal.swift similarity index 99% rename from ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportKeysSelectionModal.swift rename to ios/PolkadotVault/Screens/KeyDetails/Modals/ExportKeysSelectionModal.swift index 9881565bb3..a765bc2ebe 100644 --- a/ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportKeysSelectionModal.swift +++ b/ios/PolkadotVault/Screens/KeyDetails/Modals/ExportKeysSelectionModal.swift @@ -29,7 +29,7 @@ struct ExportKeysSelectionModal: View { .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.titleS.font) Spacer() - CloseModalButton(action: viewModel.cancelAction) + CircleButton(action: viewModel.cancelAction) } .padding(.leading, Spacing.large) .padding(.trailing, Spacing.medium) diff --git a/ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportMultipleKeysModal+ViewModel.swift b/ios/PolkadotVault/Screens/KeyDetails/Modals/ExportMultipleKeysModal+ViewModel.swift similarity index 100% rename from ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportMultipleKeysModal+ViewModel.swift rename to ios/PolkadotVault/Screens/KeyDetails/Modals/ExportMultipleKeysModal+ViewModel.swift diff --git a/ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportMultipleKeysModal.swift b/ios/PolkadotVault/Screens/KeyDetails/Modals/ExportMultipleKeysModal.swift similarity index 99% rename from ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportMultipleKeysModal.swift rename to ios/PolkadotVault/Screens/KeyDetails/Modals/ExportMultipleKeysModal.swift index d8ade2fbbb..37b86a3772 100644 --- a/ios/PolkadotVault/Screens/KeyDetails/ExportKeys/ExportMultipleKeysModal.swift +++ b/ios/PolkadotVault/Screens/KeyDetails/Modals/ExportMultipleKeysModal.swift @@ -95,7 +95,7 @@ private extension ExportMultipleKeysModal { .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.titleS.font) Spacer() - CloseModalButton(action: animateDismissal) + CircleButton(action: animateDismissal) } } } diff --git a/ios/PolkadotVault/Screens/KeyDetails/Modals/KeyDetailsActionsModal.swift b/ios/PolkadotVault/Screens/KeyDetails/Modals/KeyDetailsActionsModal.swift new file mode 100644 index 0000000000..754d3c0167 --- /dev/null +++ b/ios/PolkadotVault/Screens/KeyDetails/Modals/KeyDetailsActionsModal.swift @@ -0,0 +1,117 @@ +// +// KeyDetailsActionsModal.swift +// Polkadot Vault +// +// Created by Krzysztof Rodak on 06/09/2022. +// + +import SwiftUI + +struct KeyDetailsActionsModal: View { + @StateObject var viewModel: ViewModel + + var body: some View { + FullScreenRoundedModal( + backgroundTapAction: { viewModel.dismissActionSheet() }, + animateBackground: $viewModel.animateBackground, + content: { + VStack(alignment: .leading, spacing: 0) { + // Export Keys + ActionSheetButton( + action: viewModel.exportKeysAction, + icon: Image(.exportKeys), + text: Localizable.KeySetsModal.Action.export.key + ) + // Banana Split Backup + ActionSheetButton( + action: viewModel.bananaSplitBackup, + icon: Image(.bananaSplitBackup), + text: Localizable.KeySetsModal.Action.bananaSplit.key + ) + // Manual Backup + ActionSheetButton( + action: viewModel.manualBackupKeysAction, + icon: Image(.backupKey), + text: Localizable.KeySetsModal.Action.backup.key + ) + // Remove Keys + ActionSheetButton( + action: viewModel.removeKeysAction, + icon: Image(.delete), + text: Localizable.KeySetsModal.Action.delete.key, + style: .destructive + ) + // Cancel + ActionButton( + action: viewModel.dismissActionSheet, + text: Localizable.AddKeySet.Button.cancel.key, + style: .emptySecondary() + ) + } + .padding(.horizontal, Spacing.large) + .padding(.top, -Spacing.extraSmall) + .padding(.bottom, Spacing.medium) + } + ) + } +} + +extension KeyDetailsActionsModal { + final class ViewModel: ObservableObject { + @Published var animateBackground: Bool = false + @Binding var isPresented: Bool + @Binding var shouldPresentRemoveConfirmationModal: Bool + @Binding var shouldPresentManualBackupModal: Bool + @Binding var shouldPresentBananaSplitModal: Bool + @Binding var shouldPresentExportKeysSelection: Bool + + init( + isPresented: Binding, + shouldPresentRemoveConfirmationModal: Binding, + shouldPresentBananaSplitModal: Binding, + shouldPresentManualBackupModal: Binding, + shouldPresentExportKeysSelection: Binding + ) { + _isPresented = isPresented + _shouldPresentRemoveConfirmationModal = shouldPresentRemoveConfirmationModal + _shouldPresentBananaSplitModal = shouldPresentBananaSplitModal + _shouldPresentManualBackupModal = shouldPresentManualBackupModal + _shouldPresentExportKeysSelection = shouldPresentExportKeysSelection + } + + func exportKeysAction() { + shouldPresentExportKeysSelection = true + dismissActionSheet() + } + + func bananaSplitBackup() { + shouldPresentBananaSplitModal = true + dismissActionSheet() + } + + func manualBackupKeysAction() { + shouldPresentManualBackupModal = true + dismissActionSheet() + } + + func removeKeysAction() { + shouldPresentRemoveConfirmationModal = true + dismissActionSheet() + } + + func dismissActionSheet() { + animateDismissal() + } + + func animateDismissal() { + Animations.chainAnimation( + animateBackground.toggle(), + // swiftformat:disable all + delayedAnimationClosure: self.hide() + ) + } + private func hide() { + isPresented = false + } + } +} diff --git a/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetails+ViewModel.swift b/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetails+ViewModel.swift index e68c70a32a..25c39dfa3e 100644 --- a/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetails+ViewModel.swift +++ b/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetails+ViewModel.swift @@ -9,6 +9,12 @@ import Combine import Foundation import SwiftUI +enum BananaSplitPresentationState { + case createBackup(BananaSplitModalView.ViewModel) + case qrCode(BananaSplitQRCodeModalView.ViewModel) + case empty +} + extension KeyDetailsView { enum OnCompletionAction: Equatable { case keySetDeleted @@ -27,14 +33,18 @@ extension KeyDetailsView { private let exportPrivateKeyService: PrivateKeyQRCodeService private let keyDetailsActionsService: KeyDetailsActionService private let seedsMediator: SeedsMediating + private let bananaSplitMediator: KeychainBananaSplitAccessMediating @Published var keyName: String @Published var keysData: MKeysNew? + @Published var bananaSplitPresentationState: BananaSplitPresentationState = .empty @Published var shouldPresentRemoveConfirmationModal = false - @Published var shouldPresentBackupModal = false + @Published var shouldPresentBananaSplitBackupModal = false + @Published var shouldPresentManualBackupModal = false @Published var shouldPresentExportKeysSelection = false @Published var isShowingActionSheet = false @Published var isShowingRemoveConfirmation = false + @Published var isPresentingBananaSplitBackupModal = false @Published var isShowingBackupModal = false @Published var isPresentingExportKeySelection = false @Published var isPresentingRootDetails = false @@ -76,7 +86,8 @@ extension KeyDetailsView { keyDetailsService: KeyDetailsService = KeyDetailsService(), networksService: GetManagedNetworksService = GetManagedNetworksService(), keyDetailsActionsService: KeyDetailsActionService = KeyDetailsActionService(), - seedsMediator: SeedsMediating = ServiceLocator.seedsMediator + seedsMediator: SeedsMediating = ServiceLocator.seedsMediator, + bananaSplitMediator: KeychainBananaSplitAccessMediating = KeychainBananaSplitMediator() ) { self.onDeleteCompletion = onDeleteCompletion self.exportPrivateKeyService = exportPrivateKeyService @@ -84,6 +95,7 @@ extension KeyDetailsView { self.networksService = networksService self.keyDetailsActionsService = keyDetailsActionsService self.seedsMediator = seedsMediator + self.bananaSplitMediator = bananaSplitMediator _keyName = .init(initialValue: initialKeyName) subscribeToNetworkChanges() } @@ -117,6 +129,7 @@ extension KeyDetailsView { self.isPresentingError = true } } + updateBananaSplitPresentationState() } func refreshNetworks() { @@ -252,6 +265,16 @@ extension KeyDetailsView { ) isSnackbarPresented = true } + + func onBananaSplitModalCompletion(_: BananaSplitModalView.OnCompletionAction) { + updateBananaSplitPresentationState() + isPresentingBananaSplitBackupModal = false + } + + func onBananaSplitBackupModalCompletion(_: BananaSplitQRCodeModalView.OnCompletionAction) { + updateBananaSplitPresentationState() + isPresentingBananaSplitBackupModal = false + } } } @@ -328,8 +351,8 @@ extension KeyDetailsView.ViewModel { shouldPresentRemoveConfirmationModal.toggle() isShowingRemoveConfirmation.toggle() } - if shouldPresentBackupModal { - shouldPresentBackupModal.toggle() + if shouldPresentManualBackupModal { + shouldPresentManualBackupModal.toggle() keyDetailsActionsService.performBackupSeed(seedName: keyName) { result in switch result { case .success: @@ -340,6 +363,10 @@ extension KeyDetailsView.ViewModel { } } } + if shouldPresentBananaSplitBackupModal { + shouldPresentBananaSplitBackupModal.toggle() + isPresentingBananaSplitBackupModal = true + } if shouldPresentExportKeysSelection { shouldPresentExportKeysSelection = false isPresentingExportKeySelection = true @@ -357,6 +384,50 @@ extension KeyDetailsView.ViewModel { base58: keysData?.root?.base58 ?? "" ) } + + func updateBananaSplitPresentationState() { + switch bananaSplitMediator.checkIfBananaSplitAlreadyExists(seedName: keyName) { + case let .success(exists): + if exists { + switch bananaSplitMediator.retrieveBananaSplit(with: keyName) { + case let .success(bananaSplitBackup): + bananaSplitPresentationState = .qrCode( + .init( + seedName: keyName, + bananaSplitBackup: bananaSplitBackup, + onCompletion: onBananaSplitBackupModalCompletion(_:) + ) + ) + case .failure: + bananaSplitPresentationState = .createBackup( + .init( + seedName: keyName, + isPresented: Binding( + get: { self.isPresentingBananaSplitBackupModal }, + set: { self.isPresentingBananaSplitBackupModal = $0 } + ), + onCompletion: onBananaSplitModalCompletion(_:) + ) + ) + } + } else { + bananaSplitPresentationState = .createBackup( + .init( + seedName: keyName, + isPresented: Binding( + get: { self.isPresentingBananaSplitBackupModal }, + set: { self.isPresentingBananaSplitBackupModal = $0 } + ), + onCompletion: onBananaSplitModalCompletion(_:) + ) + ) + } + case let .failure(failure): + presentableError = .alertError(message: failure.localizedDescription) + isPresentingError = true + bananaSplitPresentationState = .empty + } + } } private extension KeyDetailsView.ViewModel { diff --git a/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetailsView.swift b/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetailsView.swift index 92706945ef..1730fdc1ce 100644 --- a/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetailsView.swift +++ b/ios/PolkadotVault/Screens/KeyDetails/Views/KeyDetailsView.swift @@ -69,10 +69,13 @@ struct KeyDetailsView: View { } ) { KeyDetailsActionsModal( - isShowingActionSheet: $viewModel.isShowingActionSheet, - shouldPresentRemoveConfirmationModal: $viewModel.shouldPresentRemoveConfirmationModal, - shouldPresentBackupModal: $viewModel.shouldPresentBackupModal, - shouldPresentExportKeysSelection: $viewModel.shouldPresentExportKeysSelection + viewModel: .init( + isPresented: $viewModel.isShowingActionSheet, + shouldPresentRemoveConfirmationModal: $viewModel.shouldPresentRemoveConfirmationModal, + shouldPresentBananaSplitModal: $viewModel.shouldPresentBananaSplitBackupModal, + shouldPresentManualBackupModal: $viewModel.shouldPresentManualBackupModal, + shouldPresentExportKeysSelection: $viewModel.shouldPresentExportKeysSelection + ) ) .clearModalBackground() } @@ -110,6 +113,16 @@ struct KeyDetailsView: View { ) .clearModalBackground() } + .fullScreenModal(isPresented: $viewModel.isPresentingBananaSplitBackupModal) { + switch viewModel.bananaSplitPresentationState { + case let .createBackup(viewModel): + BananaSplitModalView(viewModel: viewModel) + case let .qrCode(viewModel): + BananaSplitQRCodeModalView(viewModel: viewModel) + case .empty: + EmptyView() + } + } .fullScreenModal( isPresented: $viewModel.isShowingBackupModal, onDismiss: viewModel.clearBackupModalState diff --git a/ios/PolkadotVault/Screens/KeyDetails/Views/ManageKeySetsView.swift b/ios/PolkadotVault/Screens/KeyDetails/Views/ManageKeySetsView.swift index 961bd19b0b..813ef94064 100644 --- a/ios/PolkadotVault/Screens/KeyDetails/Views/ManageKeySetsView.swift +++ b/ios/PolkadotVault/Screens/KeyDetails/Views/ManageKeySetsView.swift @@ -28,7 +28,7 @@ struct ManageKeySetsView: View { .foregroundColor(.textAndIconsPrimary) .font(PrimaryFont.titleS.font) Spacer() - CloseModalButton(action: viewModel.onClose) + CircleButton(action: viewModel.onClose) } .padding(.bottom, Spacing.small) // Spacer diff --git a/ios/PolkadotVault/Screens/Onboarding/Airgap/AirgapMediator.swift b/ios/PolkadotVault/Screens/Onboarding/Airgap/AirgapMediator.swift deleted file mode 100644 index 861c739784..0000000000 --- a/ios/PolkadotVault/Screens/Onboarding/Airgap/AirgapMediator.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// AirgapMediator.swift -// NativeSigner -// -// Created by Krzysztof Rodak on 15/02/2023. -// - -import Combine -import Foundation -import Network - -enum AirgapComponent: Equatable, Hashable { - case aiplaneMode - case wifi -} - -// sourcery: AutoMockable -protocol AirgapMediating: AnyObject { - func startMonitoringAirgap(_ update: @escaping (Bool, Bool) -> Void) -} - -final class AirgapMediator: AirgapMediating { - private let adaptee: NWPathMonitor - private let monitoringQueue: DispatchQueue - private let notificationQueue: DispatchQueue - - var isWifiOn: AnyPublisher! - var isAirplaneModeOn: AnyPublisher! - - init( - adaptee: NWPathMonitor = NWPathMonitor(), - monitoringQueue: DispatchQueue = DispatchQueue.global(qos: .background), - notificationQueue: DispatchQueue = DispatchQueue.main - ) { - self.adaptee = adaptee - self.monitoringQueue = monitoringQueue - self.notificationQueue = notificationQueue - } - - func startMonitoringAirgap(_ update: @escaping (Bool, Bool) -> Void) { - adaptee.pathUpdateHandler = { [weak self] path in - guard let self else { return } - let isWifiOn: Bool = path.usesInterfaceType(.wifi) - var currentInterfaces = path.availableInterfaces - currentInterfaces.removeAll(where: { $0.type == .wifi }) - let isAirplaneModeOn = currentInterfaces.isEmpty - notificationQueue.async { - update(isAirplaneModeOn, isWifiOn) - } - } - adaptee.start(queue: monitoringQueue) - } -} - -final class AirgapMediatingStub: AirgapMediating { - func startMonitoringAirgap(_ update: @escaping (Bool, Bool) -> Void) { - update(true, false) - } -} diff --git a/ios/PolkadotVault/Screens/Onboarding/Airgap/NoAirgapView.swift b/ios/PolkadotVault/Screens/Onboarding/NoAirgapView.swift similarity index 86% rename from ios/PolkadotVault/Screens/Onboarding/Airgap/NoAirgapView.swift rename to ios/PolkadotVault/Screens/Onboarding/NoAirgapView.swift index b0205fdc65..708fc14d05 100644 --- a/ios/PolkadotVault/Screens/Onboarding/Airgap/NoAirgapView.swift +++ b/ios/PolkadotVault/Screens/Onboarding/NoAirgapView.swift @@ -35,6 +35,10 @@ struct NoAirgapView: View { Divider() cell(.wifi, isChecked: viewModel.isWifiChecked) .padding(.top, Spacing.small) + .padding(.bottom, Spacing.small) + Divider() + cell(.location, isChecked: viewModel.isLocationChecked) + .padding(.top, Spacing.small) } .padding(Spacing.medium) } @@ -128,6 +132,8 @@ extension NoAirgapView { @Published var isActionDisabled: Bool = true @Published var isAirplaneModeChecked: Bool = false @Published var isWifiChecked: Bool = false + @Published var isLocationChecked: Bool = false + private let cancelBag = CancelBag() private let mode: Mode private let airgapMediator: AirgapMediating private let onActionTap: () -> Void @@ -151,12 +157,17 @@ extension NoAirgapView { subscribeToUpdates() } - func subscribeToUpdates() { - airgapMediator.startMonitoringAirgap { [weak self] isAirplaneModeOn, isWifiOn in - self?.isAirplaneModeChecked = isAirplaneModeOn - self?.isWifiChecked = !isWifiOn - self?.updateActionState() - } + private func subscribeToUpdates() { + airgapMediator.airgapPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] airgapState in + self?.isAirplaneModeChecked = airgapState.isAirplaneModeOn + self?.isWifiChecked = !airgapState.isWifiOn + self?.isLocationChecked = !airgapState.isLocationServiceEnabled + self?.updateActionState() + } + .store(in: cancelBag) + airgapMediator.startMonitoringAirgap() } func onDoneTap() { @@ -169,7 +180,10 @@ extension NoAirgapView { } private func updateActionState() { - isActionDisabled = !isCableCheckBoxSelected || !isWifiChecked || !isAirplaneModeChecked + isActionDisabled = !isCableCheckBoxSelected + || !isWifiChecked + || !isAirplaneModeChecked + || !isLocationChecked } } } diff --git a/ios/PolkadotVault/Screens/Onboarding/Airgap/OnboadingAirgapView+Components.swift b/ios/PolkadotVault/Screens/Onboarding/OnboadingAirgapView+Components.swift similarity index 74% rename from ios/PolkadotVault/Screens/Onboarding/Airgap/OnboadingAirgapView+Components.swift rename to ios/PolkadotVault/Screens/Onboarding/OnboadingAirgapView+Components.swift index 906b1f2275..0deea2ebef 100644 --- a/ios/PolkadotVault/Screens/Onboarding/Airgap/OnboadingAirgapView+Components.swift +++ b/ios/PolkadotVault/Screens/Onboarding/OnboadingAirgapView+Components.swift @@ -7,6 +7,12 @@ import SwiftUI +enum AirgapComponent: Equatable, Hashable { + case aiplaneMode + case wifi + case location +} + extension AirgapComponent { var uncheckedForegroundColor: Color { .accentRed300 @@ -22,6 +28,8 @@ extension AirgapComponent { Localizable.Airgap.Label.airplane.string case .wifi: Localizable.Airgap.Label.wifi.string + case .location: + Localizable.Airgap.Label.location.string } } @@ -31,6 +39,8 @@ extension AirgapComponent { Image(.airgapAirplaneError) case .wifi: Image(.airgapWifiError) + case .location: + Image(.airgapLocationError) } } @@ -40,6 +50,8 @@ extension AirgapComponent { Image(.airgapAirplane) case .wifi: Image(.airgapWifi) + case .location: + Image(.airgapLocation) } } } diff --git a/ios/PolkadotVault/Screens/PublicKey/KeyDetailsPublicKeyView.swift b/ios/PolkadotVault/Screens/PublicKey/KeyDetailsPublicKeyView.swift index 3ae2f8c8bb..4acc886f7f 100644 --- a/ios/PolkadotVault/Screens/PublicKey/KeyDetailsPublicKeyView.swift +++ b/ios/PolkadotVault/Screens/PublicKey/KeyDetailsPublicKeyView.swift @@ -334,7 +334,6 @@ extension KeyDetailsPublicKeyView { } .previewLayout(.sizeThatFits) .preferredColorScheme(.dark) - .environmentObject(ConnectivityMediator()) } } #endif diff --git a/ios/PolkadotVault/Modals/KeySet/PublicKeyActionsModal.swift b/ios/PolkadotVault/Screens/PublicKey/PublicKeyActionsModal.swift similarity index 100% rename from ios/PolkadotVault/Modals/KeySet/PublicKeyActionsModal.swift rename to ios/PolkadotVault/Screens/PublicKey/PublicKeyActionsModal.swift diff --git a/ios/PolkadotVault/Screens/Scan/BananaSplit/EnterBananaSplitPasswordModal.swift b/ios/PolkadotVault/Screens/Scan/BananaSplit/EnterBananaSplitPasswordModal.swift index 69a09e5100..c00f38a171 100644 --- a/ios/PolkadotVault/Screens/Scan/BananaSplit/EnterBananaSplitPasswordModal.swift +++ b/ios/PolkadotVault/Screens/Scan/BananaSplit/EnterBananaSplitPasswordModal.swift @@ -115,7 +115,7 @@ struct EnterBananaSplitPasswordView: View { extension EnterBananaSplitPasswordView { final class ViewModel: ObservableObject { @Binding var isPresented: Bool - @Published var seedName: String = "" + @Published var seedName: String @Published var password: String = "" @Published var isNameValid: Bool = true @Published var isPasswordValid: Bool = true @@ -133,11 +133,13 @@ extension EnterBananaSplitPasswordView { init( seedsMediator: SeedsMediating = ServiceLocator.seedsMediator, + seedName: String = "", isPresented: Binding, qrCodeData: [String], onCompletion: @escaping (CreateKeysForNetworksView.OnCompletionAction) -> Void ) { self.seedsMediator = seedsMediator + _seedName = .init(initialValue: seedName) self.onCompletion = onCompletion self.qrCodeData = qrCodeData _isPresented = isPresented diff --git a/ios/PolkadotVault/Screens/Scan/CameraView.swift b/ios/PolkadotVault/Screens/Scan/CameraView.swift index 672218194e..441c5ec1d4 100644 --- a/ios/PolkadotVault/Screens/Scan/CameraView.swift +++ b/ios/PolkadotVault/Screens/Scan/CameraView.swift @@ -128,6 +128,7 @@ struct CameraView: View { ) { EnterBananaSplitPasswordView( viewModel: .init( + seedName: viewModel.bananaSplitQRCodeRecovery?.seedName ?? "", isPresented: $viewModel.isPresentingEnterBananaSplitPassword, qrCodeData: model.bucket, onCompletion: viewModel.onKeySetAddCompletion(_:) @@ -262,16 +263,19 @@ extension CameraView { private let dynamicDerivationsService: DynamicDerivationsService private let seedsMediator: SeedsMediating private let runtimePropertiesProvider: RuntimePropertiesProviding + let bananaSplitQRCodeRecovery: BananaSplitQRCodeRecovery? private weak var cameraModel: CameraService? init( isPresented: Binding, + bananaSplitQRCodeRecovery: BananaSplitQRCodeRecovery? = nil, seedsMediator: SeedsMediating = ServiceLocator.seedsMediator, scanService: ScanTabService = ScanTabService(), dynamicDerivationsService: DynamicDerivationsService = DynamicDerivationsService(), runtimePropertiesProvider: RuntimePropertiesProviding = RuntimePropertiesProvider() ) { _isPresented = isPresented + self.bananaSplitQRCodeRecovery = bananaSplitQRCodeRecovery self.seedsMediator = seedsMediator self.scanService = scanService self.dynamicDerivationsService = dynamicDerivationsService @@ -372,6 +376,13 @@ extension CameraView { } func onKeySetAddCompletion(_ completionAction: CreateKeysForNetworksView.OnCompletionAction) { + if let onRecoveryComplete = bananaSplitQRCodeRecovery?.onRecoveryComplete { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + onRecoveryComplete(completionAction) + self.dismissView() + } + return + } let message: String = switch completionAction { case let .createKeySet(seedName): @@ -384,6 +395,7 @@ extension CameraView { style: .info ) isSnackbarPresented = true + resumeCamera() } func onEnterPasswordDismissal() { diff --git a/ios/PolkadotVault/Screens/Settings/Subviews/Backup/SettingsBackupModal.swift b/ios/PolkadotVault/Screens/Settings/Subviews/Backup/SettingsBackupModal.swift index be35fefc00..83c8ea7b2e 100644 --- a/ios/PolkadotVault/Screens/Settings/Subviews/Backup/SettingsBackupModal.swift +++ b/ios/PolkadotVault/Screens/Settings/Subviews/Backup/SettingsBackupModal.swift @@ -31,7 +31,7 @@ struct SettingsBackupModal: View { .font(PrimaryFont.titleS.font) } Spacer() - CloseModalButton(action: viewModel.dismissModal) + CircleButton(action: viewModel.dismissModal) } .padding(.leading, Spacing.large) .padding(.trailing, Spacing.medium) diff --git a/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMonitoringAssemblerTests.swift b/ios/PolkadotVaultTests/Core/Airgap/AirgapMediatorAssemblerTests.swift similarity index 68% rename from ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMonitoringAssemblerTests.swift rename to ios/PolkadotVaultTests/Core/Airgap/AirgapMediatorAssemblerTests.swift index 11dd4faf04..e582989265 100644 --- a/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMonitoringAssemblerTests.swift +++ b/ios/PolkadotVaultTests/Core/Airgap/AirgapMediatorAssemblerTests.swift @@ -1,21 +1,21 @@ // -// ConnectivityMonitoringAssemblerTests.swift +// AirgapMediatorAssemblerTests.swift // PolkadotVaultTests // -// Created by Krzysztof Rodak on 02/08/2022. +// Created by Krzysztof Rodak on 04/03/2024. // @testable import PolkadotVault import XCTest -final class ConnectivityMonitoringAssemblerTests: XCTestCase { +final class AirgapMediatorAssemblerTests: XCTestCase { private var runtimePropertiesProvider: RuntimePropertiesProvidingMock! - private var subject: ConnectivityMonitoringAssembler! + private var subject: AirgapMediatorAssembler! override func setUp() { super.setUp() runtimePropertiesProvider = RuntimePropertiesProvidingMock() - subject = ConnectivityMonitoringAssembler( + subject = AirgapMediatorAssembler( runtimePropertiesProvider: runtimePropertiesProvider ) } @@ -28,7 +28,7 @@ final class ConnectivityMonitoringAssemblerTests: XCTestCase { let result = subject.assemble() // Then - XCTAssertTrue(result is ConnectivityMonitoringStub) + XCTAssertTrue(result is AirgapMediatingStub) } func test_assemble_whenProduction_returnsSystemAdapter() { @@ -39,7 +39,7 @@ final class ConnectivityMonitoringAssemblerTests: XCTestCase { let result = subject.assemble() // Then - XCTAssertTrue(result is ConnectivityMonitoringAdapter) + XCTAssertTrue(result is AirgapMediator) } func test_assemble_whenQA_returnsSystemAdapter() { @@ -50,6 +50,6 @@ final class ConnectivityMonitoringAssemblerTests: XCTestCase { let result = subject.assemble() // Then - XCTAssertTrue(result is ConnectivityMonitoringAdapter) + XCTAssertTrue(result is AirgapMediator) } } diff --git a/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMediatorTests.swift b/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMediatorTests.swift deleted file mode 100644 index 2b366db23f..0000000000 --- a/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMediatorTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// ConnectivityMediatorTests.swift -// PolkadotVaultTests -// -// Created by Krzysztof Rodak on 20/11/2023. -// - -import Foundation -@testable import PolkadotVault -import XCTest - -final class ConnectivityMediatorTests: XCTestCase { - private var connectivityMonitoringMock: ConnectivityMonitoringMock! - private var connectivityMediator: ConnectivityMediator! - - override func setUp() { - super.setUp() - connectivityMonitoringMock = ConnectivityMonitoringMock() - connectivityMediator = ConnectivityMediator( - connectivityMonitor: connectivityMonitoringMock - ) - } - - override func tearDown() { - connectivityMonitoringMock = nil - connectivityMediator = nil - super.tearDown() - } - - func testConnectivityMediator_StartMonitoring_ShouldBeCalledOnce() { - // Given: ConnectivityMediator is initialized - - // When: setUpConnectivityMonitoring is called during initialization - - // Then: startMonitoring should be called once - XCTAssertEqual(connectivityMonitoringMock.startMonitoringCallsCount, 1) - } - - func testConnectivityMediator_WhenConnected_ShouldUpdateIsConnectivityOnToTrue() { - // Given - XCTAssertFalse(connectivityMediator.isConnectivityOn) - - // When - connectivityMonitoringMock.simulateConnectivityChange(isConnected: true) - - // Then - XCTAssertTrue(connectivityMediator.isConnectivityOn) - } - - func testConnectivityMediator_WhenDisconnected_ShouldUpdateIsConnectivityOnToFalse() { - // Given - connectivityMonitoringMock.simulateConnectivityChange(isConnected: true) - - // When - connectivityMonitoringMock.simulateConnectivityChange(isConnected: false) - - // Then - XCTAssertFalse(connectivityMediator.isConnectivityOn) - } -} - -// MARK: - Mocks - -final class ConnectivityMonitoringMock: ConnectivityMonitoring { - var startMonitoringCallsCount = 0 - var startMonitoringReceivedUpdate: ((Bool) -> Void)? - - func startMonitoring(_ update: @escaping (Bool) -> Void) { - startMonitoringCallsCount += 1 - startMonitoringReceivedUpdate = update - } - - // Helper method to simulate connectivity change - func simulateConnectivityChange(isConnected: Bool) { - startMonitoringReceivedUpdate?(isConnected) - } -} diff --git a/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMonitoringAdapterTests.swift b/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMonitoringAdapterTests.swift deleted file mode 100644 index 0ea753bb4c..0000000000 --- a/ios/PolkadotVaultTests/Core/Connectivity/ConnectivityMonitoringAdapterTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ConnectivityMonitoringAdapterTests.swift -// PolkadotVaultTests -// -// Created by Krzysztof Rodak on 02/08/2022. -// - -import Network -@testable import PolkadotVault -import XCTest - -final class ConnectivityMonitoringAdapterTests: XCTestCase { - private var adaptee: PathMonitorProtocolMock! - private var monitoringQueue: DispatchQueue! - private var notificationQueue: DispatchQueue! - private var subject: ConnectivityMonitoringAdapter! - - override func setUp() { - super.setUp() - adaptee = PathMonitorProtocolMock() - notificationQueue = DispatchQueue.main - monitoringQueue = DispatchQueue.global(qos: .background) - subject = ConnectivityMonitoringAdapter( - adaptee: adaptee, - monitoringQueue: monitoringQueue, - notificationQueue: notificationQueue - ) - } - - func test_startMonitoring_setsUpdateHandler() { - // Then - XCTAssertNil(adaptee.pathUpdateHandler) - - // When - subject.startMonitoring { _ in } - - // Then - XCTAssertNotNil(adaptee.pathUpdateHandler) - } - - func test_startMonitoring_startsListeningOnMonitoringQueue() { - // When - subject.startMonitoring { _ in } - - // Then - XCTAssertEqual(adaptee.startQueueCallsCount, 1) - XCTAssertEqual(adaptee.startQueueReceivedQueue, [monitoringQueue]) - } -} - -// MARK: - Mocks - -final class PathMonitorProtocolMock: PathMonitorProtocol { - @preconcurrency - var pathUpdateHandler: (@Sendable (NWPath) -> Void)? - - var startQueueCallsCount = 0 - var startQueueReceivedQueue: [DispatchQueue] = [] - - func start(queue: DispatchQueue) { - startQueueCallsCount += 1 - startQueueReceivedQueue.append(queue) - } -} diff --git a/ios/PolkadotVaultTests/Core/Keychain/BananaSplit/KeychainBananaSplitQueryProviderTests.swift b/ios/PolkadotVaultTests/Core/Keychain/BananaSplit/KeychainBananaSplitQueryProviderTests.swift new file mode 100644 index 0000000000..e232c0e323 --- /dev/null +++ b/ios/PolkadotVaultTests/Core/Keychain/BananaSplit/KeychainBananaSplitQueryProviderTests.swift @@ -0,0 +1,161 @@ +// +// KeychainBananaSplitQueryProviderTests.swift +// PolkadotVaultTests +// +// Created by Krzysztof Rodak on 04/03/2024. +// + +import Foundation +@testable import PolkadotVault +import Security +import XCTest + +final class KeychainBananaSplitQueryProviderTests: XCTestCase { + private var subject: KeychainBananaSplitQueryProvider! + private var jsonEncoder: JSONEncoder! + + override func setUp() { + super.setUp() + jsonEncoder = JSONEncoder() + subject = KeychainBananaSplitQueryProvider(jsonEncoder: jsonEncoder) + } + + override func tearDown() { + jsonEncoder = nil + subject = nil + super.tearDown() + } + + func test_query_fetchBananaSplit_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let queryType = KeychainBananaSplitQuery.fetch(seedName: seedName) + + // When + let result = subject.query(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual(result[kSecMatchLimit] as! CFString, kSecMatchLimitOne) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.bananaSplitSuffix + ) + XCTAssertEqual(result[kSecReturnData] as! Bool, true) + } + + func test_query_checkBananaSplit_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let queryType = KeychainBananaSplitQuery.check(seedName: seedName) + + // When + let result = subject.query(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual(result[kSecMatchLimit] as! CFString, kSecMatchLimitOne) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.bananaSplitSuffix + ) + XCTAssertEqual(result[kSecReturnData] as! Bool, false) + } + + func test_query_deleteBananaSplit_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let queryType = KeychainBananaSplitQuery.delete(seedName: seedName) + + // When + let result = subject.query(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.bananaSplitSuffix + ) + } + + func test_query_saveBananaSplit_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let bananaSplit = BananaSplitBackup(qrCodes: [[10]]) + let queryType = KeychainBananaSplitQuery.save(seedName: seedName, bananaSplit: bananaSplit) + let expectedData = try? jsonEncoder.encode(bananaSplit) + + // When + let result = subject.query(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.bananaSplitSuffix + ) + XCTAssertEqual(result[kSecValueData] as? Data, expectedData) + XCTAssertEqual(result[kSecReturnData] as! Bool, false) + } + + func test_query_fetchPassphrase_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let queryType = KeychainBananaSplitPassphraseQuery.fetch(seedName: seedName) + + // When + let result = subject.passhpraseQuery(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual(result[kSecMatchLimit] as! CFString, kSecMatchLimitOne) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.passphraseSuffix + ) + XCTAssertEqual(result[kSecReturnData] as! Bool, true) + } + + func test_query_deletePassphrase_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let queryType = KeychainBananaSplitPassphraseQuery.delete(seedName: seedName) + + // When + let result = subject.passhpraseQuery(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.passphraseSuffix + ) + } + + func test_query_savePassphrase_returnsExpectedValues() { + // Given + let seedName = "testSeed" + let passphrase = BananaSplitPassphrase(passphrase: "dummyPassphrase") + let expectedAccessControl: SecAccessControl! = try? SimulatorAccessControlProvider() + .accessControl() // it's fine to use it instead of mock, as! it's just dedicated to be used on simulator + let queryType = KeychainBananaSplitPassphraseQuery.save( + seedName: seedName, + passphrase: passphrase, + accessControl: expectedAccessControl + ) + let expectedData = try? jsonEncoder.encode(passphrase) + + // When + let result = subject.passhpraseQuery(for: queryType) as! [CFString: Any] + + // Then + XCTAssertEqual(result[kSecClass] as! CFString, kSecClassGenericPassword) + XCTAssertEqual( + result[kSecAttrAccount] as! String, + seedName + KeychainBananaSplitQueryProvider.Constants.passphraseSuffix + ) + XCTAssertEqual(result[kSecValueData] as? Data, expectedData) + XCTAssertTrue(result[kSecAttrAccessControl] as! SecAccessControl === expectedAccessControl) + XCTAssertEqual(result[kSecReturnData] as! Bool, false) + } +} diff --git a/ios/PolkadotVaultTests/Core/Keychain/KeychainAccessAdapterTests.swift b/ios/PolkadotVaultTests/Core/Keychain/Seeds/KeychainSeedsAccessAdapterTests.swift similarity index 97% rename from ios/PolkadotVaultTests/Core/Keychain/KeychainAccessAdapterTests.swift rename to ios/PolkadotVaultTests/Core/Keychain/Seeds/KeychainSeedsAccessAdapterTests.swift index 4f09c6bc4d..4c681c6d3e 100644 --- a/ios/PolkadotVaultTests/Core/Keychain/KeychainAccessAdapterTests.swift +++ b/ios/PolkadotVaultTests/Core/Keychain/Seeds/KeychainSeedsAccessAdapterTests.swift @@ -1,5 +1,5 @@ // -// KeychainAccessAdapterTests.swift +// KeychainSeedsAccessAdapterTests.swift // PolkadotVaultTests // // Created by Krzysztof Rodak on 22/11/2023. @@ -9,18 +9,18 @@ import Foundation @testable import PolkadotVault import XCTest -final class KeychainAccessAdapterTests: XCTestCase { - private var keychainQueryProviderMock: KeychainQueryProvidingMock! +final class KeychainSeedsAccessAdapterTests: XCTestCase { + private var keychainQueryProviderMock: KeychainSeedsQueryProvidingMock! private var accessControlProviderMock: AccessControlProvidingMock! private var keychainService: KeychainServiceMock! - private var keychainAccessAdapter: KeychainAccessAdapter! + private var keychainAccessAdapter: KeychainSeedsAccessAdapter! override func setUp() { super.setUp() - keychainQueryProviderMock = KeychainQueryProvidingMock() + keychainQueryProviderMock = KeychainSeedsQueryProvidingMock() accessControlProviderMock = AccessControlProvidingMock() keychainService = KeychainServiceMock() - keychainAccessAdapter = KeychainAccessAdapter( + keychainAccessAdapter = KeychainSeedsAccessAdapter( keychainService: keychainService, acccessControlProvider: accessControlProviderMock, queryProvider: keychainQueryProviderMock diff --git a/ios/PolkadotVaultTests/Core/Keychain/KeychainQueryProviderTests.swift b/ios/PolkadotVaultTests/Core/Keychain/Seeds/KeychainSeedsQueryProviderTests.swift similarity index 87% rename from ios/PolkadotVaultTests/Core/Keychain/KeychainQueryProviderTests.swift rename to ios/PolkadotVaultTests/Core/Keychain/Seeds/KeychainSeedsQueryProviderTests.swift index 505bb74741..12e07072bd 100644 --- a/ios/PolkadotVaultTests/Core/Keychain/KeychainQueryProviderTests.swift +++ b/ios/PolkadotVaultTests/Core/Keychain/Seeds/KeychainSeedsQueryProviderTests.swift @@ -1,5 +1,5 @@ // -// KeychainQueryProviderTests.swift +// KeychainSeedsQueryProviderTests.swift // PolkadotVaultTests // // Created by Krzysztof Rodak on 29/08/2022. @@ -9,17 +9,17 @@ import XCTest // swiftlint:disable force_cast -final class KeychainQueryProviderTests: XCTestCase { - private var subject: KeychainQueryProvider! +final class KeychainSeedsQueryProviderTests: XCTestCase { + private var subject: KeychainSeedsQueryProvider! override func setUp() { super.setUp() - subject = KeychainQueryProvider() + subject = KeychainSeedsQueryProvider() } func test_query_fetch_returnsExpectedValues() { // Given - let queryType: KeychainQuery = .fetch + let queryType: KeychainSeedsQuery = .fetch let expectedSecClass = kSecClassGenericPassword let expectedMatchLimit = kSecMatchLimitAll let expectedReturnAttributes = true @@ -37,7 +37,7 @@ final class KeychainQueryProviderTests: XCTestCase { func test_query_deleteAll_returnsExpectedValues() { // Given - let queryType: KeychainQuery = .deleteAll + let queryType: KeychainSeedsQuery = .deleteAll let expectedSecClass = kSecClassGenericPassword // When @@ -49,7 +49,7 @@ final class KeychainQueryProviderTests: XCTestCase { func test_query_check_returnsExpectedValues() { // Given - let queryType: KeychainQuery = .check + let queryType: KeychainSeedsQuery = .check let expectedSecClass = kSecClassGenericPassword let expectedMatchLimit = kSecMatchLimitAll let expectedReturnData = true @@ -66,7 +66,7 @@ final class KeychainQueryProviderTests: XCTestCase { func test_query_search_returnsExpectedValues() { // Given let seedName = "account" - let queryType: KeychainQuery = .search(seedName: seedName) + let queryType: KeychainSeedsQuery = .search(seedName: seedName) let expectedSecClass = kSecClassGenericPassword let expectedMatchLimit = kSecMatchLimitOne let expectedReturnData = true @@ -85,7 +85,7 @@ final class KeychainQueryProviderTests: XCTestCase { // Given let seedName = "account" let expectedSecClass = kSecClassGenericPassword - let queryType: KeychainQuery = .delete(seedName: seedName) + let queryType: KeychainSeedsQuery = .delete(seedName: seedName) // When let result = subject.query(for: queryType) as! [CFString: Any] @@ -103,7 +103,7 @@ final class KeychainQueryProviderTests: XCTestCase { let expectedAccessControl: SecAccessControl! = try? SimulatorAccessControlProvider() .accessControl() // it's fine to use it instead of mock, as! it's just dedicated to be used on simulator let expectedReturnData = true - let queryType: KeychainQuery = .restoreQuery( + let queryType: KeychainSeedsQuery = .restoreQuery( seedName: seedName, finalSeedPhrase: finalSeedPhrase, accessControl: expectedAccessControl diff --git a/ios/PolkadotVaultTests/Core/Keychain/SeedsMediatorTests.swift b/ios/PolkadotVaultTests/Core/Keychain/Seeds/SeedsMediatorTests.swift similarity index 99% rename from ios/PolkadotVaultTests/Core/Keychain/SeedsMediatorTests.swift rename to ios/PolkadotVaultTests/Core/Keychain/Seeds/SeedsMediatorTests.swift index 51e0a86212..b2581e39a9 100644 --- a/ios/PolkadotVaultTests/Core/Keychain/SeedsMediatorTests.swift +++ b/ios/PolkadotVaultTests/Core/Keychain/Seeds/SeedsMediatorTests.swift @@ -407,7 +407,7 @@ final class DatabaseMediatorMock: DatabaseMediating { } } -final class KeychainAccessAdapterMock: KeychainAccessAdapting { +final class KeychainAccessAdapterMock: KeychainSeedsAccessAdapting { // Properties to track method calls and arguments var fetchSeedNamesCallsCount = 0 var saveSeedCallsCount = 0 diff --git a/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitActionModalViewModelTests.swift b/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitActionModalViewModelTests.swift new file mode 100644 index 0000000000..7bcae7e444 --- /dev/null +++ b/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitActionModalViewModelTests.swift @@ -0,0 +1,101 @@ +// +// BananaSplitActionModalViewModelTests.swift +// PolkadotVaultTests +// +// Created by Krzysztof Rodak on 04/03/2024. +// + +import Combine +import Foundation +@testable import PolkadotVault +import SwiftUI +import XCTest + +final class BananaSplitActionModalViewModelTests: XCTestCase { + private var viewModel: BananaSplitActionModal.ViewModel! + private var isPresented: Bool = false + private var shouldPresentDeleteBackupWarningModal: Bool! + private var shouldPresentPassphraseModal: Bool! + + override func setUp() { + super.setUp() + isPresented = true + shouldPresentDeleteBackupWarningModal = false + shouldPresentPassphraseModal = false + viewModel = BananaSplitActionModal.ViewModel( + isPresented: Binding( + get: { self.isPresented }, + + set: { self.isPresented = $0 } + ), + shouldPresentDeleteBackupWarningModal: Binding( + get: { self.shouldPresentDeleteBackupWarningModal }, + set: { self.shouldPresentDeleteBackupWarningModal = $0 } + ), + shouldPresentPassphraseModal: Binding( + get: { self.shouldPresentPassphraseModal }, + set: { self.shouldPresentPassphraseModal = $0 } + ) + ) + } + + override func tearDown() { + shouldPresentDeleteBackupWarningModal = nil + shouldPresentPassphraseModal = nil + isPresented = false + viewModel = nil + super.tearDown() + } + + func testRemoveBackup_TriggerWarningModalAndDismiss() { + // Given + let expectation = expectation(description: "RemoveBackupDismissal") + + // When + viewModel.removeBackup() + + // Then + XCTAssertTrue(shouldPresentDeleteBackupWarningModal) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + XCTAssertFalse(self.isPresented) + expectation.fulfill() + } + + waitForExpectations(timeout: 2) + } + + func testShowPassphrase_TriggerPassphraseModalAndDismiss() { + // Given + let expectation = expectation(description: "ShowPassphraseDismissal") + + // When + viewModel.showPassphrase() + + // Then + XCTAssertTrue(shouldPresentPassphraseModal) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + XCTAssertFalse(self.isPresented) + expectation.fulfill() + } + + waitForExpectations(timeout: 2) + } + + func testDismissActionSheet_TriggersAnimationAndDismissal() { + // Given + let expectation = expectation(description: "AnimateDismissal") + + // When + viewModel.dismissActionSheet() + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + XCTAssertFalse(self.isPresented) + expectation.fulfill() + } + + waitForExpectations(timeout: 2) + } +} diff --git a/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitPassphraseModalViewModelTests.swift b/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitPassphraseModalViewModelTests.swift new file mode 100644 index 0000000000..79ea709239 --- /dev/null +++ b/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitPassphraseModalViewModelTests.swift @@ -0,0 +1,93 @@ +// +// BananaSplitPassphraseModalViewModelTests.swift +// PolkadotVaultTests +// +// Created by Krzysztof Rodak on 04/03/2024. +// + +import Combine +import Foundation +@testable import PolkadotVault +import SwiftUI +import XCTest + +final class BananaSplitPassphraseModalViewModelTests: XCTestCase { + private var viewModel: BananaSplitPassphraseModal.ViewModel! + private var mediatorMock: KeychainBananaSplitAccessMediatingMock! + private var isPresented: Bool! + private var seedName: String! + + override func setUp() { + super.setUp() + seedName = "testSeed" + mediatorMock = KeychainBananaSplitAccessMediatingMock() + isPresented = true + } + + override func tearDown() { + viewModel = nil + mediatorMock = nil + seedName = nil + isPresented = nil + super.tearDown() + } + + func testInit_LoadsPassphraseOnSuccess() { + // Given + let expectedPassphrase = "loadedPassphrase" + mediatorMock + .retrieveBananaSplitPassphraseWithReturnValue = + .success(BananaSplitPassphrase(passphrase: expectedPassphrase)) + + // When + viewModel = BananaSplitPassphraseModal.ViewModel( + seedName: seedName, + isPresented: Binding(get: { self.isPresented }, set: { self.isPresented = $0 }), + bananaSplitMediator: mediatorMock + ) + + // Then + XCTAssertEqual(mediatorMock.retrieveBananaSplitPassphraseWithCallsCount, 1) + XCTAssertEqual(mediatorMock.retrieveBananaSplitPassphraseWithReceivedSeedName, [seedName]) + XCTAssertEqual(viewModel.passphrase, expectedPassphrase) + } + + func testInit_DoesNotLoadPassphraseOnFailure() { + // Given + mediatorMock.retrieveBananaSplitPassphraseWithReturnValue = .failure(.fetchError) + + // When + viewModel = BananaSplitPassphraseModal.ViewModel( + seedName: "testSeed", + isPresented: Binding(get: { self.isPresented }, set: { self.isPresented = $0 }), + bananaSplitMediator: mediatorMock + ) + + // Then + XCTAssertEqual(mediatorMock.retrieveBananaSplitPassphraseWithCallsCount, 1) + XCTAssertEqual(mediatorMock.retrieveBananaSplitPassphraseWithReceivedSeedName, ["testSeed"]) + XCTAssertEqual(viewModel.passphrase, "") + } + + func testDismissActionSheet_TriggersAnimationAndDismissal() { + // Given + let expectation = expectation(description: "AnimateDismissal") + mediatorMock.retrieveBananaSplitPassphraseWithReturnValue = .failure(.fetchError) + viewModel = BananaSplitPassphraseModal.ViewModel( + seedName: seedName, + isPresented: Binding(get: { self.isPresented }, set: { self.isPresented = $0 }), + bananaSplitMediator: mediatorMock + ) + + // When + viewModel.dismissActionSheet() + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + XCTAssertFalse(self.isPresented) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } +} diff --git a/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitQRCodeModalViewModelTests.swift b/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitQRCodeModalViewModelTests.swift new file mode 100644 index 0000000000..d243e287ac --- /dev/null +++ b/ios/PolkadotVaultTests/Screens/KeyDetails/BananaSplit/BananaSplitQRCodeModalViewModelTests.swift @@ -0,0 +1,106 @@ +// +// BananaSplitQRCodeModalViewModelTests.swift +// PolkadotVaultTests +// +// Created by Krzysztof Rodak on 04/03/2024. +// + +import Combine +import Foundation +@testable import PolkadotVault +import SwiftUI +import XCTest + +final class BananaSplitQRCodeModalViewModelTests: XCTestCase { + private var viewModel: BananaSplitQRCodeModalView.ViewModel! + private var mediatorMock: KeychainBananaSplitAccessMediatingMock! + private var completionAction: BananaSplitQRCodeModalView.OnCompletionAction? + private var testSeedName: String! + private var testBananaSplitBackup: BananaSplitBackup! + + override func setUp() { + super.setUp() + testSeedName = "testSeed" + testBananaSplitBackup = BananaSplitBackup(qrCodes: [[10]]) + mediatorMock = KeychainBananaSplitAccessMediatingMock() + viewModel = BananaSplitQRCodeModalView.ViewModel( + seedName: testSeedName, + bananaSplitBackup: testBananaSplitBackup, + bananaSplitMediator: mediatorMock, + onCompletion: { [weak self] action in + self?.completionAction = action + } + ) + } + + override func tearDown() { + testSeedName = nil + testBananaSplitBackup = nil + viewModel = nil + mediatorMock = nil + completionAction = nil + super.tearDown() + } + + func testOnMoreButtonTap_PresentsActionSheet() { + // When + viewModel.onMoreButtonTap() + + // Then + XCTAssertTrue(viewModel.isPresentingActionSheet) + } + + func testOnCloseTap_CompletesWithClose() { + // When + viewModel.onCloseTap() + + // Then + XCTAssertEqual(completionAction, .close) + } + + func testCheckForActionsPresentation_PresentsPassphraseModal() { + // Given + viewModel.shouldPresentPassphraseModal = true + + // When + viewModel.checkForActionsPresentation() + + // Then + XCTAssertTrue(viewModel.isPresentingPassphraseModal) + } + + func testCheckForActionsPresentation_PresentsDeleteBackupWarningModal() { + // Given + viewModel.shouldPresentDeleteBackupWarningModal = true + + // When + viewModel.checkForActionsPresentation() + + // Then + XCTAssertTrue(viewModel.isPresentingDeleteBackupWarningModal) + } + + func testOnDeleteBackupTap_DeletesBackupSuccessfully() { + // Given + mediatorMock.removeBananaSplitBackupSeedNameReturnValue = .success(()) + + // When + viewModel.onDeleteBackupTap() + + // Then + XCTAssertEqual(completionAction, .backupDeleted) + } + + func testOnDeleteBackupTap_FailsToDeleteBackup() { + // Given + let expectedError = KeychainError.checkError + mediatorMock.removeBananaSplitBackupSeedNameReturnValue = .failure(expectedError) + + // When + viewModel.onDeleteBackupTap() + + // Then + XCTAssertTrue(viewModel.isPresentingError) + XCTAssertEqual(viewModel.presentableError, .alertError(message: expectedError.localizedDescription)) + } +} diff --git a/ios/PolkadotVaultTests/Screens/Onboarding/Airgap/NoAirgapViewModelTests.swift b/ios/PolkadotVaultTests/Screens/Onboarding/Airgap/NoAirgapViewModelTests.swift index f7cd90395a..a191b6593d 100644 --- a/ios/PolkadotVaultTests/Screens/Onboarding/Airgap/NoAirgapViewModelTests.swift +++ b/ios/PolkadotVaultTests/Screens/Onboarding/Airgap/NoAirgapViewModelTests.swift @@ -5,6 +5,7 @@ // Created by Krzysztof Rodak on 02/02/2024. // +import Combine import Foundation @testable import PolkadotVault import XCTest @@ -12,11 +13,13 @@ import XCTest final class NoAirgapViewModelTests: XCTestCase { private var viewModel: NoAirgapView.ViewModel! private var airgapMediatorMock: AirgapMediatingMock! + private var cancellables: Set = [] private var onActionTapExecuted: Bool = false override func setUp() { super.setUp() airgapMediatorMock = AirgapMediatingMock() + viewModel = NoAirgapView.ViewModel( mode: .onboarding, airgapMediator: airgapMediatorMock, @@ -28,20 +31,36 @@ final class NoAirgapViewModelTests: XCTestCase { viewModel = nil airgapMediatorMock = nil onActionTapExecuted = false + cancellables = [] super.tearDown() } func testAirgapStatusUpdate_UpdatesCheckBoxState() { // Given - let isAirplaneModeOn = true - let isWifiOn = false + let expectedState = AirgapState(isAirplaneModeOn: true, isWifiOn: false, isLocationServiceEnabled: false) + airgapMediatorMock.simulateAirgapState(expectedState) + + let expectation = XCTestExpectation(description: "Receive AirgapState Update") // When - airgapMediatorMock.startMonitoringAirgapReceivedUpdate.first?(isAirplaneModeOn, isWifiOn) + viewModel.$isAirplaneModeChecked + .dropFirst() + .sink { isAirplaneModeOn in + XCTAssertTrue(isAirplaneModeOn) + expectation.fulfill() + } + .store(in: &cancellables) + + viewModel.$isWifiChecked + .dropFirst() + .sink { isWifiOn in + XCTAssertTrue(isWifiOn) + expectation.fulfill() + } + .store(in: &cancellables) // Then - XCTAssertTrue(viewModel.isAirplaneModeChecked) - XCTAssertTrue(viewModel.isWifiChecked) + wait(for: [expectation], timeout: 1.0) } func testToggleCheckbox_TogglesCheckboxState() { @@ -65,18 +84,71 @@ final class NoAirgapViewModelTests: XCTestCase { func testUpdateActionState_WhenAllConditionsMet_EnablesAction() { // Given - airgapMediatorMock.startMonitoringAirgapReceivedUpdate.first?(true, false) + airgapMediatorMock.simulateAirgapState(AirgapState( + isAirplaneModeOn: true, + isWifiOn: false, + isLocationServiceEnabled: false + )) + + // When viewModel.toggleCheckbox() + let expectation = XCTestExpectation(description: "Wait for updates") + DispatchQueue.main.async { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + // Then XCTAssertFalse(viewModel.isActionDisabled) } func testUpdateActionState_WhenNotAllConditionsMet_DisablesAction() { // Given - airgapMediatorMock.startMonitoringAirgapReceivedUpdate.first?(true, true) + airgapMediatorMock.simulateAirgapState(AirgapState( + isAirplaneModeOn: false, + isWifiOn: true, + isLocationServiceEnabled: true + )) + + // When + let expectation = XCTestExpectation(description: "Wait for updates") + DispatchQueue.main.async { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) // Then XCTAssertTrue(viewModel.isActionDisabled) } } + +// MARK: - Mocks + +final class AirgapMediatingMock: AirgapMediating { + private let airgapSubject = PassthroughSubject() + private let isConnectedSubject = PassthroughSubject() + + var isConnectedPublisher: AnyPublisher { + isConnectedSubject.eraseToAnyPublisher() + } + + var airgapPublisher: AnyPublisher { + airgapSubject.eraseToAnyPublisher() + } + + var startMonitoringAirgapCallsCount = 0 + + func startMonitoringAirgap() { + startMonitoringAirgapCallsCount += 1 + } + + // Helper methods to simulate updates + func simulateIsConnected(_ isConnected: Bool) { + isConnectedSubject.send(isConnected) + } + + func simulateAirgapState(_ state: AirgapState) { + airgapSubject.send(state) + } +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 41ff3b772c..a06ccd8c7c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -43,7 +43,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.14", "once_cell", "version_check", ] @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "array-bytes" @@ -552,9 +552,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -562,9 +562,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -1376,14 +1376,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1422,9 +1422,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -2218,9 +2218,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2233,7 +2233,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.14", ] [[package]] @@ -2978,7 +2978,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.14", ] [[package]] @@ -3001,9 +3001,9 @@ dependencies = [ [[package]] name = "raptorq" -version = "1.8.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9cf9270cc5903afdef387f06ef1cd89fb77f45c357c2a425bae78b839fd866" +checksum = "90b1b1fad69672f0b901b5004863ea4307f03d168a3db5f2bcba4d3dfed88e97" [[package]] name = "rayon" @@ -3119,11 +3119,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom 0.2.14", + "libc", + "spin 0.9.3", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -3171,13 +3185,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.6" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring", - "rustls-webpki 0.101.4", + "ring 0.17.3", + "rustls-webpki 0.101.7", "sct", ] @@ -3208,18 +3222,18 @@ version = "0.100.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "rustls-webpki" -version = "0.101.4" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.3", + "untrusted 0.9.0", ] [[package]] @@ -3423,8 +3437,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -4214,7 +4228,7 @@ dependencies = [ "derivative", "frame-metadata 15.1.0", "futures", - "getrandom 0.2.6", + "getrandom 0.2.14", "hex", "impl-serde", "parity-scale-codec", @@ -4406,9 +4420,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -4427,9 +4441,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -4914,6 +4928,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.0" @@ -4973,12 +4993,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/rust/db_handling/Cargo.toml b/rust/db_handling/Cargo.toml index 098aaf5ac5..f621abac5d 100644 --- a/rust/db_handling/Cargo.toml +++ b/rust/db_handling/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.80" +anyhow = "1.0.82" constants = {path = "../constants"} defaults = {path = "../defaults", default-features = false, optional = true} definitions = {path = "../definitions", default-features = false} @@ -19,7 +19,7 @@ sled = "0.34.6" sp-core = {git = "https://github.com/paritytech/substrate", default-features = false, features = ["full_crypto"], optional = true} sp-runtime = {git = "https://github.com/paritytech/substrate", default-features = false, optional = true} thiserror = "1.0.57" -time = {version = "0.3.34", features = ["formatting", "macros"]} +time = {version = "0.3.36", features = ["formatting", "macros"]} tiny-bip39 = {version = "1.0.0", default-features = false, optional = true} zeroize = { version = "1.7.0", optional = true, features = ["std"] } diff --git a/rust/definitions/src/metadata.rs b/rust/definitions/src/metadata.rs index 20329f9ec0..d676f105e8 100644 --- a/rust/definitions/src/metadata.rs +++ b/rust/definitions/src/metadata.rs @@ -460,7 +460,7 @@ pub struct AddressBookEntry { #[cfg(feature = "active")] impl AddressBookEntry { - /// Gets [`AddressBookEntry`] from from hot database tree `ADDRESS_BOOK` + /// Gets [`AddressBookEntry`] from hot database tree `ADDRESS_BOOK` /// (key, value) entry. pub fn from_entry( (address_book_key_encoded, address_book_entry_encoded): (IVec, IVec), @@ -470,7 +470,7 @@ impl AddressBookEntry { } /// Gets network address book title and [`AddressBookEntry`] as a tuple from - /// from hot database tree `ADDRESS_BOOK` (key, value) entry. + /// hot database tree `ADDRESS_BOOK` (key, value) entry. /// /// Network address book title **differs** from `title` in network specs. /// This is just a key in hot database `ADDRESS_BOOK`, and is not displayed diff --git a/rust/generate_message/src/lib.rs b/rust/generate_message/src/lib.rs index c0c815a93f..cec353a7cf 100644 --- a/rust/generate_message/src/lib.rs +++ b/rust/generate_message/src/lib.rs @@ -698,7 +698,7 @@ //! //! Keys to be used in command line: //! -//! - Key `--goal` followed by the type to to generate +//! - Key `--goal` followed by the type to generate //! - `qr` will generate only a png QR code //! - `text` will generate only text file with hex-encoded update. //! - default, i.e. if goal is not provided, both QR code and text file are generated. @@ -742,7 +742,7 @@ //! //! Keys to be used in command line: //! -//! - Key `--goal` followed by the type to to generate +//! - Key `--goal` followed by the type to generate //! - `qr` will generate only a png QR code //! - `text` will generate only text file with hex-encoded update. //! - default, i.e. if goal is not provided, both QR code and text file are generated. diff --git a/rust/qr_reader_pc/Cargo.toml b/rust/qr_reader_pc/Cargo.toml index 9d82e2aeb5..9451b35599 100644 --- a/rust/qr_reader_pc/Cargo.toml +++ b/rust/qr_reader_pc/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] hex = "0.4.3" qr_reader_phone = {path = "../qr_reader_phone"} -anyhow = "1.0.80" +anyhow = "1.0.82" image = "0.24.9" quircs = "0.10.2" indicatif = "0.17.8" diff --git a/rust/qr_reader_phone/Cargo.toml b/rust/qr_reader_phone/Cargo.toml index 145d003ed2..9524bef1a1 100644 --- a/rust/qr_reader_phone/Cargo.toml +++ b/rust/qr_reader_phone/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" [dependencies] hex = "0.4.3" -raptorq = "1.8.0" +raptorq = "2.0.0" nom = "7.1.3" thiserror = "1.0.57" constants = {path = "../constants"} diff --git a/rust/qrcode_rtx/Cargo.toml b/rust/qrcode_rtx/Cargo.toml index e623a335e6..e1af43c162 100644 --- a/rust/qrcode_rtx/Cargo.toml +++ b/rust/qrcode_rtx/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] bitvec = "1.0.1" hex = "0.4.3" -raptorq = "1.8.0" +raptorq = "2.0.0" qrcodegen = "1.8.0" png = "0.17.13" diff --git a/rust/qrcode_static/Cargo.toml b/rust/qrcode_static/Cargo.toml index ff09ba8cee..3f91c6f540 100644 --- a/rust/qrcode_static/Cargo.toml +++ b/rust/qrcode_static/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.80" +anyhow = "1.0.82" bitvec = "1.0.1" constants = {path = "../constants"} png = "0.17.13" diff --git a/rust/transaction_signing/Cargo.toml b/rust/transaction_signing/Cargo.toml index ffd42f1904..059b7c8e16 100644 --- a/rust/transaction_signing/Cargo.toml +++ b/rust/transaction_signing/Cargo.toml @@ -15,7 +15,7 @@ db_handling = { path = "../db_handling", default-features = false } sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, features = ["full_crypto"] } sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false } thiserror = "1.0.57" -anyhow = "1.0.80" +anyhow = "1.0.82" sled = "0.34"