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 0ba158d0a99..f0a5630707d 100644 Binary files a/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_LoggedOut.1.png and b/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_LoggedOut.1.png differ 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 4d25c1c7411..335454ecd5e 100644 Binary files a/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_NewDevice.1.png and b/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testBackupScreen_NewDevice.1.png differ 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 036993aede7..e015bb0fd6b 100644 Binary files a/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testLoginScreen_Email_WithProxyAuthenticated.1.png and b/wire-ios/Wire-iOS Tests/ReferenceImages/AuthenticationInterfaceBuilderTests/testLoginScreen_Email_WithProxyAuthenticated.1.png differ 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,