From fbef37e38aa539c01c5065039666208d36acc687 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:10:10 +0100 Subject: [PATCH] feat: preparations for universal backup/restore - WPB-14616 (#2523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Benaiteau Co-authored-by: KaterinaWire <57407805+KaterinaWire@users.noreply.github.com> Co-authored-by: Christoph Aldrian Co-authored-by: Christoph Aldrian Co-authored-by: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: François Benaiteau --- Cartfile.resolved | 2 +- WireDomain/Package.swift | 2 +- .../Protocols/CreateLegacyBackupError.swift | 24 ++ .../Protocols/ImportBackupError.swift | 33 +++ .../Protocols/ImportBackupProgress.swift | 10 +- .../ImportBackupUseCaseProtocol.swift | 4 +- .../generated/AutoMockable.generated.swift | 27 ++ .../project.pbxproj | 11 +- .../XCTest}/XCTestCase+waitForPredicate.swift | 2 +- .../XCTest}/XCTestExpectation+inverted.swift | 2 +- .../WireLogging/WireLogger+Instances.swift | 4 +- WireUI/Package.swift | 23 +- .../UIButton.Configuration+ButtonStyles.swift | 45 +++ .../BlockingActivityIndicator.swift | 2 +- WireUI/Sources/WireSettingsUI/.swiftgen.yml | 15 + .../BackupImportExportBuilder.swift | 130 +++++++++ .../BackupImportExportRootView.swift | 36 +++ ...pProgressViewControllerRepresentable.swift | 43 +++ .../CreatingBackupProgressModel.swift | 24 ++ .../CreatingBackupProgressView.swift | 81 ++++++ ...CreatingBackupProgressViewController.swift | 169 +++++++++++ .../Export/ExportBackupState.swift | 26 ++ .../Export/ExportBackupView.swift | 60 ++++ .../Export/ExportBackupViewModel.swift | 178 ++++++++++++ .../SetBackupPasswordPreview.swift | 37 +++ .../SetBackupPasswordView.swift | 155 ++++++++++ .../SetBackupPasswordViewModel.swift | 61 ++++ .../EnterPasswordView/EnterPasswordView.swift | 164 +++++++++++ .../Import/ImportBackupState.swift | 18 +- .../Import/ImportBackupView.swift | 88 ++++++ .../Import/ImportBackupViewModel.swift | 269 ++++++++++++++++++ .../ImportProgressView.swift | 72 +++++ .../Misc/ToggleablePasswordField.swift | 110 +++++++ .../Misc/WireBackupUTIs.swift | 29 ++ .../BackupImportExportRootPreview.swift | 25 ++ .../CreatingBackupProgressPreview.swift | 30 ++ .../Previews/EnterPasswordPreview.swift | 44 +++ .../Previews/ExportBackupPreview.swift | 28 ++ .../Previews/ImportBackupPreview.swift | 32 +++ .../Previews/ImportProgressPreview.swift | 29 ++ .../PreviewBackupPasswordValidator.swift | 29 ++ .../PreviewCleanUpBackupsUseCase.swift | 21 ++ .../Previews/PreviewCreateBackupUseCase.swift | 63 ++++ .../Previews/PreviewImportBackupUseCase.swift | 61 ++++ .../Previews/PreviewLogger.swift | 52 ++++ .../BackupPasswordValidatorProtocol.swift | 17 +- .../CleanUpBackupsUseCaseProtocol.swift | 22 ++ .../Protocols/CreateBackupProgress.swift} | 10 +- .../CreateBackupUseCaseProtocol.swift | 23 ++ .../Resources/en.lproj/Accessibility.strings | 22 ++ .../Resources/en.lproj/Localizable.strings | 67 +++++ .../Sourcery/AutoMockable.manual.swift | 49 ++++ .../Sourcery/AutoMockable.stencil | 1 + .../Sourcery/sourcery.yml | 8 + .../WireSettingsUI.swift | 21 ++ WireUI/Sources/WireSidebarUI/.swiftgen.yml | 1 + .../Views/SidebarAccountInfoView.swift | 4 +- .../WireSidebarUI/Views/SidebarView.swift | 45 ++- ...kupImportExportRootViewSnapshotTests.swift | 66 +++++ ...atingBackupProgressViewSnapshotTests.swift | 89 ++++++ .../Export/ExportBackupViewModelTests.swift | 121 ++++++++ .../SetBackupPasswordViewSnapshotTests.swift | 114 ++++++++ .../EnterPasswordViewSnapshotTests.swift | 109 +++++++ .../Import/ImportBackupViewModelTest.swift | 125 ++++++++ .../ImportProgressViewSnapshotTests.swift | 63 ++++ .../MockImportBackupUseCaseProtocol.swift | 46 +++ .../testColorSchemeVariants.dark.png | 3 + ...testDynamicTypeVariants.accessibility1.png | 3 + ...testDynamicTypeVariants.accessibility2.png | 3 + ...testDynamicTypeVariants.accessibility3.png | 3 + ...testDynamicTypeVariants.accessibility4.png | 3 + ...testDynamicTypeVariants.accessibility5.png | 3 + .../testDynamicTypeVariants.large.png | 3 + .../testDynamicTypeVariants.medium.png | 3 + .../testDynamicTypeVariants.small.png | 3 + .../testDynamicTypeVariants.xLarge.png | 3 + .../testDynamicTypeVariants.xSmall.png | 3 + .../testDynamicTypeVariants.xxLarge.png | 3 + .../testDynamicTypeVariants.xxxLarge.png | 3 + .../testFinishedColorSchemeVariants.dark.png | 3 + ...shedDynamicTypeVariants.accessibility1.png | 3 + ...shedDynamicTypeVariants.accessibility2.png | 3 + ...shedDynamicTypeVariants.accessibility3.png | 3 + ...shedDynamicTypeVariants.accessibility4.png | 3 + ...shedDynamicTypeVariants.accessibility5.png | 3 + .../testFinishedDynamicTypeVariants.large.png | 3 + ...testFinishedDynamicTypeVariants.medium.png | 3 + .../testFinishedDynamicTypeVariants.small.png | 3 + ...testFinishedDynamicTypeVariants.xLarge.png | 3 + ...testFinishedDynamicTypeVariants.xSmall.png | 3 + ...estFinishedDynamicTypeVariants.xxLarge.png | 3 + ...stFinishedDynamicTypeVariants.xxxLarge.png | 3 + .../testOngoingColorSchemeVariants.dark.png | 3 + ...oingDynamicTypeVariants.accessibility1.png | 3 + ...oingDynamicTypeVariants.accessibility2.png | 3 + ...oingDynamicTypeVariants.accessibility3.png | 3 + ...oingDynamicTypeVariants.accessibility4.png | 3 + ...oingDynamicTypeVariants.accessibility5.png | 3 + .../testOngoingDynamicTypeVariants.large.png | 3 + .../testOngoingDynamicTypeVariants.medium.png | 3 + .../testOngoingDynamicTypeVariants.small.png | 3 + .../testOngoingDynamicTypeVariants.xLarge.png | 3 + .../testOngoingDynamicTypeVariants.xSmall.png | 3 + ...testOngoingDynamicTypeVariants.xxLarge.png | 3 + ...estOngoingDynamicTypeVariants.xxxLarge.png | 3 + .../testColorSchemeVariants.dark.png | 3 + ...testDynamicTypeVariants.accessibility1.png | 3 + ...testDynamicTypeVariants.accessibility2.png | 3 + ...testDynamicTypeVariants.accessibility3.png | 3 + ...testDynamicTypeVariants.accessibility4.png | 3 + ...testDynamicTypeVariants.accessibility5.png | 3 + .../testDynamicTypeVariants.large.png | 3 + .../testDynamicTypeVariants.medium.png | 3 + .../testDynamicTypeVariants.small.png | 3 + .../testDynamicTypeVariants.xLarge.png | 3 + .../testDynamicTypeVariants.xSmall.png | 3 + .../testDynamicTypeVariants.xxLarge.png | 3 + .../testDynamicTypeVariants.xxxLarge.png | 3 + .../testInvalidPassword.dark.png | 3 + .../testInvalidPassword.light.png | 3 + .../testNonEmptyPassword.dark.png | 3 + .../testNonEmptyPassword.light.png | 3 + .../testColorSchemeVariants.dark.png | 3 + ...testDynamicTypeVariants.accessibility1.png | 3 + ...testDynamicTypeVariants.accessibility2.png | 3 + ...testDynamicTypeVariants.accessibility3.png | 3 + ...testDynamicTypeVariants.accessibility4.png | 3 + ...testDynamicTypeVariants.accessibility5.png | 3 + .../testDynamicTypeVariants.large.png | 3 + .../testDynamicTypeVariants.medium.png | 3 + .../testDynamicTypeVariants.small.png | 3 + .../testDynamicTypeVariants.xLarge.png | 3 + .../testDynamicTypeVariants.xSmall.png | 3 + .../testDynamicTypeVariants.xxLarge.png | 3 + .../testDynamicTypeVariants.xxxLarge.png | 3 + .../testColorSchemeVariants.dark.png | 3 + ...testDynamicTypeVariants.accessibility1.png | 3 + ...testDynamicTypeVariants.accessibility2.png | 3 + ...testDynamicTypeVariants.accessibility3.png | 3 + ...testDynamicTypeVariants.accessibility4.png | 3 + ...testDynamicTypeVariants.accessibility5.png | 3 + .../testDynamicTypeVariants.large.png | 3 + .../testDynamicTypeVariants.medium.png | 3 + .../testDynamicTypeVariants.small.png | 3 + .../testDynamicTypeVariants.xLarge.png | 3 + .../testDynamicTypeVariants.xSmall.png | 3 + .../testDynamicTypeVariants.xxLarge.png | 3 + .../testDynamicTypeVariants.xxxLarge.png | 3 + .../testInvalidPassword.dark.png | 3 + .../testInvalidPassword.light.png | 3 + .../testNonEmptyPassword.dark.png | 3 + .../testNonEmptyPassword.light.png | 3 + .../SnapshotTestReferenceImageDirectory.swift | 23 ++ .../Tests/MLS/MLSActionExecutorTests.swift | 1 + .../ZMAssetClientMessageTests+Ephemeral.swift | 1 + .../WireDataModel.xcodeproj/project.pbxproj | 6 + .../Helpers/MessageExpirationTimerTests.swift | 1 + .../Decoding/EventDecoderTest.swift | 1 + .../project.pbxproj | 7 + .../SessionManager+Backup.swift | 124 +------- .../SessionManager/SessionManager.swift | 31 +- .../ImportBackupFileArchiver.swift | 3 +- .../ImportBackupStreamDecryptor.swift | 9 +- .../ImportBackupUseCase.swift | 179 +++++++----- .../ImportBackupAppStateUpdaterProtocol.swift | 7 +- .../ImportBackupEntityStorageProtocol.swift | 2 +- .../ImportBackupFileArchiverProtocol.swift | 2 +- .../ImportBackupStreamDecryptorProtocol.swift | 2 +- .../SessionManager+importBackupUseCase.swift | 29 +- .../Source/UserSession/UserSession.swift | 2 + .../ZMUserSession/ZMUserSession.swift | 8 +- .../generated/AutoMockable.generated.swift | 44 --- .../generated/AutoMockable.manual.swift | 9 + ...ferenceStaleParticipantsRemoverTests.swift | 1 + .../Use cases/ImportBackupUseCaseTests.swift | 18 +- .../UserSession/OperationStatusTests.swift | 1 + .../UserSession/ZMUserSessionTests.swift | 1 + .../WireSyncEngine.xcodeproj/project.pbxproj | 21 ++ wire-ios-system/Source/ZMSDispatchGroup.swift | 2 +- wire-ios/Tests/Mocks/UserSessionMock.swift | 2 + .../generated/AutoMockable.generated.swift | 38 --- .../BackupPasswordViewControllerTests.swift | 103 ------- .../BackupViewControllerTests.swift | 55 ---- .../testBackupScreen_LoggedOut.1.png | Bin 135117 -> 125507 bytes .../testBackupScreen_NewDevice.1.png | Bin 143055 -> 134171 bytes ...nScreen_Email_WithProxyAuthenticated.1.png | Bin 74111 -> 164041 bytes ...ingsTableViewControllerSnapshotTests.swift | 6 + wire-ios/Wire-iOS.xcodeproj/project.pbxproj | 148 +++------- .../xcshareddata/xcschemes/Wire-iOS.xcscheme | 15 + .../Generated/Strings+Generated.swift | 71 +---- .../Resources/Base.lproj/Localizable.strings | 45 +-- .../AuthenticationInterfaceBuilder.swift | 2 +- .../BackupRestoreController+Failed.swift | 70 ----- .../BackupRestoreController+Password.swift | 75 ----- .../Backup/BackupRestoreController.swift | 184 ------------ .../AuthenticationCoordinator.swift | 8 - .../AuthenticationCoordinatorAction.swift | 1 - ...ift => NoHistoryHintStepDescription.swift} | 28 +- .../ViewModel/ConversationListViewModel.swift | 2 +- .../Settings/Backup/BackupActionCell.swift | 57 ---- .../Backup/BackupPasswordValidator.swift | 40 +++ .../Backup/BackupPasswordViewController.swift | 187 ------------ .../Settings/Backup/BackupStatusCell.swift | 71 ----- .../Backup/BackupViewController.swift | 161 ----------- .../Backup/CleanUpBackupsUseCase.swift | 35 +++ .../Backup/CreateLegacyBackupUseCase.swift | 58 ++++ ...ettingsCellDescriptorFactory+Account.swift | 25 +- .../SettingsCellDescriptorFactory.swift | 1 + 208 files changed, 4073 insertions(+), 1601 deletions(-) create mode 100644 WireDomain/Sources/WireDomain/UseCases/Protocols/CreateLegacyBackupError.swift create mode 100644 WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupError.swift rename wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/BackupRestoreError.swift => WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupProgress.swift (79%) rename {wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase => WireDomain/Sources/WireDomain/UseCases}/Protocols/ImportBackupUseCaseProtocol.swift (82%) rename {wire-ios-testing/Source/Public => WireFoundation/Sources/WireTesting/SDKExtensions/XCTest}/XCTestCase+waitForPredicate.swift (98%) rename {wire-ios-testing/Source/Public => WireFoundation/Sources/WireTesting/SDKExtensions/XCTest}/XCTestExpectation+inverted.swift (97%) create mode 100644 WireUI/Sources/WireDesign/Buttons/UIButton.Configuration+ButtonStyles.swift create mode 100644 WireUI/Sources/WireSettingsUI/.swiftgen.yml create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportBuilder.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportRootView.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/BackupProgressViewControllerRepresentable.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressModel.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressView.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressViewController.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupState.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupView.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupViewModel.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordView.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordViewModel.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/EnterPasswordView/EnterPasswordView.swift rename wire-ios/Wire-iOS/Sources/Authentication/Coordinator/Coordinator+Delegates/AuthenticationCoordinator+Backup.swift => WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupState.swift (68%) create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupView.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupViewModel.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportProgressView/ImportProgressView.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/ToggleablePasswordField.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/WireBackupUTIs.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/BackupImportExportRootPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/CreatingBackupProgressPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/EnterPasswordPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ExportBackupPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportBackupPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportProgressPreview.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewBackupPasswordValidator.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCleanUpBackupsUseCase.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCreateBackupUseCase.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewImportBackupUseCase.swift create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewLogger.swift rename wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupSource.swift => WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/BackupPasswordValidatorProtocol.swift (72%) create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CleanUpBackupsUseCaseProtocol.swift rename WireUI/{Tests/WireSettingsUITests/PlaceholderTests.swift => Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupProgress.swift} (85%) create mode 100644 WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupUseCaseProtocol.swift create mode 100644 WireUI/Sources/WireSettingsUI/Resources/en.lproj/Accessibility.strings create mode 100644 WireUI/Sources/WireSettingsUI/Resources/en.lproj/Localizable.strings create mode 100644 WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.manual.swift create mode 120000 WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.stencil create mode 100644 WireUI/Sources/WireSettingsUISupport/Sourcery/sourcery.yml create mode 100644 WireUI/Sources/WireSettingsUISupport/WireSettingsUI.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/BackupImportExportRootViewSnapshotTests.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/CreatingBackupProgressViewSnapshotTests.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/ExportBackupViewModelTests.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/SetBackupPasswordViewSnapshotTests.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/EnterPasswordViewSnapshotTests.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportBackupViewModelTest.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportProgressViewSnapshotTests.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Mocks/MockImportBackupUseCaseProtocol.swift create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testColorSchemeVariants.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility1.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility2.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility3.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility4.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility5.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.large.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.medium.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.small.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xSmall.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedColorSchemeVariants.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility1.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility2.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility3.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility4.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility5.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.large.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.medium.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.small.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xSmall.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingColorSchemeVariants.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility1.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility2.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility3.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility4.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility5.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.large.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.medium.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.small.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xSmall.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testColorSchemeVariants.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.large.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.small.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.light.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.light.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testColorSchemeVariants.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility1.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility2.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility3.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility4.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility5.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.large.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.medium.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.small.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xSmall.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testColorSchemeVariants.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.large.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.small.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.light.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.dark.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.light.png create mode 100644 WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SnapshotTestReferenceImageDirectory.swift delete mode 100644 wire-ios/Wire-iOS Tests/BackupPasswordViewControllerTests.swift delete mode 100644 wire-ios/Wire-iOS Tests/BackupViewControllerTests.swift delete mode 100644 wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController+Failed.swift delete mode 100644 wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController+Password.swift delete mode 100644 wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController.swift rename wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/{BackupRestoreStepDescription.swift => NoHistoryHintStepDescription.swift} (65%) delete mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupActionCell.swift create mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordValidator.swift delete mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordViewController.swift delete mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupStatusCell.swift delete mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupViewController.swift create mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CleanUpBackupsUseCase.swift create mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CreateLegacyBackupUseCase.swift diff --git a/Cartfile.resolved b/Cartfile.resolved index f806950551d..f5fa15f6f48 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -binary "wire-avs.json" "10.0.4" +binary "wire-avs.json" "10.0.5" github "wireapp/ZipArchive" "v2.4.2" github "wireapp/core-crypto" "v3.0.2" github "wireapp/cryptobox-ios" "v1.1.0_xcframework_arm64simulator" diff --git a/WireDomain/Package.swift b/WireDomain/Package.swift index 28cb68b548b..a9ac967bff6 100644 --- a/WireDomain/Package.swift +++ b/WireDomain/Package.swift @@ -16,7 +16,7 @@ let package = Package( .target( name: "WireDomainPkg", path: "./Sources/WireDomain", - sources: ["./UseCases/Protocols/IndividualToTeamMigrationUseCaseProtocol.swift"] + sources: ["./UseCases/Protocols"] ) ] ) diff --git a/WireDomain/Sources/WireDomain/UseCases/Protocols/CreateLegacyBackupError.swift b/WireDomain/Sources/WireDomain/UseCases/Protocols/CreateLegacyBackupError.swift new file mode 100644 index 00000000000..8ae58fba4e7 --- /dev/null +++ b/WireDomain/Sources/WireDomain/UseCases/Protocols/CreateLegacyBackupError.swift @@ -0,0 +1,24 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +public enum CreateLegacyBackupError: Error { + case noActiveAccountForExport + case compressionError + /// Failed to create `InputStream` or `OutputStream` from `URL`. + case failedToCreateStreamsForEncryption +} diff --git a/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupError.swift b/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupError.swift new file mode 100644 index 00000000000..34fff097b74 --- /dev/null +++ b/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupError.swift @@ -0,0 +1,33 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +public enum ImportBackupError: Error, Equatable, CaseIterable { + case noActiveAccountForImport + /// The backup file is encrypted and a password is needed for decryption. + case passwordRequired + /// E.g. if the file to import was created with a different (incompatible) version of the app. + case incompatibleFileFormat + case invalidAccountID + case compressionError + case invalidFileExtension + case keyCreationFailed + case decryptionError + case faildToBackUpUserClient + /// Failed to create `InputStream` or `OutputStream` from `URL`. + case failedToCreateStreamForDecryption +} diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/BackupRestoreError.swift b/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupProgress.swift similarity index 79% rename from wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/BackupRestoreError.swift rename to WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupProgress.swift index 7529da4036e..ea2cf02ab24 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/BackupRestoreError.swift +++ b/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupProgress.swift @@ -16,11 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -enum BackupRestoreError: Error { - case noActiveAccount - case compressionError - case invalidFileExtension - case keyCreationFailed - case decryptionError - case unknown +public enum ImportBackupProgress: Equatable, Sendable { + case progress(Float) + case done } diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupUseCaseProtocol.swift b/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupUseCaseProtocol.swift similarity index 82% rename from wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupUseCaseProtocol.swift rename to WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupUseCaseProtocol.swift index 0e4e0388c6e..558feacf50b 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupUseCaseProtocol.swift +++ b/WireDomain/Sources/WireDomain/UseCases/Protocols/ImportBackupUseCaseProtocol.swift @@ -19,6 +19,6 @@ import Foundation // sourcery: AutoMockable -public protocol ImportBackupUseCaseProtocol { - func invoke(url: URL, password: String) async throws +public protocol ImportBackupUseCaseProtocol: Sendable { + func invoke(url: URL, password: String) -> AsyncThrowingStream } diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index 20f49428af1..4fbae941152 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -1181,6 +1181,33 @@ public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol } +public class MockImportBackupUseCaseProtocol: ImportBackupUseCaseProtocol { + + // MARK: - Life cycle + + public init() {} + + + // MARK: - invoke + + public var invokeUrlPassword_Invocations: [(url: URL, password: String)] = [] + public var invokeUrlPassword_MockMethod: ((URL, String) -> AsyncThrowingStream)? + public var invokeUrlPassword_MockValue: AsyncThrowingStream? + + public func invoke(url: URL, password: String) -> AsyncThrowingStream { + invokeUrlPassword_Invocations.append((url: url, password: password)) + + if let mock = invokeUrlPassword_MockMethod { + return mock(url, password) + } else if let mock = invokeUrlPassword_MockValue { + return mock + } else { + fatalError("no mock for `invokeUrlPassword`") + } + } + +} + public class MockIndividualToTeamMigrationUseCaseProtocol: IndividualToTeamMigrationUseCaseProtocol { // MARK: - Life cycle diff --git a/WireDomain/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/WireDomain Project.xcodeproj/project.pbxproj index ee0029af14b..94fec76ab8f 100644 --- a/WireDomain/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/WireDomain Project.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 5904B1B32D31582700E866D1 /* WireDomainSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 017F67822C207A3200B6E02D /* WireDomainSupport.framework */; }; 591B6E452C8B09BA009F8A7B /* WireDataModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCC32C1C8CC20076CB1C /* WireDataModel.framework */; }; 591B6E472C8B09BD009F8A7B /* WireDataModelSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01BDA5442C20762200636E50 /* WireDataModelSupport.framework */; }; + 59202AD22D54D3D500143413 /* WireDomainPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59202AD12D54D3D500143413 /* WireDomainPackage */; }; 594904952D0710BF00238104 /* WireAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 594904942D0710BF00238104 /* WireAnalytics */; }; 598D042D2C89C63100B64D71 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 598D042C2C89C63100B64D71 /* WireFoundation */; }; 59909A5E2C5BBEA8009C41DE /* WireAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 59909A5D2C5BBEA8009C41DE /* WireAPI */; }; @@ -70,7 +71,7 @@ 59DBDB312D395B620069C64C /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - UseCases/Protocols/IndividualToTeamMigrationUseCaseProtocol.swift, + UseCases/Protocols, ); target = 01D0DCA52C1C8C870076CB1C /* WireDomain */; }; @@ -80,7 +81,7 @@ 5904B7822D315AAC00E866D1 /* TestPlans */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TestPlans; sourceTree = ""; }; 59EA78992D00CF1C002CA0B8 /* WireDomainTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WireDomainTests; sourceTree = ""; }; 59EA78D42D00CF22002CA0B8 /* WireDomainSupport */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (5904B1B92D31586500E866D1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WireDomainSupport; sourceTree = ""; }; - 59EA7A282D00CFB2002CA0B8 /* WireDomain */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (59DBDB312D395B620069C64C /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WireDomain; sourceTree = ""; }; + 59EA7A282D00CFB2002CA0B8 /* WireDomain */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (59DBDB312D395B620069C64C /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (UseCases/Protocols, ); path = WireDomain; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -89,6 +90,7 @@ buildActionMask = 2147483647; files = ( C97BCCAA2C98704B004F2D0D /* WireDomain.framework in Frameworks */, + 59202AD22D54D3D500143413 /* WireDomainPackage in Frameworks */, 59909A692C5BC001009C41DE /* WireAPI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -193,6 +195,7 @@ name = WireDomainSupport; packageProductDependencies = ( 59909A682C5BC001009C41DE /* WireAPI */, + 59202AD12D54D3D500143413 /* WireDomainPackage */, ); productName = WireDomainSupport; productReference = 017F67822C207A3200B6E02D /* WireDomainSupport.framework */; @@ -741,6 +744,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 59202AD12D54D3D500143413 /* WireDomainPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = WireDomainPackage; + }; 594904942D0710BF00238104 /* WireAnalytics */ = { isa = XCSwiftPackageProductDependency; productName = WireAnalytics; diff --git a/wire-ios-testing/Source/Public/XCTestCase+waitForPredicate.swift b/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+waitForPredicate.swift similarity index 98% rename from wire-ios-testing/Source/Public/XCTestCase+waitForPredicate.swift rename to WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+waitForPredicate.swift index bb09bf200ad..b74a3bb4616 100644 --- a/wire-ios-testing/Source/Public/XCTestCase+waitForPredicate.swift +++ b/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+waitForPredicate.swift @@ -16,7 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest +public import XCTest public extension XCTestCase { diff --git a/wire-ios-testing/Source/Public/XCTestExpectation+inverted.swift b/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestExpectation+inverted.swift similarity index 97% rename from wire-ios-testing/Source/Public/XCTestExpectation+inverted.swift rename to WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestExpectation+inverted.swift index af60862da2c..814e4844d52 100644 --- a/wire-ios-testing/Source/Public/XCTestExpectation+inverted.swift +++ b/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestExpectation+inverted.swift @@ -16,7 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest +public import XCTest public extension XCTestExpectation { diff --git a/WireLogging/Sources/WireLogging/WireLogger+Instances.swift b/WireLogging/Sources/WireLogging/WireLogger+Instances.swift index e3470f1766f..24d09724b4e 100644 --- a/WireLogging/Sources/WireLogging/WireLogger+Instances.swift +++ b/WireLogging/Sources/WireLogging/WireLogger+Instances.swift @@ -24,9 +24,11 @@ public extension WireLogger { static let appLock = WireLogger(tag: "AppLock") static let assets = WireLogger(tag: "assets") static let authentication = WireLogger(tag: "authentication") + static let backend = WireLogger(tag: "backend") + static let backupExport = WireLogger(tag: "backup-export") + static let backupImport = WireLogger(tag: "backup-import") static let backgroundActivity = WireLogger(tag: "background-activity") static let badgeCount = WireLogger(tag: "badge-count") - static let backend = WireLogger(tag: "backend") static let calling = WireLogger(tag: "calling") static let conversation = WireLogger(tag: "conversation") static let coreCrypto = WireLogger(tag: "core-crypto") diff --git a/WireUI/Package.swift b/WireUI/Package.swift index f898aa72654..e9e8f4b3eb6 100644 --- a/WireUI/Package.swift +++ b/WireUI/Package.swift @@ -20,6 +20,7 @@ let package = Package( .library(name: "WireMoveToFolderUISupport", targets: ["WireMoveToFolderUISupport"]), .library(name: "WireReusableUIComponents", targets: ["WireReusableUIComponents"]), .library(name: "WireSettingsUI", targets: ["WireSettingsUI"]), + .library(name: "WireSettingsUISupport", targets: ["WireSettingsUISupport"]), .library(name: "WireSidebarUI", targets: ["WireSidebarUI"]), ], dependencies: [ @@ -27,6 +28,7 @@ let package = Package( .package(path: "../WireAnalytics"), .package(name: "WireDomainPackage", path: "../WireDomain"), .package(name: "WireFoundation", path: "../WireFoundation"), + .package(path: "../WireLogging"), .package(path: "../WirePlugins") ], targets: [ @@ -78,8 +80,25 @@ let package = Package( ), .testTarget(name: "WireReusableUIComponentsTests", dependencies: ["WireReusableUIComponents"]), - .target(name: "WireSettingsUI"), - .testTarget(name: "WireSettingsUITests", dependencies: ["WireSettingsUI"]), + .target( + name: "WireSettingsUI", + dependencies: [ + "WireDesign", + .product(name: "WireDomainPackage", package: "WireDomainPackage"), + "WireFoundation", + "WireLogging", + "WireReusableUIComponents", + ], + plugins: [.plugin(name: "SwiftGenPlugin", package: "WirePlugins")] + ), + .target( + name: "WireSettingsUISupport", + dependencies: ["WireSettingsUI"], + plugins: [ + .plugin(name: "SourceryPlugin", package: "WirePlugins") + ] + ), + .testTarget(name: "WireSettingsUITests", dependencies: ["WireSettingsUI", "WireSettingsUISupport"]), .target( name: "WireSidebarUI", diff --git a/WireUI/Sources/WireDesign/Buttons/UIButton.Configuration+ButtonStyles.swift b/WireUI/Sources/WireDesign/Buttons/UIButton.Configuration+ButtonStyles.swift new file mode 100644 index 00000000000..d1a14d4c6cc --- /dev/null +++ b/WireUI/Sources/WireDesign/Buttons/UIButton.Configuration+ButtonStyles.swift @@ -0,0 +1,45 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import UIKit +import WireFoundation + +public extension UIButton.Configuration { + + static var primary: Self { + + var configuration = shared + configuration.baseBackgroundColor = ColorTheme.Buttons.Primary.enabled + return configuration + + } + + private static var shared: Self { + + var configuration = UIButton.Configuration.filled() + configuration.buttonSize = .large + configuration.titleTextAttributesTransformer = .init { attributeContainer in + var attributeContainer = attributeContainer + attributeContainer.font = .preferredFont(forTextStyle: .headline) + return attributeContainer + } + return configuration + + } + +} diff --git a/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift b/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift index bf859550547..3216bfabf9c 100644 --- a/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift +++ b/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift @@ -138,7 +138,7 @@ private extension UIView { } } -private var stateKey = 0 +@MainActor private var stateKey = 0 // MARK: - Previews diff --git a/WireUI/Sources/WireSettingsUI/.swiftgen.yml b/WireUI/Sources/WireSettingsUI/.swiftgen.yml new file mode 100644 index 00000000000..1c04a15290b --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/.swiftgen.yml @@ -0,0 +1,15 @@ +# Every input/output paths in the rest of the config will then be expressed relative to these. + +input_dir: ./ +output_dir: ${GENERATED}/ + +# Generate constants for your localized strings. + +strings: + inputs: + - Resources/en.lproj/Accessibility.strings + - Resources/en.lproj/Localizable.strings + filter: + outputs: + - templateName: structured-swift5 + output: Strings+Generated.swift diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportBuilder.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportBuilder.swift new file mode 100644 index 00000000000..074687bd5a8 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportBuilder.swift @@ -0,0 +1,130 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDomainPkg +import WireLogging + +public struct BackupImportExportBuilder { + + let backupPasswordValidator: any BackupPasswordValidatorProtocol + let createBackupUseCase: any CreateBackupUseCaseProtocol + let importBackupUseCase: any ImportBackupUseCaseProtocol + let cleanUpBackupsUseCase: any CleanUpBackupsUseCaseProtocol + let exportBackupLogger: any LoggerProtocol + let importBackupLogger: any LoggerProtocol + + public init( + backupPasswordValidator: any BackupPasswordValidatorProtocol, + createBackupUseCase: any CreateBackupUseCaseProtocol, + importBackupUseCase: any ImportBackupUseCaseProtocol, + cleanUpBackupsUseCase: any CleanUpBackupsUseCaseProtocol, + exportBackupLogger: any LoggerProtocol, + importBackupLogger: any LoggerProtocol + ) { + self.backupPasswordValidator = backupPasswordValidator + self.createBackupUseCase = createBackupUseCase + self.importBackupUseCase = importBackupUseCase + self.cleanUpBackupsUseCase = cleanUpBackupsUseCase + self.exportBackupLogger = exportBackupLogger + self.importBackupLogger = importBackupLogger + } + + @MainActor + public func build() -> UIViewController { + UIHostingController(rootView: buildRootView()) + } + + @MainActor @ViewBuilder + func buildRootView() -> some View { + BackupImportExportRootView { + buildExportBackupView() + buildImportBackupView() + } + } + + @MainActor @ViewBuilder + func buildExportBackupView() -> some View { + + let viewModel = ExportBackupViewModel( + createBackupUseCase: createBackupUseCase, + cleanUpBackupsUseCase: cleanUpBackupsUseCase, + logger: exportBackupLogger + ) + + ExportBackupView( + viewModel: viewModel, + setBackupPasswordView: { + buildSetBackupPasswordView( + cancelAction: { [weak viewModel] in viewModel?.cancel() }, + setPasswordAction: { [weak viewModel] password in viewModel?.createBackup(password: password) } + ) + }, + creatingBackupProgressView: { + CreatingBackupProgressView( + progress: viewModel.backupProgress, + cancelAction: { viewModel.cancel() } + ) + } + ) + + } + + @MainActor @ViewBuilder + func buildSetBackupPasswordView( + cancelAction: @escaping () -> Void, + setPasswordAction: @escaping (_ password: String) -> Void + ) -> some View { + + let setBackupPasswordViewModel = SetBackupPasswordViewModel( + passwordValidator: backupPasswordValidator, + cancelAction: cancelAction, + setPasswordAction: setPasswordAction + ) + + SetBackupPasswordView(viewModel: setBackupPasswordViewModel) + + } + + @MainActor @ViewBuilder + func buildImportBackupView() -> some View { + + let viewModel = ImportBackupViewModel( + importBackupUseCase: importBackupUseCase, + logger: importBackupLogger + ) + ImportBackupView(viewModel: viewModel) + + } +} + +// MARK: - BackupImportExportBuilder + preview + +extension BackupImportExportBuilder { + + static var previewBuilder: BackupImportExportBuilder { + BackupImportExportBuilder( + backupPasswordValidator: PreviewBackupPasswordValidator(), + createBackupUseCase: PreviewCreateBackupUseCase(), + importBackupUseCase: PreviewImportBackupUseCase(), + cleanUpBackupsUseCase: PreviewCleanUpBackupsUseCase(), + exportBackupLogger: PreviewLogger(), + importBackupLogger: PreviewLogger() + ) + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportRootView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportRootView.swift new file mode 100644 index 00000000000..f24cdf1f6c5 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/BackupImportExportRootView.swift @@ -0,0 +1,36 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +struct BackupImportExportRootView: View { + + @ViewBuilder var content: () -> Content + + var body: some View { + List(content: content) + .listStyle(.grouped) + .background(Color(ColorTheme.Backgrounds.background)) + .scrollContentBackground(.hidden) + } +} + +#Preview { + BackupImportExportRootPreview() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/BackupProgressViewControllerRepresentable.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/BackupProgressViewControllerRepresentable.swift new file mode 100644 index 00000000000..7ef22bf7d2d --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/BackupProgressViewControllerRepresentable.swift @@ -0,0 +1,43 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +struct BackupProgressViewControllerRepresentable: UIViewControllerRepresentable { + + var progressDescription = "" + var progressValue = Float() + var backupURL: URL? + var completedAction: (_ completed: Bool) -> Void = { _ in } + + func makeUIViewController(context: Context) -> CreatingBackupProgressViewController { + let viewController = CreatingBackupProgressViewController() + viewController.progressDescription = progressDescription + viewController.progressValue = progressValue + viewController.backupURL = backupURL + viewController.completedAction = completedAction + return viewController + } + + func updateUIViewController(_ viewController: CreatingBackupProgressViewController, context: Context) { + viewController.progressDescription = progressDescription + viewController.progressValue = progressValue + viewController.backupURL = backupURL + viewController.completedAction = completedAction + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressModel.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressModel.swift new file mode 100644 index 00000000000..68b9398a55a --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressModel.swift @@ -0,0 +1,24 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +enum CreatingBackupProgressModel: Equatable { + case ongoing(_ percentage: Float) + case finished(_ url: URL) +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressView.swift new file mode 100644 index 00000000000..97c39379773 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressView.swift @@ -0,0 +1,81 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +struct CreatingBackupProgressView: View { + + var progress: CreatingBackupProgressModel + var cancelAction: () -> Void + + private typealias Strings = L10n.Localizable.ExportBackup + private typealias Labels = L10n.Accessibility.ExportBackup + + var body: some View { + NavigationStack { + backupProgressView + .background(ColorTheme.Backgrounds.background.color) + .navigationTitle(Strings.CreatingBackup.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(Strings.Cancel.title, action: cancelAction) + .foregroundStyle(ColorTheme.Base.primary.color) + .accessibilityLabel(Labels.Cancel.label) + .accessibilityIdentifier("cancel") + } + } + } + } + + @ViewBuilder private var backupProgressView: some View { + + let completedAction: (Bool) -> Void = { completed in + completed ? cancelAction() : () + } + + switch progress { + + case let .ongoing(progress): + BackupProgressViewControllerRepresentable( + progressDescription: .init(localized: "exportBackup.creatingBackup.saving", bundle: .module), + progressValue: progress, + backupURL: nil, + completedAction: completedAction + ) + + case let .finished(url): + BackupProgressViewControllerRepresentable( + progressDescription: .init(localized: "exportBackup.creatingBackup.success", bundle: .module), + progressValue: 1, + backupURL: url, + completedAction: completedAction + ) + } + } + +} + +#Preview("in progress") { + CreatingBackupProgressPreview(.ongoing(0.25)) +} + +#Preview("ready") { + CreatingBackupProgressPreview(.finished(.init(fileURLWithPath: "/"))) +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressViewController.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressViewController.swift new file mode 100644 index 00000000000..3bf926754a3 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/CreatingBackupProgressView/CreatingBackupProgressViewController.swift @@ -0,0 +1,169 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import UIKit +import WireDesign + +/// This view controller was created because `UIActivityViewController` allows for assigning a +/// `completionWithItemsHandler` closure, which SwiftUI's fileExporter doesn't. +final class CreatingBackupProgressViewController: UIViewController { + + // MARK: State Properties + + var progressDescription = "" { + didSet { + guard isViewLoaded else { return } + descriptionLabel.text = progressDescription + } + } + + var progressValue = Float() { + didSet { + guard isViewLoaded else { return } + progressView.progress = progressValue + progressLabel.text = "\(Int(progressValue * 100))%" + } + } + + var backupURL: URL? { + didSet { + guard isViewLoaded else { return } + exportButton.isEnabled = backupURL != nil + } + } + + var completedAction: (_ completed: Bool) -> Void = { _ in } + + // MARK: - Subviews + + private lazy var scrollView = UIScrollView() + + private lazy var stackView = { + let stackView = UIStackView(arrangedSubviews: [descriptionLabel, progressLabel, progressView, exportButton]) + stackView.axis = .vertical + stackView.spacing = 16 + return stackView + }() + + private var descriptionLabel = { + let descriptionLabel = UILabel() + descriptionLabel.numberOfLines = 0 + descriptionLabel.font = .preferredFont(forTextStyle: .caption1) + descriptionLabel.textColor = BaseColorPalette.Grays.gray70 + descriptionLabel.adjustsFontForContentSizeCategory = true + descriptionLabel.accessibilityIdentifier = "descriptionLabel" + return descriptionLabel + }() + + private lazy var progressLabel = { + let progressLabel = UILabel() + let font = UIFont.preferredFont(forTextStyle: .caption2) + let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) + progressLabel.font = .init(descriptor: fontDescriptor ?? font.fontDescriptor, size: 0) + progressLabel.adjustsFontForContentSizeCategory = true + progressLabel.textColor = BaseColorPalette.Grays.gray70 + progressLabel.textAlignment = .center + progressLabel.accessibilityIdentifier = "progressLabel" + return progressLabel + }() + + private lazy var progressView = { + let progressView = UIProgressView() + progressView.progressTintColor = ColorTheme.Base.primary + progressView.accessibilityIdentifier = "progressView" + return progressView + }() + + private lazy var exportButton = { + let title = String(localized: "exportBackup.creatingBackup.saveButton.title", bundle: .module) + let exportButton = UIButton(configuration: .primary, primaryAction: .init(title: title) { _ in }) + exportButton.addTarget(self, action: #selector(showActivityViewController(_:)), for: .primaryActionTriggered) + exportButton.accessibilityIdentifier = "exportButton" + return exportButton + }() + + // MARK: - Methods + + override func viewDidLoad() { + super.viewDidLoad() + setupSubviews() + } + + private func setupSubviews() { + + stackView.setCustomSpacing(4, after: progressLabel) + stackView.setCustomSpacing(32, after: progressView) + + descriptionLabel.text = progressDescription + + progressLabel.text = "\(Int(progressValue * 100))%" + + progressView.progress = progressValue + + exportButton.isEnabled = backupURL != nil + + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + stackView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stackView) + + // constraints + let svLayoutGuide = scrollView.contentLayoutGuide + NSLayoutConstraint.activate([ + + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + + scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor), + + stackView.leadingAnchor.constraint(equalToSystemSpacingAfter: svLayoutGuide.leadingAnchor, multiplier: 3), + stackView.topAnchor.constraint(equalToSystemSpacingBelow: svLayoutGuide.topAnchor, multiplier: 1), + svLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 3), + svLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor) + + ]) + + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let stackViewHeight = stackView.frame.maxY + (navigationController?.navigationBar.frame.height ?? 0) + if let sheetPresentationController = navigationController?.sheetPresentationController { + sheetPresentationController.detents = [.custom { _ in stackViewHeight }] + } + } + + @objc + private func showActivityViewController(_ sender: UIButton) { + guard let backupURL else { return assertionFailure() } + + let activityViewController = UIActivityViewController(activityItems: [backupURL], applicationActivities: nil) + if let popoverPresentationController = activityViewController.popoverPresentationController { + popoverPresentationController.sourceView = sender.superview + popoverPresentationController.sourceRect = sender.frame + } + activityViewController.completionWithItemsHandler = { [weak self] _, completed, _, _ in + self?.completedAction(completed) + } + present(activityViewController, animated: true) + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupState.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupState.swift new file mode 100644 index 00000000000..d32943e02a6 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupState.swift @@ -0,0 +1,26 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +enum ExportBackupState { + case requestingPassword(password: String) + case creatingBackup(progress: Float) + case backupReady(url: URL) + case backupFailed(any Error) +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupView.swift new file mode 100644 index 00000000000..fac612eb9ad --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupView.swift @@ -0,0 +1,60 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +struct ExportBackupView: View { + + @StateObject var viewModel: ExportBackupViewModel + + private(set) var setBackupPasswordView: () -> PasswordView + private(set) var creatingBackupProgressView: () -> ProgressView + + private typealias Strings = L10n.Localizable + + var body: some View { + + Section(footer: Text(Strings.Backup.Export.description)) { + + Button(Strings.Backup.Export.action, action: viewModel.showPasswordDialog) + .font(.callout.weight(.semibold)) + .foregroundStyle(Color.primaryText) + .sheet(isPresented: $viewModel.isCreatingBackupProgressPresented) { + creatingBackupProgressView() + .interactiveDismissDisabled() + .presentationDetents([.medium]) + .sheet(isPresented: $viewModel.isSetBackupPasswordPresented) { + setBackupPasswordView() + .interactiveDismissDisabled() + .presentationDetents([.medium]) + } + } + + .alert(Strings.ExportBackup.ErrorAlert.title, isPresented: $viewModel.isErrorAlertPresented) { + Button(Strings.ExportBackup.ErrorAlert.ok, action: viewModel.reset) + } message: { + Text(Strings.ExportBackup.ErrorAlert.message) + } + + } + } +} + +#Preview { + ExportBackupPreview() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupViewModel.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupViewModel.swift new file mode 100644 index 00000000000..bc96837ca0c --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/ExportBackupViewModel.swift @@ -0,0 +1,178 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireLogging + +@MainActor +final class ExportBackupViewModel: ObservableObject { + + let createBackupUseCase: any CreateBackupUseCaseProtocol + let cleanUpBackupsUseCase: any CleanUpBackupsUseCaseProtocol + + private var state: ExportBackupState? { + didSet { updatePublishedProperties() } + } + + // CreatingBackupProgress is the outer sheet, which contains/presents SetBackupPassword + @Published var isCreatingBackupProgressPresented = false + @Published var isSetBackupPasswordPresented = false + @Published var isErrorAlertPresented = false + + @Published private(set) var backupProgress: CreatingBackupProgressModel = .ongoing(0) + @Published private(set) var backupURL: URL? + + private var backupTask: Task? + + private let logger: any LoggerProtocol + + init( + createBackupUseCase: any CreateBackupUseCaseProtocol, + cleanUpBackupsUseCase: any CleanUpBackupsUseCaseProtocol, + logger: any LoggerProtocol + ) { + self.createBackupUseCase = createBackupUseCase + self.cleanUpBackupsUseCase = cleanUpBackupsUseCase + self.logger = logger + } + + func reset() { + backupTask?.cancel() + state = nil + } + + func showPasswordDialog() { + guard state == nil else { + logger.error("\(#function): state != nil") + return assertionFailure() + } + state = .requestingPassword(password: "") + } + + func createBackup(password: String) { + backupTask?.cancel() + backupTask = Task { + do { + state = .creatingBackup(progress: 0) + for try await update in createBackupUseCase.invoke(password: password) { + switch update { + case let .progress(fraction): + state = .creatingBackup(progress: fraction) + case let .done(url): + state = .backupReady(url: url) + } + } + } catch is CancellationError { + logger.info("backup cancelled") + state = nil + } catch { + logger.error("backup failed unexpectedly: " + String(reflecting: error)) + state = .backupFailed(error) + } + } + } + + func cancel() { + switch state { + case .requestingPassword: + state = nil + case .creatingBackup: + reset() + case .backupReady: + cleanUpBackups() + state = nil + case .backupFailed, .none: + logger.error("unexpected state while received cancel: \(state == nil ? "nil" : ".backupFailed")") + assertionFailure("unexpected state") + } + } + + private func updatePublishedProperties() { + + backupProgress = switch state { + case let .creatingBackup(progress): + .ongoing(progress) + case let .backupReady(url): + .finished(url) + default: + .ongoing(0) + } + + // outer sheet + let isCreatingBackupProgressPresented = switch state { + case .requestingPassword, .creatingBackup, .backupReady: + true + default: + false + } + + // inner sheet + let isSetBackupPasswordPresented = if case .requestingPassword = state { + true + } else { + false + } + + let isErrorAlertPresented = switch state { + case .backupFailed: + true + default: + false + } + + // Workarounds for presentation issues with several sheet or alert presentation flags toggled at once. + // This code assumes the presentation or dismissal of a modal view controller lasts less than 400ms. + if !isCreatingBackupProgressPresented, self.isSetBackupPasswordPresented { + // The outer sheet is dismissed while the inner sheet is still presented, so delay the outer dismissal. + self.isSetBackupPasswordPresented = false + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + if isSetBackupPasswordPresented, !self.isCreatingBackupProgressPresented { + // The inner sheet is being presented while the outer sheet is not yet presented, so delay the inner. + self.isCreatingBackupProgressPresented = true + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + if isErrorAlertPresented, self.isCreatingBackupProgressPresented { + // The alert is being presented while there is still a sheet presented, so delay the alert. + self.isCreatingBackupProgressPresented = false + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + + self.isCreatingBackupProgressPresented = isCreatingBackupProgressPresented + self.isSetBackupPasswordPresented = isSetBackupPasswordPresented + self.isErrorAlertPresented = isErrorAlertPresented + + } + + private func cleanUpBackups() { + Task { + do { + try await cleanUpBackupsUseCase.invoke() + } catch { + logger.error("cleaning up backups failed: \(String(reflecting: error))") + } + } + } + +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordPreview.swift new file mode 100644 index 00000000000..2a762bfbd0b --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordPreview.swift @@ -0,0 +1,37 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +struct SetBackupPasswordPreview: View { + + @State private var isPresented = true + + var body: some View { + Color(uiColor: .systemBackground) + .sheet(isPresented: .constant(true)) { + BackupImportExportBuilder.previewBuilder + .buildSetBackupPasswordView( + cancelAction: {}, + setPasswordAction: { _ in } + ) + .interactiveDismissDisabled() + .presentationDetents([.medium]) + } + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordView.swift new file mode 100644 index 00000000000..12ffdb4881a --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordView.swift @@ -0,0 +1,155 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +struct SetBackupPasswordView: View { + + @StateObject var viewModel: SetBackupPasswordViewModel + + private typealias Strings = L10n.Localizable + private typealias Labels = L10n.Accessibility.ExportBackup + + var body: some View { + NavigationStack { + setBackupPasswordView + .background(Color.viewBackground) + .scrollContentBackground(.hidden) + .navigationTitle(Strings.ExportBackup.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(Strings.ExportBackup.Cancel.title, action: viewModel.cancel) + .foregroundStyle(ColorTheme.Base.primary.color) + .accessibilityLabel(Labels.Cancel.label) + .accessibilityIdentifier("cancel") + } + } + } + } + + @ViewBuilder private var setBackupPasswordView: some View { + VStack { + + if #available(iOS 16.4, *) { + ScrollView(content: scrollViewContent) + .scrollBounceBehavior(.basedOnSize) + } else { + ScrollView(content: scrollViewContent) + } + + Spacer() + + Button(Strings.ExportBackup.button, action: viewModel.triggerExport) + .bold() + .disabled(!viewModel.isPasswordValid) + .wireButtonStyle(.primary) + .padding() + } + } + + @ViewBuilder + private func scrollViewContent() -> some View { + VStack(alignment: .leading, spacing: 0) { + Spacer() + + Text(Strings.ExportBackup.description) + .font(.body) + .padding(.bottom, 28) + + Text(Strings.ExportBackup.SetBackupPassword.title) + .foregroundStyle(passwordFieldTitleColor) + .font(.subheadline) + .padding(.bottom, 2) + + ToggleablePasswordField( + password: $viewModel.password, + placeholder: Strings.ExportBackup.SetBackupPassword.placeholder, + placeholderColor: passwordFieldPlaceholderColor, + borderColor: passwordFieldBorderColor, + focusOnAppear: true + ) + .padding(.bottom, 8) + + Text(viewModel.localizedPasswordRules) + .foregroundStyle(passwordFooterColor) + .font(.caption) + + Spacer() + } + .padding() + } + + // TODO: [WPB-16061] the following code is almost identical to the one in EnterPasswordView.swift, try to reuse + + private var passwordFieldTitleColor: Color { + if !viewModel.isPasswordValid { + ColorTheme.Base.error.color + } else if viewModel.password.isEmpty { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray80 + : BaseColorPalette.Grays.gray40 + }.color + } else { + ColorTheme.Base.primary.color + } + } + + private var passwordFieldPlaceholderColor: Color { + if !viewModel.isPasswordValid { + ColorTheme.Base.error.color + } else if viewModel.password.isEmpty { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray70 + : BaseColorPalette.Grays.gray60 + }.color + } else { + ColorTheme.Base.primary.color + } + } + + private var passwordFieldBorderColor: Color { + if !viewModel.isPasswordValid { + ColorTheme.Base.error.color + } else if viewModel.password.isEmpty { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray40 + : BaseColorPalette.Grays.gray80 + }.color + } else { + ColorTheme.Base.primary.color + } + } + + private var passwordFooterColor: Color { + if !viewModel.isPasswordValid { + ColorTheme.Base.error.color + } else { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray70 + : BaseColorPalette.Grays.gray40 + }.color + } + } + +} + +#Preview { + SetBackupPasswordPreview() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordViewModel.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordViewModel.swift new file mode 100644 index 00000000000..8edd44bbbe4 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Export/SetBackupBasswordView/SetBackupPasswordViewModel.swift @@ -0,0 +1,61 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +@MainActor +final class SetBackupPasswordViewModel: ObservableObject { + + @Published var password = "" { + didSet { validatePassword() } + } + + @Published private(set) var isPasswordValid = true + + var localizedPasswordRules: String { + passwordValidator.localizedRulesDescription + } + + private let passwordValidator: any BackupPasswordValidatorProtocol + private let setPasswordAction: (_ password: String) -> Void + private let cancelAction: () -> Void + + init( + passwordValidator: any BackupPasswordValidatorProtocol, + cancelAction: @escaping () -> Void, + setPasswordAction: @escaping (_ password: String) -> Void + ) { + self.passwordValidator = passwordValidator + self.cancelAction = cancelAction + self.setPasswordAction = setPasswordAction + } + + private func validatePassword() { + isPasswordValid = password.isEmpty || passwordValidator.isPasswordValid(password) + } + + func cancel() { + cancelAction() + } + + func triggerExport() { + if isPasswordValid { + setPasswordAction(password) + } + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/EnterPasswordView/EnterPasswordView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/EnterPasswordView/EnterPasswordView.swift new file mode 100644 index 00000000000..11378ed1e0a --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/EnterPasswordView/EnterPasswordView.swift @@ -0,0 +1,164 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +struct EnterPasswordView: View { + + @Binding var password: String + @Binding var passwordIsWrong: Bool + + let continueAction: (_ password: String) -> Void + let cancelAction: () -> Void + + private typealias Strings = L10n.Localizable.ImportBackup + private typealias Labels = L10n.Accessibility.ImportBackup + + var body: some View { + NavigationStack { + enterPasswordView + .background(ColorTheme.Backgrounds.background.color) + .navigationTitle(Strings.EnterPassword.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(Strings.Cancel.title, action: cancelAction) + .foregroundStyle(ColorTheme.Base.primary.color) + .accessibilityLabel(Labels.Cancel.label) + .accessibilityIdentifier("cancel") + } + } + } + } + + @ViewBuilder private var enterPasswordView: some View { + VStack { + + if #available(iOS 16.4, *) { + ScrollView(content: scrollViewContent) + .scrollBounceBehavior(.basedOnSize) + } else { + ScrollView(content: scrollViewContent) + } + + Spacer() + + Button(Strings.EnterPassword.Button.title) { + continueAction(password) + } + .bold() + .disabled(password.isEmpty || passwordIsWrong) + .wireButtonStyle(.primary) + .padding() + } + } + + @ViewBuilder + private func scrollViewContent() -> some View { + VStack(alignment: .leading, spacing: 0) { + Spacer() + + Text(Strings.EnterPassword.description) + .font(.body) + .padding(.bottom, 28) + + Text(Strings.EnterPassword.TextField.title) + .foregroundStyle(passwordFieldTitleColor) + .font(.subheadline) + .padding(.bottom, 2) + + ToggleablePasswordField( + password: $password, + placeholder: Strings.EnterPassword.TextField.placeholder, + placeholderColor: passwordFieldPlaceholderColor, + borderColor: passwordFieldBorderColor, + focusOnAppear: true + ) + .padding(.bottom, 8) + + if passwordIsWrong { + Text(Strings.EnterPassword.wrongPassword) + .foregroundStyle(passwordFieldTitleColor) + .font(.caption) + } + + Spacer() + } + .padding() + } + + private var passwordFieldTitleColor: Color { + if passwordIsWrong { + ColorTheme.Base.error.color + } else if password.isEmpty { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray80 + : BaseColorPalette.Grays.gray40 + }.color + } else { + ColorTheme.Base.primary.color + } + } + + private var passwordFieldPlaceholderColor: Color { + if passwordIsWrong { + ColorTheme.Base.error.color + } else if password.isEmpty { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray70 + : BaseColorPalette.Grays.gray60 + }.color + } else { + ColorTheme.Base.primary.color + } + } + + private var passwordFieldBorderColor: Color { + if passwordIsWrong { + ColorTheme.Base.error.color + } else if password.isEmpty { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray40 + : BaseColorPalette.Grays.gray80 + }.color + } else { + ColorTheme.Base.primary.color + } + } + + private var passwordFooterColor: Color { + if passwordIsWrong { + ColorTheme.Base.error.color + } else { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Grays.gray70 + : BaseColorPalette.Grays.gray40 + }.color + } + } + +} + +#Preview("empty") { + EnterPasswordPreview() +} + +#Preview("wrong") { + EnterPasswordPreview(password: "some", isPasswordWrong: true) +} diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/Coordinator+Delegates/AuthenticationCoordinator+Backup.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupState.swift similarity index 68% rename from wire-ios/Wire-iOS/Sources/Authentication/Coordinator/Coordinator+Delegates/AuthenticationCoordinator+Backup.swift rename to WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupState.swift index dd46432b042..7e433bb4d19 100644 --- a/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/Coordinator+Delegates/AuthenticationCoordinator+Backup.swift +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupState.swift @@ -18,16 +18,10 @@ import Foundation -extension AuthenticationCoordinator: BackupRestoreControllerDelegate { - - func backupResoreControllerDidFinishRestoring( - _ controller: BackupRestoreController, - didSucceed: Bool - ) { - executeActions([ - .configureNotifications, - .completeBackupStep(didSucceed: didSucceed) - ]) - } - +enum ImportBackupState { + case requestConfirmation(url: URL) + case importingBackup(progress: Float) + case requestingPassword(url: URL, isPasswordIncorrect: Bool) + case success + case restoreFailed } diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupView.swift new file mode 100644 index 00000000000..bca75ff3fe7 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupView.swift @@ -0,0 +1,88 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +struct ImportBackupView: View { + + @StateObject var viewModel: ImportBackupViewModel + + @State private var isFileImporterPresented = false + + private typealias BackupStrings = L10n.Localizable.Backup + private typealias ImportBackupAlertStrings = L10n.Localizable.ImportBackup.Alert + private typealias OverwriteConfirmationStrings = L10n.Localizable.ImportBackup.OverwriteConfirmation + + var body: some View { + Section(footer: Text(BackupStrings.Import.description)) { + + Button(BackupStrings.Import.action) { + isFileImporterPresented = true + } + .font(.callout.weight(.semibold)) + .foregroundStyle(Color.primaryText) + .fileImporter( + isPresented: $isFileImporterPresented, + allowedContentTypes: WireBackupUTIs, + onCompletion: viewModel.pickedBackupFile + ) + + .sheet(isPresented: $viewModel.isImportProgressPresented) { + + ImportProgressView( + progressValue: viewModel.importProgress, + cancelAction: viewModel.reset + ) + .interactiveDismissDisabled() + .presentationDetents([.medium]) + .sheet(isPresented: $viewModel.isEnterBackupPasswordPresented) { + EnterPasswordView( + password: $viewModel.backupPassword, + passwordIsWrong: $viewModel.isBackupPasswordWrong, + continueAction: { viewModel.enterPassword($0) }, + cancelAction: viewModel.reset + ) + .interactiveDismissDisabled() + .presentationDetents([.large]) + .onChange(of: viewModel.backupPassword) { _ in + if viewModel.isBackupPasswordWrong { + viewModel.isBackupPasswordWrong = false + } + } + } + + .alert(viewModel.alertContent.title, isPresented: $viewModel.isImportConfirmationPresented) { + Button(OverwriteConfirmationStrings.cancel, role: .cancel, action: viewModel.reset) + Button(OverwriteConfirmationStrings.proceed, role: .destructive, action: viewModel.confirmOverwrite) + } message: { + Text(viewModel.alertContent.message) + } + } + + .alert(viewModel.alertContent.title, isPresented: $viewModel.isAlertPresented) { + Button(ImportBackupAlertStrings.ok, action: viewModel.reset) + } message: { + Text(viewModel.alertContent.message) + } + } + } +} + +#Preview { + ImportBackupPreview() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupViewModel.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupViewModel.swift new file mode 100644 index 00000000000..c1888dd3c4c --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportBackupViewModel.swift @@ -0,0 +1,269 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireDomainPkg +import WireLogging + +@MainActor +final class ImportBackupViewModel: ObservableObject { + + let importBackupUseCase: any ImportBackupUseCaseProtocol + + private var state: ImportBackupState? { + didSet { updatePublishedProperties() } + } + + @Published var backupPassword = "" + @Published var isBackupPasswordWrong = false + + @Published var isImportProgressPresented = false + @Published var isEnterBackupPasswordPresented = false + @Published var alertContent = AlertContent(title: "", message: "", action: "") + @Published var isImportConfirmationPresented = false + @Published var isAlertPresented = false + + @Published private(set) var importProgress = Float() + + private var importTask: Task? + + private let logger: any LoggerProtocol + private let fileManager = FileManager.default + + private typealias Strings = L10n.Localizable.ImportBackup + + init( + importBackupUseCase: any ImportBackupUseCaseProtocol, + logger: any LoggerProtocol + ) { + self.importBackupUseCase = importBackupUseCase + self.logger = logger + } + + // MARK: - Methods + + func reset() { + importTask?.cancel() + state = nil + } + + func pickedBackupFile(result: Result) { + do { + switch result { + + case let .failure(error): + throw error + + case let .success(url): + let gotAccess = url.startAccessingSecurityScopedResource() + // let the file manager throw the error in case `gotAccess` is `false`. + + let tmpDirectory = try fileManager.url( + for: .itemReplacementDirectory, + in: .userDomainMask, + appropriateFor: url, + create: true + ) + let copy = tmpDirectory.appendingPathComponent(url.lastPathComponent) + try fileManager.copyItem(at: url, to: copy) + if gotAccess { + url.stopAccessingSecurityScopedResource() + } + + alertContent = .init( + title: Strings.OverwriteConfirmation.title, + message: Strings.OverwriteConfirmation.message, + cancel: Strings.OverwriteConfirmation.cancel, + action: Strings.OverwriteConfirmation.proceed + ) + state = .requestConfirmation(url: copy) + } + } catch { + logger.error("failed to pick backup file to restore: " + String(reflecting: error)) + state = .restoreFailed + } + } + + func confirmOverwrite() { + guard case let .requestConfirmation(url) = state else { + logger.error("confirmOverwrite called while not in state `.requestConfirmation`") + return assertionFailure() + } + importBackup(from: url, password: "") + } + + func enterPassword(_ password: String) { + guard case let .requestingPassword(url, _) = state else { + logger.error("enterPassword called while not in state `.requestingPassword`") + return assertionFailure() + } + importBackup(from: url, password: password) + } + + private func importBackup(from url: URL, password: String) { + importTask?.cancel() + importTask = Task { + do { + state = .importingBackup(progress: 0) + for try await update in importBackupUseCase.invoke(url: url, password: password) { + switch update { + case let .progress(fraction): + state = .importingBackup(progress: fraction) + case .done: + alertContent = .init( + title: Strings.Alert.Success.title, + message: Strings.Alert.Success.message, + action: Strings.Alert.ok + ) + state = .success + } + } + } catch ImportBackupError.passwordRequired { + logger.debug("password is required to open backup file") + state = .requestingPassword(url: url, isPasswordIncorrect: false) + return // don't clean up temporary file + } catch ImportBackupError.decryptionError { + logger.warn("failed to decrypt backup file, presenting the password input again") + state = .requestingPassword(url: url, isPasswordIncorrect: true) + return // don't clean up temporary file + } catch ImportBackupError.incompatibleFileFormat { + logger.warn("restore failed due to incompatible file format") + alertContent = .init( + title: Strings.Alert.IncompatibleBackupError.title, + message: Strings.Alert.IncompatibleBackupError.message, + action: Strings.Alert.ok + ) + state = .restoreFailed + } catch ImportBackupError.invalidAccountID { + logger.warn("restore failed due to invalid account ID") + alertContent = .init( + title: Strings.Alert.WrongFileError.title, + message: Strings.Alert.WrongFileError.message, + action: Strings.Alert.ok + ) + state = .restoreFailed + } catch is CancellationError { + logger.info("restore cancelled") + reset() + } catch { + logger.error("unexpected error while restoring: " + String(reflecting: error)) + alertContent = .init( + title: Strings.Alert.GenericError.title, + message: Strings.Alert.GenericError.message, + action: Strings.Alert.ok + ) + state = .restoreFailed + } + do { + try fileManager.removeItem(at: url) + } catch { + logger.error("failed to remove temporary file: " + String(reflecting: error)) + } + } + } + + private func updatePublishedProperties() { + + importProgress = switch state { + case let .importingBackup(progress): + progress + case .success: + 1 + default: + 0 + } + + let isImportProgressPresented = switch state { + case .requestConfirmation, .importingBackup, .requestingPassword: + true + default: + false + } + + let isImportConfirmationPresented = switch state { + case .requestConfirmation: + true + default: false + } + + let isEnterBackupPasswordPresented = if case .requestingPassword = state { + true + } else { + false + } + + let isAlertPresented = if case .success = state { + true + } else { + false + } + + isBackupPasswordWrong = if case let .requestingPassword(_, isWrong) = state { + isWrong + } else { + false + } + + // Workarounds for presentation issues with several sheet or alert presentation flags toggled at once. + // This code assumes the presentation or dismissal of a modal view controller lasts less than 400ms. + if !isImportProgressPresented, self.isImportConfirmationPresented { + // The outer sheet is dismissed while the inner sheet/alert is presented, so delay the outer dismissal. + self.isImportConfirmationPresented = false + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + if !isImportProgressPresented, self.isEnterBackupPasswordPresented { + // The outer sheet is dismissed while the inner sheet is still presented, so delay the outer dismissal. + self.isEnterBackupPasswordPresented = false + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + if isEnterBackupPasswordPresented, !self.isImportProgressPresented { + // The inner sheet is being presented while the outer sheet is not yet presented, so delay the inner. + self.isImportProgressPresented = true + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + if isAlertPresented, self.isImportProgressPresented { + // The alert is being presented while there is still a sheet presented, so delay the alert. + self.isImportProgressPresented = false + return DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in + self?.updatePublishedProperties() + } + } + + self.isImportProgressPresented = isImportProgressPresented + self.isImportConfirmationPresented = isImportConfirmationPresented + self.isEnterBackupPasswordPresented = isEnterBackupPasswordPresented + self.isAlertPresented = isAlertPresented + + } + + struct AlertContent: Equatable { + + let title: String + let message: String + var cancel = "" + let action: String + + } + +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportProgressView/ImportProgressView.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportProgressView/ImportProgressView.swift new file mode 100644 index 00000000000..cd03fb3e81e --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Import/ImportProgressView/ImportProgressView.swift @@ -0,0 +1,72 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +struct ImportProgressView: View { + + var progressValue = Float() + var cancelAction: () -> Void + + private typealias Strings = L10n.Localizable.ImportBackup + private typealias Labels = L10n.Accessibility.ImportBackup + + var body: some View { + NavigationStack { + progressView + .background(ColorTheme.Backgrounds.background.color) + .navigationTitle(Strings.RestoringHistory.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: cancelAction) { + Text(Strings.Cancel.title) + } + .foregroundStyle(ColorTheme.Base.primary.color) + .accessibilityLabel(Labels.Cancel.label) + .accessibilityIdentifier("cancel") + } + } + } + } + + @ViewBuilder private var progressView: some View { + VStack { + Spacer() + HStack { + Text(Strings.RestoringHistory.message) + Spacer() + } + .padding(.bottom) + HStack { + Spacer() + Text("\(Int(progressValue * 100))%") + .font(.caption2) + Spacer() + } + ProgressView(value: progressValue) + Spacer() + } + .padding() + } +} + +#Preview { + ImportProgressPreview() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/ToggleablePasswordField.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/ToggleablePasswordField.swift new file mode 100644 index 00000000000..91a0744e763 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/ToggleablePasswordField.swift @@ -0,0 +1,110 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +// TODO: [WPB-15571] Add accessibility strings to the mask / unmask buttons +struct ToggleablePasswordField: View { + + @Binding var password: String + var placeholder: String + var placeholderColor: Color + var borderColor: Color + var focusOnAppear = true + + @State private var isPasswordVisible = false + + @FocusState private var isFocused: Bool + + @Environment(\.colorScheme) private var colorScheme + + private typealias Labels = L10n.Accessibility.Backup + + var body: some View { + HStack { + + if isPasswordVisible { + TextField(text: $password) { + Text(placeholder) + .font(.body) + .foregroundStyle(placeholderColor) + } + .focused($isFocused) + .textContentType(.password) + .autocapitalization(.none) + } else { + SecureField(text: $password) { + Text(placeholder) + .font(.body) + .foregroundStyle(placeholderColor) + } + .focused($isFocused) + .textContentType(.password) + } + + let accessibilityLabel = isPasswordVisible ? Labels.Password.Hide.label : Labels.Password.Show.label + Button { + isPasswordVisible.toggle() + } label: { + Image(systemName: isPasswordVisible ? "eye" : "eye.slash") + .foregroundColor(toggleVisibilityButtonColor) + } + .accessibilityLabel(accessibilityLabel) + + } + .padding() + .background(textFieldBackground) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(borderColor, lineWidth: 1) + ) + .onAppear { + if focusOnAppear { + isFocused = true + } + } + } + + private var textFieldBackground: Color { + if colorScheme != .dark { + BaseColorPalette.Neutrals.white.color + } else { + ColorTheme.Backgrounds.background.color + } + } + + private var toggleVisibilityButtonColor: Color { + UIColor { $0.userInterfaceStyle != .dark + ? BaseColorPalette.Neutrals.black + : BaseColorPalette.Grays.gray70 + }.color + } + +} + +#Preview { + ToggleablePasswordField( + password: .constant(""), + placeholder: "Placeholder Text", + placeholderColor: BaseColorPalette.Neutrals.black.color, + borderColor: BaseColorPalette.Neutrals.black.color + ) + .padding() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/WireBackupUTIs.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/WireBackupUTIs.swift new file mode 100644 index 00000000000..7f867aa921a --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Misc/WireBackupUTIs.swift @@ -0,0 +1,29 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import UniformTypeIdentifiers + +// There are some external apps that users can use to transfer backup files, which can modify +// their attachments and change the underscore with a dash. This is the reason we accept two types +// of legacy iOS backup file extensions: 'ios_wbu' and 'ios-wbu'. + +public let WireBackupUTIs = [ + UTType("com.wire.backup-universal"), + UTType("com.wire.backup-ios-underscore"), + UTType("com.wire.backup-ios-hyphen") +].compactMap(\.self) // in Xcode previews UTType.init returns nil diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/BackupImportExportRootPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/BackupImportExportRootPreview.swift new file mode 100644 index 00000000000..a174ffb0fae --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/BackupImportExportRootPreview.swift @@ -0,0 +1,25 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +@MainActor @ViewBuilder +func BackupImportExportRootPreview() -> some View { + BackupImportExportBuilder.previewBuilder + .buildRootView() +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/CreatingBackupProgressPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/CreatingBackupProgressPreview.swift new file mode 100644 index 00000000000..458b3dcd6f7 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/CreatingBackupProgressPreview.swift @@ -0,0 +1,30 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +@MainActor @ViewBuilder +func CreatingBackupProgressPreview(_ progress: CreatingBackupProgressModel) -> some View { + Color(uiColor: .systemBackground) + .sheet(isPresented: .constant(true)) { + CreatingBackupProgressView(progress: progress) {} + .presentationDetents([.medium]) + .interactiveDismissDisabled() + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/EnterPasswordPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/EnterPasswordPreview.swift new file mode 100644 index 00000000000..8a4e9a8ed76 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/EnterPasswordPreview.swift @@ -0,0 +1,44 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +struct EnterPasswordPreview: View { + + @State private(set) var password = "" + @State private(set) var isPasswordWrong = false + + var body: some View { + Color(uiColor: .systemBackground) + .sheet(isPresented: .constant(true)) { + EnterPasswordView( + password: $password, + passwordIsWrong: $isPasswordWrong, + continueAction: { _ in }, + cancelAction: {} + ) + .presentationDetents([.medium]) + .interactiveDismissDisabled() + .onChange(of: password) { _ in + if isPasswordWrong { + isPasswordWrong = false + } + } + } + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ExportBackupPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ExportBackupPreview.swift new file mode 100644 index 00000000000..846d90b6ad0 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ExportBackupPreview.swift @@ -0,0 +1,28 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +@MainActor +func ExportBackupPreview() -> some View { + List { + BackupImportExportBuilder.previewBuilder + .buildExportBackupView() + } + .listStyle(.grouped) +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportBackupPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportBackupPreview.swift new file mode 100644 index 00000000000..794744bba64 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportBackupPreview.swift @@ -0,0 +1,32 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +@MainActor +func ImportBackupPreview() -> some View { + List { + ImportBackupView( + viewModel: ImportBackupViewModel( + importBackupUseCase: PreviewImportBackupUseCase(), + logger: PreviewLogger() + ) + ) + } + .listStyle(.grouped) +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportProgressPreview.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportProgressPreview.swift new file mode 100644 index 00000000000..28ff1b93893 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/ImportProgressPreview.swift @@ -0,0 +1,29 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +@MainActor @ViewBuilder +func ImportProgressPreview() -> some View { + Color(uiColor: .systemBackground) + .sheet(isPresented: .constant(true)) { + ImportProgressView(progressValue: 0.25) {} + .interactiveDismissDisabled() + .presentationDetents([.medium]) + } +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewBackupPasswordValidator.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewBackupPasswordValidator.swift new file mode 100644 index 00000000000..11dd90abc3c --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewBackupPasswordValidator.swift @@ -0,0 +1,29 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +struct PreviewBackupPasswordValidator: BackupPasswordValidatorProtocol { + + func isPasswordValid(_ password: String) -> Bool { + password.count > 3 + } + + var localizedRulesDescription: String { + "Use at least 3 characters." + } + +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCleanUpBackupsUseCase.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCleanUpBackupsUseCase.swift new file mode 100644 index 00000000000..ce4a481e850 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCleanUpBackupsUseCase.swift @@ -0,0 +1,21 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +struct PreviewCleanUpBackupsUseCase: CleanUpBackupsUseCaseProtocol { + func invoke() async throws {} +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCreateBackupUseCase.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCreateBackupUseCase.swift new file mode 100644 index 00000000000..5186f061e6f --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewCreateBackupUseCase.swift @@ -0,0 +1,63 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +struct PreviewCreateBackupUseCase: CreateBackupUseCaseProtocol { + + func invoke(password: String) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = Task.detached { + do { + let steps = 10 + + var failAtIndex: Int? + if .random() { + failAtIndex = .random(in: 0 ... steps) + } + + for i in 0 ... steps { + + try Task.checkCancellation() + + if i == failAtIndex { + throw PreviewExportBackupError() + } + + continuation.yield(.progress(Float(i) / Float(steps))) + + try await Task.sleep(for: .milliseconds(.random(in: 50 ... 300))) + } + + let fileURL = URL(fileURLWithPath: "/path/to/final/backup.zip") + continuation.yield(.done(fileURL)) + continuation.finish() + + } catch { + continuation.finish(throwing: error) + } + } + continuation.onTermination = { _ in + task.cancel() + } + } + } + + struct PreviewExportBackupError: Error {} + +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewImportBackupUseCase.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewImportBackupUseCase.swift new file mode 100644 index 00000000000..67dc66430f7 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewImportBackupUseCase.swift @@ -0,0 +1,61 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireDomainPkg + +struct PreviewImportBackupUseCase: ImportBackupUseCaseProtocol { + + func invoke(url: URL, password: String) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = Task.detached { + do { + let steps = 10 + + var failAtIndex: Int? + if .random() { + failAtIndex = .random(in: 0 ... steps) + } + + for i in 0 ... steps { + + try Task.checkCancellation() + + if i == failAtIndex { + throw ImportBackupError.allCases.randomElement()! + } + + continuation.yield(.progress(Float(i) / Float(steps))) + + try await Task.sleep(for: .milliseconds(.random(in: 50 ... 300))) + } + + continuation.yield(.done) + continuation.finish() + + } catch { + continuation.finish(throwing: error) + } + } + continuation.onTermination = { _ in + task.cancel() + } + } + } + +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewLogger.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewLogger.swift new file mode 100644 index 00000000000..9e9e5fd74a1 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Previews/PreviewLogger.swift @@ -0,0 +1,52 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireLogging + +struct PreviewLogger: LoggerProtocol { + + let logFiles = [URL]() + + func addTag(_ key: LogAttributesKey, value: String?) {} + + func debug(_ message: any LogConvertible, attributes: LogAttributes...) { + print("[debug] \(message)") + } + + func info(_ message: any LogConvertible, attributes: LogAttributes...) { + print("[info] \(message)") + } + + func notice(_ message: any LogConvertible, attributes: LogAttributes...) { + print("[notice] \(message)") + } + + func warn(_ message: any LogConvertible, attributes: LogAttributes...) { + print("[warn] \(message)") + } + + func error(_ message: any LogConvertible, attributes: LogAttributes...) { + print("[error] \(message)") + } + + func critical(_ message: any LogConvertible, attributes: LogAttributes...) { + print("[critical] \(message)") + } + +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupSource.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/BackupPasswordValidatorProtocol.swift similarity index 72% rename from wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupSource.swift rename to WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/BackupPasswordValidatorProtocol.swift index 337f322e341..1a54fc8596f 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupSource.swift +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/BackupPasswordValidatorProtocol.swift @@ -16,17 +16,12 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import Foundation -import class WireSyncEngine.SessionManager - // sourcery: AutoMockable -protocol BackupSource { - func backupActiveAccount( - password: String, - completion: @escaping (Result) -> Void - ) +/// Determines if a given password is valid for encrypting a backup. +public protocol BackupPasswordValidatorProtocol { - func clearPreviousBackups() -} + func isPasswordValid(_ password: String) -> Bool -extension SessionManager: BackupSource {} + var localizedRulesDescription: String { get } + +} diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CleanUpBackupsUseCaseProtocol.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CleanUpBackupsUseCaseProtocol.swift new file mode 100644 index 00000000000..3fb5e3e9c66 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CleanUpBackupsUseCaseProtocol.swift @@ -0,0 +1,22 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +/// A use case which cleans up old generated backup files from the temporary directory. +public protocol CleanUpBackupsUseCaseProtocol: Sendable { + func invoke() async throws +} diff --git a/WireUI/Tests/WireSettingsUITests/PlaceholderTests.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupProgress.swift similarity index 85% rename from WireUI/Tests/WireSettingsUITests/PlaceholderTests.swift rename to WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupProgress.swift index cd66deaba62..1adcb0cb4f5 100644 --- a/WireUI/Tests/WireSettingsUITests/PlaceholderTests.swift +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupProgress.swift @@ -16,11 +16,9 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest +import Foundation -@testable import WireSettingsUI - -final class PlaceholderTests: XCTestCase { - - func testNothing() {} +public enum CreateBackupProgress: Sendable { + case progress(Float) + case done(URL) } diff --git a/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupUseCaseProtocol.swift b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupUseCaseProtocol.swift new file mode 100644 index 00000000000..c30d7684aa8 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Account/BackupImportExport/Protocols/CreateBackupUseCaseProtocol.swift @@ -0,0 +1,23 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +// sourcery: AutoMockable +/// A use case to export the current app state using a provided `password`. +public protocol CreateBackupUseCaseProtocol: Sendable { + func invoke(password: String) -> AsyncThrowingStream +} diff --git a/WireUI/Sources/WireSettingsUI/Resources/en.lproj/Accessibility.strings b/WireUI/Sources/WireSettingsUI/Resources/en.lproj/Accessibility.strings new file mode 100644 index 00000000000..e542e6ca8e5 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Resources/en.lproj/Accessibility.strings @@ -0,0 +1,22 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +"exportBackup.cancel.label" = "Cancel backup"; +"importBackup.cancel.label" = "Cancel restore"; +"backup.password.show.label" = "Show password"; +"backup.password.hide.label" = "Hide password"; diff --git a/WireUI/Sources/WireSettingsUI/Resources/en.lproj/Localizable.strings b/WireUI/Sources/WireSettingsUI/Resources/en.lproj/Localizable.strings new file mode 100644 index 00000000000..26c6031f7c1 --- /dev/null +++ b/WireUI/Sources/WireSettingsUI/Resources/en.lproj/Localizable.strings @@ -0,0 +1,67 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +"backup.export.action" = "Back Up Now"; +"backup.export.description" = "Create a backup to preserve your conversation history. You can use this to restore history if you lose your computer or switch to a new one. The backup file is not protected by Wire end-to-end encryption, so store it in a safe place."; + +"backup.import.action" = "Restore from Backup"; +"backup.import.description" = "The existing history on this device remains and will be completed by the new backup. You can restore history from all your devices and different platforms but not from another account."; + +"exportBackup.title" = "Set password"; +"exportBackup.description" = "The backup will be compressed. If you use a password, Wire also encrypts the backup file. Make sure to store the password in a secure place."; +"exportBackup.button" = "Back Up Now"; +"exportBackup.setBackupPassword.title" = "Password (OPTIONAL)"; +"exportBackup.setBackupPassword.placeholder" = "Enter password"; +"exportBackup.setBackupPassword.rules" = "Use at least 8 characters, with one lowercase letter, one capital letter, a number, and a special character."; + +"exportBackup.cancel.title" = "Cancel"; +"exportBackup.errorAlert.title" = "Something went wrong"; +"exportBackup.errorAlert.message" = "The backup could not be created. Please try again or contact Wire support."; +"exportBackup.errorAlert.ok" = "OK"; + +"exportBackup.creatingBackup.title" = "Creating Backup"; +"exportBackup.creatingBackup.saving" = "Saving conversation history…"; +"exportBackup.creatingBackup.success" = "Backup successfully created."; +"exportBackup.creatingBackup.saveButton.title" = "Save File"; + +"importBackup.cancel.title" = "Cancel"; + +"importBackup.overwriteConfirmation.title" = "File overwrites your history"; +"importBackup.overwriteConfirmation.message" = "The backup contents will replace the current conversation history on this device."; +"importBackup.overwriteConfirmation.cancel" = "Cancel"; +"importBackup.overwriteConfirmation.proceed" = "Proceed"; + +"importBackup.alert.ok" = "OK"; +"importBackup.alert.success.title" = "Success"; +"importBackup.alert.success.message" = "Your history is restored."; +"importBackup.alert.genericError.title" = "Something went wrong"; +"importBackup.alert.genericError.message" = "Your history could not be restored. Please try again or contact the Wire customer support."; +"importBackup.alert.incompatibleBackupError.title" = "Incompatible backup"; +"importBackup.alert.incompatibleBackupError.message" = "This backup was created by a newer or outdated version of Wire and cannot be restored here."; +"importBackup.alert.wrongFileError.title" = "Wrong backup file"; +"importBackup.alert.wrongFileError.message" = "You can't restore history from a different account."; + +"importBackup.enterPassword.title" = "Enter password"; +"importBackup.enterPassword.description" = "This backup is password protected."; +"importBackup.enterPassword.textField.title" = "Password"; +"importBackup.enterPassword.textField.placeholder" = "Enter password"; +"importBackup.enterPassword.wrongPassword" = "Wrong password. Please verify your input and try again"; +"importBackup.enterPassword.button.title" = "Continue"; + +"importBackup.restoringHistory.title" = "Restoring history…"; +"importBackup.restoringHistory.message" = "Loading conversations…"; diff --git a/WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.manual.swift b/WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.manual.swift new file mode 100644 index 00000000000..d1e89c7a483 --- /dev/null +++ b/WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.manual.swift @@ -0,0 +1,49 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +@testable import WireSettingsUI + +public class MockCleanUpBackupsUseCaseProtocol: CleanUpBackupsUseCaseProtocol { + + // MARK: - Life cycle + + public init() {} + + // MARK: - invoke + + public var invoke_Invocations: [Void] = [] + public var invoke_MockError: (any Error)? + public var invoke_MockMethod: (() async throws -> Void)? + + public func invoke() async throws { + invoke_Invocations.append(()) + + if let error = invoke_MockError { + throw error + } + + guard let mock = invoke_MockMethod else { + fatalError("no mock for `invoke`") + } + + try await mock() + } + +} diff --git a/WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.stencil b/WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.stencil new file mode 120000 index 00000000000..384e2627f10 --- /dev/null +++ b/WireUI/Sources/WireSettingsUISupport/Sourcery/AutoMockable.stencil @@ -0,0 +1 @@ +../../../../WirePlugins/Plugins/SourceryPlugin/Stencils/AutoMockable.stencil \ No newline at end of file diff --git a/WireUI/Sources/WireSettingsUISupport/Sourcery/sourcery.yml b/WireUI/Sources/WireSettingsUISupport/Sourcery/sourcery.yml new file mode 100644 index 00000000000..1aec2f5a3e3 --- /dev/null +++ b/WireUI/Sources/WireSettingsUISupport/Sourcery/sourcery.yml @@ -0,0 +1,8 @@ +sources: +- ${PACKAGE_ROOT_DIR}/Sources/WireSettingsUI +templates: +- ${TARGET_DIR}/Sourcery/AutoMockable.stencil +output: + ${DERIVED_SOURCES_DIR} +args: + autoMockableImports: ["Foundation", "WireSettingsUI"] diff --git a/WireUI/Sources/WireSettingsUISupport/WireSettingsUI.swift b/WireUI/Sources/WireSettingsUISupport/WireSettingsUI.swift new file mode 100644 index 00000000000..74d5741b771 --- /dev/null +++ b/WireUI/Sources/WireSettingsUISupport/WireSettingsUI.swift @@ -0,0 +1,21 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +// This target generates mocks via 'sourcery'. It uses the plugin configured in `Package.swift`. +// The generated mocks are processed from the sandbox directory and are not visible in the project folder: +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md#implementing-the-build-tool-plugin-script diff --git a/WireUI/Sources/WireSidebarUI/.swiftgen.yml b/WireUI/Sources/WireSidebarUI/.swiftgen.yml index 668e9ff4faf..1c04a15290b 100644 --- a/WireUI/Sources/WireSidebarUI/.swiftgen.yml +++ b/WireUI/Sources/WireSidebarUI/.swiftgen.yml @@ -7,6 +7,7 @@ output_dir: ${GENERATED}/ strings: inputs: + - Resources/en.lproj/Accessibility.strings - Resources/en.lproj/Localizable.strings filter: outputs: diff --git a/WireUI/Sources/WireSidebarUI/Views/SidebarAccountInfoView.swift b/WireUI/Sources/WireSidebarUI/Views/SidebarAccountInfoView.swift index da8165fbd8c..74a4173f640 100644 --- a/WireUI/Sources/WireSidebarUI/Views/SidebarAccountInfoView.swift +++ b/WireUI/Sources/WireSidebarUI/Views/SidebarAccountInfoView.swift @@ -71,7 +71,7 @@ struct SidebarAccountInfoView: @State private var iconSize: CGSize? + private typealias Strings = L10n.Localizable.Sidebar + private typealias Labels = L10n.Accessibility.Sidebar + public init( accountInfo: SidebarAccountInfo, selectedMenuItem: Binding, @@ -118,7 +121,7 @@ public struct SidebarView: @ViewBuilder private var scrollableMenuItems: some View { VStack(alignment: .leading, spacing: 0) { - menuItemHeader(L10n.Sidebar.ConversationFilter.title, addTopPadding: false) + menuItemHeader(Strings.ConversationFilter.title, addTopPadding: false) let conversationFilters: [SidebarSelectableMenuItem] = [ .all, .favorites, @@ -131,7 +134,7 @@ public struct SidebarView: selectableMenuItem(conversationFilter) } - menuItemHeader(L10n.Sidebar.Contacts.title) + menuItemHeader(Strings.Contacts.title) nonselectableMenuItem(.connect) } .padding(.horizontal, 16) @@ -160,15 +163,15 @@ public struct SidebarView: let action: () -> Void switch menuItem { case .connect: - text = Text(L10n.Sidebar.Contacts.Connect.title) + text = Text(Strings.Contacts.Connect.title) accessibilityLabel = Text("sidebar.contacts.connect.title", bundle: .module) icon = "person.badge.plus" isLink = false action = connectAction case .support: - text = Text(L10n.Sidebar.Support.title) - accessibilityLabel = Text("sidebar.support.description", tableName: "Accessibility", bundle: .module) + text = Text(Strings.Support.title) + accessibilityLabel = Text(Labels.Support.description) icon = "questionmark.circle" isLink = true action = supportAction @@ -204,43 +207,39 @@ public struct SidebarView: let accessibilityLabel: Text switch menuItem { case .all: - text = Text(L10n.Sidebar.ConversationFilter.All.title) + text = Text(Strings.ConversationFilter.All.title) icon = "text.bubble" - accessibilityLabel = Text(L10n.Sidebar.ConversationFilter.All.title) + accessibilityLabel = Text(Strings.ConversationFilter.All.title) case .favorites: - text = Text(L10n.Sidebar.ConversationFilter.Favorites.title) + text = Text(Strings.ConversationFilter.Favorites.title) icon = "star" - accessibilityLabel = Text(L10n.Sidebar.ConversationFilter.Favorites.title) + accessibilityLabel = Text(Strings.ConversationFilter.Favorites.title) case .groups: - text = Text(L10n.Sidebar.ConversationFilter.Groups.title) + text = Text(Strings.ConversationFilter.Groups.title) icon = "person.3" - accessibilityLabel = Text(L10n.Sidebar.ConversationFilter.Groups.title) + accessibilityLabel = Text(Strings.ConversationFilter.Groups.title) case .oneOnOne: - text = Text(L10n.Sidebar.ConversationFilter.OneOnOneConversations.title) + text = Text(Strings.ConversationFilter.OneOnOneConversations.title) icon = "person" - accessibilityLabel = Text( - "sidebar.conversation_filter.oneOnOneConversations.description", - tableName: "Accessibility", - bundle: .module - ) + accessibilityLabel = Text(Labels.ConversationFilter.OneOnOneConversations.description) case .folders: - text = Text(L10n.Sidebar.ConversationFilter.Folders.title) + text = Text(Strings.ConversationFilter.Folders.title) icon = "folder" - accessibilityLabel = Text(L10n.Sidebar.ConversationFilter.Folders.title) + accessibilityLabel = Text(Strings.ConversationFilter.Folders.title) case .archive: - text = Text(L10n.Sidebar.ConversationFilter.Archived.title) + text = Text(Strings.ConversationFilter.Archived.title) icon = "archivebox" - accessibilityLabel = Text(L10n.Sidebar.ConversationFilter.Archived.title) + accessibilityLabel = Text(Strings.ConversationFilter.Archived.title) case .settings: - text = Text(L10n.Sidebar.Settings.title) + text = Text(Strings.Settings.title) icon = "gearshape" - accessibilityLabel = Text("sidebar.settings.description", tableName: "Accessibility", bundle: .module) + accessibilityLabel = Text(Labels.Settings.description) } return SidebarMenuItemView( diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/BackupImportExportRootViewSnapshotTests.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/BackupImportExportRootViewSnapshotTests.swift new file mode 100644 index 00000000000..6994b5f6a6d --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/BackupImportExportRootViewSnapshotTests.swift @@ -0,0 +1,66 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI + +@MainActor +final class BackupImportExportRootViewSnapshotTests: XCTestCase { + + private var backupPasswordValidator: (any BackupPasswordValidatorProtocol)! + private var snapshotHelper: SnapshotHelper! + + override func setUp() async throws { + backupPasswordValidator = BackupImportExportBuilder.previewBuilder.backupPasswordValidator + snapshotHelper = .init() + .withSnapshotDirectory(SnapshotTestReferenceImageDirectory) + } + + override func tearDown() async throws { + snapshotHelper = nil + backupPasswordValidator = nil + } + + func testColorSchemeVariants() async throws { + let screenBounds = UIScreen.main.bounds + let sut = BackupImportExportBuilder.previewBuilder.buildRootView() + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testDynamicTypeVariants() { + let screenBounds = UIScreen.main.bounds + let sut = BackupImportExportBuilder.previewBuilder.buildRootView() + .frame(width: screenBounds.width, height: screenBounds.height) + + for dynamicTypeSize in DynamicTypeSize.allCases { + snapshotHelper + .verify( + matching: sut.dynamicTypeSize(dynamicTypeSize), + named: "\(dynamicTypeSize)" + ) + } + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/CreatingBackupProgressViewSnapshotTests.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/CreatingBackupProgressViewSnapshotTests.swift new file mode 100644 index 00000000000..21492dfc1e4 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/CreatingBackupProgressViewSnapshotTests.swift @@ -0,0 +1,89 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI + +@MainActor +final class CreatingBackupProgressViewSnapshotTests: XCTestCase { + + private var snapshotHelper: SnapshotHelper! + + override func setUp() async throws { + snapshotHelper = .init() + .withSnapshotDirectory(SnapshotTestReferenceImageDirectory) + } + + override func tearDown() async throws { + snapshotHelper = nil + } + + func testOngoingColorSchemeVariants() async throws { + let screenBounds = UIScreen.main.bounds + let sut = CreatingBackupProgressView(progress: .ongoing(0.25)) {} + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testFinishedColorSchemeVariants() async throws { + let screenBounds = UIScreen.main.bounds + let sut = CreatingBackupProgressView(progress: .finished(URL(fileURLWithPath: "/"))) {} + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testOngoingDynamicTypeVariants() { + let screenBounds = UIScreen.main.bounds + + let sut = CreatingBackupProgressView(progress: .ongoing(0.25)) {} + .frame(width: screenBounds.width, height: screenBounds.height) + + for dynamicTypeSize in DynamicTypeSize.allCases { + snapshotHelper + .verify( + matching: sut.dynamicTypeSize(dynamicTypeSize), + named: "\(dynamicTypeSize)" + ) + } + } + + func testFinishedDynamicTypeVariants() { + let screenBounds = UIScreen.main.bounds + + let sut = CreatingBackupProgressView(progress: .finished(URL(fileURLWithPath: "/"))) {} + .frame(width: screenBounds.width, height: screenBounds.height) + + for dynamicTypeSize in DynamicTypeSize.allCases { + snapshotHelper + .verify( + matching: sut.dynamicTypeSize(dynamicTypeSize), + named: "\(dynamicTypeSize)" + ) + } + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/ExportBackupViewModelTests.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/ExportBackupViewModelTests.swift new file mode 100644 index 00000000000..8de69da19a1 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/ExportBackupViewModelTests.swift @@ -0,0 +1,121 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireLogging +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI +@testable import WireSettingsUISupport + +@MainActor +final class ExportBackupViewModelTests: XCTestCase { + + private var mockCreateBackupUseCase: MockCreateBackupUseCaseProtocol! + private var mockCleanUpBackupsUseCase: MockCleanUpBackupsUseCaseProtocol! + private var mockLogger: (any LoggerProtocol)! + private var sut: ExportBackupViewModel! + + override func setUp() async throws { + mockCreateBackupUseCase = .init() + + mockCleanUpBackupsUseCase = .init() + mockCleanUpBackupsUseCase.invoke_MockMethod = {} + + mockLogger = WireLogger(tag: "mock") + + sut = .init( + createBackupUseCase: mockCreateBackupUseCase, + cleanUpBackupsUseCase: mockCleanUpBackupsUseCase, + logger: mockLogger + ) + } + + override func tearDown() async throws { + sut = nil + mockLogger = nil + mockCleanUpBackupsUseCase = nil + mockCreateBackupUseCase = nil + } + + func testInitialValues() { + XCTAssertFalse(sut.isCreatingBackupProgressPresented) + XCTAssertFalse(sut.isSetBackupPasswordPresented) + XCTAssertFalse(sut.isErrorAlertPresented) + } + + func testProgressIsReported() { + // Given + var continuation: AsyncThrowingStream.Continuation! + mockCreateBackupUseCase.invokePassword_MockValue = .init { continuation = $0 } + let url = URL(fileURLWithPath: "/") + let sut = sut as ExportBackupViewModel + + // When / Then + sut.showPasswordDialog() + wait(forConditionToBeTrue: sut.isSetBackupPasswordPresented, timeout: 3) + + sut.createBackup(password: "pw") + continuation.yield(.progress(0.5)) + wait(forConditionToBeTrue: sut.backupProgress == .ongoing(0.5), timeout: 3) + + continuation.yield(.done(url)) + wait(forConditionToBeTrue: sut.backupProgress == .finished(url), timeout: 3) + + continuation.finish() + sut.cancel() + wait(forConditionToBeTrue: !self.mockCleanUpBackupsUseCase.invoke_Invocations.isEmpty, timeout: 3) + } + + func testCancelTerminatesTask() { + // Given + var continuation: AsyncThrowingStream.Continuation! + mockCreateBackupUseCase.invokePassword_MockValue = .init { continuation = $0 } + let sut = sut as ExportBackupViewModel + let expectation = XCTestExpectation() + continuation.onTermination = { @Sendable _ in expectation.fulfill() } + + // When + sut.showPasswordDialog() + sut.createBackup(password: "pw") + continuation.yield(.progress(0.5)) + wait(forConditionToBeTrue: sut.backupProgress == .ongoing(0.5), timeout: 3) + sut.cancel() + + // Then + wait(for: [expectation], timeout: 3) + } + + func testErrorPresentsAlert() { + // Given + var continuation: AsyncThrowingStream.Continuation! + mockCreateBackupUseCase.invokePassword_MockValue = .init { continuation = $0 } + let sut = sut as ExportBackupViewModel + + // When + sut.showPasswordDialog() + sut.createBackup(password: "pw") + continuation.yield(.progress(0.5)) + wait(forConditionToBeTrue: sut.backupProgress == .ongoing(0.5), timeout: 3) + continuation.finish(throwing: NSError(domain: "ExportBackupViewModelTests", code: 987)) + + // Then + wait(forConditionToBeTrue: sut.isErrorAlertPresented, timeout: 3) + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/SetBackupPasswordViewSnapshotTests.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/SetBackupPasswordViewSnapshotTests.swift new file mode 100644 index 00000000000..bb6796f2a9e --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Export/SetBackupPasswordViewSnapshotTests.swift @@ -0,0 +1,114 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI + +@MainActor +final class SetBackupPasswordViewSnapshotTests: XCTestCase { + + private var backupPasswordValidator: (any BackupPasswordValidatorProtocol)! + private var snapshotHelper: SnapshotHelper! + + override func setUp() async throws { + backupPasswordValidator = BackupImportExportBuilder.previewBuilder.backupPasswordValidator + snapshotHelper = .init() + .withSnapshotDirectory(SnapshotTestReferenceImageDirectory) + } + + override func tearDown() async throws { + snapshotHelper = nil + backupPasswordValidator = nil + } + + func testInvalidPassword() async throws { + let screenBounds = UIScreen.main.bounds + let viewModel = SetBackupPasswordViewModel( + passwordValidator: backupPasswordValidator, + cancelAction: {}, + setPasswordAction: { _ in } + ) + viewModel.password = "invalid" + let sut = SetBackupPasswordView(viewModel: viewModel) + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.light) + .verify(matching: sut, named: "light") + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testNonEmptyPassword() async throws { + let screenBounds = UIScreen.main.bounds + let viewModel = SetBackupPasswordViewModel( + passwordValidator: backupPasswordValidator, + cancelAction: {}, + setPasswordAction: { _ in } + ) + viewModel.password = "G00dPassword" + let sut = SetBackupPasswordView(viewModel: viewModel) + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.light) + .verify(matching: sut, named: "light") + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testColorSchemeVariants() async throws { + let screenBounds = UIScreen.main.bounds + let viewModel = SetBackupPasswordViewModel( + passwordValidator: backupPasswordValidator, + cancelAction: {}, + setPasswordAction: { _ in } + ) + let sut = SetBackupPasswordView(viewModel: viewModel) + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testDynamicTypeVariants() { + let screenBounds = UIScreen.main.bounds + let viewModel = SetBackupPasswordViewModel( + passwordValidator: backupPasswordValidator, + cancelAction: {}, + setPasswordAction: { _ in } + ) + let sut = SetBackupPasswordView(viewModel: viewModel) + .frame(width: screenBounds.width, height: screenBounds.height) + + for dynamicTypeSize in DynamicTypeSize.allCases { + snapshotHelper + .verify( + matching: sut.dynamicTypeSize(dynamicTypeSize), + named: "\(dynamicTypeSize)" + ) + } + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/EnterPasswordViewSnapshotTests.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/EnterPasswordViewSnapshotTests.swift new file mode 100644 index 00000000000..597cea94efb --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/EnterPasswordViewSnapshotTests.swift @@ -0,0 +1,109 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI + +@MainActor +final class EnterPasswordViewSnapshotTests: XCTestCase { + + private var snapshotHelper: SnapshotHelper! + + override func setUp() async throws { + snapshotHelper = .init() + .withSnapshotDirectory(SnapshotTestReferenceImageDirectory) + } + + override func tearDown() async throws { + snapshotHelper = nil + } + + func testInvalidPassword() async throws { + let screenBounds = UIScreen.main.bounds + let sut = EnterPasswordView( + password: .constant("invalid"), + passwordIsWrong: .constant(true), + continueAction: { _ in }, + cancelAction: {} + ) + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.light) + .verify(matching: sut, named: "light") + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testNonEmptyPassword() async throws { + let screenBounds = UIScreen.main.bounds + let sut = EnterPasswordView( + password: .constant("G00dPassword!"), + passwordIsWrong: .constant(false), + continueAction: { _ in }, + cancelAction: {} + ) + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.light) + .verify(matching: sut, named: "light") + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testColorSchemeVariants() async throws { + let screenBounds = UIScreen.main.bounds + let sut = EnterPasswordView( + password: .constant(""), + passwordIsWrong: .constant(false), + continueAction: { _ in }, + cancelAction: {} + ) + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testDynamicTypeVariants() { + let screenBounds = UIScreen.main.bounds + let sut = EnterPasswordView( + password: .constant(""), + passwordIsWrong: .constant(false), + continueAction: { _ in }, + cancelAction: {} + ) + .frame(width: screenBounds.width, height: screenBounds.height) + + for dynamicTypeSize in DynamicTypeSize.allCases { + snapshotHelper + .verify( + matching: sut.dynamicTypeSize(dynamicTypeSize), + named: "\(dynamicTypeSize)" + ) + } + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportBackupViewModelTest.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportBackupViewModelTest.swift new file mode 100644 index 00000000000..4dd475a9b5c --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportBackupViewModelTest.swift @@ -0,0 +1,125 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireDomainPkg +import WireLogging +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI +@testable import WireSettingsUISupport + +@MainActor +final class ImportBackupViewModelTest: XCTestCase { + + private var temporaryDirectory: URL! + private var temporaryFile: URL! + private var mockImportBackupUseCase: MockImportBackupUseCaseProtocol! + private var mockLogger: (any LoggerProtocol)! + private var sut: ImportBackupViewModel! + + private var fileManager: FileManager { .default } + + override func setUp() async throws { + + temporaryDirectory = try fileManager.url( + for: .itemReplacementDirectory, + in: .userDomainMask, + appropriateFor: Bundle(for: Self.self).bundleURL, + create: true + ) + temporaryFile = temporaryDirectory + .appending(component: "someFile", directoryHint: .notDirectory) + try Data("data".utf8).write(to: temporaryFile) + + mockImportBackupUseCase = .init() + + mockLogger = WireLogger(tag: "mock") + + sut = .init( + importBackupUseCase: mockImportBackupUseCase, + logger: mockLogger + ) + } + + override func tearDown() async throws { + sut = nil + mockLogger = nil + mockImportBackupUseCase = nil + + try fileManager.removeItem(at: temporaryDirectory) + } + + func testConfirmationIsNeededBeforeProceeding() throws { + // Given + let sut = sut as ImportBackupViewModel + + // When + sut.pickedBackupFile(result: .success(temporaryFile)) + + // Then + wait(forConditionToBeTrue: sut.isImportConfirmationPresented, timeout: 3) + XCTAssertFalse(sut.alertContent.title.isEmpty) + XCTAssertFalse(sut.alertContent.message.isEmpty) + XCTAssertFalse(sut.alertContent.cancel.isEmpty) + XCTAssertFalse(sut.alertContent.action.isEmpty) + } + + func testPasswordIsRequested() { + // Given + var continuation: AsyncThrowingStream.Continuation! + mockImportBackupUseCase.invokeUrlPassword_MockValue = .init { continuation = $0 } + let sut = sut as ImportBackupViewModel + + // When + sut.pickedBackupFile(result: .success(temporaryFile)) + wait(forConditionToBeTrue: sut.isImportConfirmationPresented, timeout: 3) + sut.confirmOverwrite() + continuation.finish(throwing: ImportBackupError.passwordRequired) + + // Then + wait(forConditionToBeTrue: sut.isEnterBackupPasswordPresented, timeout: 3) + XCTAssertFalse(sut.isBackupPasswordWrong) + } + + func testProgressIsReported() { + // Given + var continuation: AsyncThrowingStream.Continuation! + mockImportBackupUseCase.invokeUrlPassword_MockValue = .init { continuation = $0 } + let sut = sut as ImportBackupViewModel + + // When + sut.pickedBackupFile(result: .success(temporaryFile)) + wait(forConditionToBeTrue: sut.isImportConfirmationPresented, timeout: 3) + sut.confirmOverwrite() + wait(forConditionToBeTrue: sut.importProgress == 0, timeout: 3) + continuation.finish(throwing: ImportBackupError.decryptionError) + wait(forConditionToBeTrue: sut.isEnterBackupPasswordPresented, timeout: 3) + mockImportBackupUseCase.invokeUrlPassword_MockValue = .init { continuation = $0 } + sut.enterPassword("pw") + + // Then + wait(forConditionToBeTrue: sut.importProgress == 0, timeout: 3) + continuation.yield(.progress(0.25)) + wait(forConditionToBeTrue: sut.importProgress == 0.25, timeout: 3) + continuation.yield(.done) + wait(forConditionToBeTrue: sut.importProgress == 1, timeout: 3) + wait(forConditionToBeTrue: sut.isAlertPresented, timeout: 3) + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportProgressViewSnapshotTests.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportProgressViewSnapshotTests.swift new file mode 100644 index 00000000000..e8e9f82b7ef --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Import/ImportProgressViewSnapshotTests.swift @@ -0,0 +1,63 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireTestingPackage +import XCTest + +@testable import WireSettingsUI + +@MainActor +final class ImportProgressViewSnapshotTests: XCTestCase { + + private var snapshotHelper: SnapshotHelper! + + override func setUp() async throws { + snapshotHelper = .init() + .withSnapshotDirectory(SnapshotTestReferenceImageDirectory) + } + + override func tearDown() async throws { + snapshotHelper = nil + } + + func testColorSchemeVariants() async throws { + let screenBounds = UIScreen.main.bounds + let sut = ImportProgressView(progressValue: 0.25) {} + .frame(width: screenBounds.width, height: screenBounds.height) + + snapshotHelper + .withUserInterfaceStyle(.dark) + .verify(matching: sut, named: "dark") + } + + func testDynamicTypeVariants() { + let screenBounds = UIScreen.main.bounds + let sut = ImportProgressView(progressValue: 0.25) {} + .frame(width: screenBounds.width, height: screenBounds.height) + + for dynamicTypeSize in DynamicTypeSize.allCases { + snapshotHelper + .verify( + matching: sut.dynamicTypeSize(dynamicTypeSize), + named: "\(dynamicTypeSize)" + ) + } + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Mocks/MockImportBackupUseCaseProtocol.swift b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Mocks/MockImportBackupUseCaseProtocol.swift new file mode 100644 index 00000000000..c18cedd3a20 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Account/BackupImportExport/Mocks/MockImportBackupUseCaseProtocol.swift @@ -0,0 +1,46 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireDomainPkg + +public final class MockImportBackupUseCaseProtocol: ImportBackupUseCaseProtocol { + + // MARK: - Life cycle + + public init() {} + + // MARK: - invoke + + public var invokeUrlPassword_Invocations: [(url: URL, password: String)] = [] + public var invokeUrlPassword_MockMethod: ((URL, String) -> AsyncThrowingStream)? + public var invokeUrlPassword_MockValue: AsyncThrowingStream? + + public func invoke(url: URL, password: String) -> AsyncThrowingStream { + invokeUrlPassword_Invocations.append((url: url, password: password)) + + if let mock = invokeUrlPassword_MockMethod { + return mock(url, password) + } else if let mock = invokeUrlPassword_MockValue { + return mock + } else { + fatalError("no mock for `invokeUrlPassword`") + } + } + +} diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testColorSchemeVariants.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testColorSchemeVariants.dark.png new file mode 100644 index 00000000000..b6c51e50dce --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testColorSchemeVariants.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac72841502bcaa875829eff80158dc9250647b389aea261676dca13a4f616a3d +size 151600 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility1.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility1.png new file mode 100644 index 00000000000..4dded597ee5 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d74f8dbbf335c760d3dce1f1a6715aa7c53ce05a6def3432944e39c0b4b5f627 +size 233023 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility2.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility2.png new file mode 100644 index 00000000000..8249e8a9cba --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e6d5a84b022af244bacc7c49c20611da0ad8f31292c0be04b98a620da5d990d +size 263612 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility3.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility3.png new file mode 100644 index 00000000000..6a7c0c989c1 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ca55604ff64d8d6fea0f3a9f7711a1b8bb2139d242788bc7b01179334f0a3c0 +size 290490 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility4.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility4.png new file mode 100644 index 00000000000..1c671cd5fb8 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:605abfaeaba67e42f7c32e7772e8c34c4740f0b28c69b96fabf68c7dba7b7b35 +size 272767 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility5.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility5.png new file mode 100644 index 00000000000..52bfbb1e150 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.accessibility5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c853df49823d58a13d288f2ded243f81bd1002c6feca77f09abe7bcceeaaf06 +size 272068 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.large.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.large.png new file mode 100644 index 00000000000..770d2ddc11f --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8808fa2cc84dd8f68b29ebdc4539f03bd36bb2c4cd7091c64f099e198bc6328e +size 150832 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.medium.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.medium.png new file mode 100644 index 00000000000..859bf94b528 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.medium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4739b551be0174716d4da8f001629cbfa0463ff3f4c3dc58c11c07f8eb8de402 +size 141706 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.small.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.small.png new file mode 100644 index 00000000000..a28ca1028e0 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29e108aa44bc060221cf7e9d0e1e17f36912a0619425654e1891881ee165d852 +size 140958 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xLarge.png new file mode 100644 index 00000000000..67bb8740e3a --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caf0f0613172d539781729bd476851fdb64850bad0de4f8566e638343ff50744 +size 164245 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xSmall.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xSmall.png new file mode 100644 index 00000000000..ee7679ad56d --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xSmall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48b1835642bc9401caefd5b6458a931bdc0d7bfbca8b1a5551a9bfe75f366b18 +size 140446 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxLarge.png new file mode 100644 index 00000000000..2a8fed58381 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35aa4447ca7d93782603efc5a08bbad529569dddae24d8d594e7de43af3b8677 +size 181011 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png new file mode 100644 index 00000000000..2b02e04821f --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/BackupImportExportRootViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04f3be71fbf26172be07d07d2f850e3066d7721caccfb9e607cf43a567642e28 +size 195547 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedColorSchemeVariants.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedColorSchemeVariants.dark.png new file mode 100644 index 00000000000..9452b5e7568 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedColorSchemeVariants.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a86f9186710491d97c4f6623d9a785c29b56aa49eeedcab710cf26f0fe43c83 +size 93610 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility1.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility1.png new file mode 100644 index 00000000000..86c61d439ce --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd3469c52a4377c5d679fec2bb43d880f622f5aa58495f7364d1e0213ddcaf3e +size 102932 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility2.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility2.png new file mode 100644 index 00000000000..3feb16b02b9 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dda346b281945f78f60959871d7fff33a32420f84fdcba379dec6d0daccd9a51 +size 105998 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility3.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility3.png new file mode 100644 index 00000000000..e09ccc8fb7a --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ed3f21e4439918a4f939f62721432003b576d5e27bd8b375eb7453f57ce901b +size 114258 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility4.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility4.png new file mode 100644 index 00000000000..5ac871bee3e --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58330cc39629418af0eeb1b23122fc9b19178cc70931ede6cf4e08b42554dfad +size 118053 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility5.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility5.png new file mode 100644 index 00000000000..a5b121bccb1 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.accessibility5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e8600f8d6b907b56e2a0404d5e4dbbd78a8e80b9b94ee08974caf0235b09185 +size 127162 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.large.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.large.png new file mode 100644 index 00000000000..5c6ff87f28e --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9e7deea497708ca6f14283ee0c0e9458310cc142b43aae663ae4268159ca1dd +size 94222 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.medium.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.medium.png new file mode 100644 index 00000000000..02d306c66fa --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.medium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea85cc532bd127d701a5833ab447020f08ea4160a92182ab1af6f1b405b70d1d +size 94619 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.small.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.small.png new file mode 100644 index 00000000000..df6df4a696c --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf2594cb95f810dcd71abb2582555f4934a5f3a1f63df2747f916af0b910fbac +size 94627 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xLarge.png new file mode 100644 index 00000000000..8de22c351c0 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51537a5eec0be08ccdd435c24470ccf014592be4e123fa6a5e749f2548bde652 +size 96885 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xSmall.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xSmall.png new file mode 100644 index 00000000000..903f232f70e --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xSmall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc9aa912fb87e28e40824a39a2decbddc12d6568605ec08aca22d57a3166ba4e +size 94569 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxLarge.png new file mode 100644 index 00000000000..e48d7411165 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:902a8bcc9fd9f518196307b3713be8466027b27dcf2b71599d8c507cfcfb7282 +size 99531 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxxLarge.png new file mode 100644 index 00000000000..ecd8f35be09 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testFinishedDynamicTypeVariants.xxxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98230d4905db9f53a292f84ce25d3630dc2ffb2837fd34bf8f668e6306a3cc58 +size 101123 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingColorSchemeVariants.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingColorSchemeVariants.dark.png new file mode 100644 index 00000000000..82769c793fb --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingColorSchemeVariants.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7ffaf452e397c943e45e1586df304ad9d4f5d9bde0e5f653473c9fcdd2e440d +size 93571 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility1.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility1.png new file mode 100644 index 00000000000..1fd23013732 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35da3e0fe361a515a85885951617121528dbd59ba7d385bddf8e12b3d0d92c3e +size 103403 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility2.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility2.png new file mode 100644 index 00000000000..d849dee4777 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb91e034c57b25f94bfe7887acdce5ed600fe8b7461c41cf26512abc4e711dc8 +size 106587 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility3.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility3.png new file mode 100644 index 00000000000..3dcb8818a6a --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7eeae49e490dd63399985bf92a4e00b7e2fa7a337ba56991ea5abf6364b7626a +size 114651 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility4.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility4.png new file mode 100644 index 00000000000..41d16d05c87 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dda6088d7c451d68bf0569cd9a647d7c02de8f28b481f615bbbbb9fe9145e8e +size 119378 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility5.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility5.png new file mode 100644 index 00000000000..5366e3c6471 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.accessibility5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cebf666913de9874d080111562d6fe9dba8e91814f77d3dda908379053ed2599 +size 129589 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.large.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.large.png new file mode 100644 index 00000000000..1bb24bd815b --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:216087509581c54c5133dbb56badede4bf1090c31b12db93c537f8b7062a3462 +size 93883 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.medium.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.medium.png new file mode 100644 index 00000000000..4eadffdecd4 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.medium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16fbc3c4493ba10ddd92b7d9283225765e0a4fc1eb15ea472df12c5c2af6c836 +size 93844 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.small.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.small.png new file mode 100644 index 00000000000..4dc6388fbc3 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42681f0adeb2f789778801452d0512450fd42a351548e16cdca8ee8f7bbd2abb +size 93790 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xLarge.png new file mode 100644 index 00000000000..2068841f885 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:243d21dd6c9f9afc945c1045429f7fd788ab0c63e77b5637ccf2da2325a44d8d +size 97052 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xSmall.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xSmall.png new file mode 100644 index 00000000000..20bccf8e2d0 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xSmall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e19079a24bd7ea3b7cdbfef82aa8ab726a5d7f24c8907508ba101c62ae23d5e3 +size 93733 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxLarge.png new file mode 100644 index 00000000000..d25efa8e425 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f435e3a209ba91e098a7bc342eab146af870046453cd26ca2f27c4d3e4ed0b4 +size 99472 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxxLarge.png new file mode 100644 index 00000000000..7e72956689c --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/CreatingBackupProgressViewSnapshotTests/testOngoingDynamicTypeVariants.xxxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d8babf2adfae30f76f2d2b380858a87a1a2c9a741e54388720c918ff975a3b6 +size 99958 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testColorSchemeVariants.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testColorSchemeVariants.dark.png new file mode 100644 index 00000000000..973e0a37e99 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testColorSchemeVariants.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c09a2de9a8a7eaac8b32d5c2bf7a1858a8d40d184a2135334bb9ce939725fcb +size 113782 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png new file mode 100644 index 00000000000..33de583af22 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13277a1afc4ffc4988cfa6262afc27e4b9bb2d3261892cca70b38c087509f7b1 +size 133308 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png new file mode 100644 index 00000000000..b08a6fbb391 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea664aca7d7dfff6dccbc62376706aadd90c5f8fd0b4e2834a443388503c458b +size 140321 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png new file mode 100644 index 00000000000..a39f014d023 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cc930c7acbdc5ec2404da5b2af4eb7706c1c3a665e98d233eb91e3357a4ccf4 +size 149854 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png new file mode 100644 index 00000000000..611d3a11fb6 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04a0d004e2d23f028e28d8313a395cd6a8a8281c7ce408694b15d95e10d8df4a +size 167184 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png new file mode 100644 index 00000000000..8fe0fba1e9e --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d67c707f0cc1466b3eed581bfbe914a00bcce56bf4067414faf36d99e68cb8e9 +size 179195 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.large.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.large.png new file mode 100644 index 00000000000..58c92d93cf9 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcf948ef4f859f1cb68959fcadbe477ee58ce46ee0c3c8bf8e17a12e112cf2b9 +size 112878 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png new file mode 100644 index 00000000000..4fac2af8034 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b68ae40cdd7813a52a61fedb58bb8d72da7bb79c9a795cb1671268afef4471e +size 111106 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.small.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.small.png new file mode 100644 index 00000000000..c16982175db --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a8905ccb4e5f1c597617dcf74e3632dbb2ba669c797baa424b5fd6991ff4654 +size 109543 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png new file mode 100644 index 00000000000..dc1617a97a0 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0966e5e0dc6df899092d50b993cc7e65bdc338e7d2bd57c2739a49327a1fa8ba +size 116408 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png new file mode 100644 index 00000000000..9768960fb81 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8a783738b5638a085c32720e31210464666534d46ed82ef04b66ac7769e00f0 +size 108137 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png new file mode 100644 index 00000000000..1c009049180 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9ff226e524ee264b5515e7201cbe69480f1d0d157d59e4a44a67db13283660a +size 120422 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png new file mode 100644 index 00000000000..5a4883b65d8 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bae1eaf2e06208b54b0cc7fac888119f5ed155a293389e259aa9c8edcf6cfb3 +size 123523 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.dark.png new file mode 100644 index 00000000000..4ffd0942d17 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b81583098458da7eb9fd7c23443cb5ae9306765c9f7e3fb2f1a6b76eab95ab7a +size 117856 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.light.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.light.png new file mode 100644 index 00000000000..05b1e7ab2e3 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testInvalidPassword.light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e81892f847dc557e77807af5a353bc84cdb0b90c90a435dc797dbd5db883b371 +size 118551 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.dark.png new file mode 100644 index 00000000000..03ac27fd9db --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69eee2115f00cc31dcfeb1ff0b6322e2856c3e4590b6422d1773793cab116122 +size 108761 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.light.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.light.png new file mode 100644 index 00000000000..f7d993ebcce --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/EnterPasswordViewSnapshotTests/testNonEmptyPassword.light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8183ddaab15ab713501dfadcf41d7940a3c39d5582e0a1d76ee4fe4ddd4affe +size 108849 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testColorSchemeVariants.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testColorSchemeVariants.dark.png new file mode 100644 index 00000000000..10e9c8be053 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testColorSchemeVariants.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd24fb70dfb948ae13de555b4a4a9aefdcd7c5713b99587d34210d0da28181c6 +size 91345 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility1.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility1.png new file mode 100644 index 00000000000..24e64666ac5 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9b6d70f5c5e6fb7b6fd0128e4b1505273ff2445227711e13048cffe216785d7 +size 102426 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility2.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility2.png new file mode 100644 index 00000000000..d70edb7e317 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de8a6bce0cde065e72dbe8b2d9711cc89c656dfa7ca97e837581d6f8bfddc25d +size 105479 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility3.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility3.png new file mode 100644 index 00000000000..e00e73f18ae --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29bbf25c53257a4c5fc8f7475b42b002f96b60a20f2bf398ff7eb71927490a61 +size 115095 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility4.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility4.png new file mode 100644 index 00000000000..54e1a268a64 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de85f84750e1be6331bc56a51e89d472a1961b09bb9e0fff31adfd70357f95a2 +size 120920 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility5.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility5.png new file mode 100644 index 00000000000..ac71535bec5 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.accessibility5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e46572927a2dd7f46d753d6458f5d0b4b9e0adbf4afce6cc1a380a96e4408ec9 +size 125942 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.large.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.large.png new file mode 100644 index 00000000000..84f1af0e341 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8f28ecd04aee34867f4bcbd792e8b6249cec3b59707f577beaf15ad7d4fe9ae +size 93790 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.medium.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.medium.png new file mode 100644 index 00000000000..b58a34e8f93 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.medium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0683f82458cca3aaa2dbb206d7c70d390f4ec93f829cb67702d39ea3fcb95d9 +size 93371 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.small.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.small.png new file mode 100644 index 00000000000..552907897fc --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:520c0511a2b306819528723de361ca28152ec72321a75c343921d40bf4494e30 +size 92735 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xLarge.png new file mode 100644 index 00000000000..eb20d232048 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76f06caff2c1bf86580bfaa40854c7b886bb628b210acf307de4e784912e0bae +size 96193 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xSmall.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xSmall.png new file mode 100644 index 00000000000..68384e856f0 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xSmall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3f12f95d67d9878ca5721fcd34ca674ed63edfd9afbabc388e336306071e38c +size 92254 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxLarge.png new file mode 100644 index 00000000000..29f720614d1 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6883ece418d1fa58d6ea2597b83c9b552071656b3e6513b0c3b5febe129cbde4 +size 98693 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png new file mode 100644 index 00000000000..8cb6545983c --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/ImportProgressViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86e0a975a69fee6208599f291880cefabf591c97aceb1e2aacbf4609c7a040f6 +size 100021 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testColorSchemeVariants.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testColorSchemeVariants.dark.png new file mode 100644 index 00000000000..13e9f6a8298 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testColorSchemeVariants.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec0bdd457dbaf509c475c053fd5d9028562051ae473b0d1f1a2c85bdd1654086 +size 153758 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png new file mode 100644 index 00000000000..651d5c9084f --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a75369bd5769d6faf6d4c9f570296e77928bb5e8d91b2fc4b080f48965c88b3 +size 209307 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png new file mode 100644 index 00000000000..2f5b2f35f0f --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1d9627b26700739139822521ea96fa14c2a142afdcece44047e280f0e24d5ab +size 232893 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png new file mode 100644 index 00000000000..d26fad83db7 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1ae9e5ec7a598c25e13e6bfafccec300d4c5668a836c89fe46dbe9234c45a14 +size 269189 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png new file mode 100644 index 00000000000..9d70e019d56 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a534d0e5e7f620298f21541bebd662405c92e81915c2c9ca8b2e8a7474f14260 +size 278046 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png new file mode 100644 index 00000000000..0a3555259ee --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.accessibility5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33a2a1f8b507ed310c60d1d768021aad03f33d6c5d322490b2f0526b9a2bb160 +size 283594 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.large.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.large.png new file mode 100644 index 00000000000..a4d6ec51745 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa0200ddcb4f7893457924564559387695751d3925947a539edab35ccf388da3 +size 153043 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png new file mode 100644 index 00000000000..43aa2439057 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.medium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4acfa131697c029c1924d8dbdc5a1170346278841f300fbfe20f0ad3985bd23b +size 149224 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.small.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.small.png new file mode 100644 index 00000000000..9509d47b246 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b849219e8ce9826be3cbf8b09db71d449d74803d6424c2bb5d999bde798b867 +size 143398 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png new file mode 100644 index 00000000000..760dde2cdd6 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:061c019145e4e0ecf1abff9b33163636041f6c15b069222430aa0595c9025de8 +size 163636 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png new file mode 100644 index 00000000000..4d555d8e742 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xSmall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca3f57e9670ed0b6a4253b05e85a35004c0c5380d79741a392481cb55b770e31 +size 139467 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png new file mode 100644 index 00000000000..c81542ccf9b --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d725b4784936da5b2af969a311002c19c7cdc39bd7afe13f1920e125afb10594 +size 173393 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png new file mode 100644 index 00000000000..c4fa50db491 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testDynamicTypeVariants.xxxLarge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f26610223e18d4cf66063351eae6746e804910453ed13823f2a47f527d96100 +size 182919 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.dark.png new file mode 100644 index 00000000000..339bcd09402 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb5afbe4b0f1beaf026652a30c4108fc7594cf00eeb88667291eca5a9ff0f8ff +size 148763 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.light.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.light.png new file mode 100644 index 00000000000..f5683ad47c4 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testInvalidPassword.light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74ccff35a126ac3978a593550bc66a4c4f238e49522c91c1a9664685730e14ae +size 148564 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.dark.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.dark.png new file mode 100644 index 00000000000..0a63467e35b --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d22842ba4251e5052eaa62c555e66bd576133143cc6cd68900259f473f8a2d76 +size 148873 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.light.png b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.light.png new file mode 100644 index 00000000000..68ec725a170 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SetBackupPasswordViewSnapshotTests/testNonEmptyPassword.light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6a7354d71efcfe5a0dcd6af0d5a1cf37ee2799e53ed6730a8c57aeb60ec2eb3 +size 148644 diff --git a/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SnapshotTestReferenceImageDirectory.swift b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SnapshotTestReferenceImageDirectory.swift new file mode 100644 index 00000000000..6b1997849e8 --- /dev/null +++ b/WireUI/Tests/WireSettingsUITests/Resources/ReferenceImages/SnapshotTestReferenceImageDirectory.swift @@ -0,0 +1,23 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +public let SnapshotTestReferenceImageDirectory = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .path diff --git a/wire-ios-data-model/Tests/MLS/MLSActionExecutorTests.swift b/wire-ios-data-model/Tests/MLS/MLSActionExecutorTests.swift index 55c14950304..5abf2951fce 100644 --- a/wire-ios-data-model/Tests/MLS/MLSActionExecutorTests.swift +++ b/wire-ios-data-model/Tests/MLS/MLSActionExecutorTests.swift @@ -19,6 +19,7 @@ import Combine import Foundation import WireCoreCrypto +import WireTestingPackage import XCTest @testable import WireDataModel diff --git a/wire-ios-data-model/Tests/Source/Model/Messages/ZMAssetClientMessageTests+Ephemeral.swift b/wire-ios-data-model/Tests/Source/Model/Messages/ZMAssetClientMessageTests+Ephemeral.swift index b6a85d9a25e..d843ab0a011 100644 --- a/wire-ios-data-model/Tests/Source/Model/Messages/ZMAssetClientMessageTests+Ephemeral.swift +++ b/wire-ios-data-model/Tests/Source/Model/Messages/ZMAssetClientMessageTests+Ephemeral.swift @@ -18,6 +18,7 @@ import Foundation import WireTesting +import WireTestingPackage @testable import WireDataModel diff --git a/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj b/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj index cecfe720db0..a25e35b0181 100644 --- a/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj +++ b/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj @@ -321,6 +321,7 @@ 59D1C3032B1DE6FF0016F6B2 /* WireDataModelSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 59D1C3022B1DE6FF0016F6B2 /* WireDataModelSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 59D1C3062B1DE6FF0016F6B2 /* WireDataModelSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59D1C3002B1DE6FF0016F6B2 /* WireDataModelSupport.framework */; }; 59D1C30E2B1DEC300016F6B2 /* WireDataModelSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59D1C3002B1DE6FF0016F6B2 /* WireDataModelSupport.framework */; }; + 59D398D02D552573001C9C5F /* WireTestingPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59D398CF2D552573001C9C5F /* WireTestingPackage */; }; 59EC73F72C20B03100E5C036 /* NSManagedObjectContext+executeFetchRequestOrAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 59EC73F62C20B03100E5C036 /* NSManagedObjectContext+executeFetchRequestOrAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 59EC73F92C20B04600E5C036 /* NSManagedObjectContext+executeFetchRequestOrAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 59EC73F82C20B04600E5C036 /* NSManagedObjectContext+executeFetchRequestOrAssert.m */; }; 59F4B6F22B87563E00AC84B1 /* IsUserE2EICertifiedUseCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F4B6F12B87563E00AC84B1 /* IsUserE2EICertifiedUseCaseProtocol.swift */; }; @@ -1915,6 +1916,7 @@ 59D1C30E2B1DEC300016F6B2 /* WireDataModelSupport.framework in Frameworks */, CB7979182C747652006FBA58 /* WireTransportSupport.framework in Frameworks */, F9C9A5071CAD5DF10039E10C /* WireDataModel.framework in Frameworks */, + 59D398D02D552573001C9C5F /* WireTestingPackage in Frameworks */, 59FFAD9D2C822977000C8085 /* WireTesting.framework in Frameworks */, 591B6E4E2C8B09CA009F8A7B /* OCMock.xcframework in Frameworks */, ); @@ -5250,6 +5252,10 @@ isa = XCSwiftPackageProductDependency; productName = WireAnalytics; }; + 59D398CF2D552573001C9C5F /* WireTestingPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = WireTestingPackage; + }; CBD35F2B2D09EBB20080DA37 /* WireCrypto */ = { isa = XCSwiftPackageProductDependency; productName = WireCrypto; diff --git a/wire-ios-request-strategy/Sources/Helpers/MessageExpirationTimerTests.swift b/wire-ios-request-strategy/Sources/Helpers/MessageExpirationTimerTests.swift index cb64f8a0443..dd7c8a41df5 100644 --- a/wire-ios-request-strategy/Sources/Helpers/MessageExpirationTimerTests.swift +++ b/wire-ios-request-strategy/Sources/Helpers/MessageExpirationTimerTests.swift @@ -19,6 +19,7 @@ import WireDataModel import WireRequestStrategy import WireTesting +import WireTestingPackage import XCTest final class MessageExpirationTimerTests: MessagingTestBase { diff --git a/wire-ios-request-strategy/Sources/Synchronization/Decoding/EventDecoderTest.swift b/wire-ios-request-strategy/Sources/Synchronization/Decoding/EventDecoderTest.swift index 3fe75529dd2..b8ce1f04969 100644 --- a/wire-ios-request-strategy/Sources/Synchronization/Decoding/EventDecoderTest.swift +++ b/wire-ios-request-strategy/Sources/Synchronization/Decoding/EventDecoderTest.swift @@ -18,6 +18,7 @@ import WireDataModelSupport import WireTesting +import WireTestingPackage @testable import WireRequestStrategy diff --git a/wire-ios-request-strategy/WireRequestStrategy.xcodeproj/project.pbxproj b/wire-ios-request-strategy/WireRequestStrategy.xcodeproj/project.pbxproj index f4a35f216d0..3197b1744cd 100644 --- a/wire-ios-request-strategy/WireRequestStrategy.xcodeproj/project.pbxproj +++ b/wire-ios-request-strategy/WireRequestStrategy.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 598D04302C89C67E00B64D71 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 598D042F2C89C67E00B64D71 /* WireFoundation */; }; 598D04332C89C6CF00B64D71 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 598D04322C89C6CF00B64D71 /* WireFoundation */; }; 598E870D2BF4E08100FC5438 /* WireUtilitiesSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 598E870C2BF4E08100FC5438 /* WireUtilitiesSupport.framework */; }; + 59D398D22D5525DB001C9C5F /* WireTestingPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59D398D12D5525DB001C9C5F /* WireTestingPackage */; }; 5E68F22722452CDC00298376 /* LinkPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E68F22622452CDC00298376 /* LinkPreprocessor.swift */; }; 5E9EA4DE2243C10400D401B2 /* LinkAttachmentsPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9EA4DD2243C10400D401B2 /* LinkAttachmentsPreprocessor.swift */; }; 5E9EA4E02243C6B200D401B2 /* LinkAttachmentsPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9EA4DF2243C6B200D401B2 /* LinkAttachmentsPreprocessorTests.swift */; }; @@ -980,6 +981,7 @@ BF7D9BE11D8C351900949267 /* WireRequestStrategy.framework in Frameworks */, CB7979052C73663B006FBA58 /* WireRequestStrategySupport.framework in Frameworks */, 598D04302C89C67E00B64D71 /* WireFoundation in Frameworks */, + 59D398D22D5525DB001C9C5F /* WireTestingPackage in Frameworks */, 598E870D2BF4E08100FC5438 /* WireUtilitiesSupport.framework in Frameworks */, 59537D872CFF9DF600920B59 /* WireLogging in Frameworks */, CB5120532C6FD69F000C8FEC /* WireTransportSupport.framework in Frameworks */, @@ -2248,6 +2250,7 @@ packageProductDependencies = ( 598D042F2C89C67E00B64D71 /* WireFoundation */, 59537D862CFF9DF600920B59 /* WireLogging */, + 59D398D12D5525DB001C9C5F /* WireTestingPackage */, ); productName = WireRequestStrategyTests; productReference = 166901741D707509000FE4AF /* WireRequestStrategyTests.xctest */; @@ -3237,6 +3240,10 @@ isa = XCSwiftPackageProductDependency; productName = WireFoundation; }; + 59D398D12D5525DB001C9C5F /* WireTestingPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = WireTestingPackage; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 166901611D707509000FE4AF /* Project object */; diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift index a893542d483..98e50eb3bfc 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift @@ -17,11 +17,8 @@ // import Foundation -import WireAnalytics import WireCrypto -import WireDataModel -import WireLogging -import WireUtilities +import WireDomainPkg import ZipArchive extension SessionManager { @@ -30,16 +27,6 @@ extension SessionManager { // MARK: - Export - public enum BackupError: Error { - case notAuthenticated - case noActiveAccount - case compressionError - case invalidFileExtension - case keyCreationFailed - case decryptionError - case unknown - } - public func backupActiveAccount(password: String, completion: @escaping (Result) -> Void) { guard let userId = accountManager.selectedAccount?.userIdentifier, @@ -47,7 +34,7 @@ extension SessionManager { let handle = activeUserSession.flatMap(ZMUser.selfUser)?.handle, let activeUserSession else { - return completion(.failure(BackupError.noActiveAccount)) + return completion(.failure(CreateLegacyBackupError.noActiveAccountForExport)) } CoreDataStack.backupLocalStorage( @@ -105,101 +92,17 @@ extension SessionManager { } } - // MARK: - Import - - // TODO: [WPB-14616] delete import related code when the restore button from the authentication flow is removed - - /// Restores the account database from the Wire iOS database back up file. - /// @param completion called when the restoration is ended. If success, Result.success with the new restored account - /// is called. - public func restoreFromBackup( - at location: URL, - password: String, - completion: @escaping (Result) -> Void - ) { - func complete(_ result: Result) { - DispatchQueue.main.async(group: dispatchGroup) { - completion(result) - } - } - - guard - let status = unauthenticatedSession?.authenticationStatus, - let userId = status.authenticatedUserIdentifier - else { - return completion(.failure(BackupError.notAuthenticated)) - } - - // Verify the imported file has the correct file extension. - guard BackupFileExtensions.allCases.contains(where: { - $0.rawValue == location.pathExtension - }) else { - return completion(.failure(BackupError.invalidFileExtension)) - } - - SessionManager.workerQueue.async(group: dispatchGroup) { [weak self] in - guard let self else { - completion(.failure(NSError( - userSessionErrorCode: .unknownError, - userInfo: ["reason": "SessionManager.self is `nil` in restoreFromBackup"] - ))) - return - } - - let decryptedURL = SessionManager.temporaryURL(for: location) - - WireLogger.localStorage.debug("coordinated file access at: \(location.absoluteString)") - - do { - try SessionManager.decrypt( - from: location, - to: decryptedURL, - password: password, - accountId: userId - ) - } catch ChaCha20Poly1305.StreamEncryption.EncryptionError.decryptionFailed { - return complete(.failure(BackupError.decryptionError)) - - } catch ChaCha20Poly1305.StreamEncryption.EncryptionError.keyGenerationFailed { - return complete(.failure(BackupError.keyCreationFailed)) - - } catch { - return complete(.failure(error)) - } - - let url = SessionManager.unzippedBackupURL(for: location) - - guard decryptedURL.unzip(to: url) else { - return complete(.failure(BackupError.compressionError)) - } - - CoreDataStack.importLocalStorage( - accountIdentifier: userId, - from: url, - applicationContainer: sharedContainerURL, - dispatchGroup: dispatchGroup - ) { result in - completion(result.map { _ in }) - } - } - } - - // MARK: - Encryption & Decryption + // MARK: - Encryption static func encrypt(from input: URL, to output: URL, password: String, accountId: UUID) throws { - guard let inputStream = InputStream(url: input) else { throw BackupError.unknown } - guard let outputStream = OutputStream(url: output, append: false) else { throw BackupError.unknown } + guard let inputStream = InputStream(url: input) + else { throw CreateLegacyBackupError.failedToCreateStreamsForEncryption } + guard let outputStream = OutputStream(url: output, append: false) + else { throw CreateLegacyBackupError.failedToCreateStreamsForEncryption } let passphrase = ChaCha20Poly1305.StreamEncryption.Passphrase(password: password, uuid: accountId) try ChaCha20Poly1305.StreamEncryption.encrypt(input: inputStream, output: outputStream, passphrase: passphrase) } - static func decrypt(from input: URL, to output: URL, password: String, accountId: UUID) throws { - guard let inputStream = InputStream(url: input) else { throw BackupError.unknown } - guard let outputStream = OutputStream(url: output, append: false) else { throw BackupError.unknown } - let passphrase = ChaCha20Poly1305.StreamEncryption.Passphrase(password: password, uuid: accountId) - try ChaCha20Poly1305.StreamEncryption.decrypt(input: inputStream, output: outputStream, passphrase: passphrase) - } - // MARK: - Helper /// Deletes all previously exported and imported backups. @@ -207,16 +110,9 @@ extension SessionManager { CoreDataStack.clearBackupDirectory(dispatchGroup: dispatchGroup) } - // MARK: - Static Helpers - - private static func unzippedBackupURL(for url: URL) -> URL { - let filename = url.deletingPathExtension().lastPathComponent - return CoreDataStack.importsDirectory.appendingPathComponent(filename) - } - private static func compress(backup: CoreDataStack.BackupInfo) throws -> URL { let url = temporaryURL(for: backup.url) - guard backup.url.zipDirectory(to: url) else { throw BackupError.compressionError } + guard backup.url.zipDirectory(to: url) else { throw CreateLegacyBackupError.compressionError } return url } @@ -262,8 +158,4 @@ private extension URL { func zipDirectory(to url: URL) -> Bool { SSZipArchive.createZipFile(atPath: url.path, withContentsOfDirectory: path) } - - func unzip(to url: URL) -> Bool { - SSZipArchive.unzipFile(atPath: path, toDestination: url.path) - } } diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift index 72b780380d6..2fe50c97a8e 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift @@ -747,7 +747,8 @@ public final class SessionManager: NSObject, SessionManagerType { account, from: selectedAccount, userSessionCanBeTornDown: { [weak self] in - self?.tearDownActiveSession(completion: tearDownCompletion) + self?.activeUserSession = nil + tearDownCompletion?() guard let self else { completion?(nil) return @@ -1007,13 +1008,28 @@ public final class SessionManager: NSObject, SessionManagerType { delegate?.sessionManagerAsksToRetryStart() } - // TODO: [WPB-14616] use this method for restoring a backup from the settings /// The active user session will be torn down and the app goes into migration state. public func prepareForRestoreWithMigration(completion: @escaping () -> Void) { - guard let delegate else { return completion() } + guard let delegate else { + WireLogger.sessionManager.debug("SessionManager.delegate is nil, aborting migration preparation") + return completion() + } + + WireLogger.sessionManager.debug("SessionManager.delegate.sessionManagerWillMigrateAccount ...") + delegate.sessionManagerWillMigrateAccount { [self] in - delegate.sessionManagerWillMigrateAccount { - self.tearDownActiveSession(completion: completion) + WireLogger.sessionManager.debug("... userSessionCanBeTornDown { ... }") + + if let accountID = activeUserSession?.account.userIdentifier { + tearDownBackgroundSession(for: accountID) { [self] in + activeUserSession = nil + accountTokens.removeValue(forKey: accountID) + completion() + } + } else { + activeUserSession = nil + completion() + } } } @@ -1192,11 +1208,6 @@ public final class SessionManager: NSObject, SessionManagerType { } } - private func tearDownActiveSession(completion: (() -> Void)?) { - activeUserSession = nil - completion?() - } - // Creates the user session for @c account given, calls @c completion when done. private func startBackgroundSession( for account: Account, diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupFileArchiver.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupFileArchiver.swift index 4bd16a55196..904d8c8ff09 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupFileArchiver.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupFileArchiver.swift @@ -16,6 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import WireDomainPkg import ZipArchive struct ImportBackupFileArchiver: ImportBackupFileArchiverProtocol { @@ -28,7 +29,7 @@ struct ImportBackupFileArchiver: ImportBackupFileArchiverProtocol { ) guard success else { - throw BackupRestoreError.compressionError + throw ImportBackupError.compressionError } } diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupStreamDecryptor.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupStreamDecryptor.swift index c54f868ee69..82e4ca092c4 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupStreamDecryptor.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupStreamDecryptor.swift @@ -18,6 +18,7 @@ import Foundation import WireCrypto +import WireDomainPkg struct ImportBackupStreamDecryptor: ImportBackupStreamDecryptorProtocol { @@ -42,10 +43,14 @@ struct ImportBackupStreamDecryptor: ImportBackupStreamDecryptorProtocol { ) } catch ChaCha20Poly1305.StreamEncryption.EncryptionError.decryptionFailed { - throw BackupRestoreError.decryptionError + if password.isEmpty { + throw ImportBackupError.passwordRequired + } else { + throw ImportBackupError.decryptionError + } } catch ChaCha20Poly1305.StreamEncryption.EncryptionError.keyGenerationFailed { - throw BackupRestoreError.keyCreationFailed + throw ImportBackupError.keyCreationFailed } } diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupUseCase.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupUseCase.swift index 64fc145bff4..76ce318be15 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/ImportBackupUseCase.swift @@ -19,13 +19,13 @@ import Foundation import WireCrypto import WireDataModel +import WireDomainPkg import WireLogging import WireSystem -import ZipArchive struct ImportBackupUseCase: ImportBackupUseCaseProtocol { - let userSession: () -> UserSession? + let userSession: @Sendable () -> UserSession? let dispatchGroup: ZMSDispatchGroup let streamDecryptor: ImportBackupStreamDecryptorProtocol let fileArchiver: ImportBackupFileArchiverProtocol @@ -35,86 +35,121 @@ struct ImportBackupUseCase: ImportBackupUseCaseProtocol { let sharedContainerURL: URL let logger: WireLogger - func invoke(url: URL, password: String) async throws { + func invoke(url: URL, password: String) -> AsyncThrowingStream { switch BackupFileExtensions(rawValue: url.pathExtension.lowercased()) { case .fileExtensionWithUnderscore, .fileExtensionWithHyphen: - try await importIOSBackup(url, password) + importIOSBackup(url, password) case nil: - throw BackupRestoreError.invalidFileExtension + AsyncThrowingStream { continuation in + continuation.finish(throwing: ImportBackupError.invalidFileExtension) + } } } - private func importIOSBackup(_ url: URL, _ password: String) async throws { - - // to start with we need an active user session, later the session will be torn down - weak var userSession = userSession() - guard let account = userSession?.contextProvider.account else { - throw BackupRestoreError.noActiveAccount - } - - // before we start the first operation let the user know, the progress has started - appStateUpdater.reportImportProgress(progress: 0.25) - - let unzippedURL = try decryptAndUnzipBackup( - url: url, - password: password, - accountID: account.userIdentifier - ) - - appStateUpdater.reportImportProgress(progress: 0.5) - - // backup the self user and the self client - let selfUserQualifiedID: QualifiedID? - let selfClientBackup: [String: Any] - // we want to avoid keeping a strong reference to the user - // session, the managed object context and the user client - if let userSession, let (qualifiedID, backup) = await userSession.contextProvider.viewContext.perform({ - userSession.selfUserClient.map { ($0.user?.qualifiedID, $0.backup()) } }) { - selfUserQualifiedID = qualifiedID - selfClientBackup = backup - } else { - throw BackupRestoreError.unknown - } - - // user session needs to be torn down - await appStateUpdater.reportMigrationNeeded() - - // the imported file replaces the existing persistent store - try await entityStorage.replacePersistentStore( - accountIdentifier: account.userIdentifier, - from: unzippedURL, - applicationContainer: sharedContainerURL, - dispatchGroup: dispatchGroup - ) - - // import the self client from the backup and set the correct self user relation - // TODO: [WPB-15714] causes warning: we should try to initialize the model only once - let temporaryStack = try await entityStorage - .createContextProvider( - account: account, - applicationContainer: sharedContainerURL, - dispatchGroup: dispatchGroup - ) - try await temporaryStack.viewContext.perform { - let context = temporaryStack.viewContext - let userID = selfUserQualifiedID?.uuid - let domain = selfUserQualifiedID?.domain - - var selfUser: ZMUser? - if let userID { - selfUser = ZMUser.fetch(with: userID, domain: domain, in: context) + private func importIOSBackup( + _ url: URL, + _ password: String + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = Task { @MainActor in + do { + + // to start with we need an active user session, later the session will be torn down + guard let account = userSession()?.contextProvider.account else { + throw ImportBackupError.noActiveAccountForImport + } + + // before we start the first operation let the user know, the progress has started + continuation.yield(.progress(0.25)) + + let unzippedURL = try decryptAndUnzipBackup( + url: url, + password: password, + accountID: account.userIdentifier + ) + + continuation.yield(.progress(0.5)) + + logger.debug("creating backup of user client") + + // backup the self user and the self client + let selfUserQualifiedID: QualifiedID? + let selfClientBackup: [String: Any] + // we want to avoid keeping a strong reference to the user + // session, the managed object context and the user client + if let userSession = userSession(), + let (qualifiedID, backup) = await userSession.contextProvider.viewContext.perform({ + userSession.selfUserClient.map { ($0.user?.qualifiedID, $0.backup()) } }) { + selfUserQualifiedID = qualifiedID + selfClientBackup = backup + } else { + throw ImportBackupError.faildToBackUpUserClient + } + + logger.debug("reporting migration required") + + // user session needs to be torn down + await appStateUpdater.reportMigrationNeeded() + + logger.debug("replacing persistent store") + + // the imported file replaces the existing persistent store + try await entityStorage.replacePersistentStore( + accountIdentifier: account.userIdentifier, + from: unzippedURL, + applicationContainer: sharedContainerURL, + dispatchGroup: dispatchGroup + ) + + logger.debug("opening a temporary context") + + // import the self client from the backup and set the correct self user relation + // TODO: [WPB-15714] causes warning: we should try to initialize the model only once + let temporaryStack = try await entityStorage + .createContextProvider( + account: account, + applicationContainer: sharedContainerURL, + dispatchGroup: dispatchGroup + ) + + logger.debug("restoring backup of userclient") + + try await temporaryStack.viewContext.perform { + let context = temporaryStack.viewContext + let userID = selfUserQualifiedID?.uuid + let domain = selfUserQualifiedID?.domain + + var selfUser: ZMUser? + if let userID { + selfUser = ZMUser.fetch(with: userID, domain: domain, in: context) + } + + let userClient = UserClient.restore(from: selfClientBackup, context: context) + userClient.user = selfUser + userClient.markAsSelfClient() + try context.save() + } + + logger.debug("select account and start the main UI again") + + await appStateUpdater.selectAccountAndTriggerSlowSync(account) + + logger.debug("done") + + continuation.yield(.done) + continuation.finish() + + } catch { + continuation.finish(throwing: error) + } + } + continuation.onTermination = { _ in + task.cancel() } - - let userClient = UserClient.restore(from: selfClientBackup, context: context) - userClient.user = selfUser - userClient.markAsSelfClient() - try context.save() } - - await appStateUpdater.selectAccountAndTriggerSlowSync(account) } private func decryptAndUnzipBackup(url: URL, password: String, accountID: UUID) throws -> URL { @@ -126,7 +161,7 @@ struct ImportBackupUseCase: ImportBackupUseCaseProtocol { guard let inputStream = InputStream(url: url), let outputStream = OutputStream(url: decryptedURL, append: false) - else { throw BackupRestoreError.unknown } + else { throw ImportBackupError.failedToCreateStreamForDecryption } try streamDecryptor.decrypt( input: inputStream, diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupAppStateUpdaterProtocol.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupAppStateUpdaterProtocol.swift index 7704409b8bc..1b6b550964d 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupAppStateUpdaterProtocol.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupAppStateUpdaterProtocol.swift @@ -17,14 +17,11 @@ // // sourcery: AutoMockable -public protocol ImportBackupAppStateUpdaterProtocol { - - /// Inform the user about the current progress (percentage). - /// - Parameter progress: A value between 0.0 and 1.0. - func reportImportProgress(progress: Float) +public protocol ImportBackupAppStateUpdaterProtocol: Sendable { /// The user session needs to be unloaded. func reportMigrationNeeded() async func selectAccountAndTriggerSlowSync(_ account: Account) async + } diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupEntityStorageProtocol.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupEntityStorageProtocol.swift index 0e231d801ab..c680457ecc0 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupEntityStorageProtocol.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupEntityStorageProtocol.swift @@ -19,7 +19,7 @@ import Foundation // sourcery: AutoMockable -public protocol ImportBackupEntityStorageProtocol { +public protocol ImportBackupEntityStorageProtocol: Sendable { var importsDirectory: URL { get } diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupFileArchiverProtocol.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupFileArchiverProtocol.swift index 6f7a9af6562..7d181361c34 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupFileArchiverProtocol.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupFileArchiverProtocol.swift @@ -19,7 +19,7 @@ import Foundation // sourcery: AutoMockable -protocol ImportBackupFileArchiverProtocol { +protocol ImportBackupFileArchiverProtocol: Sendable { func unzipFile( at sourceURL: URL, diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupStreamDecryptorProtocol.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupStreamDecryptorProtocol.swift index 4366a9a3589..9f80033cd92 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupStreamDecryptorProtocol.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/Protocols/ImportBackupStreamDecryptorProtocol.swift @@ -19,7 +19,7 @@ import Foundation // sourcery: AutoMockable -public protocol ImportBackupStreamDecryptorProtocol { +public protocol ImportBackupStreamDecryptorProtocol: Sendable { func decrypt( input: InputStream, diff --git a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/SessionManager+importBackupUseCase.swift b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/SessionManager+importBackupUseCase.swift index 9a26846926f..81dadcb9ed7 100644 --- a/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/SessionManager+importBackupUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/ImportBackupUseCase/SessionManager+importBackupUseCase.swift @@ -16,9 +16,11 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import WireDomainPkg + public extension SessionManager { - func importBackupUseCase(appStateUpdater: ImportBackupAppStateUpdaterProtocol) -> ImportBackupUseCaseProtocol? { + var importBackupUseCase: ImportBackupUseCaseProtocol? { // return `nil` immediately if there is no active user session activeUserSession.map { _ in @@ -29,10 +31,33 @@ public extension SessionManager { streamDecryptor: ImportBackupStreamDecryptor(), fileArchiver: ImportBackupFileArchiver(), entityStorage: ImportBackupEntityStorage(), - appStateUpdater: appStateUpdater, + appStateUpdater: ImportBackupAppStateUpdater(sessionManager: self), sharedContainerURL: sharedContainerURL, logger: .localStorage ) } } } + +private struct ImportBackupAppStateUpdater: ImportBackupAppStateUpdaterProtocol { + + let sessionManager: SessionManager + + @MainActor + func reportMigrationNeeded() async { + await withCheckedContinuation { continuation in + sessionManager.prepareForRestoreWithMigration(completion: continuation.resume) + } + } + + @MainActor + func selectAccountAndTriggerSlowSync(_ account: Account) async { + let userSession = await withCheckedContinuation { continuation in + sessionManager.select(account, completion: { continuation.resume(returning: $0) }) + } + guard let userSession else { return } + userSession.syncManagedObjectContext.performGroupedBlock { + userSession.syncStatus.forceSlowSync() + } + } +} diff --git a/wire-ios-sync-engine/Source/UserSession/UserSession.swift b/wire-ios-sync-engine/Source/UserSession/UserSession.swift index 018d22e3e1f..7b72dedfa52 100644 --- a/wire-ios-sync-engine/Source/UserSession/UserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/UserSession.swift @@ -27,6 +27,8 @@ public protocol UserSession: AnyObject { // MARK: - Mixed properties and methods + var isTornDown: Bool { get } + // swiftlint:disable:next todo_requires_jira_link // TODO: structure mixed methods and properties in sections diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift index 7b8977d27ce..7fb5838ef93 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift @@ -40,7 +40,7 @@ public final class ZMUserSession: NSObject { private let appVersion: String private var tokens: [Any] = [] - private var tornDown: Bool = false + public private(set) var isTornDown = false private(set) var isNetworkOnline = true @@ -532,11 +532,11 @@ public final class ZMUserSession: NSObject { // MARK: - Deinitalize deinit { - require(tornDown, "tearDown must be called before the ZMUserSession is deallocated") + require(isTornDown, "tearDown must be called before the ZMUserSession is deallocated") } public func tearDown() { - guard !tornDown else { return } + guard !isTornDown else { return } tearDownMLSGroupVerification() @@ -556,7 +556,7 @@ public final class ZMUserSession: NSObject { NotificationCenter.default.removeObserver(self) WireLogger.authentication.addTag(.selfClientId, value: nil) - tornDown = true + isTornDown = true } // MARK: - Methods diff --git a/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.generated.swift b/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.generated.swift index 6b47868789d..5d83da9fbc0 100644 --- a/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.generated.swift +++ b/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.generated.swift @@ -386,21 +386,6 @@ public class MockImportBackupAppStateUpdaterProtocol: ImportBackupAppStateUpdate public init() {} - // MARK: - reportImportProgress - - public var reportImportProgressProgress_Invocations: [Float] = [] - public var reportImportProgressProgress_MockMethod: ((Float) -> Void)? - - public func reportImportProgress(progress: Float) { - reportImportProgressProgress_Invocations.append(progress) - - guard let mock = reportImportProgressProgress_MockMethod else { - fatalError("no mock for `reportImportProgressProgress`") - } - - mock(progress) - } - // MARK: - reportMigrationNeeded public var reportMigrationNeeded_Invocations: [Void] = [] @@ -555,35 +540,6 @@ public class MockImportBackupStreamDecryptorProtocol: ImportBackupStreamDecrypto } -public class MockImportBackupUseCaseProtocol: ImportBackupUseCaseProtocol { - - // MARK: - Life cycle - - public init() {} - - - // MARK: - invoke - - public var invokeUrlPassword_Invocations: [(url: URL, password: String)] = [] - public var invokeUrlPassword_MockError: Error? - public var invokeUrlPassword_MockMethod: ((URL, String) async throws -> Void)? - - public func invoke(url: URL, password: String) async throws { - invokeUrlPassword_Invocations.append((url: url, password: password)) - - if let error = invokeUrlPassword_MockError { - throw error - } - - guard let mock = invokeUrlPassword_MockMethod else { - fatalError("no mock for `invokeUrlPassword`") - } - - try await mock(url, password) - } - -} - public class MockIsE2EICertificateEnrollmentRequiredProtocol: IsE2EICertificateEnrollmentRequiredProtocol { // MARK: - Life cycle diff --git a/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.manual.swift b/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.manual.swift index 82e884e014b..7f8c4f829c7 100644 --- a/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.manual.swift +++ b/wire-ios-sync-engine/Support/Sourcery/generated/AutoMockable.manual.swift @@ -267,6 +267,15 @@ public class MockUserSession: UserSession { public var underlyingUserProfile: UserProfile! + // MARK: - isTornDown + + public var isTornDown: Bool { + get { return underlyingIsTornDown } + set(value) { underlyingIsTornDown = value } + } + + public var underlyingIsTornDown: Bool! + // MARK: - lock public var lock: SessionLock? diff --git a/wire-ios-sync-engine/Tests/Source/Calling/MLSConferenceStaleParticipantsRemoverTests.swift b/wire-ios-sync-engine/Tests/Source/Calling/MLSConferenceStaleParticipantsRemoverTests.swift index 6b8459d0796..9d45a33a755 100644 --- a/wire-ios-sync-engine/Tests/Source/Calling/MLSConferenceStaleParticipantsRemoverTests.swift +++ b/wire-ios-sync-engine/Tests/Source/Calling/MLSConferenceStaleParticipantsRemoverTests.swift @@ -19,6 +19,7 @@ import Foundation import WireDataModelSupport import WireTesting +import WireTestingPackage import XCTest @testable import WireSyncEngine diff --git a/wire-ios-sync-engine/Tests/Source/Use cases/ImportBackupUseCaseTests.swift b/wire-ios-sync-engine/Tests/Source/Use cases/ImportBackupUseCaseTests.swift index 276430b05c9..83bec41cea0 100644 --- a/wire-ios-sync-engine/Tests/Source/Use cases/ImportBackupUseCaseTests.swift +++ b/wire-ios-sync-engine/Tests/Source/Use cases/ImportBackupUseCaseTests.swift @@ -17,6 +17,7 @@ // import WireDataModelSupport +import WireDomainPkg import XCTest @testable import WireSyncEngine @@ -72,7 +73,6 @@ final class ImportBackupUseCaseTests: XCTestCase { } mockAppStateUpdater = .init() - mockAppStateUpdater.reportImportProgressProgress_MockMethod = { _ in } mockAppStateUpdater.reportMigrationNeeded_MockMethod = { // This closure is called when the user session should be torn down and the core data stack closed. self.coreDataStack = nil @@ -136,15 +136,16 @@ final class ImportBackupUseCaseTests: XCTestCase { func testFileExtensionsAreAccepted() async throws { // Given let extensions = ["ios_Wbu", "ioS-wbu"] - mockUserSession = nil // expect `BackupRestoreError.noActiveAccount` + // produce another error which is thrown after the file extension check + mockUserSession = nil // expect `BackupRestoreError.noActiveAccount` (but not `.invalidFileExtension`) for extensions in extensions { do { // When let filePath = "/path/to/file.\(extensions)" - try await sut.invoke(url: URL(fileURLWithPath: filePath), password: "") + for try await _ in sut.invoke(url: URL(fileURLWithPath: filePath), password: "") {} XCTFail("Unexpected success") - } catch BackupRestoreError.noActiveAccount { + } catch ImportBackupError.noActiveAccountForImport { // Then } } @@ -159,9 +160,9 @@ final class ImportBackupUseCaseTests: XCTestCase { do { // When let filePath = "/path/to/file.\(extensions)" - try await sut.invoke(url: URL(fileURLWithPath: filePath), password: "") + for try await _ in sut.invoke(url: URL(fileURLWithPath: filePath), password: "") {} XCTFail("Unexpected success") - } catch BackupRestoreError.invalidFileExtension { + } catch ImportBackupError.invalidFileExtension { // Then } } @@ -173,10 +174,11 @@ final class ImportBackupUseCaseTests: XCTestCase { let accountID = coreDataStack.account.userIdentifier // When - try await sut.invoke(url: url, password: "c<%I2f41\"6!'") + let sequence = try await sut.invoke(url: url, password: "c<%I2f41\"6!'") + .reduce(into: [ImportBackupProgress]()) { $0 += [$1] } // Then - XCTAssertFalse(mockAppStateUpdater.reportImportProgressProgress_Invocations.isEmpty) + XCTAssertEqual(sequence, [.progress(0.25), .progress(0.5), .done]) XCTAssertEqual(mockStreamDecryptor.decryptInputOutputAccountIDPassword_Invocations.first?.accountID, accountID) XCTAssertEqual( mockStreamDecryptor.decryptInputOutputAccountIDPassword_Invocations.first?.password, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/OperationStatusTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/OperationStatusTests.swift index 16a3daf590c..26079ff42aa 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/OperationStatusTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/OperationStatusTests.swift @@ -17,6 +17,7 @@ // import Foundation +import WireTestingPackage @testable import WireSyncEngine diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests.swift index ace1cdfe7af..e43f7fd93a1 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests.swift @@ -20,6 +20,7 @@ import Foundation import WireDataModelSupport import WireSyncEngine import WireTesting +import WireTestingPackage @testable import WireSyncEngineSupport diff --git a/wire-ios-sync-engine/WireSyncEngine.xcodeproj/project.pbxproj b/wire-ios-sync-engine/WireSyncEngine.xcodeproj/project.pbxproj index af453013404..da21404ed2a 100644 --- a/wire-ios-sync-engine/WireSyncEngine.xcodeproj/project.pbxproj +++ b/wire-ios-sync-engine/WireSyncEngine.xcodeproj/project.pbxproj @@ -248,9 +248,12 @@ 591B6E0C2C8B091A009F8A7B /* WireSyncEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549815931A43232400A7CE2E /* WireSyncEngine.framework */; }; 591B6E102C8B0926009F8A7B /* WireMockTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE668BB92954AA9300D939E7 /* WireMockTransport.framework */; }; 591B6E132C8B092B009F8A7B /* WireDataModelSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59D1C30F2B1DEE6E0016F6B2 /* WireDataModelSupport.framework */; }; + 59202AD42D54D49400143413 /* WireDomainPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59202AD32D54D49400143413 /* WireDomainPackage */; }; 59271BE82B908DAC0019B726 /* SecurityClassificationProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59271BE72B908DAC0019B726 /* SecurityClassificationProviding.swift */; }; 59271BEA2B908E150019B726 /* SecurityClassification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59271BE92B908E150019B726 /* SecurityClassification.swift */; }; 5943E9BC2D11CB3B00D39FFF /* CallEndedReason+initWithCallClosedReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5943E9BB2D11CB3300D39FFF /* CallEndedReason+initWithCallClosedReason.swift */; }; + 5946F37E2D53DC550039C059 /* WireTestingPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 5946F37D2D53DC550039C059 /* WireTestingPackage */; }; + 594C0FCE2D541643003D8183 /* WireDomainPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 594C0FCD2D541643003D8183 /* WireDomainPackage */; }; 59537D892CFF9E7700920B59 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59537D882CFF9E7700920B59 /* WireLogging */; }; 597B70C32B03984C006C2121 /* ZMUserSession+DeveloperMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597B70C22B03984C006C2121 /* ZMUserSession+DeveloperMenu.swift */; }; 5989A70E2D3FD3190081D811 /* WireAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 5989A70D2D3FD3190081D811 /* WireAnalytics */; }; @@ -1267,9 +1270,11 @@ 0155194E2C20D29400037358 /* WireDomainSupport.framework in Frameworks */, 5996E8922C19CB28007A52F0 /* WireSystemSupport.framework in Frameworks */, 59FFADA02C8229BB000C8085 /* WireTransportSupport.framework in Frameworks */, + 5946F37E2D53DC550039C059 /* WireTestingPackage in Frameworks */, 5996E8952C19CB36007A52F0 /* WireUtilitiesSupport.framework in Frameworks */, 01A532512CCA218A005FD421 /* WireAnalyticsSupport in Frameworks */, CB4895532C4FB77C00CA2C25 /* WireTesting.framework in Frameworks */, + 59202AD42D54D49400143413 /* WireDomainPackage in Frameworks */, 0145AE902B1155760097E3B8 /* WireSyncEngineSupport.framework in Frameworks */, 598D04392C89C70500B64D71 /* WireFoundation in Frameworks */, 591B6E132C8B092B009F8A7B /* WireDataModelSupport.framework in Frameworks */, @@ -1291,6 +1296,7 @@ EE67F6C8296F0622001D7C88 /* libPhoneNumberiOS.xcframework in Frameworks */, 59537D892CFF9E7700920B59 /* WireLogging in Frameworks */, 01F0F8922CCA2B2C00FE4170 /* avs.xcframework in Frameworks */, + 594C0FCE2D541643003D8183 /* WireDomainPackage in Frameworks */, EE67F6CA296F0622001D7C88 /* ZipArchive.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2614,6 +2620,8 @@ 598D04382C89C70500B64D71 /* WireFoundation */, 01A532502CCA218A005FD421 /* WireAnalyticsSupport */, 017F32C82D03AC3000471B3D /* WireAPI */, + 5946F37D2D53DC550039C059 /* WireTestingPackage */, + 59202AD32D54D49400143413 /* WireDomainPackage */, ); productName = "WireSyncEngine-iOS-Tests"; productReference = 3E1860C3191A649D000FE027 /* UnitTests.xctest */; @@ -2644,6 +2652,7 @@ E9C60E912C259F3C004E5F13 /* WireAnalytics */, 59537D882CFF9E7700920B59 /* WireLogging */, CBD35F292D09EBA50080DA37 /* WireCrypto */, + 594C0FCD2D541643003D8183 /* WireDomainPackage */, ); productName = "WireSyncEngine-ios"; productReference = 549815931A43232400A7CE2E /* WireSyncEngine.framework */; @@ -3762,6 +3771,18 @@ isa = XCSwiftPackageProductDependency; productName = WireAnalyticsSupport; }; + 59202AD32D54D49400143413 /* WireDomainPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = WireDomainPackage; + }; + 5946F37D2D53DC550039C059 /* WireTestingPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = WireTestingPackage; + }; + 594C0FCD2D541643003D8183 /* WireDomainPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = WireDomainPackage; + }; 59537D882CFF9E7700920B59 /* WireLogging */ = { isa = XCSwiftPackageProductDependency; productName = WireLogging; diff --git a/wire-ios-system/Source/ZMSDispatchGroup.swift b/wire-ios-system/Source/ZMSDispatchGroup.swift index e84f9ed82e1..d24d2030bb0 100644 --- a/wire-ios-system/Source/ZMSDispatchGroup.swift +++ b/wire-ios-system/Source/ZMSDispatchGroup.swift @@ -19,7 +19,7 @@ import Foundation @objc(ZMSDispatchGroup) @objcMembers -public final class ZMSDispatchGroup: NSObject { +public final class ZMSDispatchGroup: NSObject, Sendable { let label: String diff --git a/wire-ios/Tests/Mocks/UserSessionMock.swift b/wire-ios/Tests/Mocks/UserSessionMock.swift index 25e086239df..4871af06e2c 100644 --- a/wire-ios/Tests/Mocks/UserSessionMock.swift +++ b/wire-ios/Tests/Mocks/UserSessionMock.swift @@ -29,6 +29,8 @@ import WireSyncEngineSupport final class UserSessionMock: UserSession { + var isTornDown = false + var userProfile: UserProfile var lastE2EIUpdateDateRepository: LastE2EIdentityUpdateDateRepositoryInterface? diff --git a/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift b/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift index f3aac87ee63..c609fab97ac 100644 --- a/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift +++ b/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift @@ -193,44 +193,6 @@ class MockAppStateCalculatorDelegate: AppStateCalculatorDelegate { } -class MockBackupSource: BackupSource { - - // MARK: - Life cycle - - - - // MARK: - backupActiveAccount - - var backupActiveAccountPasswordCompletion_Invocations: [(password: String, completion: (Result) -> Void)] = [] - var backupActiveAccountPasswordCompletion_MockMethod: ((String, @escaping (Result) -> Void) -> Void)? - - func backupActiveAccount(password: String, completion: @escaping (Result) -> Void) { - backupActiveAccountPasswordCompletion_Invocations.append((password: password, completion: completion)) - - guard let mock = backupActiveAccountPasswordCompletion_MockMethod else { - fatalError("no mock for `backupActiveAccountPasswordCompletion`") - } - - mock(password, completion) - } - - // MARK: - clearPreviousBackups - - var clearPreviousBackups_Invocations: [Void] = [] - var clearPreviousBackups_MockMethod: (() -> Void)? - - func clearPreviousBackups() { - clearPreviousBackups_Invocations.append(()) - - guard let mock = clearPreviousBackups_MockMethod else { - fatalError("no mock for `clearPreviousBackups`") - } - - mock() - } - -} - class MockCallQualityRouterProtocol: CallQualityRouterProtocol { // MARK: - Life cycle diff --git a/wire-ios/Wire-iOS Tests/BackupPasswordViewControllerTests.swift b/wire-ios/Wire-iOS Tests/BackupPasswordViewControllerTests.swift deleted file mode 100644 index 4df2d1cb4ab..00000000000 --- a/wire-ios/Wire-iOS Tests/BackupPasswordViewControllerTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import WireTestingPackage -import XCTest - -@testable import Wire - -final class BackupPasswordViewControllerTests: XCTestCase { - - private var snapshotHelper: SnapshotHelper! - - override func setUp() { - super.setUp() - snapshotHelper = SnapshotHelper() - } - - override func tearDown() { - snapshotHelper = nil - super.tearDown() - } - - func testDefaultState() { - // GIVEN - let sut = makeViewController() - - // WHEN & THEN - snapshotHelper.verify(matching: sut.view) - } - - func testThatItCallsTheCallback() { - // GIVEN - let validPassword = "Password123!" - let expectation = expectation(description: "Callback called") - let sut = makeViewController() - sut.onCompletion = { password in - XCTAssertEqual(password, validPassword) - expectation.fulfill() - } - - // WHEN - XCTAssertTrue( - sut.textField( - UITextField(), - shouldChangeCharactersIn: NSRange(location: 0, length: 0), - replacementString: validPassword - ) - ) - XCTAssertFalse( - sut.textField( - UITextField(), - shouldChangeCharactersIn: NSRange(location: 0, length: 0), - replacementString: "\n" - ) - ) - - // THEN - waitForExpectations(timeout: 2) { error in - XCTAssertNil(error) - } - } - - func testThatWhitespacesPasswordIsNotGood() { - // GIVEN - let sut = makeViewController() - sut.onCompletion = { _ in - XCTFail("Sut is nil") - } - - // WHEN - XCTAssertFalse(sut.textField( - UITextField(), - shouldChangeCharactersIn: NSRange(location: 0, length: 0), - replacementString: " " - )) - XCTAssertFalse(sut.textField( - UITextField(), - shouldChangeCharactersIn: NSRange(location: 0, length: 0), - replacementString: "\n" - )) - } - - // MARK: - Helpers - - private func makeViewController() -> BackupPasswordViewController { - BackupPasswordViewController() - } -} diff --git a/wire-ios/Wire-iOS Tests/BackupViewControllerTests.swift b/wire-ios/Wire-iOS Tests/BackupViewControllerTests.swift deleted file mode 100644 index 1e1f71484b1..00000000000 --- a/wire-ios/Wire-iOS Tests/BackupViewControllerTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import WireDesign -import WireTestingPackage -import XCTest - -@testable import Wire - -final class BackupViewControllerTests: XCTestCase { - - private var snapshotHelper: SnapshotHelper! - - override func setUp() { - super.setUp() - snapshotHelper = SnapshotHelper() - } - - override func tearDown() { - snapshotHelper = nil - super.tearDown() - } - - func testInitialState() { - // GIVEN - let sut = makeViewController() - - // WHEN && THEN - snapshotHelper.verify(matching: sut.view) - } - - // MARK: Helpers - - private func makeViewController() -> BackupViewController { - let backupSource = MockBackupSource() - let vc = BackupViewController(backupSource: backupSource) - vc.view.backgroundColor = SemanticColors.View.backgroundDefault - return vc - } -} diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_LoggedOut.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_LoggedOut.1.png index 0ba158d0a994d6fdef3ff190cfb3a4ec35e09baa..f0a5630707d0ee3c112d9ca4cfb75669105d84e6 100644 GIT binary patch delta 17172 zcmX>*pX2Zq_J%Etp8Xof`!X3A5+vF_rrUB&{1(b~VfDU449q+R2Vg{wgutX06~)OP z9*ZckwWtILi9D@XEc5$fKav`dYI$a-NfT723q~@Egq&=cz~a^PY30;yS?0)Qfz(L@ z4N>&;NNQkYlSn`zCNU~F?jk~=Os=}Jd51Hg;Pc09-wdyWfu<(qA-)Q)aroYkr z3o9{3%fHe3W2n|2kyNXHPa6A7W?*n&@N{tu0oL@Rb@6ChX0$B`O#`EC>Cv|IXkP@S zI2i5A!J3c2?&WA(8WtF%ZRydr^iXX}kB()Hjs=d6rH+oJg6p!;{_kl27p1{FIu|lJ zmoz$;gglovIu|!OmkH_jjrM;>`@f@W5k}V{z=z96=a1lvWk%P+Kvorvwx37a&!g?< z(e^W9k>TiCKv-%RZ9k8;pGVuzqwVL>_Vegk^3k>Uqk9=(wIQT^FuIpvbT7#0UYgPI zgVFH=s*E2b5Wg30o~6+t1_u5j(3*Hezm<_~G%b#%MXIF5(S4b)ArRnB(9wOFqx&*P z_hpWb1;Tniqie}W*OCv_wd6WT`!Ww~O+IeIz+k|0_{V0sR*&CW%r8RY6XA;sZx~E} zFo97b1bXbjsgG`!-xkYDqMpQHN8pTxr@`8}SM$)%Z!iO%f}jvov?s9d?+bTS$;N&H zM>PDr;uXDhHgc;MwB?5vai)Yaq#eCTCVxt@{jZ6rDjy`HpVMHs^-|c|G`O!J-b*+P dJVZiDv_RittJ${h*vAY&;OXk;vd$@?2>_Nqe~|zH delta 26214 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 WAKz@pwGRA)oi#X9wBfYft^WszTuu%E diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_NewDevice.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_NewDevice.1.png index 4d25c1c7411926ce28c634ebc4b71922429a6fc0..335454ecd5e42281555d42d113584dbe1fdf06f1 100644 GIT binary patch delta 10926 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 O{P^wg_qpqhPx?1`GoSnu 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 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testLoginScreen_Email_WithProxyAuthenticated.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testLoginScreen_Email_WithProxyAuthenticated.1.png index 036993aede75c8a4edce752963195342b220e38e..e015bb0fd6b5321b8c1cf9e234ff4a9913d1a42c 100644 GIT binary patch literal 164041 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

u?yV-)hUY469AMqMah< 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 F{{h0Rei#4% literal 74111 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%0DWO4B1tF|7l76%8;x>@KD)(91qZ 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Π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 aFXZ79dZ&mQt{m_HqHsgydhWG55C0F%ISMNP diff --git a/wire-ios/Wire-iOS Tests/SettingsTableViewControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/SettingsTableViewControllerSnapshotTests.swift index d489321b4ac..f4d8c8d8dd7 100644 --- a/wire-ios/Wire-iOS Tests/SettingsTableViewControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/SettingsTableViewControllerSnapshotTests.swift @@ -90,6 +90,7 @@ final class SettingsTableViewControllerSnapshotTests: XCTestCase { // MARK: - Snapshot Tests + @MainActor func testForSettingGroup() throws { let group = settingsCellDescriptorFactory.settingsGroup( isPublicDomain: true, @@ -99,6 +100,7 @@ final class SettingsTableViewControllerSnapshotTests: XCTestCase { try verify(group: group) } + @MainActor private func testForAccountGroup( federated: Bool, disabledEditing: Bool = false, @@ -117,18 +119,22 @@ final class SettingsTableViewControllerSnapshotTests: XCTestCase { try verify(group: group, file: file, testName: testName, line: line) } + @MainActor func testForAccountGroup_Federated() throws { try testForAccountGroup(federated: true) } + @MainActor func testForAccountGroup_NotFederated() throws { try testForAccountGroup(federated: false) } + @MainActor func testForAccountGroupWithDisabledEditing_Federated() throws { try testForAccountGroup(federated: true, disabledEditing: true) } + @MainActor func testForAccountGroupWithDisabledEditing_NotFederated() throws { try testForAccountGroup(federated: false, disabledEditing: true) } diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj index a07a358fe59..5f0860415fc 100644 --- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj +++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 060A36902CBCF1010066908C /* ConversationListViewController+EmptyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060A368F2CBCF1010066908C /* ConversationListViewController+EmptyState.swift */; }; 060C06652B73DFC700B484C6 /* E2EINotificationActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060C06642B73DFC700B484C6 /* E2EINotificationActionsHandler.swift */; }; 060E5336257668EE00BDDEBB /* SettingsPropertyFactory+AppLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060E5335257668EE00BDDEBB /* SettingsPropertyFactory+AppLock.swift */; }; + 060F032B2D2C12930016431F /* BackupPasswordValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060F032A2D2C12910016431F /* BackupPasswordValidator.swift */; }; 061275DE26F304CB006E8D4C /* DragInteractionRestrictionTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061275DD26F304CB006E8D4C /* DragInteractionRestrictionTextView.swift */; }; 0617001523E48B66005C262D /* VerticalTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0617001423E48B66005C262D /* VerticalTransition.swift */; }; 0619A95C2D3FE78700876BDE /* WireAuthenticationUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0619A95B2D3FE78700876BDE /* WireAuthenticationUI */; }; @@ -314,11 +315,9 @@ 5902F8D72BF78FC200F1D392 /* ArchivedListViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5902F8D62BF78FC200F1D392 /* ArchivedListViewControllerDelegate.swift */; }; 5902F8E32BF7B18B00F1D392 /* ArchivedListViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5902F8DF2BF7B14F00F1D392 /* ArchivedListViewControllerSnapshotTests.swift */; }; 59076F952C934A9800AE7529 /* WireAccountImageUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59076F942C934A9800AE7529 /* WireAccountImageUI */; }; - 59076F972C934AA100AE7529 /* WireAccountImageUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59076F962C934AA100AE7529 /* WireAccountImageUI */; }; 590A5F092BF4CB6B008E87D8 /* PermissionDeniedViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590A5F082BF4CB6B008E87D8 /* PermissionDeniedViewController+notifications.swift */; }; 590A5F0B2BF4CBCE008E87D8 /* PermissionDeniedViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 590A5F0A2BF4CBCE008E87D8 /* PermissionDeniedViewControllerDelegate.swift */; }; 590DCA082C971A56002D0A2C /* WireSidebarUI in Frameworks */ = {isa = PBXBuildFile; productRef = 590DCA072C971A56002D0A2C /* WireSidebarUI */; }; - 590DCA0A2C971AFF002D0A2C /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 590DCA092C971AFF002D0A2C /* WireFoundation */; }; 59152B392D2841E7004425A0 /* WireAnalyticsSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59152B382D2841E7004425A0 /* WireAnalyticsSupport */; }; 59152B3B2D284222004425A0 /* WireAnalyticsSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59152B3A2D284222004425A0 /* WireAnalyticsSupport */; }; 5915B94B2BF4A70900215817 /* ShouldPresentNotificationPermissionHintUseCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5915B94A2BF4A70900215817 /* ShouldPresentNotificationPermissionHintUseCaseProtocol.swift */; }; @@ -327,7 +326,6 @@ 5915B9582BF4BA9700215817 /* DidPresentNotificationPermissionHintUseCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5915B9572BF4BA9700215817 /* DidPresentNotificationPermissionHintUseCaseProtocol.swift */; }; 5915B95A2BF4BAFF00215817 /* DidPresentNotificationPermissionHintUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5915B9592BF4BAFE00215817 /* DidPresentNotificationPermissionHintUseCase.swift */; }; 5915B95C2BF4BB5300215817 /* NativelySupportedUserDefaultsKey+lastTimeNotificationPermissionHintWasShown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5915B95B2BF4BB5300215817 /* NativelySupportedUserDefaultsKey+lastTimeNotificationPermissionHintWasShown.swift */; }; - 59191A652D0051C7001AB388 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59191A642D0051C7001AB388 /* WireLogging */; }; 591B6E172C8B095B009F8A7B /* WireNotificationEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9A8585298B0A3B00064A9C /* WireNotificationEngine.framework */; }; 591B6E182C8B095B009F8A7B /* WireNotificationEngine.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EE9A8585298B0A3B00064A9C /* WireNotificationEngine.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 591B6E192C8B0960009F8A7B /* WireCommonComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1FEA14A21DCEB1700790A54 /* WireCommonComponents.framework */; }; @@ -362,6 +360,9 @@ 59537D912CFFA0BA00920B59 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59537D902CFFA0BA00920B59 /* WireLogging */; }; 59537D932CFFA0DA00920B59 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59537D922CFFA0DA00920B59 /* WireLogging */; }; 59537D952CFFA11A00920B59 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59537D942CFFA11A00920B59 /* WireLogging */; }; + 59592D1F2D4B9527005EDF16 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 590DCA092C971AFF002D0A2C /* WireFoundation */; }; + 59592D202D4B9527005EDF16 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59191A642D0051C7001AB388 /* WireLogging */; }; + 595930662D4B980C005EDF16 /* CleanUpBackupsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595930652D4B980C005EDF16 /* CleanUpBackupsUseCase.swift */; }; 595BFD7D2CA9365800D02361 /* ConversationListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595BFD7C2CA9365200D02361 /* ConversationListCoordinator.swift */; }; 595C49362CA93D7200F8F881 /* AnyConversationListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595C49352CA93D6D00F8F881 /* AnyConversationListCoordinator.swift */; }; 595C49692CA995E900F8F881 /* WireSettingsUI in Frameworks */ = {isa = PBXBuildFile; productRef = 595C49682CA995E900F8F881 /* WireSettingsUI */; }; @@ -399,17 +400,16 @@ 59AADE272BB429B200D9E658 /* WireRequestStrategySupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59AADE262BB429B200D9E658 /* WireRequestStrategySupport.framework */; }; 59AADE282BB429D300D9E658 /* AutoMockable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = E644B79A2B7CBBA3005D0BFD /* AutoMockable.generated.swift */; }; 59AF77A12CC7FB3B002438D1 /* AnyMainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AF77A02CC7FB39002438D1 /* AnyMainCoordinator.swift */; }; - 59AF77A32CC7FF89002438D1 /* WireMainNavigationUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59AF77A22CC7FF89002438D1 /* WireMainNavigationUI */; }; 59B404512CAA937400CC33BF /* SettingsContent+MainSettingsContentRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B404502CAA936E00CC33BF /* SettingsContent+MainSettingsContentRepresentable.swift */; }; 59B404652CAAC3AC00CC33BF /* SettingsViewControllerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B404642CAAC3A700CC33BF /* SettingsViewControllerBuilder.swift */; }; 59B404672CAB05CA00CC33BF /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B404662CAB05C900CC33BF /* SettingsCoordinator.swift */; }; 59B404692CAB13D400CC33BF /* MockSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B404682CAB13CF00CC33BF /* MockSettingsCoordinator.swift */; }; 59B4046A2CAB13D400CC33BF /* MockSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B404682CAB13CF00CC33BF /* MockSettingsCoordinator.swift */; }; - 59B4046C2CAB140A00CC33BF /* WireSettingsUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59B4046B2CAB140A00CC33BF /* WireSettingsUI */; }; - 59B4046E2CAB141000CC33BF /* WireSettingsUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59B4046D2CAB141000CC33BF /* WireSettingsUI */; }; - 59B48C5E2C89CCAB00EA7999 /* WireFoundationSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59B48C5D2C89CCAB00EA7999 /* WireFoundationSupport */; }; - 59B48C602C89CCCC00EA7999 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 59B48C5F2C89CCCC00EA7999 /* WireFoundation */; }; 59B48C622C89CD3D00EA7999 /* WireTestingPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59B48C612C89CD3D00EA7999 /* WireTestingPackage */; }; + 59B768432D2D58BE007B5F1E /* WireFoundationSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59B768422D2D58BE007B5F1E /* WireFoundationSupport */; }; + 59B768472D2D59E3007B5F1E /* WireLoggingSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59B768462D2D59E3007B5F1E /* WireLoggingSupport */; }; + 59B768492D2D59E8007B5F1E /* WireLoggingSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59B768482D2D59E8007B5F1E /* WireLoggingSupport */; }; + 59B7684B2D2D5A17007B5F1E /* WireFoundationSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59B7684A2D2D5A17007B5F1E /* WireFoundationSupport */; }; 59B99FAA2C89DE8600201827 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 59B99FA92C89DE8600201827 /* WireFoundation */; }; 59B99FAC2C89DF2100201827 /* WireAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 59B99FAB2C89DF2100201827 /* WireAPI */; }; 59BFBFC42CB68E8C005C3375 /* CreateGroupConversationViewControllerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BFBFC32CB68E8B005C3375 /* CreateGroupConversationViewControllerBuilder.swift */; }; @@ -417,7 +417,6 @@ 59BFBFC92CB7E0F7005C3375 /* SidebarViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BFBFC72CB7E0F7005C3375 /* SidebarViewControllerDelegate.swift */; }; 59BFBFCA2CB7E0F7005C3375 /* SidebarAccountInfo+initWithUserSessionAndAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BFBFC82CB7E0F7005C3375 /* SidebarAccountInfo+initWithUserSessionAndAccount.swift */; }; 59BFBFCE2CB7E25B005C3375 /* ConversationListViewController+NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BFBFCD2CB7E25B005C3375 /* ConversationListViewController+NavigationBar.swift */; }; - 59C2F09F2CA54E4900B25E5D /* WireMainNavigationUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59C2F09E2CA54E4900B25E5D /* WireMainNavigationUI */; }; 59C4FBF12C45B7130037030B /* WireShareEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE33C48C296485FA00C058D1 /* WireShareEngine.framework */; }; 59CDB3F62C4EA08F0049D1AB /* WireReusableUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 59CDB3F52C4EA08F0049D1AB /* WireReusableUIComponents */; }; 59D038272C85D31E009FE583 /* WireMainNavigationUI in Frameworks */ = {isa = PBXBuildFile; productRef = 59D038262C85D31E009FE583 /* WireMainNavigationUI */; }; @@ -528,11 +527,9 @@ 5E8FFC0321ECC5CF0052DF03 /* AuthenticationFeatureProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0221ECC5CF0052DF03 /* AuthenticationFeatureProvider.swift */; }; 5E8FFC0621ECCC7C0052DF03 /* AuthenticationInterfaceBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0521ECCC7C0052DF03 /* AuthenticationInterfaceBuilderTests.swift */; }; 5E8FFC0821ECCC9B0052DF03 /* MockAuthenticationFeatureProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0721ECCC9B0052DF03 /* MockAuthenticationFeatureProvider.swift */; }; - 5E8FFC0A21ECE3920052DF03 /* BackupRestoreStepDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0921ECE3920052DF03 /* BackupRestoreStepDescription.swift */; }; + 5E8FFC0A21ECE3920052DF03 /* NoHistoryHintStepDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0921ECE3920052DF03 /* NoHistoryHintStepDescription.swift */; }; 5E8FFC0C21EDDB970052DF03 /* SolidButtonDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0B21EDDB970052DF03 /* SolidButtonDescription.swift */; }; 5E8FFC1021EE0CA60052DF03 /* AuthenticationButtonTapInputHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC0F21EE0CA60052DF03 /* AuthenticationButtonTapInputHandler.swift */; }; - 5E8FFC1421EF3F9B0052DF03 /* BackupRestoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC1321EF3F9B0052DF03 /* BackupRestoreController.swift */; }; - 5E8FFC1621EF46600052DF03 /* AuthenticationCoordinator+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC1521EF46600052DF03 /* AuthenticationCoordinator+Backup.swift */; }; 5E8FFC1821EF61A90052DF03 /* ClientUnregisterInvitationStepDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC1721EF61A80052DF03 /* ClientUnregisterInvitationStepDescription.swift */; }; 5E8FFC1A21EF6EB10052DF03 /* RemoveClientStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC1921EF6EB10052DF03 /* RemoveClientStepViewController.swift */; }; 5E8FFC1C21EF7CB50052DF03 /* AddEmailPasswordStepDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FFC1B21EF7CB50052DF03 /* AddEmailPasswordStepDescription.swift */; }; @@ -788,7 +785,6 @@ 8750A0112195BEE800DC8DB6 /* UpsideDownTableView+Scrolling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8750A0102195BEE800DC8DB6 /* UpsideDownTableView+Scrolling.swift */; }; 8750B1CB2124236200B807A9 /* SecondaryTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8750B1CA2124236200B807A9 /* SecondaryTextButton.swift */; }; 8751A6CB1FF6573D00804A58 /* QuickActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8751A6CA1FF6573D00804A58 /* QuickActionsManager.swift */; }; - 8751BA442069455E00DF8667 /* BackupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8751BA432069455E00DF8667 /* BackupViewController.swift */; }; 875753D1211DC61C00A80F5E /* EmptySearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875753D0211DC61C00A80F5E /* EmptySearchResultsView.swift */; }; 8758CDB92191A7D60031BE0F /* ReplyComposingViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8758CDB82191A7D60031BE0F /* ReplyComposingViewTests.swift */; }; 8759E2C51FC86090008E17C9 /* UIAlertController+TOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8759E2C41FC86090008E17C9 /* UIAlertController+TOS.swift */; }; @@ -844,7 +840,6 @@ 87AC33CD2182064F00069C79 /* ReplyComposingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AC33CC2182064F00069C79 /* ReplyComposingView.swift */; }; 87AC77DB1DE48C01009F6D56 /* Copyable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AC77DA1DE48C01009F6D56 /* Copyable.swift */; }; 87AC9F561C0749FB00E1ED6F /* TailEditingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AC9F551C0749FB00E1ED6F /* TailEditingTextField.swift */; }; - 87AE8BDC207B99540058715E /* BackupPasswordViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AE8BDB207B99540058715E /* BackupPasswordViewControllerTests.swift */; }; 87B8C33F1DF9788B0015EC89 /* BrowserOpening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B8C33B1DF9788B0015EC89 /* BrowserOpening.swift */; }; 87B8C3401DF9788B0015EC89 /* LinkOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B8C33C1DF9788B0015EC89 /* LinkOpener.swift */; }; 87B8C3411DF9788B0015EC89 /* MapOpening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B8C33D1DF9788B0015EC89 /* MapOpening.swift */; }; @@ -858,7 +853,6 @@ 87BEB0E01C734DA60094BFE9 /* MockLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 87BEB0DF1C734DA60094BFE9 /* MockLoader.m */; }; 87BEB0E21C734DB60094BFE9 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BEB0E11C734DB60094BFE9 /* MockMessage.swift */; }; 87C39A8A206947A1008DA100 /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C39A89206947A1008DA100 /* UIView+Constraints.swift */; }; - 87C39A90206BF00A008DA100 /* BackupViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C39A8F206BF00A008DA100 /* BackupViewControllerTests.swift */; }; 87C539561E8E516400084F94 /* BoundsAwareFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C539551E8E516400084F94 /* BoundsAwareFlowLayout.swift */; }; 87CA886E1DDF0075004101B6 /* UserConnectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CA886D1DDF0075004101B6 /* UserConnectionViewController.swift */; }; 87D21AD71D8A98620075AB7A /* AccentColorPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D21AD61D8A98620075AB7A /* AccentColorPickerController.swift */; }; @@ -1178,7 +1172,6 @@ D3DA6E7B292F67680045CC57 /* CallingActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DA6E7A292F67680045CC57 /* CallingActionsView.swift */; }; D3DA6E7E292FDEEA0045CC57 /* CallingActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DA6E7D292FDEEA0045CC57 /* CallingActionButton.swift */; }; D3DE46052923BB13000F1055 /* BottomSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DE46042923BB13000F1055 /* BottomSheetContainerViewController.swift */; }; - D503538C207B5DB900CE34A5 /* BackupRestoreController+Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503538B207B5DB900CE34A5 /* BackupRestoreController+Password.swift */; }; D50892FB2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50892FA2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift */; }; D50892FD2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50892FC2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift */; }; D5168F282008ED0700F8222A /* KeyboardBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5168F272008ED0700F8222A /* KeyboardBlockObserver.swift */; }; @@ -1209,7 +1202,6 @@ D550F5772044532D009E09DD /* ConversationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D550F5762044532D009E09DD /* ConversationAction.swift */; }; D550F57920445AD7009E09DD /* UIAlertController+ConversationGuestOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D550F57820445AD7009E09DD /* UIAlertController+ConversationGuestOptions.swift */; }; D5694B5220441EE900C84C8F /* UILabel+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5694B5120441EE900C84C8F /* UILabel+Convenience.swift */; }; - D56A2D44207DFC9E00DB59F5 /* BackupRestoreController+Failed.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56A2D43207DFC9E00DB59F5 /* BackupRestoreController+Failed.swift */; }; D58191FD2091CAE8003BA7EC /* CallActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58191FC2091CAE8003BA7EC /* CallActionsView.swift */; }; D58192002091CEBF003BA7EC /* UIStackView+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58191FF2091CEBF003BA7EC /* UIStackView+Helper.swift */; }; D58192022091E2C0003BA7EC /* IconLabelButton+CallActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58192012091E2C0003BA7EC /* IconLabelButton+CallActions.swift */; }; @@ -1221,7 +1213,6 @@ D5C34E93203D7B2E004A0986 /* CellConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C34E92203D7B2E004A0986 /* CellConfiguration.swift */; }; D5CEBFC3202CA3BA00AFBD3A /* AddParticipantsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CEBFC2202CA3BA00AFBD3A /* AddParticipantsViewModel.swift */; }; D5D65A0F2074CFBB00D7F3C3 /* AuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D65A0E2074CFBB00D7F3C3 /* AuthenticationType.swift */; }; - D5D65A11207509F300D7F3C3 /* BackupPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D65A10207509F300D7F3C3 /* BackupPasswordViewController.swift */; }; D5D89780201A12D300FAF69C /* Account+ShareExtensionDisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D8977E201A121900FAF69C /* Account+ShareExtensionDisplayName.swift */; }; D5F22D4C2048052900879444 /* UIAlertController+RemoveAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F22D4B2048052900879444 /* UIAlertController+RemoveAction.swift */; }; D5F8BFF42007AC84008F8C3D /* UIView+TableViewHeaderFooterSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F8BFF32007AC84008F8C3D /* UIView+TableViewHeaderFooterSizing.swift */; }; @@ -1294,9 +1285,7 @@ E66258892B4D39D900C23E79 /* DeveloperDebugActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66258882B4D39D900C23E79 /* DeveloperDebugActionsViewModel.swift */; }; E662588B2B4D3C9D00C23E79 /* DeveloperDebugActionsDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E662588A2B4D3C9D00C23E79 /* DeveloperDebugActionsDisplayModel.swift */; }; E66258942B4D661900C23E79 /* DeveloperDebugActionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66258932B4D661900C23E79 /* DeveloperDebugActionsViewModelTests.swift */; }; - E666EDD62B73E62800C03E2B /* BackupSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E666EDD52B73E62800C03E2B /* BackupSource.swift */; }; - E666EDDA2B73E9C400C03E2B /* BackupStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E666EDD92B73E9C400C03E2B /* BackupStatusCell.swift */; }; - E666EDDC2B73EA3500C03E2B /* BackupActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E666EDDB2B73EA3500C03E2B /* BackupActionCell.swift */; }; + E666EDD62B73E62800C03E2B /* CreateLegacyBackupUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E666EDD52B73E62800C03E2B /* CreateLegacyBackupUseCase.swift */; }; E66C51D52C13375700F82F88 /* WireAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66C51D42C13375700F82F88 /* WireAnalytics.swift */; }; E66C51D92C13434B00F82F88 /* WireDatadog+LoggerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66C51D82C13434B00F82F88 /* WireDatadog+LoggerProtocol.swift */; }; E66D4E822BE525DF00C7F374 /* AVSVideoContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66D4E812BE525DF00C7F374 /* AVSVideoContainerView.swift */; }; @@ -1370,7 +1359,6 @@ E97661812BDA4D1E0033AACC /* SecureLinkHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97661802BDA4D1E0033AACC /* SecureLinkHeaderCell.swift */; }; E9816C902CC9244700D77F22 /* WireSyncEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9816C8F2CC9244700D77F22 /* WireSyncEngine.framework */; }; E9816C962CC9281000D77F22 /* LaunchScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9816C952CC9280400D77F22 /* LaunchScreenViewController.swift */; }; - E9816C9A2CC929FC00D77F22 /* WireFoundationSupport in Frameworks */ = {isa = PBXBuildFile; productRef = E9816C992CC929FC00D77F22 /* WireFoundationSupport */; }; E985CB8F2CEB4FCB0075DAD6 /* WireDatadog in Frameworks */ = {isa = PBXBuildFile; productRef = 016A141C2CE6BFC4006A7EF5 /* WireDatadog */; }; E989695728EC430B0088F0CE /* SecondaryButtonDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E989695628EC430B0088F0CE /* SecondaryButtonDescription.swift */; }; E98B61102B5820BD0030E021 /* SwiftMockConversation+ConversationCreation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98B610F2B5820BD0030E021 /* SwiftMockConversation+ConversationCreation.swift */; }; @@ -1985,6 +1973,7 @@ 060A368F2CBCF1010066908C /* ConversationListViewController+EmptyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationListViewController+EmptyState.swift"; sourceTree = ""; }; 060C06642B73DFC700B484C6 /* E2EINotificationActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2EINotificationActionsHandler.swift; sourceTree = ""; }; 060E5335257668EE00BDDEBB /* SettingsPropertyFactory+AppLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsPropertyFactory+AppLock.swift"; sourceTree = ""; }; + 060F032A2D2C12910016431F /* BackupPasswordValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupPasswordValidator.swift; sourceTree = ""; }; 061275DD26F304CB006E8D4C /* DragInteractionRestrictionTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragInteractionRestrictionTextView.swift; sourceTree = ""; }; 061282622379C25500C1A53C /* UITextView+ReplaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+ReplaceTests.swift"; sourceTree = ""; }; 0617001423E48B66005C262D /* VerticalTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalTransition.swift; sourceTree = ""; }; @@ -2305,6 +2294,7 @@ 5950A9462C2F1C31005AB9CE /* AvailabilityMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailabilityMappings.swift; sourceTree = ""; }; 59510DD22CB3E6E700BCD5FD /* SidebarViewControllerBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewControllerBuilder.swift; sourceTree = ""; }; 5952693E2BE8C93B001C1E8B /* AppLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockView.swift; sourceTree = ""; }; + 595930652D4B980C005EDF16 /* CleanUpBackupsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanUpBackupsUseCase.swift; sourceTree = ""; }; 595BFD7C2CA9365200D02361 /* ConversationListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationListCoordinator.swift; sourceTree = ""; }; 595C49352CA93D6D00F8F881 /* AnyConversationListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyConversationListCoordinator.swift; sourceTree = ""; }; 596184AC2CC7B5F600787AF0 /* DefaultSettingsPropertyFactoryDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSettingsPropertyFactoryDelegate.swift; sourceTree = ""; }; @@ -2450,12 +2440,10 @@ 5E8FFC0221ECC5CF0052DF03 /* AuthenticationFeatureProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationFeatureProvider.swift; sourceTree = ""; }; 5E8FFC0521ECCC7C0052DF03 /* AuthenticationInterfaceBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationInterfaceBuilderTests.swift; sourceTree = ""; }; 5E8FFC0721ECCC9B0052DF03 /* MockAuthenticationFeatureProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationFeatureProvider.swift; sourceTree = ""; }; - 5E8FFC0921ECE3920052DF03 /* BackupRestoreStepDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupRestoreStepDescription.swift; sourceTree = ""; }; + 5E8FFC0921ECE3920052DF03 /* NoHistoryHintStepDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoHistoryHintStepDescription.swift; sourceTree = ""; }; 5E8FFC0B21EDDB970052DF03 /* SolidButtonDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidButtonDescription.swift; sourceTree = ""; }; 5E8FFC0D21EE0B180052DF03 /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; 5E8FFC0F21EE0CA60052DF03 /* AuthenticationButtonTapInputHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationButtonTapInputHandler.swift; sourceTree = ""; }; - 5E8FFC1321EF3F9B0052DF03 /* BackupRestoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupRestoreController.swift; sourceTree = ""; }; - 5E8FFC1521EF46600052DF03 /* AuthenticationCoordinator+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthenticationCoordinator+Backup.swift"; sourceTree = ""; }; 5E8FFC1721EF61A80052DF03 /* ClientUnregisterInvitationStepDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientUnregisterInvitationStepDescription.swift; sourceTree = ""; }; 5E8FFC1921EF6EB10052DF03 /* RemoveClientStepViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveClientStepViewController.swift; sourceTree = ""; }; 5E8FFC1B21EF7CB50052DF03 /* AddEmailPasswordStepDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmailPasswordStepDescription.swift; sourceTree = ""; }; @@ -2728,7 +2716,6 @@ 8750A0102195BEE800DC8DB6 /* UpsideDownTableView+Scrolling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UpsideDownTableView+Scrolling.swift"; sourceTree = ""; }; 8750B1CA2124236200B807A9 /* SecondaryTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryTextButton.swift; sourceTree = ""; }; 8751A6CA1FF6573D00804A58 /* QuickActionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionsManager.swift; sourceTree = ""; }; - 8751BA432069455E00DF8667 /* BackupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupViewController.swift; sourceTree = ""; }; 8756CA121D2EBE1F002E7CB7 /* LaunchImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchImageViewController.swift; sourceTree = ""; }; 875753D0211DC61C00A80F5E /* EmptySearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptySearchResultsView.swift; sourceTree = ""; }; 8758CDB82191A7D60031BE0F /* ReplyComposingViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyComposingViewTests.swift; sourceTree = ""; }; @@ -2800,7 +2787,6 @@ 87AC33CC2182064F00069C79 /* ReplyComposingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyComposingView.swift; sourceTree = ""; }; 87AC77DA1DE48C01009F6D56 /* Copyable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Copyable.swift; sourceTree = ""; }; 87AC9F551C0749FB00E1ED6F /* TailEditingTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TailEditingTextField.swift; sourceTree = ""; }; - 87AE8BDB207B99540058715E /* BackupPasswordViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupPasswordViewControllerTests.swift; sourceTree = ""; }; 87B8C33B1DF9788B0015EC89 /* BrowserOpening.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserOpening.swift; sourceTree = ""; }; 87B8C33C1DF9788B0015EC89 /* LinkOpener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkOpener.swift; sourceTree = ""; }; 87B8C33D1DF9788B0015EC89 /* MapOpening.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapOpening.swift; sourceTree = ""; }; @@ -2817,7 +2803,6 @@ 87BEB0DF1C734DA60094BFE9 /* MockLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MockLoader.m; sourceTree = ""; }; 87BEB0E11C734DB60094BFE9 /* MockMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMessage.swift; sourceTree = ""; }; 87C39A89206947A1008DA100 /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; - 87C39A8F206BF00A008DA100 /* BackupViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupViewControllerTests.swift; sourceTree = ""; }; 87C539551E8E516400084F94 /* BoundsAwareFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundsAwareFlowLayout.swift; sourceTree = ""; }; 87C8D7FB1BA8261C00B0530B /* Entitlements-Dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = "Entitlements-Dev.entitlements"; path = "Wire-iOS/Entitlements-Dev.entitlements"; sourceTree = ""; }; 87C8D7FC1BA8261C00B0530B /* Entitlements-Prod.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = "Entitlements-Prod.entitlements"; path = "Wire-iOS/Entitlements-Prod.entitlements"; sourceTree = ""; }; @@ -3210,7 +3195,6 @@ D3DA6E7A292F67680045CC57 /* CallingActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallingActionsView.swift; sourceTree = ""; }; D3DA6E7D292FDEEA0045CC57 /* CallingActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallingActionButton.swift; sourceTree = ""; }; D3DE46042923BB13000F1055 /* BottomSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetContainerViewController.swift; sourceTree = ""; }; - D503538B207B5DB900CE34A5 /* BackupRestoreController+Password.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackupRestoreController+Password.swift"; sourceTree = ""; }; D50892FA2056BD51004D3AE2 /* ZMUser+ExpirationTimeFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ZMUser+ExpirationTimeFormatting.swift"; sourceTree = ""; }; D50892FC2056C2AA004D3AE2 /* WirelessExpirationTimeFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WirelessExpirationTimeFormatterTests.swift; sourceTree = ""; }; D5168F272008ED0700F8222A /* KeyboardBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardBlockObserver.swift; sourceTree = ""; }; @@ -3236,7 +3220,6 @@ D550F5762044532D009E09DD /* ConversationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAction.swift; sourceTree = ""; }; D550F57820445AD7009E09DD /* UIAlertController+ConversationGuestOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+ConversationGuestOptions.swift"; sourceTree = ""; }; D5694B5120441EE900C84C8F /* UILabel+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Convenience.swift"; sourceTree = ""; }; - D56A2D43207DFC9E00DB59F5 /* BackupRestoreController+Failed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackupRestoreController+Failed.swift"; sourceTree = ""; }; D58191FC2091CAE8003BA7EC /* CallActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallActionsView.swift; sourceTree = ""; }; D58191FF2091CEBF003BA7EC /* UIStackView+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Helper.swift"; sourceTree = ""; }; D58192012091E2C0003BA7EC /* IconLabelButton+CallActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IconLabelButton+CallActions.swift"; sourceTree = ""; }; @@ -3248,7 +3231,6 @@ D5C34E92203D7B2E004A0986 /* CellConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellConfiguration.swift; sourceTree = ""; }; D5CEBFC2202CA3BA00AFBD3A /* AddParticipantsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsViewModel.swift; sourceTree = ""; }; D5D65A0E2074CFBB00D7F3C3 /* AuthenticationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationType.swift; sourceTree = ""; }; - D5D65A10207509F300D7F3C3 /* BackupPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupPasswordViewController.swift; sourceTree = ""; }; D5D8977E201A121900FAF69C /* Account+ShareExtensionDisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+ShareExtensionDisplayName.swift"; sourceTree = ""; }; D5F22D4B2048052900879444 /* UIAlertController+RemoveAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+RemoveAction.swift"; sourceTree = ""; }; D5F8BFF32007AC84008F8C3D /* UIView+TableViewHeaderFooterSizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+TableViewHeaderFooterSizing.swift"; sourceTree = ""; }; @@ -3273,9 +3255,7 @@ E66258882B4D39D900C23E79 /* DeveloperDebugActionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperDebugActionsViewModel.swift; sourceTree = ""; }; E662588A2B4D3C9D00C23E79 /* DeveloperDebugActionsDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperDebugActionsDisplayModel.swift; sourceTree = ""; }; E66258932B4D661900C23E79 /* DeveloperDebugActionsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperDebugActionsViewModelTests.swift; sourceTree = ""; }; - E666EDD52B73E62800C03E2B /* BackupSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSource.swift; sourceTree = ""; }; - E666EDD92B73E9C400C03E2B /* BackupStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupStatusCell.swift; sourceTree = ""; }; - E666EDDB2B73EA3500C03E2B /* BackupActionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupActionCell.swift; sourceTree = ""; }; + E666EDD52B73E62800C03E2B /* CreateLegacyBackupUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLegacyBackupUseCase.swift; sourceTree = ""; }; E66C51D42C13375700F82F88 /* WireAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireAnalytics.swift; sourceTree = ""; }; E66C51D82C13434B00F82F88 /* WireDatadog+LoggerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireDatadog+LoggerProtocol.swift"; sourceTree = ""; }; E66D4E812BE525DF00C7F374 /* AVSVideoContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVSVideoContainerView.swift; sourceTree = ""; }; @@ -3906,22 +3886,19 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 59592D202D4B9527005EDF16 /* WireLogging in Frameworks */, + 59592D1F2D4B9527005EDF16 /* WireFoundation in Frameworks */, 59152B392D2841E7004425A0 /* WireAnalyticsSupport in Frameworks */, 59B48C622C89CD3D00EA7999 /* WireTestingPackage in Frameworks */, - 59076F972C934AA100AE7529 /* WireAccountImageUI in Frameworks */, - 59B4046E2CAB141000CC33BF /* WireSettingsUI in Frameworks */, 01C1A7C72A54C45A0058D578 /* SnapshotTesting in Frameworks */, CB4870F22C7F4FE5001E9151 /* WireTransportSupport.framework in Frameworks */, + 59B7684B2D2D5A17007B5F1E /* WireFoundationSupport in Frameworks */, + 59B768492D2D59E8007B5F1E /* WireLoggingSupport in Frameworks */, 5996E8A82C19D09D007A52F0 /* WireSyncEngineSupport.framework in Frameworks */, 5996E8A42C19D074007A52F0 /* WireRequestStrategySupport.framework in Frameworks */, 598E86F72BF4DD5C00FC5438 /* WireSystemSupport.framework in Frameworks */, 598E86D12BF4D97800FC5438 /* WireUtilitiesSupport.framework in Frameworks */, - 59191A652D0051C7001AB388 /* WireLogging in Frameworks */, - E9816C9A2CC929FC00D77F22 /* WireFoundationSupport in Frameworks */, - 590DCA0A2C971AFF002D0A2C /* WireFoundation in Frameworks */, 591B6E1C2C8B0964009F8A7B /* WireDataModelSupport.framework in Frameworks */, - 59C2F09F2CA54E4900B25E5D /* WireMainNavigationUI in Frameworks */, - 59B48C5E2C89CCAB00EA7999 /* WireFoundationSupport in Frameworks */, 5996E8AD2C19D0DF007A52F0 /* WireTesting.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3932,13 +3909,12 @@ files = ( 59AADE272BB429B200D9E658 /* WireRequestStrategySupport.framework in Frameworks */, 5996E8AA2C19D0D6007A52F0 /* WireSystemSupport.framework in Frameworks */, - 59B4046C2CAB140A00CC33BF /* WireSettingsUI in Frameworks */, - 59B48C602C89CCCC00EA7999 /* WireFoundation in Frameworks */, 59152B3B2D284222004425A0 /* WireAnalyticsSupport in Frameworks */, 5977ED1F2D26747900F5C78E /* WireLogging in Frameworks */, 0145AE992B1156FC0097E3B8 /* WireSyncEngineSupport.framework in Frameworks */, E6C25FC52AFFAAC300406A1C /* WireTesting.framework in Frameworks */, - 59AF77A32CC7FF89002438D1 /* WireMainNavigationUI in Frameworks */, + 59B768472D2D59E3007B5F1E /* WireLoggingSupport in Frameworks */, + 59B768432D2D58BE007B5F1E /* WireFoundationSupport in Frameworks */, 591B6E242C8B0970009F8A7B /* WireDataModelSupport.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4682,7 +4658,7 @@ 5E21344A21E351D400273D0D /* LogInStepDescription.swift */, 16D74BED2B5933D000160298 /* AddUsernameStepDescription.swift */, 5E90418A21F61CBF00C3B413 /* ReauthenticateStepDescription.swift */, - 5E8FFC0921ECE3920052DF03 /* BackupRestoreStepDescription.swift */, + 5E8FFC0921ECE3920052DF03 /* NoHistoryHintStepDescription.swift */, 5E8FFC1721EF61A80052DF03 /* ClientUnregisterInvitationStepDescription.swift */, 5E8FFC1B21EF7CB50052DF03 /* AddEmailPasswordStepDescription.swift */, 5E8FFC2121EF86000052DF03 /* EmailLinkVerificationStepDescription.swift */, @@ -4878,7 +4854,6 @@ 5E8C38AF212DC472002AE12B /* AuthenticationCoordinator+LandingScreen.swift */, 5E65A79D21303F97008BFCC0 /* AuthenticationCoordinator+UserChange.swift */, 5E65A79F21303FCE008BFCC0 /* AuthenticationCoordinator+CompanyLogin.swift */, - 5E8FFC1521EF46600052DF03 /* AuthenticationCoordinator+Backup.swift */, 63F65EFC2469967600534A69 /* AuthenticationCoordinator+PreBackendSwitch.swift */, ); path = "Coordinator+Delegates"; @@ -6920,8 +6895,6 @@ 8732670C1D2D0C49005A62C1 /* AudioRecordKeyboardViewControllerTests.swift */, BFAF4CAE1CEB791B00780537 /* AudioRecordViewControllerTests.swift */, 7C6A69041FDFD80E007EBE41 /* AvailabilityLabelTests.swift */, - 87AE8BDB207B99540058715E /* BackupPasswordViewControllerTests.swift */, - 87C39A8F206BF00A008DA100 /* BackupViewControllerTests.swift */, 543F26CC2562AC2F0097F5EB /* CallControllerTests.swift */, 5EE115E0204EEC7D002AD6C2 /* CallQualityControllerTests.swift */, BFE3FEEB20975D1E003D9AB5 /* CallStatusViewTests.swift */, @@ -7514,11 +7487,9 @@ E666EDD42B73E5F500C03E2B /* Backup */ = { isa = PBXGroup; children = ( - E666EDD52B73E62800C03E2B /* BackupSource.swift */, - E666EDDB2B73EA3500C03E2B /* BackupActionCell.swift */, - E666EDD92B73E9C400C03E2B /* BackupStatusCell.swift */, - 8751BA432069455E00DF8667 /* BackupViewController.swift */, - D5D65A10207509F300D7F3C3 /* BackupPasswordViewController.swift */, + 595930652D4B980C005EDF16 /* CleanUpBackupsUseCase.swift */, + 060F032A2D2C12910016431F /* BackupPasswordValidator.swift */, + E666EDD52B73E62800C03E2B /* CreateLegacyBackupUseCase.swift */, ); path = Backup; sourceTree = ""; @@ -7910,7 +7881,6 @@ 5E8DA763211B2CF000360979 /* Coordinator */, EF2126B51FB9DFE300625A9B /* Interface */, 5E8DA753211AF7BA00360979 /* Delegates */, - EF2126DD1FB9DFE300625A9B /* Backup */, 5E8DA74A211AEB6C00360979 /* Event Handlers */, 5E8DA772211B450F00360979 /* Helpers */, ); @@ -7936,16 +7906,6 @@ path = CountryCode; sourceTree = ""; }; - EF2126DD1FB9DFE300625A9B /* Backup */ = { - isa = PBXGroup; - children = ( - 5E8FFC1321EF3F9B0052DF03 /* BackupRestoreController.swift */, - D56A2D43207DFC9E00DB59F5 /* BackupRestoreController+Failed.swift */, - D503538B207B5DB900CE34A5 /* BackupRestoreController+Password.swift */, - ); - path = Backup; - sourceTree = ""; - }; EF2128EB2056D4CB00C1673B /* NetworkStatusView */ = { isa = PBXGroup; children = ( @@ -8478,11 +8438,8 @@ packageProductDependencies = ( 01C1A7C62A54C45A0058D578 /* SnapshotTesting */, 59B48C612C89CD3D00EA7999 /* WireTestingPackage */, - 59076F962C934AA100AE7529 /* WireAccountImageUI */, 590DCA092C971AFF002D0A2C /* WireFoundation */, - E9816C992CC929FC00D77F22 /* WireFoundationSupport */, - 59C2F09E2CA54E4900B25E5D /* WireMainNavigationUI */, - 59B4046D2CAB141000CC33BF /* WireSettingsUI */, + 59B7684A2D2D5A17007B5F1E /* WireFoundationSupport */, 59191A642D0051C7001AB388 /* WireLogging */, 59152B382D2841E7004425A0 /* WireAnalyticsSupport */, ); @@ -8505,9 +8462,6 @@ ); name = "Wire-iOS UnitTests"; packageProductDependencies = ( - 59B48C5F2C89CCCC00EA7999 /* WireFoundation */, - 59B4046B2CAB140A00CC33BF /* WireSettingsUI */, - 59AF77A22CC7FF89002438D1 /* WireMainNavigationUI */, 5977ED1E2D26747900F5C78E /* WireLogging */, 59152B3A2D284222004425A0 /* WireAnalyticsSupport */, ); @@ -9078,7 +9032,6 @@ E9ACC8382C1089D10002E608 /* OpenServicesAdminCell.swift in Sources */, 87AAE4251E4347F000E1D13A /* ConversationContentViewController+PeekPop.swift in Sources */, EF17B0D9223A8F7C006252A8 /* SketchColorCollectionViewCell.swift in Sources */, - D503538C207B5DB900CE34A5 /* BackupRestoreController+Password.swift in Sources */, 6328283F24A110CF000748D2 /* MicrophoneIconStyle.swift in Sources */, 879489E51E54500A0071B2C1 /* TextSearchResultsView.swift in Sources */, F15650D621DD0FC600210504 /* VerticalColumnCollectionViewLayout.swift in Sources */, @@ -9093,7 +9046,6 @@ D5168F532010F20B00F8222A /* BlurEffectTransition.swift in Sources */, 1671D6D1200CDC380022D289 /* InviteTeamMemberCell.swift in Sources */, E95B0A142B55991E0088B778 /* ConversationNewDeviceSystemMessageCellDescription.swift in Sources */, - D5D65A11207509F300D7F3C3 /* BackupPasswordViewController.swift in Sources */, 5EC7C87A219038F6004662AC /* ImageResourceThumbnailView.swift in Sources */, EE0768EE2A9FB99700BA47A3 /* EmojiRepository.swift in Sources */, 87AC77DB1DE48C01009F6D56 /* Copyable.swift in Sources */, @@ -9287,7 +9239,6 @@ 06E097982A20BEE000B38C4A /* ZMConversation+IncompleteMetadata.swift in Sources */, 8775BF1C2007C6B900A8AD93 /* SearchGroupSelector.swift in Sources */, 5915B9582BF4BA9700215817 /* DidPresentNotificationPermissionHintUseCaseProtocol.swift in Sources */, - E666EDDC2B73EA3500C03E2B /* BackupActionCell.swift in Sources */, E66258892B4D39D900C23E79 /* DeveloperDebugActionsViewModel.swift in Sources */, BF5DF5C720F4C1C5002BCB67 /* GroupParticipantsDetailViewModel.swift in Sources */, EEE81E9823E0815600A1D035 /* ProfilePresenter.swift in Sources */, @@ -9517,7 +9468,7 @@ 068500DF26BD4E4B00721F80 /* UIView+Subviews.swift in Sources */, E92A07A02817E553006BA73B /* DynamicFontButton.swift in Sources */, 5E35F77C2182124E00D3F4FE /* PerformanceDebugger.swift in Sources */, - 5E8FFC0A21ECE3920052DF03 /* BackupRestoreStepDescription.swift in Sources */, + 5E8FFC0A21ECE3920052DF03 /* NoHistoryHintStepDescription.swift in Sources */, EEA65C2725D27A8300AA7519 /* Strings+Generated.swift in Sources */, 87DCF49C1D34E9E600BB420F /* LocationSendViewController.swift in Sources */, F1199D421FC849450070FAC3 /* VerificationCodeFieldDescription.swift in Sources */, @@ -9556,7 +9507,6 @@ E90052782C34A52C00020291 /* WireTextField.swift in Sources */, 6328BBAD2C2EFEA2007A3288 /* SettingsDebugReportViewModel.swift in Sources */, 87F18BBC1E01577C00C69D9B /* FileMessageViewState.swift in Sources */, - D56A2D44207DFC9E00DB59F5 /* BackupRestoreController+Failed.swift in Sources */, D51894792050490E00C095C1 /* ActionCell.swift in Sources */, D39210A12A1C9C8700FA616A /* ReactionSectionViewController.swift in Sources */, 59A27BB02C2ACC8F00195393 /* TopOverlayPresenting.swift in Sources */, @@ -9589,6 +9539,7 @@ F16E8EA8214A910F005ED9E2 /* ConversationInputBarViewController+Mentions.swift in Sources */, 87D56DF61E003A5300DFF722 /* CollectionsViewController.swift in Sources */, EE6C212928438B3A0031EFB9 /* SettingsCellDescriptorFactory+Developer.swift in Sources */, + 595930662D4B980C005EDF16 /* CleanUpBackupsUseCase.swift in Sources */, 5965E9732C9B1491001D8AE1 /* ConversationListViewController+MainConversationListProtocol.swift in Sources */, 010D391E2CDBB0730038FA95 /* ConversationListViewController+UISearchBarDelegate.swift in Sources */, 5E766D9B211472C0005242B4 /* AuthenticationFlowStep.swift in Sources */, @@ -9630,7 +9581,7 @@ BFBB03081D61AF8D0065AF4F /* InputBarEditView.swift in Sources */, 5501FFCD22B399F30050D8FC /* UserInputRequest+LegalHold.swift in Sources */, E6E557982BBD49A60033E62B /* NetworkQualityType.swift in Sources */, - E666EDD62B73E62800C03E2B /* BackupSource.swift in Sources */, + E666EDD62B73E62800C03E2B /* CreateLegacyBackupUseCase.swift in Sources */, 6330FCC2249264C900E40B06 /* GridView.swift in Sources */, 5E8C38AE212DC44E002AE12B /* AuthenticationCoordinator+Navigation.swift in Sources */, 87A773CF1DD0B48400ACAA73 /* ObfuscationView.swift in Sources */, @@ -9731,7 +9682,6 @@ 7CED307C1FD97895009F0DAC /* AvailabilityStringBuilder.swift in Sources */, F1CB5D38215CD462001CCF5D /* MarkdownTextView+Recognizers.swift in Sources */, 5E8DA75C211B085A00360979 /* AnyAuthenticationEventHandler.swift in Sources */, - E666EDDA2B73E9C400C03E2B /* BackupStatusCell.swift in Sources */, 1682AEBD20483DA5003A052A /* ServicesSectionController.swift in Sources */, E90A2F0D28EEC674005AF571 /* TextFieldStyle.swift in Sources */, 7C6878DB201B3785003A0C7A /* StartUIViewController+SearchResults.swift in Sources */, @@ -9760,6 +9710,7 @@ 8750B1CB2124236200B807A9 /* SecondaryTextButton.swift in Sources */, 5E62802A2224090D0039A8AB /* SeparatorTableViewCell.swift in Sources */, 596841CC2BD14E550009C6B8 /* CoreImageBasedImageTransformer.swift in Sources */, + 060F032B2D2C12930016431F /* BackupPasswordValidator.swift in Sources */, EF25F7DE1FC45F8E0040C3CC /* ValidatedTextField.swift in Sources */, F15650DA21DD107B00210504 /* RoundedView.swift in Sources */, BFE65AB320A047EB00689063 /* CallAccessoryViewController.swift in Sources */, @@ -9858,7 +9809,6 @@ 5E8DA751211AEFC800360979 /* AuthenticationStatusProvider.swift in Sources */, 5E21345121E38A8400273D0D /* EmailPasswordFieldDescription.swift in Sources */, 5E0F75C02268A0D7006C991E /* UIBarButtonItem+StyleKit.swift in Sources */, - 5E8FFC1421EF3F9B0052DF03 /* BackupRestoreController.swift in Sources */, 59E67CAE2CA8B3A6000F1C17 /* ConversationViewControllerBuilder.swift in Sources */, 6305206224FFEF9600ED295A /* OrientableView.swift in Sources */, EE08ADEF29C8863000B6C14D /* SwitchBackendViewModel.swift in Sources */, @@ -9917,7 +9867,6 @@ 5E8DA758211B055100360979 /* AuthenticationClientLimitErrorHandler.swift in Sources */, BF10B58D1E6452B800E7036E /* ZMConversationMessage+Reactions.swift in Sources */, 5E2945992190A1680045ACFA /* SenderNameCellComponent.swift in Sources */, - 8751BA442069455E00DF8667 /* BackupViewController.swift in Sources */, A9859D6A23FEDD7400DC3F36 /* FullscreenImageViewController.swift in Sources */, A9076C8223B1047E004FD3C9 /* ConversationContentViewController+Header.swift in Sources */, A96A21F123D5B865005B5579 /* ConversationInputBarViewController+Files.swift in Sources */, @@ -9940,7 +9889,6 @@ 59A1F1492B7BB7A7002CB679 /* UserStatusViewControllerDelegate.swift in Sources */, 5E35F786218340EB00D3F4FE /* ConversationLocationMessageCell.swift in Sources */, 87AC9F561C0749FB00E1ED6F /* TailEditingTextField.swift in Sources */, - 5E8FFC1621EF46600052DF03 /* AuthenticationCoordinator+Backup.swift in Sources */, F14FB864214941160012A131 /* MentionsTextAttachment.swift in Sources */, EF2F6DA320ED11D2007B6D70 /* UIColor+Accent.swift in Sources */, 061275DE26F304CB006E8D4C /* DragInteractionRestrictionTextView.swift in Sources */, @@ -10116,7 +10064,6 @@ BF16FC4C1DAFCA0000FF4325 /* EphemeralKeyboardViewControllerTests.swift in Sources */, D30E113C2A98CE3000D8C62D /* EmoliRepositoryTests.swift in Sources */, 63F239352694A151000BFFC6 /* Array+Prefix.swift in Sources */, - 87AE8BDC207B99540058715E /* BackupPasswordViewControllerTests.swift in Sources */, EE52378423F5567300D4FE79 /* MockUserType+Creation.swift in Sources */, A90800752372F0F600A530FC /* ConversationListHeaderViewTests.swift in Sources */, EF1FDC7E21DE255C00C9CEB1 /* NSString+EmoticonSubstitutionTests.swift in Sources */, @@ -10277,7 +10224,6 @@ F1F5E57C216CA6D4006BF3D5 /* ConversationStatusTests+Icon.swift in Sources */, EF5741EE2102001A0041AD47 /* MessageTests.swift in Sources */, 5EF7BA61221AB89300815625 /* ProfileViewTests.swift in Sources */, - 87C39A90206BF00A008DA100 /* BackupViewControllerTests.swift in Sources */, EF2A8DE7214A816D002C9058 /* StartUIViewControllerSnapshotTests.swift in Sources */, 164B533C20A0A5E800EC8265 /* CallInfoConfigurationTests.swift in Sources */, 5950467D2C6F3DA9005315DE /* NetworkStatusViewSnapshotTests.swift in Sources */, @@ -11426,10 +11372,6 @@ isa = XCSwiftPackageProductDependency; productName = WireAccountImageUI; }; - 59076F962C934AA100AE7529 /* WireAccountImageUI */ = { - isa = XCSwiftPackageProductDependency; - productName = WireAccountImageUI; - }; 590DCA072C971A56002D0A2C /* WireSidebarUI */ = { isa = XCSwiftPackageProductDependency; productName = WireSidebarUI; @@ -11494,29 +11436,25 @@ isa = XCSwiftPackageProductDependency; productName = WireLogging; }; - 59AF77A22CC7FF89002438D1 /* WireMainNavigationUI */ = { - isa = XCSwiftPackageProductDependency; - productName = WireMainNavigationUI; - }; - 59B4046B2CAB140A00CC33BF /* WireSettingsUI */ = { + 59B48C612C89CD3D00EA7999 /* WireTestingPackage */ = { isa = XCSwiftPackageProductDependency; - productName = WireSettingsUI; + productName = WireTestingPackage; }; - 59B4046D2CAB141000CC33BF /* WireSettingsUI */ = { + 59B768422D2D58BE007B5F1E /* WireFoundationSupport */ = { isa = XCSwiftPackageProductDependency; - productName = WireSettingsUI; + productName = WireFoundationSupport; }; - 59B48C5D2C89CCAB00EA7999 /* WireFoundationSupport */ = { + 59B768462D2D59E3007B5F1E /* WireLoggingSupport */ = { isa = XCSwiftPackageProductDependency; - productName = WireFoundationSupport; + productName = WireLoggingSupport; }; - 59B48C5F2C89CCCC00EA7999 /* WireFoundation */ = { + 59B768482D2D59E8007B5F1E /* WireLoggingSupport */ = { isa = XCSwiftPackageProductDependency; - productName = WireFoundation; + productName = WireLoggingSupport; }; - 59B48C612C89CD3D00EA7999 /* WireTestingPackage */ = { + 59B7684A2D2D5A17007B5F1E /* WireFoundationSupport */ = { isa = XCSwiftPackageProductDependency; - productName = WireTestingPackage; + productName = WireFoundationSupport; }; 59B99FA92C89DE8600201827 /* WireFoundation */ = { isa = XCSwiftPackageProductDependency; @@ -11526,10 +11464,6 @@ isa = XCSwiftPackageProductDependency; productName = WireAPI; }; - 59C2F09E2CA54E4900B25E5D /* WireMainNavigationUI */ = { - isa = XCSwiftPackageProductDependency; - productName = WireMainNavigationUI; - }; 59CDB3F52C4EA08F0049D1AB /* WireReusableUIComponents */ = { isa = XCSwiftPackageProductDependency; productName = WireReusableUIComponents; @@ -11555,10 +11489,6 @@ package = CB4E15102C81CC81005DDEC8 /* XCRemoteSwiftPackageReference "Down" */; productName = Down; }; - E9816C992CC929FC00D77F22 /* WireFoundationSupport */ = { - isa = XCSwiftPackageProductDependency; - productName = WireFoundationSupport; - }; E9BA75C52CD51DF100F6EDDF /* WireMoveToFolderUI */ = { isa = XCSwiftPackageProductDependency; productName = WireMoveToFolderUI; diff --git a/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme b/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme index 27e9287c6bc..fd557528c70 100644 --- a/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme +++ b/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme @@ -179,6 +179,21 @@ value = "oslogToStdio" isEnabled = "NO"> + + + + + + Void, - onCancel: @escaping () -> Void - ) -> UIAlertController { - let controller = UIAlertController( - title: title(for: error), - message: message(for: error), - preferredStyle: .alert - ) - - let tryAgainAction = UIAlertAction( - title: L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.tryAgain, - style: .default, - handler: { _ in onTryAgain() } - ) - - controller.addAction(tryAgainAction) - controller.addAction(.cancel { onCancel() }) - return controller - } - - private func title(for error: Error) -> String { - switch error { - case - CoreDataStack.BackupImportError - .incompatibleBackup(BackupMetadata.VerificationError.backupFromNewerAppVersion): - L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.WrongVersion.title - case CoreDataStack.BackupImportError.incompatibleBackup(BackupMetadata.VerificationError.userMismatch): - L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.WrongAccount.title - default: - L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.title - } - } - - private func message(for error: Error) -> String { - switch error { - case CoreDataStack.BackupImportError - .incompatibleBackup(BackupMetadata.VerificationError.backupFromNewerAppVersion): - L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.WrongVersion.message - case CoreDataStack.BackupImportError.incompatibleBackup(BackupMetadata.VerificationError.userMismatch): - L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.WrongAccount.message - default: - L10n.Localizable.Registration.NoHistory.RestoreBackupFailed.message + "\n\(error.localizedDescription)" - } - } - -} diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController+Password.swift b/wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController+Password.swift deleted file mode 100644 index e7db90a9a0f..00000000000 --- a/wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController+Password.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import UIKit - -extension BackupRestoreController { - - func requestRestorePassword(completion: @escaping (String?) -> Void) -> UIAlertController { - let controller = UIAlertController( - title: L10n.Localizable.Registration.NoHistory.RestoreBackup.Password.title, - message: nil, - preferredStyle: .alert - ) - - var token: Any? - - func complete(_ result: String?) { - token.map(NotificationCenter.default.removeObserver) - completion(result) - } - - let okAction = UIAlertAction(title: L10n.Localizable.General.ok, style: .default) { [controller] _ in - complete(controller.textFields?.first?.text) - } - - okAction.isEnabled = false - - controller.addTextField { textField in - textField.isSecureTextEntry = true - textField.placeholder = L10n.Localizable.Registration.NoHistory.RestoreBackup.Password.placeholder - token = NotificationCenter.default.addObserver( - forName: UITextField.textDidChangeNotification, - object: textField, - queue: .main - ) { _ in - okAction.isEnabled = textField.text?.count ?? 0 >= 0 - } - } - - controller.addAction(.cancel { complete(nil) }) - controller.addAction(okAction) - return controller - } - - func importWrongPasswordError(completion: @escaping (UIAlertAction) -> Void) -> UIAlertController { - let controller = UIAlertController( - title: L10n.Localizable.Registration.NoHistory.RestoreBackup.PasswordError.title, - message: nil, - preferredStyle: .alert - ) - controller.addAction(UIAlertAction( - title: L10n.Localizable.General.ok, - style: .default, - handler: completion - )) - - return controller - } - -} diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController.swift b/wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController.swift deleted file mode 100644 index 676bf091c6b..00000000000 --- a/wire-ios/Wire-iOS/Sources/Authentication/Backup/BackupRestoreController.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import Foundation -import UniformTypeIdentifiers -import WireDataModel -import WireLogging -import WireReusableUIComponents -import WireSyncEngine - -protocol BackupRestoreControllerDelegate: AnyObject { - - func backupResoreControllerDidFinishRestoring( - _ controller: BackupRestoreController, - didSucceed: Bool - ) - -} - -/// An object that coordinates restoring a backup. - -final class BackupRestoreController: NSObject { - - // There are some external apps that users can use to transfer backup files, which can modify - // their attachments and change the underscore with a dash. This is the reason we accept 2 types - // of file extensions: 'ios_wbu' and 'ios-wbu'. - - static let WireBackupUTIs = [ - "com.wire.backup-universal", - "com.wire.backup-ios-underscore", - "com.wire.backup-ios-hyphen" - ] - - weak var delegate: BackupRestoreControllerDelegate? - - private let target: UIViewController - private let activityIndicator: BlockingActivityIndicator - private var temporaryFilesService: TemporaryFileServiceInterface - - // MARK: - Initialization - - init( - target: UIViewController, - temporaryFilesService: TemporaryFileServiceInterface = TemporaryFileService() - ) { - self.target = target - self.temporaryFilesService = temporaryFilesService - self.activityIndicator = .init(view: target.view) - super.init() - } - - // MARK: - Flow - - func startBackupFlow() { - let controller = UIAlertController( - title: L10n.Localizable.Registration.NoHistory.RestoreBackupWarning.title, - message: L10n.Localizable.Registration.NoHistory.RestoreBackupWarning.message, - preferredStyle: .alert - ) - controller.addAction(.cancel()) - controller.addAction(UIAlertAction( - title: L10n.Localizable.Registration.NoHistory.RestoreBackupWarning.proceed, - style: .default, - handler: { [showFilePicker] _ in - showFilePicker() - } - )) - - target.present(controller, animated: true) - } - - private func showFilePicker() { - let picker = UIDocumentPickerViewController( - forOpeningContentTypes: BackupRestoreController.WireBackupUTIs.compactMap { UTType($0) }, - asCopy: true - ) - - picker.delegate = self - target.present(picker, animated: true) - } - - private func restore(with url: URL) { - requestPassword { password in - self.performRestore( - using: password, - from: url - ) - } - } - - private func performRestore( - using password: String, - from url: URL - ) { - guard - let sessionManager = SessionManager.shared, - let activity = BackgroundActivityFactory.shared.startBackgroundActivity(name: "restore backup") - else { - return - } - - Task { @MainActor in activityIndicator.start() } - - sessionManager.restoreFromBackup(at: url, password: password) { [weak self] result in - guard let self else { - BackgroundActivityFactory.shared.endBackgroundActivity(activity) - WireLogger.localStorage.error("SessionManager.self is `nil` in performRestore") - return - } - - switch result { - case .failure(SessionManager.BackupError.decryptionError): - WireLogger.localStorage.error("Failed restoring backup: \(SessionManager.BackupError.decryptionError)") - Task { @MainActor in self.activityIndicator.stop() } - BackgroundActivityFactory.shared.endBackgroundActivity(activity) - showWrongPasswordAlert { _ in - self.restore(with: url) - } - - case let .failure(error): - WireLogger.localStorage.error("Failed restoring backup: \(error)") - showRestoreError(error) - Task { @MainActor in self.activityIndicator.stop() } - BackgroundActivityFactory.shared.endBackgroundActivity(activity) - - case .success: - temporaryFilesService.removeTemporaryData() - delegate?.backupResoreControllerDidFinishRestoring(self, didSucceed: true) - BackgroundActivityFactory.shared.endBackgroundActivity(activity) - } - } - } - - // MARK: - Alerts - - private func requestPassword(completion: @escaping (String) -> Void) { - let controller = requestRestorePassword { password in - password.map(completion) - } - - target.present(controller, animated: true, completion: nil) - } - - private func showWrongPasswordAlert(completion: @escaping (UIAlertAction) -> Void) { - let controller = importWrongPasswordError(completion: completion) - target.present(controller, animated: true, completion: nil) - } - - private func showRestoreError(_ error: Error) { - let controller = restoreBackupFailed( - error: error, - onTryAgain: { [unowned self] in showFilePicker() }, - onCancel: { [unowned self] in delegate?.backupResoreControllerDidFinishRestoring(self, didSucceed: false) } - ) - - target.present(controller, animated: true) - } -} - -extension BackupRestoreController: UIDocumentPickerDelegate { - func documentPicker( - _ controller: UIDocumentPickerViewController, - didPickDocumentAt url: URL - ) { - WireLogger.localStorage.debug("opening file at: \(url.absoluteString)") - - restore(with: url) - } -} diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift b/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift index 2256e8ea2b0..4a67b250d1e 100644 --- a/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift +++ b/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift @@ -89,9 +89,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationEventResponderCha /// The object to use to start and control the company login flow. let companyLoginController = CompanyLoginController(withDefaultEnvironment: ()) - /// The object to use to restore backups. - let backupRestoreController: BackupRestoreController - // MARK: - Internal State private var loginObservers: [Any] = [] @@ -128,13 +125,11 @@ final class AuthenticationCoordinator: NSObject, AuthenticationEventResponderCha self.stateController = AuthenticationStateController() self.interfaceBuilder = AuthenticationInterfaceBuilder(featureProvider: featureProvider) self.eventResponderChain = AuthenticationEventResponderChain(featureProvider: featureProvider) - self.backupRestoreController = BackupRestoreController(target: presenter) super.init() updateLoginObservers() self.unauthenticatedSessionObserver = sessionManager .addUnauthenticatedSessionManagerCreatedSessionObserver(self) companyLoginController?.delegate = self - backupRestoreController.delegate = self presenter.delegate = self stateController.delegate = self eventResponderChain.configure(delegate: self) @@ -372,9 +367,6 @@ extension AuthenticationCoordinator: AuthenticationActioner, SessionManagerCreat case let .startLoginFlow(request, credentials): startLoginFlow(request: request, proxyCredentials: credentials) - case .startBackupFlow: - backupRestoreController.startBackupFlow() - case let .signOut(warn): signOut(warn: warn) diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Event Handlers/AuthenticationCoordinatorAction.swift b/wire-ios/Wire-iOS/Sources/Authentication/Event Handlers/AuthenticationCoordinatorAction.swift index bd381ff1b29..d66ecc2a52b 100644 --- a/wire-ios/Wire-iOS/Sources/Authentication/Event Handlers/AuthenticationCoordinatorAction.swift +++ b/wire-ios/Wire-iOS/Sources/Authentication/Event Handlers/AuthenticationCoordinatorAction.swift @@ -47,7 +47,6 @@ enum AuthenticationCoordinatorAction { case updateBackendEnvironment(url: URL) case startCompanyLogin(code: UUID?) case startSSOFlow - case startBackupFlow case signOut(warn: Bool) case addEmailAndPassword(UserEmailCredentials) case configureDevicePermissions diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/BackupRestoreStepDescription.swift b/wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/NoHistoryHintStepDescription.swift similarity index 65% rename from wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/BackupRestoreStepDescription.swift rename to wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/NoHistoryHintStepDescription.swift index 2623509d6fa..0522b379bd9 100644 --- a/wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/BackupRestoreStepDescription.swift +++ b/wire-ios/Wire-iOS/Sources/Authentication/Interface/Descriptions/ScreenDescriptions/Login/NoHistoryHintStepDescription.swift @@ -19,37 +19,16 @@ import UIKit import WireCommonComponents -/// The view that displays the restore from backup button. +/// The step that displays information about not having any conversation history. -final class BackupRestoreStepDescriptionFooterView: AuthenticationFooterViewDescription { +final class NoHistoryHintStepDescription: AuthenticationStepDescription { - let views: [ViewDescriptor] - weak var actioner: AuthenticationActioner? - - typealias NoHistory = L10n.Localizable.Registration.NoHistory - - init() { - let restoreButton = SecondaryButtonDescription( - title: NoHistory.restoreBackup.capitalized, - accessibilityIdentifier: "restore_backup" - ) - self.views = [restoreButton] - - restoreButton.buttonTapped = { [weak self] in - self?.actioner?.executeAction(.startBackupFlow) - } - } -} - -/// The step that displays information about the history. - -final class BackupRestoreStepDescription: AuthenticationStepDescription { let backButton: BackButtonDescription? let mainView: ViewDescriptor & ValueSubmission let headline: String let subtext: NSAttributedString? let secondaryView: AuthenticationSecondaryViewDescription? - let footerView: AuthenticationFooterViewDescription? + let footerView: AuthenticationFooterViewDescription? = nil init(context: NoHistoryContext) { @@ -67,7 +46,6 @@ final class BackupRestoreStepDescription: AuthenticationStepDescription { self.headline = L10n.Localizable.Registration.NoHistory.LoggedOut.hero self.subtext = .markdown(from: L10n.Localizable.Registration.NoHistory.LoggedOut.subtitle, style: .login) } - self.footerView = BackupRestoreStepDescriptionFooterView() } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift index a4465970798..8b5095af6d1 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift @@ -242,7 +242,7 @@ final class ConversationListViewModel: NSObject { .removeDuplicates() .receive(on: RunLoop.main) .sink { [weak userSession] _ in - guard let userSession else { return } + guard let userSession, !userSession.isTornDown else { return } userSession.conversationDirectory.refetchAllLists(in: userSession.contextProvider.viewContext) }.store(in: &tokens) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupActionCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupActionCell.swift deleted file mode 100644 index 12397d4bdbd..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupActionCell.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import UIKit -import WireDesign - -final class BackupActionCell: UITableViewCell { - let actionTitleLabel: DynamicFontLabel = { - let text = L10n.Localizable.Self.Settings.HistoryBackup.action - let label = DynamicFontLabel( - text: text, - style: .body2, - color: SemanticColors.Label.textDefault - ) - label.textAlignment = .left - return label - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - selectionStyle = .none - backgroundColor = SemanticColors.View.backgroundUserCell - accessibilityTraits = .button - contentView.backgroundColor = .clear - - actionTitleLabel.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(actionTitleLabel) - NSLayoutConstraint.activate([ - actionTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), - actionTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), - actionTitleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), - actionTitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0) - ]) - actionTitleLabel.heightAnchor.constraint(equalToConstant: 44).isActive = true - addBorder(for: .bottom) - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordValidator.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordValidator.swift new file mode 100644 index 00000000000..02bf6281505 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordValidator.swift @@ -0,0 +1,40 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireSettingsUI + +struct BackupPasswordValidator: BackupPasswordValidatorProtocol { + + func isPasswordValid(_ password: String) -> Bool { + guard !password.isEmpty else { + return true + } + + switch PasswordRuleSet.shared.validatePassword(password) { + case .valid: + return true + case .invalid: + return false + } + } + + var localizedRulesDescription: String { + PasswordRuleSet.localizedErrorMessage + } + +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordViewController.swift deleted file mode 100644 index 4efa04d78bd..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupPasswordViewController.swift +++ /dev/null @@ -1,187 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import UIKit -import WireDesign - -final class BackupPasswordViewController: UIViewController { - - typealias ViewColors = SemanticColors.View - typealias LabelColors = SemanticColors.Label - typealias HistoryBackup = L10n.Localizable.Self.Settings.HistoryBackup - - var onCompletion: ((_ password: String?) -> Void)? - - private var password: String? - private let passwordView = SimpleTextField() - - private let subtitleLabel: DynamicFontLabel = { - let label = DynamicFontLabel( - text: HistoryBackup.Password.description, - style: .subline1, - color: LabelColors.textSectionHeader - ) - label.numberOfLines = 0 - return label - }() - - private let passwordRulesLabel: DynamicFontLabel = { - let label = DynamicFontLabel( - style: .subline1, - color: LabelColors.textSectionHeader - ) - label.numberOfLines = 0 - return label - }() - - init() { - super.init(nibName: nil, bundle: nil) - - setupViews() - createConstraints() - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - setupNavigationBar() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - passwordView.becomeFirstResponder() - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - wr_supportedInterfaceOrientations - } - - private func setupViews() { - view.backgroundColor = ViewColors.backgroundDefault - passwordRulesLabel.text = PasswordRuleSet.localizedErrorMessage - - [passwordView, subtitleLabel, passwordRulesLabel].forEach { - view.addSubview($0) - $0.translatesAutoresizingMaskIntoConstraints = false - } - - passwordView.placeholder = HistoryBackup.Password.placeholder - passwordView.accessibilityIdentifier = "password input" - passwordView.accessibilityHint = PasswordRuleSet.localizedErrorMessage - passwordView.returnKeyType = .done - passwordView.isSecureTextEntry = true - passwordView.delegate = self - passwordView.textColor = LabelColors.textSectionHeader - passwordView.backgroundColor = ViewColors.backgroundUserCell - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: SemanticColors.SearchBar.textInputViewPlaceholder, - .font: UIFont.font(for: .body1) - ] - passwordView.updatePlaceholderAttributedText(attributes: attributes) - } - - private func createConstraints() { - NSLayoutConstraint.activate([ - passwordView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - passwordView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - passwordView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - passwordView.heightAnchor.constraint(equalToConstant: 56), - subtitleLabel.bottomAnchor.constraint(equalTo: passwordView.topAnchor, constant: -16), - subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - passwordRulesLabel.topAnchor.constraint(equalTo: passwordView.bottomAnchor, constant: 16), - passwordRulesLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - passwordRulesLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16) - ]) - } - - private func setupNavigationBar() { - navigationController?.navigationBar.backgroundColor = ViewColors.backgroundDefault - - setupNavigationBarTitle(HistoryBackup.Password.title) - - let cancelButtonItem = UIBarButtonItem.createNavigationLeftBarButtonItem( - title: HistoryBackup.Password.cancel, - action: UIAction { [weak self] _ in - self?.onCompletion?(nil) - } - ) - - let nextButtonItem = UIBarButtonItem.createNavigationRightBarButtonItem( - title: HistoryBackup.Password.next, - action: UIAction { [weak self] _ in - self?.onCompletion?(self?.password) - } - ) - - nextButtonItem.tintColor = UIColor.accent() - nextButtonItem.isEnabled = false - - navigationItem.leftBarButtonItem = cancelButtonItem - navigationItem.rightBarButtonItem = nextButtonItem - } - - private func updateState(with text: String) { - switch PasswordRuleSet.shared.validatePassword(text) { - case .valid: - password = text - navigationItem.rightBarButtonItem?.isEnabled = true - case .invalid: - password = nil - navigationItem.rightBarButtonItem?.isEnabled = false - } - } - - @objc - private dynamic func completeWithCurrentResult() { - onCompletion?(password) - } -} - -// MARK: - UITextFieldDelegate - -extension BackupPasswordViewController: UITextFieldDelegate { - - func textField( - _ textField: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - - if string.containsCharacters(from: .whitespaces) { - return false - } - - if string.containsCharacters(from: .newlines) { - if password != nil { - completeWithCurrentResult() - } - return false - } - - let newString = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) - - updateState(with: newString) - - return true - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupStatusCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupStatusCell.swift deleted file mode 100644 index d4e856ff07c..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupStatusCell.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import UIKit -import WireDesign - -final class BackupStatusCell: UITableViewCell { - - let descriptionLabel: DynamicFontLabel = { - let label = DynamicFontLabel( - style: .body1, - color: SemanticColors.Label.textDefault - ) - label.textAlignment = .left - label.numberOfLines = 0 - return label - }() - - let iconView = UIImageView() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - contentView.backgroundColor = .clear - - iconView.setTemplateIcon(.restore, size: .large) - iconView.tintColor = SemanticColors.Label.textDefault - iconView.contentMode = .center - iconView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(iconView) - - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(descriptionLabel) - - NSLayoutConstraint.activate([ - iconView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), - iconView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor), - iconView.widthAnchor.constraint(equalToConstant: 48), - descriptionLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 24), - descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), - descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), - descriptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) - ]) - - let description = L10n.Localizable.Self.Settings.HistoryBackup.description - descriptionLabel.attributedText = description && .paragraphSpacing(2) - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupViewController.swift deleted file mode 100644 index 3eb5111a07c..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/BackupViewController.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// Wire -// Copyright (C) 2025 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import UIKit -import WireDesign -import WireReusableUIComponents - -final class BackupViewController: UIViewController { - - private let tableView = UITableView(frame: .zero) - private var cells: [UITableViewCell.Type] = [] - private let backupSource: BackupSource - private lazy var activityIndicator = BlockingActivityIndicator(view: navigationController?.view ?? view) - - init(backupSource: BackupSource) { - self.backupSource = backupSource - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setupViews() - setupLayout() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - setupNavigationBarTitle(L10n.Localizable.Self.Settings.HistoryBackup.title.capitalized) - } - - private func setupViews() { - view.backgroundColor = ColorTheme.Backgrounds.background - - tableView.isScrollEnabled = false - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 80 - tableView.backgroundColor = .clear - tableView.separatorColor = UIColor(white: 1, alpha: 0.1) - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) - tableView.delegate = self - tableView.dataSource = self - - // this is necessary to remove the placeholder cells - tableView.tableFooterView = UIView() - cells = [BackupStatusCell.self, BackupActionCell.self] - - cells.forEach { - tableView.register($0.self, forCellReuseIdentifier: $0.reuseIdentifier) - } - } - - private func setupLayout() { - tableView.fitIn(view: view) - } -} - -// MARK: - UITableViewDataSource & UITableViewDelegate - -extension BackupViewController: UITableViewDataSource, UITableViewDelegate { - - func numberOfSections(in tableView: UITableView) -> Int { - 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - cells.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - tableView.dequeueReusableCell(withIdentifier: cells[indexPath.row].reuseIdentifier, for: indexPath) - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - guard indexPath.row == 1 else { return } - backupActiveAccount(indexPath: indexPath) - } -} - -// MARK: - Backup Logic - -private extension BackupViewController { - - func backupActiveAccount(indexPath: IndexPath) { - requestBackupPassword { [weak self] result in - guard let self, let password = result else { return } - activityIndicator.start() - - backupSource.backupActiveAccount(password: password) { backupResult in - self.activityIndicator.stop() - - switch backupResult { - case let .failure(error): - self.presentAlert(for: error) - case let .success(url): - self.presentShareSheet(with: url, from: indexPath) - } - } - } - } - - private func requestBackupPassword(completion: @escaping (String?) -> Void) { - let passwordController = BackupPasswordViewController() - passwordController.onCompletion = { [weak passwordController] password in - passwordController?.dismiss(animated: true) { - completion(password) - } - } - let navigationController = KeyboardAvoidingViewController(viewController: passwordController) - .wrapInNavigationController() - navigationController.modalPresentationStyle = .formSheet - present(navigationController, animated: true) - } - - private func presentAlert(for error: Error) { - let alert = UIAlertController( - title: L10n.Localizable.Self.Settings.HistoryBackup.Error.title, - message: error.localizedDescription, - preferredStyle: .alert - ) - alert.addAction(UIAlertAction( - title: L10n.Localizable.General.ok, - style: .cancel - )) - - present(alert, animated: true) - } - - private func presentShareSheet(with url: URL, from indexPath: IndexPath) { - let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil) - activityController.completionWithItemsHandler = { [weak self] _, _, _, _ in - self?.backupSource.clearPreviousBackups() - } - activityController.popoverPresentationController.map { - $0.sourceView = tableView - $0.sourceRect = tableView.rectForRow(at: indexPath) - } - present(activityController, animated: true) - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CleanUpBackupsUseCase.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CleanUpBackupsUseCase.swift new file mode 100644 index 00000000000..25e089ae225 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CleanUpBackupsUseCase.swift @@ -0,0 +1,35 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireSettingsUI +import WireSyncEngine + +struct CleanUpBackupsUseCase: CleanUpBackupsUseCaseProtocol { + + var sessionManager: @Sendable @MainActor () -> SessionManager + + init(sessionManager: @autoclosure @escaping @Sendable @MainActor () -> SessionManager) { + self.sessionManager = sessionManager + } + + @MainActor + func invoke() async throws { + let sessionManager = sessionManager() + sessionManager.clearPreviousBackups() + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CreateLegacyBackupUseCase.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CreateLegacyBackupUseCase.swift new file mode 100644 index 00000000000..e5f02581600 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Backup/CreateLegacyBackupUseCase.swift @@ -0,0 +1,58 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireDomainPkg +import WireSettingsUI +import WireSyncEngine + +/// Use case for creating a backup file which can only used by iOS apps. +struct CreateLegacyBackupUseCase: CreateBackupUseCaseProtocol { + + var sessionManager: @Sendable @MainActor () -> SessionManager + + init(sessionManager: @autoclosure @Sendable @MainActor @escaping () -> SessionManager) { + self.sessionManager = sessionManager + } + + func invoke(password: String) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { @MainActor in + do { + + let sessionManager = sessionManager() + + continuation.yield(.progress(0.5)) + + let url = try await withCheckedThrowingContinuation { continuation in + sessionManager.backupActiveAccount(password: password) { result in + continuation.resume(with: result) + } + } + + continuation.yield(.done(url)) + continuation.finish() + + } catch { + continuation.finish(throwing: error) + } + } + } + } + +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Account.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Account.swift index 380c4fab76c..f79df7db88a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Account.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Account.swift @@ -20,6 +20,8 @@ import SwiftUI import WireCommonComponents import WireDataModel import WireDesign +import WireLogging +import WireSettingsUI import WireSyncEngine extension ZMUser { @@ -34,6 +36,7 @@ extension ZMUser { extension SettingsCellDescriptorFactory { + @MainActor func accountGroup( isPublicDomain: Bool, userSession: UserSession, @@ -158,6 +161,7 @@ extension SettingsCellDescriptorFactory { ) } + @MainActor func conversationsSection() -> SettingsSectionDescriptorType { SettingsSectionDescriptor( cellDescriptors: [backUpElement()], @@ -364,6 +368,23 @@ extension SettingsCellDescriptorFactory { SettingsPropertyToggleCellDescriptor(settingsProperty: settingsPropertyFactory.property(.encryptMessagesAtRest)) } + private var backupImportExportBuilder: BackupImportExportBuilder { + + // force-unwrapping should be fine, since we should have a session manager and an active user session here + let sessionManager = SessionManager.shared! + let importBackupUseCase = sessionManager.importBackupUseCase! + + return BackupImportExportBuilder( + backupPasswordValidator: BackupPasswordValidator(), + createBackupUseCase: CreateLegacyBackupUseCase(sessionManager: sessionManager), + importBackupUseCase: importBackupUseCase, + cleanUpBackupsUseCase: CleanUpBackupsUseCase(sessionManager: sessionManager), + exportBackupLogger: WireLogger.backupExport, + importBackupLogger: WireLogger.backupImport + ) + } + + @MainActor func backUpElement() -> any SettingsCellDescriptorType { SettingsExternalScreenCellDescriptor( title: L10n.Localizable.Self.Settings.HistoryBackup.title, @@ -375,7 +396,9 @@ extension SettingsCellDescriptorFactory { return .none } if selfUser.hasValidEmail || selfUser.usesCompanyLogin { - return BackupViewController(backupSource: SessionManager.shared!) + let backupRestoreController = backupImportExportBuilder.build() + backupRestoreController.setupNavigationBarTitle(L10n.Localizable.Self.Settings.HistoryBackup.title) + return backupRestoreController } else { let alert = UIAlertController( title: L10n.Localizable.Self.Settings.HistoryBackup.SetEmail.title, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory.swift index 210e55e78c9..5276bae25d0 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory.swift @@ -115,6 +115,7 @@ struct SettingsCellDescriptorFactory { ) } + @MainActor func settingsGroup( isPublicDomain: Bool, userSession: UserSession,