diff --git a/.gitignore b/.gitignore index c8e1b674a15d27d9b158ddfd077cdd45b0b16339..839d127c8fff949c405a77d770e8f22feb8b2b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,8 @@ xcuserdata/ *.dSYM.zip *.dSYM *.p8 -*/cert_mainnet -*/GoogleService-Info.plist +Sources/Integration/Resources/udContact-test.bin +Sources/Integration/Resources/ud.elixxir.io.crt +Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist +Sources/DropboxFeature/Resources/Dropbox-Keys.plist +App/client-ios/Resources/GoogleService-Info.plist \ No newline at end of file diff --git a/App/NotificationExtension/NotificationService.swift b/App/NotificationExtension/NotificationService.swift index 7a733b0d02dc77c886c246c63b6c1e0934c0231c..1c92ce4a7750684edcf92cbf933f616c89881f13 100644 --- a/App/NotificationExtension/NotificationService.swift +++ b/App/NotificationExtension/NotificationService.swift @@ -68,6 +68,8 @@ class NotificationService: UNNotificationServiceExtension { content.title = "New media received" case "groupRq": content.title = "Group request received" + case "reset": + content.title = "One of your contacts has restored their account" default: break } diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index e7eaeac4bfeb69bf04b15ccdc40ad51676a77f62..76f38ed41ee371a895528771b590f85c69276b6c 100644 --- a/App/client-ios.xcodeproj/project.pbxproj +++ b/App/client-ios.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 32179BAC26410149008B26EC /* NotificationExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 32179BA526410149008B26EC /* NotificationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3273327126C7391D0027D79D /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 3273327026C7391D0027D79D /* App */; }; 32824C8F26EAE13D005D3FAC /* Bindings in Frameworks */ = {isa = PBXBuildFile; productRef = 32824C8E26EAE13D005D3FAC /* Bindings */; }; + 32C194E02808C65500876917 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 32C194DF2808C65500876917 /* GoogleService-Info.plist */; }; + 32C194E12808C65500876917 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 32C194DF2808C65500876917 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -53,6 +55,7 @@ 32179BA526410149008B26EC /* NotificationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 32179BA726410149008B26EC /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; }; 32179BA926410149008B26EC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 32C194DF2808C65500876917 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; }; 32DB0549264DD42000FDCCEB /* NotificationExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationExtension.entitlements; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -82,6 +85,7 @@ children = ( 02CB8FEB2326B5BE00A39834 /* client-ios.entitlements */, 02FDD06C21EDA39B000F1286 /* Assets.xcassets */, + 32C194DF2808C65500876917 /* GoogleService-Info.plist */, 02FDD07121EDA39B000F1286 /* Info.plist */, ); path = Resources; @@ -145,8 +149,8 @@ 02FDD05E21EDA39A000F1286 /* Sources */, 02FDD05F21EDA39A000F1286 /* Frameworks */, 02FDD06021EDA39A000F1286 /* Resources */, - 028B1320260A99C800054510 /* ShellScript */, 32179BAD26410149008B26EC /* Embed App Extensions */, + 3289935B27DB3EEF003E1407 /* ShellScript */, ); buildRules = ( ); @@ -168,7 +172,6 @@ 32179BA126410149008B26EC /* Sources */, 32179BA226410149008B26EC /* Frameworks */, 32179BA326410149008B26EC /* Resources */, - 32151006264CAE8700A0F50D /* ShellScript */, ); buildRules = ( ); @@ -234,6 +237,7 @@ files = ( 02FDD07021EDA39B000F1286 /* LaunchScreen.storyboard in Resources */, 02FDD06D21EDA39B000F1286 /* Assets.xcassets in Resources */, + 32C194E02808C65500876917 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -241,13 +245,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 32C194E12808C65500876917 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 028B1320260A99C800054510 /* ShellScript */ = { + 3289935B27DB3EEF003E1407 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -262,24 +267,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "$SRCROOT/set_build_number.sh\n"; - }; - 32151006264CAE8700A0F50D /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "$SRCROOT/set_build_number.sh\n"; + shellScript = "${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -368,6 +356,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + "SWIFT_PACKAGE=1", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -381,6 +370,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = ""; SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -427,6 +417,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = "SWIFT_PACKAGE=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -438,6 +429,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -456,7 +448,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 48; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -471,7 +463,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.7; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -495,7 +487,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 48; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -511,7 +503,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.7; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -530,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -544,7 +536,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock.notifications; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -561,7 +553,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -575,7 +567,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.notifications; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/App/client-ios/Resources/GoogleService-Info.plist b/App/client-ios/Resources/GoogleService-Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..03e09469daae0502a5202f6e63aca9db140d1d77 --- /dev/null +++ b/App/client-ios/Resources/GoogleService-Info.plist @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CLIENT_ID</key> + <string></string> + <key>REVERSED_CLIENT_ID</key> + <string></string> + <key>ANDROID_CLIENT_ID</key> + <string></string> + <key>API_KEY</key> + <string></string> + <key>GCM_SENDER_ID</key> + <string></string> + <key>PLIST_VERSION</key> + <string></string> + <key>BUNDLE_ID</key> + <string></string> + <key>PROJECT_ID</key> + <string></string> + <key>STORAGE_BUCKET</key> + <string></string> + <key>IS_ADS_ENABLED</key> + <false/> + <key>IS_ANALYTICS_ENABLED</key> + <false/> + <key>IS_APPINVITE_ENABLED</key> + <false/> + <key>IS_GCM_ENABLED</key> + <false/> + <key>IS_SIGNIN_ENABLED</key> + <false/> + <key>GOOGLE_APP_ID</key> + <string></string> +</dict> +</plist> diff --git a/App/client-ios/Resources/Info.plist b/App/client-ios/Resources/Info.plist index 100e1405c342feb92325536fc967cc83456bacc5..ed25d86cf62d659ee34ae0f720d9de7f8a56fefb 100644 --- a/App/client-ios/Resources/Info.plist +++ b/App/client-ios/Resources/Info.plist @@ -24,6 +24,14 @@ <string>$(MARKETING_VERSION)</string> <key>CFBundleURLTypes</key> <array> + <dict> + <key>CFBundleURLName</key> + <string></string> + <key>CFBundleURLSchemes</key> + <array> + <string>db-ppx0de5f16p9aq2</string> + </array> + </dict> <dict> <key>CFBundleURLName</key> <string>xxmessenger</string> @@ -32,6 +40,14 @@ <string>xxmessenger</string> </array> </dict> + <dict> + <key>CFBundleTypeRole</key> + <string>Editor</string> + <key>CFBundleURLSchemes</key> + <array> + <string>com.googleusercontent.apps.662236151640-30i07ubg6ukodg15u0bnpk322p030u3j</string> + </array> + </dict> </array> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> @@ -39,7 +55,8 @@ <false/> <key>LSApplicationQueriesSchemes</key> <array> - <string>prelixxir</string> + <string>dbapi-8-emm</string> + <string>dbapi-2</string> </array> <key>LSRequiresIPhoneOS</key> <true/> @@ -58,6 +75,18 @@ <string>This permission is required to create voice notes</string> <key>NSPhotoLibraryUsageDescription</key> <string>This permission is required to send photos in chat and change your profile avatar.</string> + <key>NSUbiquitousContainers</key> + <dict> + <key>iCloud.xxm-icloud</key> + <dict> + <key>NSUbiquitousContainerIsDocumentScopePublic</key> + <true/> + <key>NSUbiquitousContainerName</key> + <string>xx messenger</string> + <key>NSUbiquitousContainerSupportedFolderLevels</key> + <string>Any</string> + </dict> + </dict> <key>UIBackgroundModes</key> <array> <string>processing</string> diff --git a/App/client-ios/Resources/client-ios.entitlements b/App/client-ios/Resources/client-ios.entitlements index 711179f6d68ce953b1647593946d418411ea0422..ce90eb85306477465bf77307eed53d5434c6ba48 100644 --- a/App/client-ios/Resources/client-ios.entitlements +++ b/App/client-ios/Resources/client-ios.entitlements @@ -2,12 +2,24 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>com.apple.developer.usernotifications.filtering</key> - <true/> <key>aps-environment</key> <string>development</string> + <key>com.apple.developer.icloud-container-identifiers</key> + <array> + <string>iCloud.xxm-cloud</string> + </array> + <key>com.apple.developer.icloud-services</key> + <array> + <string>CloudDocuments</string> + </array> <key>com.apple.developer.networking.multipath</key> <true/> + <key>com.apple.developer.ubiquity-container-identifiers</key> + <array> + <string>iCloud.xxm-cloud</string> + </array> + <key>com.apple.developer.usernotifications.filtering</key> + <true/> <key>com.apple.security.application-groups</key> <array> <string>group.io.xxlabs.notification</string> diff --git a/Package.swift b/Package.swift index 495a08601d5bc952358ca872dbe461c34fff922f..ed7f12f2320f58eff87fb47cfcecc45d369c8d83 100644 --- a/Package.swift +++ b/Package.swift @@ -30,11 +30,15 @@ let package = Package( .library(name: "ChatFeature", targets: ["ChatFeature"]), .library(name: "CrashService", targets: ["CrashService"]), .library(name: "Presentation", targets: ["Presentation"]), + .library(name: "BackupFeature", targets: ["BackupFeature"]), + .library(name: "iCloudFeature", targets: ["iCloudFeature"]), .library(name: "SearchFeature", targets: ["SearchFeature"]), + .library(name: "RestoreFeature", targets: ["RestoreFeature"]), .library(name: "CrashReporting", targets: ["CrashReporting"]), .library(name: "ProfileFeature", targets: ["ProfileFeature"]), .library(name: "ContactFeature", targets: ["ContactFeature"]), .library(name: "NetworkMonitor", targets: ["NetworkMonitor"]), + .library(name: "DropboxFeature", targets: ["DropboxFeature"]), .library(name: "VersionChecking", targets: ["VersionChecking"]), .library(name: "SettingsFeature", targets: ["SettingsFeature"]), .library(name: "ChatListFeature", targets: ["ChatListFeature"]), @@ -42,6 +46,7 @@ let package = Package( .library(name: "ChatInputFeature", targets: ["ChatInputFeature"]), .library(name: "PushNotifications", targets: ["PushNotifications"]), .library(name: "OnboardingFeature", targets: ["OnboardingFeature"]), + .library(name: "GoogleDriveFeature", targets: ["GoogleDriveFeature"]), .library(name: "ContactListFeature", targets: ["ContactListFeature"]), .library(name: "DependencyInjection", targets: ["DependencyInjection"]) ], @@ -61,11 +66,26 @@ let package = Package( url: "https://github.com/Quick/Nimble", from: "9.0.0" ), + .package( + name: "FilesProvider", + url: "https://github.com/amosavian/FileProvider.git", + from: "0.26.0" + ), .package( name: "GRDB", url: "https://github.com/groue/GRDB.swift", from: "5.3.0" ), + .package( + name: "GoogleSignIn", + url: "https://github.com/google/GoogleSignIn-iOS", + from: "6.1.0" + ), + .package( + name: "GoogleAPIClientForREST", + url: "https://github.com/google/google-api-objectivec-client-for-rest", + from: "1.6.0" + ), .package( name: "SnapKit", url: "https://github.com/SnapKit/SnapKit", @@ -81,6 +101,11 @@ let package = Package( url: "https://github.com/apple/swift-protobuf", from: "1.14.0" ), + .package( + name: "SwiftyDropbox", + url: "https://github.com/dropbox/SwiftyDropbox.git", + from: "8.2.1" + ), .package( name: "KeychainAccess", url: "https://github.com/kishikawakatsumi/KeychainAccess", @@ -128,14 +153,19 @@ let package = Package( "ChatFeature", "MenuFeature", "CrashService", + "BackupFeature", "SearchFeature", + "iCloudFeature", + "DropboxFeature", "ContactFeature", + "RestoreFeature", "ProfileFeature", "CrashReporting", "ChatListFeature", "SettingsFeature", "PushNotifications", "OnboardingFeature", + "GoogleDriveFeature", "ContactListFeature" ] ), @@ -236,6 +266,48 @@ let package = Package( ] ), + // MARK: - GoogleDriveFeature + + .target( + name: "GoogleDriveFeature", + dependencies: [ + .product( + name: "GoogleSignIn", + package: "GoogleSignIn" + ), + .product( + name: "GoogleAPIClientForREST_Drive", + package: "GoogleAPIClientForREST" + ) + ], + resources: [.process("Resources")] + ), + + // MARK: - iCloudFeature + + .target( + name: "iCloudFeature", + dependencies: [ + .product( + name: "FilesProvider", + package: "FilesProvider" + ) + ] + ), + + // MARK: - DropboxFeature + + .target( + name: "DropboxFeature", + dependencies: [ + .product( + name: "SwiftyDropbox", + package: "SwiftyDropbox" + ) + ], + resources: [.process("Resources")] + ), + // MARK: - Countries .target( @@ -346,6 +418,7 @@ let package = Package( "Shared", "Database", "Bindings", + "BackupFeature", "CrashReporting", "NetworkMonitor", "DependencyInjection", @@ -384,6 +457,22 @@ let package = Package( ] ), + // MARK: - RestoreFeature + + .target( + name: "RestoreFeature", + dependencies: [ + "HUD", + "Shared", + "Integration", + "Presentation", + "iCloudFeature", + "DropboxFeature", + "GoogleDriveFeature", + "DependencyInjection" + ] + ), + // MARK: - ContactFeature .target( @@ -560,6 +649,22 @@ let package = Package( ] ), + // MARK: - BackupFeature + + .target( + name: "BackupFeature", + dependencies: [ + "HUD", + "Shared", + "Models", + "Presentation", + "GoogleDriveFeature", + "iCloudFeature", + "DropboxFeature", + "DependencyInjection" + ] + ), + // MARK: - ScanFeature .target( diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index b177f06784bfb4cde7d1eee1cc6ae7c86ee3771f..927bcf4d17dcb4b3825b36374390634effc168b3 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -6,17 +6,20 @@ import Theme import XXLogger import Defaults import Integration +import SwiftyDropbox import CrashReporting import PushNotifications import DependencyInjection import OnboardingFeature +import DropboxFeature let logger = Logger(subsystem: "logs_xxmessenger", category: "AppDelegate.swift") public class AppDelegate: UIResponder, UIApplicationDelegate { @Dependency private var pushHandler: PushHandling @Dependency private var crashReporter: CrashReporter + @Dependency private var dropboxService: DropboxInterface @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool @@ -148,6 +151,10 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { application.applicationIconBadgeNumber = 0 coverView?.removeFromSuperview() } + + public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + dropboxService.handleOpenUrl(url) + } } // MARK: Notifications diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 41b6eabd9459622ea764ebfc9bc8697fe58541b2..59b18eb00646812bbd93ee249335a2b65bd2d6bf 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -1,6 +1,9 @@ // MARK: SDK +import UIKit import Network +import QuickLook +import MobileCoreServices // MARK: Isolated features @@ -10,22 +13,28 @@ import Bindings import XXLogger import Keychain import Defaults +import Countries +import Voxophone import Integration import Permissions import CrashService +import iCloudFeature import CrashReporting import NetworkMonitor +import DropboxFeature import VersionChecking import PushNotifications +import GoogleDriveFeature import DependencyInjection -import Voxophone // MARK: UI Features import ScanFeature import ChatFeature import MenuFeature +import BackupFeature import SearchFeature +import RestoreFeature import ContactFeature import ProfileFeature import ChatListFeature @@ -49,21 +58,36 @@ struct DependencyRegistrator { container.register(MockPushHandler() as PushHandling) container.register(MockKeychainHandler() as KeychainHandling) container.register(MockPermissionHandler() as PermissionHandling) + + /// Restore / Backup + + container.register(iCloudServiceMock() as iCloudInterface) + container.register(DropboxServiceMock() as DropboxInterface) + container.register(GoogleDriveServiceMock() as GoogleDriveInterface) + registerCommonDependencies() } // MARK: LIVE static func registerForLive() { + container.register(KeyObjectStore.userDefaults) container.register(XXLogger.live()) container.register(CrashReporter.live) container.register(VersionChecker.live()) + container.register(XXNetwork<BindingsClient>() as XXNetworking) container.register(NetworkMonitor() as NetworkMonitoring) - container.register(KeyObjectStore.userDefaults) container.register(PushHandler() as PushHandling) container.register(KeychainHandler() as KeychainHandling) container.register(PermissionHandler() as PermissionHandling) + + /// Restore / Backup + + container.register(iCloudService() as iCloudInterface) + container.register(DropboxService() as DropboxInterface) + container.register(GoogleDriveService() as GoogleDriveInterface) + registerCommonDependencies() } @@ -71,6 +95,7 @@ struct DependencyRegistrator { static private func registerCommonDependencies() { container.register(Voxophone()) + container.register(BackupService()) // MARK: Isolated @@ -80,29 +105,77 @@ struct DependencyRegistrator { // MARK: Coordinators - container.register(SearchCoordinator() as SearchCoordinating) - container.register(ProfileCoordinator() as ProfileCoordinating) - container.register(SettingsCoordinator() as SettingsCoordinating) + container.register(BackupCoordinator() as BackupCoordinating) + + container.register( + SearchCoordinator( + contactFactory: ContactController.init(_:), + countriesFactory: CountryListController.init(_:) + ) as SearchCoordinating) + + container.register( + ProfileCoordinator( + emailFactory: ProfileEmailController.init, + phoneFactory: ProfilePhoneController.init, + imagePickerFactory: UIImagePickerController.init, + permissionFactory: RequestPermissionController.init, + countriesFactory: CountryListController.init(_:), + codeFactory: ProfileCodeController.init(_:_:) + ) as ProfileCoordinating) + + container.register( + SettingsCoordinator( + backupFactory: BackupController.init, + advancedFactory: SettingsAdvancedController.init, + accountDeleteFactory: AccountDeleteController.init + ) as SettingsCoordinating) + + container.register( + RestoreCoordinator( + successFactory: RestoreSuccessController.init, + chatListFactory: ChatListController.init, + restoreFactory: RestoreController.init(_:_:) + ) as RestoreCoordinating) container.register( ChatCoordinator( retryFactory: RetrySheetController.init, - contactFactory: ContactController.init(_:) + webFactory: WebScreen.init(url:), + previewFactory: QLPreviewController.init, + contactFactory: ContactController.init(_:), + imagePickerFactory: UIImagePickerController.init, + permissionFactory: RequestPermissionController.init ) as ChatCoordinating) container.register( ContactCoordinator( - requestsFactory: RequestsContainerController.init + requestsFactory: RequestsContainerController.init, + singleChatFactory: SingleChatController.init(_:), + imagePickerFactory: UIImagePickerController.init, + nicknameFactory: NickameController.init(_:_:) ) as ContactCoordinating) container.register( RequestsCoordinator( - searchFactory: SearchController.init + searchFactory: SearchController.init, + verifyingFactory: VerifyingController.init, + contactFactory: ContactController.init(_:), + nicknameFactory: NickameController.init(_:_:) ) as RequestsCoordinating) container.register( OnboardingCoordinator( - chatListFactory: ChatListController.init + emailFactory: OnboardingEmailController.init, + phoneFactory: OnboardingPhoneController.init, + welcomeFactory: OnboardingWelcomeController.init, + chatListFactory: ChatListController.init, + startFactory: OnboardingStartController.init(_:), + usernameFactory: OnboardingUsernameController.init(_:), + restoreListFactory: RestoreListController.init(_:), + successFactory: OnboardingSuccessController.init(_:), + countriesFactory: CountryListController.init(_:), + phoneConfirmationFactory: OnboardingPhoneConfirmationController.init(_:_:), + emailConfirmationFactory: OnboardingEmailConfirmationController.init(_:_:) ) as OnboardingCoordinating) container.register( @@ -110,13 +183,17 @@ struct DependencyRegistrator { scanFactory: ScanContainerController.init, searchFactory: SearchController.init, newGroupFactory: CreateGroupController.init, - requestsFactory: RequestsContainerController.init + requestsFactory: RequestsContainerController.init, + contactFactory: ContactController.init(_:), + groupChatFactory: GroupChatController.init(_:), + groupPopupFactory: CreatePopupController.init(_:_:) ) as ContactListCoordinating) container.register( ScanCoordinator( contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init + requestsFactory: RequestsContainerController.init, + contactFactory: ContactController.init(_:) ) as ScanCoordinating) container.register( @@ -126,7 +203,10 @@ struct DependencyRegistrator { profileFactory: ProfileController.init, settingsFactory: SettingsController.init, contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init + requestsFactory: RequestsContainerController.init, + singleChatFactory: SingleChatController.init(_:), + sideMenuFactory: MenuController.init(_:), + groupChatFactory: GroupChatController.init(_:) ) as ChatListCoordinating) } } diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift new file mode 100644 index 0000000000000000000000000000000000000000..aa48d9e1db6874afb18d2dcb0d29519497c3e1e8 --- /dev/null +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -0,0 +1,297 @@ +import UIKit +import Popup +import Models +import Shared +import Combine +import DependencyInjection + +final class BackupConfigController: UIViewController { + @Dependency private var coordinator: BackupCoordinating + + lazy private var screenView = BackupConfigView() + + private let viewModel: BackupConfigViewModel + private var cancellables = Set<AnyCancellable>() + private var popupCancellables = Set<AnyCancellable>() + + private var wifiOnly = false + private var manualBackups = false + private var serviceName: String = "" + + override func loadView() { + view = screenView + } + + init(_ viewModel: BackupConfigViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } + + private func setupBindings() { + viewModel.actionState() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in screenView.actionView.setState($0) } + .store(in: &cancellables) + + viewModel.connectedServices() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in decorate(connectedServices: $0) } + .store(in: &cancellables) + + viewModel.enabledService() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in decorate(enabledService: $0) } + .store(in: &cancellables) + + viewModel.automatic() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.frequencyDetailView.subtitleLabel.text = $0 ? "Automatic" : "Manual" + manualBackups = !$0 + }.store(in: &cancellables) + + viewModel.wifiOnly() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.infrastructureDetailView.subtitleLabel.text = $0 ? "Wi-Fi Only" : "Wi-Fi and Cellular" + wifiOnly = $0 + }.store(in: &cancellables) + + viewModel.lastBackup() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let backup = $0 else { + screenView.latestBackupDetailView.subtitleLabel.text = "Never" + return + } + + screenView.latestBackupDetailView.subtitleLabel.text = backup.date.backupStyle() + }.store(in: &cancellables) + + screenView.actionView.backupNowButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapBackupNow() } + .store(in: &cancellables) + + screenView.frequencyDetailView + .publisher(for: .touchUpInside) + .sink { [unowned self] in presentFrequencyPopup(manual: manualBackups) } + .store(in: &cancellables) + + screenView.infrastructureDetailView + .publisher(for: .touchUpInside) + .sink { [unowned self] in presentInfrastructurePopup(wifiOnly: wifiOnly) } + .store(in: &cancellables) + + screenView.googleDriveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.drive, self) } + .store(in: &cancellables) + + screenView.googleDriveButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(.drive, screenView.googleDriveButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.dropboxButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(.dropbox, screenView.dropboxButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.iCloudButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(.icloud, screenView.iCloudButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.dropboxButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } + .store(in: &cancellables) + + screenView.iCloudButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.icloud, self) } + .store(in: &cancellables) + } + + private func decorate(enabledService: CloudService?) { + var button: BackupSwitcherButton? + + switch enabledService { + case .none: + break + case .icloud: + serviceName = "iCloud" + button = screenView.iCloudButton + + case .dropbox: + serviceName = "Dropbox" + button = screenView.dropboxButton + + case .drive: + serviceName = "Google Drive" + button = screenView.googleDriveButton + } + + screenView.enabledSubtitleLabel.text + = Localized.Backup.Config.disclaimer(serviceName) + screenView.frequencyDetailView.titleLabel.text + = Localized.Backup.Config.frequency(serviceName).uppercased() + + guard let button = button else { + screenView.iCloudButton.isHidden = false + screenView.dropboxButton.isHidden = false + screenView.googleDriveButton.isHidden = false + + screenView.iCloudButton.switcherView.isOn = false + screenView.dropboxButton.switcherView.isOn = false + screenView.googleDriveButton.switcherView.isOn = false + + screenView.frequencyDetailView.isHidden = true + screenView.enabledSubtitleView.isHidden = true + screenView.latestBackupDetailView.isHidden = true + screenView.infrastructureDetailView.isHidden = true + return + } + + screenView.frequencyDetailView.isHidden = false + screenView.enabledSubtitleView.isHidden = false + screenView.latestBackupDetailView.isHidden = false + screenView.infrastructureDetailView.isHidden = false + + [screenView.iCloudButton, screenView.dropboxButton, screenView.googleDriveButton] + .forEach { + $0.isHidden = $0 != button + $0.switcherView.isOn = $0 == button + } + } + + private func decorate(connectedServices: Set<CloudService>) { + if connectedServices.contains(.icloud) { + screenView.iCloudButton.showSwitcher(enabled: false) + } else { + screenView.iCloudButton.showChevron() + } + + if connectedServices.contains(.dropbox) { + screenView.dropboxButton.showSwitcher(enabled: false) + } else { + screenView.dropboxButton.showChevron() + } + + if connectedServices.contains(.drive) { + screenView.googleDriveButton.showSwitcher(enabled: false) + } else { + screenView.googleDriveButton.showChevron() + } + } + + private func presentInfrastructurePopup(wifiOnly: Bool) { + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.ChatList.Dashboard.cancel, for: .normal) + + let wifiOnlyButton = PopupRadioButton(title: "Wi-Fi Only", isSelected: wifiOnly) + let wifiAndCellularButton = PopupRadioButton(title: "Wi-Fi and Cellular", isSelected: !wifiOnly) + + let popup = BottomPopup(with: [ + PopupLabel( + font: Fonts.Mulish.extraBold.font(size: 28.0), + text: Localized.Backup.Config.infrastructure, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 30 + ), + wifiOnlyButton, + wifiAndCellularButton, + PopupEmptyView(height: 20.0), + PopupStackView(spacing: 20.0, views: [cancelButton]) + ]) + + wifiOnlyButton.action + .sink { [unowned self] in + viewModel.didChooseWifiOnly(true) + + popup.dismiss(animated: true) { [weak self] in + self?.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + wifiAndCellularButton.action + .sink { [unowned self] in + viewModel.didChooseWifiOnly(false) + + popup.dismiss(animated: true) { [weak self] in + self?.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + cancelButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { + popup.dismiss(animated: true) { [weak self] in + self?.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + coordinator.toPopup(popup, from: self) + } + + private func presentFrequencyPopup(manual: Bool) { + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.ChatList.Dashboard.cancel, for: .normal) + + let manualButton = PopupRadioButton(title: "Manual", isSelected: manual) + let automaticButton = PopupRadioButton(title: "Automatic", isSelected: !manual) + + let popup = BottomPopup(with: [ + PopupLabel( + font: Fonts.Mulish.extraBold.font(size: 28.0), + text: Localized.Backup.Config.frequency(serviceName), + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 30 + ), + manualButton, + automaticButton, + PopupEmptyView(height: 20.0), + PopupStackView(spacing: 20.0, views: [cancelButton]) + ]) + + manualButton.action + .sink { [unowned self] in + viewModel.didChooseAutomatic(false) + + popup.dismiss(animated: true) { [weak self] in + self?.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + automaticButton.action + .sink { [unowned self] in + viewModel.didChooseAutomatic(true) + + popup.dismiss(animated: true) { [weak self] in + self?.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + cancelButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { + popup.dismiss(animated: true) { [weak self] in + self?.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + coordinator.toPopup(popup, from: self) + } +} diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift new file mode 100644 index 0000000000000000000000000000000000000000..65ef336de786ffb0b62789f21c0059995d643591 --- /dev/null +++ b/Sources/BackupFeature/Controllers/BackupController.swift @@ -0,0 +1,77 @@ +import HUD +import UIKit +import Shared +import Models +import Combine +import DependencyInjection + +public final class BackupController: UIViewController { + @Dependency private var hud: HUDType + + private let viewModel = BackupViewModel.live() + private var cancellables = Set<AnyCancellable>() + + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = Asset.neutralWhite.color + hud.update(with: .on) + + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let title = UILabel() + title.text = Localized.Backup.header + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + + let back = UIButton.back() + back.addTarget(self, action: #selector(didTapBack), for: .touchUpInside) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [back, title]) + ) + } + + private func setupBindings() { + viewModel.state() + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + hud.update(with: .none) + + switch $0 { + case .setup: + contentViewController = BackupSetupController(viewModel.setupViewModel()) + case .config: + contentViewController = BackupConfigController(viewModel.configViewModel()) + } + }.store(in: &cancellables) + } + + private var contentViewController: UIViewController? { + didSet { + guard contentViewController != oldValue else { return } + + if let oldValue = oldValue { + oldValue.willMove(toParent: nil) + oldValue.view.removeFromSuperview() + oldValue.removeFromParent() + } + + if let newValue = contentViewController { + addChild(newValue) + view.addSubview(newValue.view) + newValue.view.snp.makeConstraints { $0.edges.equalToSuperview() } + newValue.didMove(toParent: self) + } + } + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } +} diff --git a/Sources/BackupFeature/Controllers/BackupSetupController.swift b/Sources/BackupFeature/Controllers/BackupSetupController.swift new file mode 100644 index 0000000000000000000000000000000000000000..49e05a2bd74ce9141e19320cfebbc33f989414f7 --- /dev/null +++ b/Sources/BackupFeature/Controllers/BackupSetupController.swift @@ -0,0 +1,41 @@ +import UIKit +import Models +import Combine +import DependencyInjection + +final class BackupSetupController: UIViewController { + lazy private var screenView = BackupSetupView() + + private let viewModel: BackupSetupViewModel + private var cancellables = Set<AnyCancellable>() + + override func loadView() { + view = screenView + } + + init(_ viewModel: BackupSetupViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + + screenView.googleDriveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.drive, self) } + .store(in: &cancellables) + + screenView.dropboxButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } + .store(in: &cancellables) + + screenView.iCloudButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.icloud, self) } + .store(in: &cancellables) + } +} diff --git a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..75181b7ce104d68e8f8b159a2b0069a725622cc6 --- /dev/null +++ b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift @@ -0,0 +1,18 @@ +import UIKit +import Presentation + +public protocol BackupCoordinating { + func toPopup(_: UIViewController, from: UIViewController) +} + +public struct BackupCoordinator: BackupCoordinating { + var bottomPresenter: Presenting = BottomPresenter() + + public init() {} +} + +public extension BackupCoordinator { + func toPopup(_ screen: UIViewController, from parent: UIViewController) { + bottomPresenter.present(screen, from: parent) + } +} diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift new file mode 100644 index 0000000000000000000000000000000000000000..4c62ba50d557ff4271ccf29b2786df73f17206c8 --- /dev/null +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -0,0 +1,296 @@ +import UIKit +import Models +import Combine +import Defaults +import iCloudFeature +import DropboxFeature +import NetworkMonitor +import GoogleDriveFeature +import DependencyInjection + +public final class BackupService { + @Dependency private var icloudService: iCloudInterface + @Dependency private var dropboxService: DropboxInterface + @Dependency private var driveService: GoogleDriveInterface + @Dependency private var networkManager: NetworkMonitoring + + @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data + + public var settingsPublisher: AnyPublisher<BackupSettings, Never> { + settings.handleEvents(receiveSubscription: { [weak self] _ in + guard let self = self else { return } + + let lastRefreshDate = self.settingsLastRefreshedDate ?? Date.distantPast + + if Date().timeIntervalSince(lastRefreshDate) < 10 { return } + + self.settingsLastRefreshedDate = Date() + self.refreshConnections() + self.refreshBackups() + }).eraseToAnyPublisher() + } + + private var connType: ConnectionType = .wifi + private var settingsLastRefreshedDate: Date? + private var cancellables = Set<AnyCancellable>() + private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings)) + + public init() { + settings + .dropFirst() + .removeDuplicates() + .sink { [unowned self] in storedSettings = $0.toData() } + .store(in: &cancellables) + + networkManager.connType + .receive(on: DispatchQueue.main) + .sink { [unowned self] in connType = $0 } + .store(in: &cancellables) + } +} + +extension BackupService { + public func performBackupIfAutomaticIsEnabled() { + guard settings.value.automaticBackups == true else { return } + performBackup() + } + + public func performBackup() { + guard let directoryUrl = try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) else { fatalError("Couldn't generate the URL to persist the backup") } + + let fileUrl = directoryUrl + .appendingPathComponent("backup") + .appendingPathExtension("xxm") + + guard let data = try? Data(contentsOf: fileUrl) else { + print(">>> Tried to backup arbitrarily but there was nothing to be backed up. Aborting...") + return + } + + performBackup(data: data) + } + + public func updateBackup(data: Data) { + guard let directoryUrl = try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) else { fatalError("Couldn't generate the URL to persist the backup") } + + let fileUrl = directoryUrl + .appendingPathComponent("backup") + .appendingPathExtension("xxm") + + do { + try data.write(to: fileUrl) + } catch { + fatalError("Couldn't write backup to fileurl") + } + + let isWifiOnly = settings.value.wifiOnlyBackup + let isAutomaticEnabled = settings.value.automaticBackups + let hasEnabledService = settings.value.enabledService != nil + + if isWifiOnly { + guard connType == .wifi else { return } + } else { + guard connType != .unknown else { return } + } + + if isAutomaticEnabled && hasEnabledService { + performBackup() + } + } + + public func setBackupOnlyOnWifi(_ enabled: Bool) { + settings.value.wifiOnlyBackup = enabled + } + + public func setBackupAutomatically(_ enabled: Bool) { + settings.value.automaticBackups = enabled + + guard enabled else { return } + performBackup() + } + + public func toggle(service: CloudService, enabling: Bool) { + settings.value.enabledService = enabling ? service : nil + } + + public func authorize(service: CloudService, presenting screen: UIViewController) { + switch service { + case .drive: + driveService.authorize(presenting: screen) { [weak self] _ in + guard let self = self else { return } + self.refreshConnections() + self.refreshBackups() + } + case .icloud: + if !icloudService.isAuthorized() { + icloudService.openSettings() + } else { + refreshConnections() + refreshBackups() + } + case .dropbox: + if !dropboxService.isAuthorized() { + dropboxService.authorize(presenting: screen) + .sink { [weak self] _ in + guard let self = self else { return } + self.refreshConnections() + self.refreshBackups() + }.store(in: &cancellables) + } + } + } +} + +extension BackupService { + private func refreshConnections() { + if icloudService.isAuthorized() && !settings.value.connectedServices.contains(.icloud) { + settings.value.connectedServices.insert(.icloud) + } else if !icloudService.isAuthorized() && settings.value.connectedServices.contains(.icloud) { + settings.value.connectedServices.remove(.icloud) + } + + if dropboxService.isAuthorized() && !settings.value.connectedServices.contains(.dropbox) { + settings.value.connectedServices.insert(.dropbox) + } else if !dropboxService.isAuthorized() && settings.value.connectedServices.contains(.dropbox) { + settings.value.connectedServices.remove(.dropbox) + } + + driveService.isAuthorized { [weak settings] isAuthorized in + guard let settings = settings else { return } + + if isAuthorized && !settings.value.connectedServices.contains(.drive) { + settings.value.connectedServices.insert(.drive) + } else if !isAuthorized && settings.value.connectedServices.contains(.drive) { + settings.value.connectedServices.remove(.drive) + } + } + } + + private func refreshBackups() { + if icloudService.isAuthorized() { + icloudService.downloadMetadata { [weak settings] in + guard let settings = settings else { return } + + guard let metadata = try? $0.get() else { + settings.value.backups[.icloud] = nil + return + } + + settings.value.backups[.icloud] = Backup( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + } + } + + if dropboxService.isAuthorized() { + dropboxService.downloadMetadata { [weak settings] in + guard let settings = settings else { return } + + guard let metadata = try? $0.get() else { + settings.value.backups[.dropbox] = nil + return + } + + settings.value.backups[.dropbox] = Backup( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + } + } + + driveService.isAuthorized { [weak settings] isAuthorized in + guard let settings = settings else { return } + + if isAuthorized { + self.driveService.downloadMetadata { + guard let metadata = try? $0.get() else { return } + + settings.value.backups[.drive] = Backup( + id: metadata.identifier, + date: metadata.modifiedDate, + size: metadata.size + ) + } + } else { + settings.value.backups[.drive] = nil + } + } + } + + private func performBackup(data: Data) { + guard let enabledService = settings.value.enabledService else { + fatalError("Trying to backup but nothing is enabled") + } + + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString) + + do { + try data.write(to: url) + } catch { + print("Couldn't write to temp: \(error.localizedDescription)") + return + } + + switch enabledService { + case .drive: + driveService.uploadBackup(url) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[.drive] = .init( + id: metadata.identifier, + date: metadata.modifiedDate, + size: metadata.size + ) + case .failure(let error): + print(error.localizedDescription) + } + + // try? FileManager.default.removeItem(at: url) + } + case .icloud: + icloudService.uploadBackup(url) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[.icloud] = .init( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + case .failure(let error): + print(error.localizedDescription) + } + + // try? FileManager.default.removeItem(at: url) + } + case .dropbox: + dropboxService.uploadBackup(url) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[.dropbox] = .init( + id: metadata.path, + date: metadata.modifiedDate, + size: metadata.size + ) + case .failure(let error): + print(error.localizedDescription) + } + + // try? FileManager.default.removeItem(at: url) + } + } + } +} diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..c11b31a8183899a257d755270d9628c39c92d987 --- /dev/null +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -0,0 +1,87 @@ +import UIKit +import Models +import Shared +import Combine +import Foundation +import DependencyInjection +import HUD + +enum BackupActionState { + case backupFinished + case backupAllowed(Bool) + case backupInProgress(Float, Float) +} + +struct BackupConfigViewModel { + var didTapBackupNow: () -> Void + var didChooseWifiOnly: (Bool) -> Void + var didChooseAutomatic: (Bool) -> Void + var didToggleService: (CloudService, Bool) -> Void + var didTapService: (CloudService, UIViewController) -> Void + + var wifiOnly: () -> AnyPublisher<Bool, Never> + var automatic: () -> AnyPublisher<Bool, Never> + var lastBackup: () -> AnyPublisher<Backup?, Never> + var actionState: () -> AnyPublisher<BackupActionState, Never> + var enabledService: () -> AnyPublisher<CloudService?, Never> + var connectedServices: () -> AnyPublisher<Set<CloudService>, Never> +} + +extension BackupConfigViewModel { + static func live() -> Self { + class Context { + @Dependency var hud: HUDType + @Dependency var service: BackupService + } + + let context = Context() + + return .init( + didTapBackupNow: { + context.service.performBackup() + context.hud.update(with: .on) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + context.hud.update(with: .none) + } + }, + didChooseWifiOnly: context.service.setBackupOnlyOnWifi(_:), + didChooseAutomatic: context.service.setBackupAutomatically(_:), + didToggleService: context.service.toggle, + didTapService: context.service.authorize, + wifiOnly: { + context.service.settingsPublisher + .map(\.wifiOnlyBackup) + .eraseToAnyPublisher() + }, + automatic: { + context.service.settingsPublisher + .map(\.automaticBackups) + .eraseToAnyPublisher() + }, + lastBackup: { + context.service.settingsPublisher + .map { + guard let enabledService = $0.enabledService else { return nil } + return $0.backups[enabledService] + }.eraseToAnyPublisher() + }, + actionState: { + context.service.settingsPublisher + .map(\.enabledService) + .map { BackupActionState.backupAllowed($0 != nil) } + .eraseToAnyPublisher() + }, + enabledService: { + context.service.settingsPublisher + .map(\.enabledService) + .eraseToAnyPublisher() + }, + connectedServices: { + context.service.settingsPublisher + .map(\.connectedServices) + .removeDuplicates() + .eraseToAnyPublisher() + } + ) + } +} diff --git a/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..cc647d9aaecc7507eed9d517909dbf3229b6901f --- /dev/null +++ b/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift @@ -0,0 +1,21 @@ +import UIKit +import Models +import Shared +import Combine +import GoogleDriveFeature +import DependencyInjection + +struct BackupSetupViewModel { + var didTapService: (CloudService, UIViewController) -> Void +} + +extension BackupSetupViewModel { + static func live() -> Self { + class Context { + @Dependency var service: BackupService + } + + let context = Context() + return .init(didTapService: context.service.authorize) + } +} diff --git a/Sources/BackupFeature/ViewModels/BackupViewModel.swift b/Sources/BackupFeature/ViewModels/BackupViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..6522ee1215a4ee84000c85600389bf53d52aa9bc --- /dev/null +++ b/Sources/BackupFeature/ViewModels/BackupViewModel.swift @@ -0,0 +1,35 @@ +import Combine +import DependencyInjection + +enum BackupViewState: Equatable { + case setup + case config +} + +struct BackupViewModel { + var setupViewModel: () -> BackupSetupViewModel + var configViewModel: () -> BackupConfigViewModel + + var state: () -> AnyPublisher<BackupViewState, Never> +} + +extension BackupViewModel { + static func live() -> Self { + class Context { + @Dependency var service: BackupService + } + + let context = Context() + + return .init( + setupViewModel: { BackupSetupViewModel.live() }, + configViewModel: { BackupConfigViewModel.live() }, + state: { + context.service.settingsPublisher + .map(\.connectedServices) + .map { $0.isEmpty ? BackupViewState.setup : .config } + .eraseToAnyPublisher() + } + ) + } +} diff --git a/Sources/BackupFeature/Views/BackupActionView.swift b/Sources/BackupFeature/Views/BackupActionView.swift new file mode 100644 index 0000000000000000000000000000000000000000..705263e0ef815ae9fab0ff5657cc536f02c98101 --- /dev/null +++ b/Sources/BackupFeature/Views/BackupActionView.swift @@ -0,0 +1,125 @@ +import UIKit +import Shared + +final class BackupActionView: UIView { + let stackView = UIStackView() + let backupNowButton = CapsuleButton() + + let progressView = UIView() + let progressLabel = UILabel() + let progressBarPartial = UIView() + let progressBarFull = UIView() + + let finishedView = UIView() + let finishedLabel = UILabel() + let finishedImage = UIImageView() + + init() { + super.init(frame: .zero) + + setupProgressView() + setupFinishedView() + + backupNowButton.set(style: .brandColored, title: Localized.Backup.Config.backupNow) + + stackView.spacing = 15 + stackView.axis = .vertical + stackView.addArrangedSubview(backupNowButton) + stackView.addArrangedSubview(progressView) + stackView.addArrangedSubview(finishedView) + + addSubview(stackView) + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + private func setupFinishedView() { + finishedImage.contentMode = .center + finishedImage.image = Asset.restoreSuccess.image + + finishedLabel.text = "Backup completed!" + finishedLabel.textColor = Asset.neutralBody.color + finishedLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + finishedView.addSubview(finishedImage) + finishedView.addSubview(finishedLabel) + + finishedImage.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.bottom.equalToSuperview() + } + + finishedLabel.snp.makeConstraints { make in + make.left.equalTo(finishedImage.snp.right).offset(10) + make.centerY.equalTo(finishedImage) + make.right.lessThanOrEqualToSuperview() + } + } + + private func setupProgressView() { + progressLabel.textColor = Asset.neutralDisabled.color + progressLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + progressBarFull.backgroundColor = Asset.neutralLine.color + progressBarPartial.backgroundColor = Asset.brandPrimary.color + progressBarFull.layer.masksToBounds = true + progressBarFull.layer.cornerRadius = 4 + + progressBarFull.addSubview(progressBarPartial) + progressView.addSubview(progressLabel) + progressView.addSubview(progressBarFull) + + progressBarFull.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.height.equalTo(8) + } + + progressLabel.snp.makeConstraints { make in + make.top.equalTo(progressBarFull.snp.bottom).offset(10) + make.left.equalToSuperview() + make.bottom.equalToSuperview() + } + + progressBarPartial.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.width.equalToSuperview().multipliedBy(0.5) + make.bottom.equalToSuperview() + } + } + + func setState(_ state: BackupActionState) { + switch state { + case .backupFinished: + backupNowButton.isHidden = true + progressView.isHidden = true + finishedView.isHidden = false + + case .backupAllowed(let bool): + backupNowButton.isHidden = false + progressView.isHidden = true + finishedView.isHidden = true + backupNowButton.isEnabled = bool + + case .backupInProgress(let uploaded, let total): + backupNowButton.isHidden = true + progressView.isHidden = false + finishedView.isHidden = true + + let uploadedKb = String(format: "%.1f kb", uploaded/1000) + let totalkb = String(format: "%.1f kb", total/1000) + + progressLabel.text = "Uploaded \(uploadedKb) of \(totalkb) (\(total/uploaded)%)" + } + } +} diff --git a/Sources/BackupFeature/Views/BackupConfigView.swift b/Sources/BackupFeature/Views/BackupConfigView.swift new file mode 100644 index 0000000000000000000000000000000000000000..8c400b3ff9162f380548d0478f2356454910833e --- /dev/null +++ b/Sources/BackupFeature/Views/BackupConfigView.swift @@ -0,0 +1,112 @@ +import UIKit +import Shared + +final class BackupConfigView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let actionView = BackupActionView() + + let stackView = UIStackView() + let iCloudButton = BackupSwitcherButton() + let dropboxButton = BackupSwitcherButton() + let googleDriveButton = BackupSwitcherButton() + + let enabledSubtitleView = UIView() + let enabledSubtitleLabel = UILabel() + let frequencyDetailView = BackupDetailView() + let latestBackupDetailView = BackupDetailView() + let infrastructureDetailView = BackupDetailView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralDark.color + titleLabel.text = Localized.Backup.Config.title + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + enabledSubtitleLabel.numberOfLines = 0 + enabledSubtitleLabel.textColor = Asset.neutralWeak.color + enabledSubtitleLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSAttributedString( + string: Localized.Backup.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = attString + + iCloudButton.titleLabel.text = Localized.Backup.iCloud + iCloudButton.logoImageView.image = Asset.restoreIcloud.image + + dropboxButton.titleLabel.text = Localized.Backup.dropbox + dropboxButton.logoImageView.image = Asset.restoreDropbox.image + + googleDriveButton.titleLabel.text = Localized.Backup.googleDrive + googleDriveButton.logoImageView.image = Asset.restoreDrive.image + + latestBackupDetailView.titleLabel.text = Localized.Backup.Config.latestBackup + frequencyDetailView.accessoryImageView.image = Asset.settingsDisclosure.image + + infrastructureDetailView.titleLabel.text = Localized.Backup.Config.infrastructure.uppercased() + infrastructureDetailView.accessoryImageView.image = Asset.settingsDisclosure.image + + enabledSubtitleView.addSubview(enabledSubtitleLabel) + + stackView.axis = .vertical + stackView.addArrangedSubview(googleDriveButton) + stackView.addArrangedSubview(iCloudButton) + stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(enabledSubtitleView) + stackView.addArrangedSubview(latestBackupDetailView) + stackView.addArrangedSubview(frequencyDetailView) + stackView.addArrangedSubview(infrastructureDetailView) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(actionView) + addSubview(stackView) + + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(15) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + enabledSubtitleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(-10) + make.left.equalToSuperview().offset(92) + make.right.equalToSuperview().offset(-48) + make.bottom.equalToSuperview() + } + + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(8) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + actionView.snp.makeConstraints { make in + make.top.equalTo(subtitleLabel.snp.bottom).offset(15) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-38) + } + + stackView.snp.makeConstraints { make in + make.top.equalTo(actionView.snp.bottom).offset(28) + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/BackupFeature/Views/BackupDetailView.swift b/Sources/BackupFeature/Views/BackupDetailView.swift new file mode 100644 index 0000000000000000000000000000000000000000..d28647330d7cd7b81d516ad7d4b506f712d94d0c --- /dev/null +++ b/Sources/BackupFeature/Views/BackupDetailView.swift @@ -0,0 +1,40 @@ +import UIKit +import Shared + +final class BackupDetailView: UIControl { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let accessoryImageView = UIImageView() + + init() { + super.init(frame: .zero) + + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + titleLabel.textColor = Asset.neutralWeak.color + subtitleLabel.textColor = Asset.neutralActive.color + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(accessoryImageView) + + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.left.equalToSuperview().offset(92) + } + + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(4) + make.left.equalTo(titleLabel) + make.bottom.equalToSuperview().offset(-2) + } + + accessoryImageView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-48) + make.centerY.equalTo(titleLabel.snp.bottom) + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/BackupFeature/Views/BackupSetupView.swift b/Sources/BackupFeature/Views/BackupSetupView.swift new file mode 100644 index 0000000000000000000000000000000000000000..eeaad48195da765ac9833293ccd7f7f9a7673ca0 --- /dev/null +++ b/Sources/BackupFeature/Views/BackupSetupView.swift @@ -0,0 +1,93 @@ +import UIKit +import Shared + +final class BackupSetupView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + + let stackView = UIStackView() + let iCloudButton = BackupSwitcherButton() + let dropboxButton = BackupSwitcherButton() + let googleDriveButton = BackupSwitcherButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + let title = Localized.Backup.Setup.title + + let attString = NSMutableAttributedString(string: title) + let firstParagraph = NSMutableParagraphStyle() + firstParagraph.alignment = .left + firstParagraph.lineHeightMultiple = 1 + + attString.addAttribute(.paragraphStyle, value: firstParagraph) + attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) + attString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 34.0) as Any) + + attString.addAttributes(attributes: [ + .font: Fonts.Mulish.bold.font(size: 34.0) as Any, + .foregroundColor: Asset.brandPrimary.color + ], betweenCharacters: "#") + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attString + + let secondParagraph = NSMutableParagraphStyle() + secondParagraph.alignment = .left + secondParagraph.lineHeightMultiple = 1.15 + + let secondAttString = NSAttributedString( + string: Localized.Backup.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: secondParagraph + ]) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = secondAttString + + iCloudButton.titleLabel.text = Localized.Backup.iCloud + iCloudButton.logoImageView.image = Asset.restoreIcloud.image + iCloudButton.showChevron() + + dropboxButton.titleLabel.text = Localized.Backup.dropbox + dropboxButton.logoImageView.image = Asset.restoreDropbox.image + dropboxButton.showChevron() + + googleDriveButton.titleLabel.text = Localized.Backup.googleDrive + googleDriveButton.logoImageView.image = Asset.restoreDrive.image + googleDriveButton.showChevron() + + stackView.axis = .vertical + stackView.addArrangedSubview(googleDriveButton) + stackView.addArrangedSubview(iCloudButton) + stackView.addArrangedSubview(dropboxButton) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(stackView) + + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(15) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(8) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + stackView.snp.makeConstraints { make in + make.top.equalTo(subtitleLabel.snp.bottom).offset(28) + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/BackupFeature/Views/BackupSwitcherButton.swift b/Sources/BackupFeature/Views/BackupSwitcherButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..2c115cdc9b723cc7c2ece188e17ef5e3a7ea7533 --- /dev/null +++ b/Sources/BackupFeature/Views/BackupSwitcherButton.swift @@ -0,0 +1,69 @@ +import UIKit +import Shared + +final class BackupSwitcherButton: UIControl { + let titleLabel = UILabel() + let separatorView = UIView() + let switcherView = UISwitch() + let logoImageView = UIImageView() + let chevronImageView = UIImageView() + + init() { + super.init(frame: .zero) + + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + + switcherView.onTintColor = Asset.brandLight.color + chevronImageView.image = Asset.settingsDisclosure.image + separatorView.backgroundColor = Asset.neutralLine.color + + addSubview(separatorView) + addSubview(logoImageView) + addSubview(titleLabel) + addSubview(switcherView) + addSubview(chevronImageView) + + logoImageView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.left.equalToSuperview().offset(36) + make.bottom.equalToSuperview().offset(-20) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(logoImageView.snp.right).offset(15) + make.centerY.equalTo(logoImageView) + } + + chevronImageView.snp.makeConstraints { make in + make.centerY.equalTo(logoImageView) + make.right.equalToSuperview().offset(-48) + } + + switcherView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-25) + make.centerY.equalTo(logoImageView) + } + + separatorView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.height.equalTo(1) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + } + } + + required init?(coder: NSCoder) { nil } + + func showSwitcher(enabled: Bool) { + switcherView.isOn = enabled + switcherView.isHidden = false + chevronImageView.isHidden = true + } + + func showChevron() { + switcherView.isOn = false + switcherView.isHidden = true + chevronImageView.isHidden = false + } +} diff --git a/Sources/ChatFeature/Coordinator/ChatCoordinator.swift b/Sources/ChatFeature/Coordinator/ChatCoordinator.swift index 451a3b579d0bcb14477ac1afc0a4c9a428e69e4f..ce1f35c8e71789508d88cca0294a03a934801804 100644 --- a/Sources/ChatFeature/Coordinator/ChatCoordinator.swift +++ b/Sources/ChatFeature/Coordinator/ChatCoordinator.swift @@ -2,118 +2,104 @@ import UIKit import Models import Shared import QuickLook -import Presentation import Permissions +import Presentation public protocol ChatCoordinating { func toCamera(from: UIViewController) func toLibrary(from: UIViewController) func toPreview(from: UIViewController) - func toPermission(type: PermissionType, from: UIViewController) - func toWebview(with: String, from: UIViewController) - func toRetrySheet(from: UIViewController) func toContact(_: Contact, from: UIViewController) + func toWebview(with: String, from: UIViewController) func toPopup(_: UIViewController, from: UIViewController) func toMenuSheet(_: UIViewController, from: UIViewController) + func toPermission(type: PermissionType, from: UIViewController) func toMembersList(_: UIViewController, from: UIViewController) } public struct ChatCoordinator: ChatCoordinating { + var pushPresenter: Presenting = PushPresenter() + var modalPresenter: Presenting = ModalPresenter() + var bottomPresenter: Presenting = BottomPresenter() + + var retryFactory: () -> UIViewController + var webFactory: (String) -> UIViewController + var previewFactory: () -> QLPreviewController + var contactFactory: (Contact) -> UIViewController + var imagePickerFactory: () -> UIImagePickerController + var permissionFactory: () -> RequestPermissionController + public init( retryFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController + webFactory: @escaping (String) -> UIViewController, + previewFactory: @escaping () -> QLPreviewController, + contactFactory: @escaping (Contact) -> UIViewController, + imagePickerFactory: @escaping () -> UIImagePickerController, + permissionFactory: @escaping () -> RequestPermissionController ) { + self.webFactory = webFactory self.retryFactory = retryFactory + self.previewFactory = previewFactory self.contactFactory = contactFactory + self.permissionFactory = permissionFactory + self.imagePickerFactory = imagePickerFactory } - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() - var presenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - // MARK: Factories - - var webFactory: (String) -> UIViewController = WebScreen.init(url:) - - var retryFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - - var previewFactory: () -> QLPreviewController = QLPreviewController.init - var permissionFactory: () -> RequestPermissionController = RequestPermissionController.init - var imagePickerFactory: () -> UIImagePickerController = UIImagePickerController.init } public extension ChatCoordinator { - func toWebview( - with urlString: String, - from parent: UIViewController - ) { - let screen = webFactory(urlString) - presenter.present(screen, from: parent) + func toPreview(from parent: UIViewController) { + let screen = previewFactory() + screen.delegate = (parent as? QLPreviewControllerDelegate) + screen.dataSource = (parent as? QLPreviewControllerDataSource) + pushPresenter.present(screen, from: parent) } - func toPermission(type: PermissionType, from parent: UIViewController) { - let screen = permissionFactory() - screen.setup(type: type) - pusher.present(screen, from: parent) + func toLibrary(from parent: UIViewController) { + let screen = imagePickerFactory() + screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) + screen.allowsEditing = false + modalPresenter.present(screen, from: parent) } - func toMembersList( - _ screen: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(screen, from: parent) + func toCamera(from parent: UIViewController) { + let screen = imagePickerFactory() + screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) + screen.sourceType = .camera + screen.allowsEditing = false + modalPresenter.present(screen, from: parent) } - func toPopup( - _ popup: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(popup, from: parent) + func toRetrySheet(from parent: UIViewController) { + let screen = retryFactory() + bottomPresenter.present(screen, from: parent) } - func toContact( - _ contact: Contact, - from parent: UIViewController - ) { + func toContact(_ contact: Contact, from parent: UIViewController) { let screen = contactFactory(contact) - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } - func toMenuSheet( - _ screen: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(screen, from: parent) + func toWebview(with urlString: String, from parent: UIViewController) { + let screen = webFactory(urlString) + modalPresenter.present(screen, from: parent) } - func toRetrySheet(from parent: UIViewController) { - let screen = retryFactory() - bottomPresenter.present(screen, from: parent) + func toPermission(type: PermissionType, from parent: UIViewController) { + let screen = permissionFactory() + screen.setup(type: type) + pushPresenter.present(screen, from: parent) } - func toLibrary(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = false - presenter.present(screen, from: parent) + func toMembersList(_ screen: UIViewController, from parent: UIViewController) { + bottomPresenter.present(screen, from: parent) } - func toCamera(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.sourceType = .camera - screen.allowsEditing = false - presenter.present(screen, from: parent) + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) } - func toPreview(from parent: UIViewController) { - let screen = previewFactory() - screen.delegate = (parent as? QLPreviewControllerDelegate) - screen.dataSource = (parent as? QLPreviewControllerDataSource) - pusher.present(screen, from: parent) + func toMenuSheet(_ screen: UIViewController, from parent: UIViewController) { + bottomPresenter.present(screen, from: parent) } } diff --git a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift index 5a7a40bcc95a3aa31043027e2d827457411e1d53..f1235d08ac6038666523b98ebe2c48ea6822d4be 100644 --- a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift +++ b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift @@ -14,25 +14,38 @@ public protocol ChatListCoordinating { func toSettings(from: UIViewController) func toContacts(from: UIViewController) func toRequests(from: UIViewController) + func toSingleChat(with: Contact, from: UIViewController) func toPopup(_: UIViewController, from: UIViewController) + func toGroupChat(with: GroupChatInfo, from: UIViewController) func toSideMenu<T: UIViewController>(from: T) where T: MenuDelegate - - func toSingleChat(with: Contact, from: UIViewController) - - func toGroupChat( - with: GroupChatInfo, - from: UIViewController - ) } public struct ChatListCoordinator: ChatListCoordinating { + var pushPresenter: Presenting = PushPresenter() + var modalPresenter: Presenting = ModalPresenter() + var sidePresenter: Presenting = SideMenuPresenter() + var bottomPresenter: Presenting = BottomPresenter() + + var scanFactory: () -> UIViewController + var searchFactory: () -> UIViewController + var profileFactory: () -> UIViewController + var settingsFactory: () -> UIViewController + var contactsFactory: () -> UIViewController + var requestsFactory: () -> UIViewController + var singleChatFactory: (Contact) -> UIViewController + var sideMenuFactory: (MenuDelegate) -> UIViewController + var groupChatFactory: (GroupChatInfo) -> UIViewController + public init( scanFactory: @escaping () -> UIViewController, searchFactory: @escaping () -> UIViewController, profileFactory: @escaping () -> UIViewController, settingsFactory: @escaping () -> UIViewController, contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController + requestsFactory: @escaping () -> UIViewController, + singleChatFactory: @escaping (Contact) -> UIViewController, + sideMenuFactory: @escaping (MenuDelegate) -> UIViewController, + groupChatFactory: @escaping (GroupChatInfo) -> UIViewController ) { self.scanFactory = scanFactory self.searchFactory = searchFactory @@ -40,90 +53,59 @@ public struct ChatListCoordinator: ChatListCoordinating { self.settingsFactory = settingsFactory self.contactsFactory = contactsFactory self.requestsFactory = requestsFactory + self.sideMenuFactory = sideMenuFactory + self.groupChatFactory = groupChatFactory + self.singleChatFactory = singleChatFactory } - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() - var sider: Presenting = SideMenuPresenter() - var presenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - // MARK: Factories - - var scanFactory: () -> UIViewController - var searchFactory: () -> UIViewController - var profileFactory: () -> UIViewController - var settingsFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - - var groupChatFactory: (GroupChatInfo) -> UIViewController - = GroupChatController.init(_:) - - var singleChatFactory: (Contact) -> UIViewController - = SingleChatController.init(_:) - - var sideMenuFactory: (MenuDelegate) -> UIViewController - = MenuController.init(_:) } public extension ChatListCoordinator { - func toSingleChat( - with contact: Contact, - from parent: UIViewController - ) { - let screen = singleChatFactory(contact) - pusher.present(screen, from: parent) - } - - func toGroupChat( - with group: GroupChatInfo, - from parent: UIViewController - ) { - let screen = groupChatFactory(group) - pusher.present(screen, from: parent) - } - - func toSideMenu<T: UIViewController>(from parent: T) where T: MenuDelegate { - let screen = sideMenuFactory(parent) - sider.present(screen, from: parent) - } - - func toPopup( - _ popup: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(popup, from: parent) - } - func toSearch(from parent: UIViewController) { let screen = searchFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toScan(from parent: UIViewController) { let screen = scanFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toProfile(from parent: UIViewController) { let screen = profileFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toContacts(from parent: UIViewController) { let screen = contactsFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toSettings(from parent: UIViewController) { let screen = settingsFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toRequests(from parent: UIViewController) { let screen = requestsFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) + } + + func toSingleChat(with contact: Contact, from parent: UIViewController) { + let screen = singleChatFactory(contact) + pushPresenter.present(screen, from: parent) + } + + func toGroupChat(with group: GroupChatInfo, from parent: UIViewController) { + let screen = groupChatFactory(group) + pushPresenter.present(screen, from: parent) + } + + func toSideMenu<T: UIViewController>(from parent: T) where T: MenuDelegate { + let screen = sideMenuFactory(parent) + sidePresenter.present(screen, from: parent) + } + + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) } } diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index b19b44c380799c3015a705dbdbfd2968ea06a3bf..5b6abe47459e937c6cf52b992a1e5cf01882dcfb 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -261,7 +261,7 @@ public final class ContactController: UIViewController { screenView.confirmedView.stackView.addArrangedSubview(phoneAttribute) let deleteButton = RowButton() - deleteButton.set( + deleteButton.setup( title: "Delete Connection", icon: Asset.settingsDelete.image, style: .delete, diff --git a/Sources/ContactFeature/Controllers/NickameController.swift b/Sources/ContactFeature/Controllers/NickameController.swift index 92f59bc1e31d096cb6a89b4a8db2038e4e0c6152..c3f00b42f9a05a2fd289d7a241787f7df9c95daf 100644 --- a/Sources/ContactFeature/Controllers/NickameController.swift +++ b/Sources/ContactFeature/Controllers/NickameController.swift @@ -5,21 +5,15 @@ import InputField import ScrollViewController public final class NickameController: UIViewController { - // MARK: UI - lazy private var screenView = NickameView() - // MARK: Properties - private let prefilled: String private let completion: StringClosure private let viewModel = NicknameViewModel() private var cancellables = Set<AnyCancellable>() private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) - // MARK: Lifecycle - - public init(prefilled: String, _ completion: @escaping StringClosure) { + public init(_ prefilled: String, _ completion: @escaping StringClosure) { self.prefilled = prefilled self.completion = completion super.init(nibName: nil, bundle: nil) @@ -50,8 +44,6 @@ public final class NickameController: UIViewController { viewModel.didInput(prefilled) } - // MARK: Private - private func setupKeyboard() { keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in guard let self = self else { return } diff --git a/Sources/ContactFeature/Coordinator/ContactCoordinator.swift b/Sources/ContactFeature/Coordinator/ContactCoordinator.swift index cf8e67dcb0763550e23495544a310486549fc303..a9ceab13dd5e0614b37bc3e8d05223d4ff9a09bd 100644 --- a/Sources/ContactFeature/Coordinator/ContactCoordinator.swift +++ b/Sources/ContactFeature/Coordinator/ContactCoordinator.swift @@ -13,57 +13,35 @@ public protocol ContactCoordinating: AnyObject { } public final class ContactCoordinator: ContactCoordinating { - public init(requestsFactory: @escaping () -> UIViewController) { - self.requestsFactory = requestsFactory - } - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() - var presenter: Presenting = ModalPresenter() + var pushPresenter: Presenting = PushPresenter() + var modalPresenter: Presenting = ModalPresenter() var bottomPresenter: Presenting = BottomPresenter() - var replacer: Presenting = ReplacePresenter(mode: .replaceBackwards(SingleChatController.self)) - - // MARK: Factories + var replacePresenter: Presenting = ReplacePresenter(mode: .replaceBackwards(SingleChatController.self)) var requestsFactory: () -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - = SingleChatController.init(_:) - var imagePickerFactory: () -> UIImagePickerController - = UIImagePickerController.init - var nicknameFactory: (String, @escaping StringClosure) -> UIViewController - = NickameController.init(prefilled:_:) -} - -public extension ContactCoordinator { - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - pusher.present(screen, from: parent) - } - - func toPopup( - _ popup: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(popup, from: parent) - } - func toSingleChat( - with contact: Contact, - from parent: UIViewController + public init( + requestsFactory: @escaping () -> UIViewController, + singleChatFactory: @escaping (Contact) -> UIViewController, + imagePickerFactory: @escaping () -> UIImagePickerController, + nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController ) { - let screen = singleChatFactory(contact) - replacer.present(screen, from: parent) + self.requestsFactory = requestsFactory + self.singleChatFactory = singleChatFactory + self.imagePickerFactory = imagePickerFactory + self.nicknameFactory = nicknameFactory } +} +public extension ContactCoordinator { func toPhotos(from parent: UIViewController) { let screen = imagePickerFactory() screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) screen.allowsEditing = true - presenter.present(screen, from: parent) + modalPresenter.present(screen, from: parent) } func toNickname( @@ -74,4 +52,18 @@ public extension ContactCoordinator { let screen = nicknameFactory(prefilled, completion) bottomPresenter.present(screen, from: parent) } + + func toRequests(from parent: UIViewController) { + let screen = requestsFactory() + pushPresenter.present(screen, from: parent) + } + + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) + } + + func toSingleChat(with contact: Contact, from parent: UIViewController) { + let screen = singleChatFactory(contact) + replacePresenter.present(screen, from: parent) + } } diff --git a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift b/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift index f3857bd8c7b7b31dab5de00aa92948b97273843e..99440fc5e403a2363639c7bd200309d0f194efc5 100644 --- a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift +++ b/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift @@ -12,97 +12,81 @@ public protocol ContactListCoordinating { func toRequests(from: UIViewController) func toNewGroup(from: UIViewController) func toContact(_: Contact, from: UIViewController) - - func toGroupChat( - with: GroupChatInfo, - from: UIViewController - ) - - func toGroupPopup( - with: Int, - from: UIViewController, - _: @escaping (String, String?) -> Void - ) + func toGroupChat(with: GroupChatInfo, from: UIViewController) + func toGroupPopup(with: Int, from: UIViewController, _: @escaping (String, String?) -> Void) } public struct ContactListCoordinator: ContactListCoordinating { - public init( - scanFactory: @escaping () -> UIViewController, - searchFactory: @escaping () -> UIViewController, - newGroupFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController - ) { - self.scanFactory = scanFactory - self.searchFactory = searchFactory - self.newGroupFactory = newGroupFactory - self.requestsFactory = requestsFactory - } - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() + var pushPresenter: Presenting = PushPresenter() var bottomPresenter: Presenting = BottomPresenter() var fullscreenPresenter: Presenting = FullscreenPresenter() - var replacer: Presenting = ReplacePresenter(mode: .replaceLast) - - // MARK: Factories + var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast) var scanFactory: () -> UIViewController var searchFactory: () -> UIViewController var newGroupFactory: () -> UIViewController var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - = ContactController.init(_:) - var groupChatFactory: (GroupChatInfo) -> UIViewController - = GroupChatController.init(_:) - var groupPopupFactory: (Int, @escaping (String, String?) -> Void) -> UIViewController - = CreatePopupController.init(_:_:) + + public init( + scanFactory: @escaping () -> UIViewController, + searchFactory: @escaping () -> UIViewController, + newGroupFactory: @escaping () -> UIViewController, + requestsFactory: @escaping () -> UIViewController, + contactFactory: @escaping (Contact) -> UIViewController, + groupChatFactory: @escaping (GroupChatInfo) -> UIViewController, + groupPopupFactory: @escaping (Int, @escaping (String, String?) -> Void) -> UIViewController + ) { + self.scanFactory = scanFactory + self.searchFactory = searchFactory + self.newGroupFactory = newGroupFactory + self.requestsFactory = requestsFactory + self.contactFactory = contactFactory + self.groupChatFactory = groupChatFactory + self.groupPopupFactory = groupPopupFactory + } } public extension ContactListCoordinator { - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - pusher.present(screen, from: parent) + func toGroupPopup( + with count: Int, + from parent: UIViewController, + _ completion: @escaping (String, String?) -> Void + ) { + let screen = ScrollViewController.embedding(groupPopupFactory(count, completion)) + fullscreenPresenter.present(screen, from: parent) } func toScan(from parent: UIViewController) { let screen = scanFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toSearch(from parent: UIViewController) { let screen = searchFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pusher.present(screen, from: parent) + func toRequests(from parent: UIViewController) { + let screen = requestsFactory() + pushPresenter.present(screen, from: parent) } func toNewGroup(from parent: UIViewController) { let screen = newGroupFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } - func toGroupChat( - with info: GroupChatInfo, - from parent: UIViewController - ) { - let screen = groupChatFactory(info) - replacer.present(screen, from: parent) + func toContact(_ contact: Contact, from parent: UIViewController) { + let screen = contactFactory(contact) + pushPresenter.present(screen, from: parent) } - func toGroupPopup( - with count: Int, - from parent: UIViewController, - _ completion: @escaping (String, String?) -> Void - ) { - let screen = ScrollViewController.embedding(groupPopupFactory(count, completion)) - fullscreenPresenter.present(screen, from: parent) + func toGroupChat(with info: GroupChatInfo, from parent: UIViewController) { + let screen = groupChatFactory(info) + replacePresenter.present(screen, from: parent) } } diff --git a/Sources/Countries/CountryListController.swift b/Sources/Countries/CountryListController.swift index 6f9377f343f70797e4a8d0b6dd92c9ae80ffec40..c0a8341b12754f055b27da96c01649395d581d3c 100644 --- a/Sources/Countries/CountryListController.swift +++ b/Sources/Countries/CountryListController.swift @@ -17,13 +17,13 @@ public final class CountryListController: UIViewController { private var cancellables = Set<AnyCancellable>() private var dataSource: UITableViewDiffableDataSource<SectionId, Country>! - public convenience init(_ didChoose: @escaping (Country) -> Void) { - logger.log("init()") - - self.init() + public init(_ didChoose: @escaping (Country) -> Void) { self.didChoose = didChoose + super.init(nibName: nil, bundle: nil) } + required init?(coder: NSCoder) { nil } + public override func viewWillAppear(_ animated: Bool) { logger.log("viewWillAppear()") diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift index 22da5e88eeab1e15d6dc5886d10689db30352d7b..385c4992b1f35e76d9232822884d3570f125e51a 100644 --- a/Sources/Defaults/KeyObject.swift +++ b/Sources/Defaults/KeyObject.swift @@ -22,6 +22,10 @@ public enum Key: String { case theme + // MARK: Backup + + case backupSettings + // MARK: Settings case biometrics @@ -29,7 +33,6 @@ public enum Key: String { case recordingLogs case crashReporting case icognitoKeyboard - case openedSettingsFirstTime case dummyTrafficOn case askedDummyTrafficOnce diff --git a/Sources/DropboxFeature/DropboxInterface.swift b/Sources/DropboxFeature/DropboxInterface.swift new file mode 100644 index 0000000000000000000000000000000000000000..1a5aa02a054ad29816530447c3ed5d725573160b --- /dev/null +++ b/Sources/DropboxFeature/DropboxInterface.swift @@ -0,0 +1,18 @@ +import UIKit +import Combine + +public protocol DropboxInterface { + func isAuthorized() -> Bool + + func unlink() + + func handleOpenUrl(_ url: URL) -> Bool + + func downloadBackup(_: String, _: @escaping (Result<Data, Error>) -> Void) + + func uploadBackup(_: URL, _: @escaping (Result<DropboxMetadata, Error>) -> Void) + + func downloadMetadata(_: @escaping (Result<DropboxMetadata?, Error>) -> Void) + + func authorize(presenting: UIViewController) -> AnyPublisher<Result<Bool, Error>, Never> +} diff --git a/Sources/DropboxFeature/DropboxMetadata.swift b/Sources/DropboxFeature/DropboxMetadata.swift new file mode 100644 index 0000000000000000000000000000000000000000..ceb4fe195242bf13ffb4e2a65a48d58aea282756 --- /dev/null +++ b/Sources/DropboxFeature/DropboxMetadata.swift @@ -0,0 +1,18 @@ +import Foundation +import SwiftyDropbox + +public struct DropboxMetadata: Equatable { + public var size: Float + public var path: String + public var modifiedDate: Date + + public init( + size: Float, + path: String, + modifiedDate: Date + ) { + self.size = size + self.path = path + self.modifiedDate = modifiedDate + } +} diff --git a/Sources/DropboxFeature/DropboxService.swift b/Sources/DropboxFeature/DropboxService.swift new file mode 100644 index 0000000000000000000000000000000000000000..e90ef79721443c67ae452e1a0105ab0b86c2e6ca --- /dev/null +++ b/Sources/DropboxFeature/DropboxService.swift @@ -0,0 +1,209 @@ +import UIKit +import Combine +import SwiftyDropbox + +public struct DropboxService: DropboxInterface { + private let didAuthorizeSubject = PassthroughSubject<Result<Bool, Error>, Never>() + + public init() { + let path = Bundle.module.path(forResource: "Dropbox-Keys", ofType: "plist") + let url = URL(fileURLWithPath: path!) + let keys = try! NSDictionary(contentsOf: url, error: ()) + + DropboxClientsManager.setupWithAppKey(keys["DROPBOX_APP_KEY"] as! String) + } + + public func unlink() { + DropboxClientsManager.unlinkClients() + } + + public func isAuthorized() -> Bool { + DropboxClientsManager.authorizedClient != nil + } + + public func authorize(presenting controller: UIViewController) -> AnyPublisher<Result<Bool, Error>, Never> { + let scopes = ["files.metadata.read", "files.content.read", "files.content.write"] + + return didAuthorizeSubject.handleEvents(receiveSubscription: { _ in + let scopeRequest = ScopeRequest(scopeType: .user, scopes: scopes, includeGrantedScopes: false) + + DropboxClientsManager.authorizeFromControllerV2( + UIApplication.shared, + controller: controller, + loadingStatusDelegate: nil, + openURL: { (url: URL) -> Void in UIApplication.shared.open(url, options: [:], completionHandler: nil) }, + scopeRequest: scopeRequest + ) + }).first().eraseToAnyPublisher() + } + + public func handleOpenUrl(_ url: URL) -> Bool { + DropboxClientsManager.handleRedirectURL(url) { + switch $0 { + case .none: + didAuthorizeSubject.send(.success(false)) + case .error(let oAuthError, _): + didAuthorizeSubject.send(.failure(oAuthError)) + case .success: + didAuthorizeSubject.send(.success(true)) + case .cancel: + didAuthorizeSubject.send(.success(false)) + } + } + } + + public func downloadBackup(_ path: String, _ completion: @escaping (Result<Data, Error>) -> Void) { + Task { + do { + guard try await folderExists() else { fatalError() } + + let data = try await fetchBackup() + completion(.success(data)) + } catch { + completion(.failure(error)) + } + } + } + + public func uploadBackup(_ url: URL, _ completion: @escaping (Result<DropboxMetadata, Error>) -> Void) { + Task { + do { + if try await !folderExists() { + try await createFolder() + } + + let data = try Data(contentsOf: url) + let metadata = try await upload(data: data) + completion(.success(metadata)) + } catch { + completion(.failure(error)) + } + } + } + + public func downloadMetadata(_ completion: @escaping (Result<DropboxMetadata?, Error>) -> Void) { + Task { + do { + guard try await folderExists() else { + completion(.success(nil)) + return + } + + let metadata = try await fetchMetadata() + completion(.success(metadata)) + } catch { + completion(.failure(error)) + } + } + } +} + +extension DropboxService { + private func folderExists() async throws -> Bool { + guard let client = DropboxClientsManager.authorizedClient else { fatalError() } + + return try await withCheckedThrowingContinuation { continuation in + client.files.listFolder(path: "/backup") + .response { result, error in + if let error = error { + if case .routeError(_, _, _, _) = error as CallError { + continuation.resume(returning: false) + return + } + + let err = NSError(domain: error.description, code: 0) + continuation.resume(throwing: err) + return + } + + continuation.resume(returning: result != nil) + } + } + } + + private func createFolder() async throws { + guard let client = DropboxClientsManager.authorizedClient else { fatalError() } + + return try await withCheckedThrowingContinuation { continuation in + client.files.createFolderV2(path: "/backup") + .response { _, error in + if let error = error { + let err = NSError(domain: error.description, code: 0) + continuation.resume(throwing: err) + return + } + + continuation.resume(returning: ()) + } + } + } + + private func fetchMetadata() async throws -> DropboxMetadata? { + guard let client = DropboxClientsManager.authorizedClient else { fatalError() } + + return try await withCheckedThrowingContinuation { continuation in + client.files.getMetadata(path: "/backup/backup.xxm") + .response { response, error in + if let error = error { + let err = NSError(domain: error.description, code: 0) + continuation.resume(throwing: err) + return + } + + if let result = response as? Files.FileMetadata { + let size = Float(result.size) + let modifiedDate = result.serverModified + continuation.resume(returning: .init( + size: size, + path: "/backup/backup.xxm", + modifiedDate: modifiedDate + )) + } else { + continuation.resume(returning: nil) + } + } + } + } + + private func fetchBackup() async throws -> Data { + guard let client = DropboxClientsManager.authorizedClient else { fatalError() } + + return try await withCheckedThrowingContinuation { continuation in + client.files.download(path: "/backup/backup.xxm") + .response(completionHandler: { response, error in + if let error = error { + let err = NSError(domain: error.description, code: 0) + continuation.resume(throwing: err) + return + } + + if let response = response { + continuation.resume(returning: response.1) + } + }) + } + } + + private func upload(data: Data) async throws -> DropboxMetadata { + guard let client = DropboxClientsManager.authorizedClient else { fatalError() } + + return try await withCheckedThrowingContinuation { continuation in + client.files.upload(path: "/backup/backup.xxm", mode: .overwrite, input: data) + .response { response, error in + if let error = error { + let err = NSError(domain: error.description, code: 0) + continuation.resume(throwing: err) + return + } + + if let response = response { + continuation.resume(returning: .init( + size: Float(response.size), + path: response.pathLower!, + modifiedDate: response.serverModified + )) + } + } + } + } +} diff --git a/Sources/DropboxFeature/DropboxServiceMock.swift b/Sources/DropboxFeature/DropboxServiceMock.swift new file mode 100644 index 0000000000000000000000000000000000000000..ba9654b69f870fb28b49cd8414676375fcb866ba --- /dev/null +++ b/Sources/DropboxFeature/DropboxServiceMock.swift @@ -0,0 +1,22 @@ +import UIKit +import Combine + +public struct DropboxServiceMock: DropboxInterface { + public init() {} + + public func unlink() {} + + public func isAuthorized() -> Bool { true } + + public func handleOpenUrl(_ url: URL) -> Bool { true } + + public func didFinishAuthFlow(withError: String?) {} + + public func downloadBackup(_: String, _: @escaping (Result<Data, Error>) -> Void) {} + + public func uploadBackup(_: URL, _: @escaping (Result<DropboxMetadata, Error>) -> Void) {} + + public func downloadMetadata(_: @escaping (Result<DropboxMetadata?, Error>) -> Void) {} + + public func authorize(presenting: UIViewController) -> AnyPublisher<Result<Bool, Error>, Never> { fatalError() } +} diff --git a/Sources/DropboxFeature/Resources/Dropbox-Keys.plist b/Sources/DropboxFeature/Resources/Dropbox-Keys.plist new file mode 100644 index 0000000000000000000000000000000000000000..ecf15d0188386cac204a2e243e59320620a6c9c5 --- /dev/null +++ b/Sources/DropboxFeature/Resources/Dropbox-Keys.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>DROPBOX_APP_KEY</key> + <string></string> +</dict> +</plist> diff --git a/Sources/GoogleDriveFeature/GoogleDriveInterface.swift b/Sources/GoogleDriveFeature/GoogleDriveInterface.swift new file mode 100644 index 0000000000000000000000000000000000000000..c6710b8610fc9081e211fe0363f10f40584ea0e6 --- /dev/null +++ b/Sources/GoogleDriveFeature/GoogleDriveInterface.swift @@ -0,0 +1,13 @@ +import UIKit + +public protocol GoogleDriveInterface { + func isAuthorized(_: @escaping (Bool) -> Void) + + func downloadMetadata(_: @escaping (Result<GoogleDriveMetadata?, Error>) -> Void) + + func uploadBackup(_: URL, _: @escaping (Result<GoogleDriveMetadata, Error>) -> Void) + + func authorize(presenting: UIViewController, _: @escaping (Result<Void, Error>) -> Void) + + func downloadBackup(_: String, progressCallback: @escaping (Float) -> Void, _: @escaping (Result<Data, Error>) -> Void) +} diff --git a/Sources/GoogleDriveFeature/GoogleDriveMetadata.swift b/Sources/GoogleDriveFeature/GoogleDriveMetadata.swift new file mode 100644 index 0000000000000000000000000000000000000000..f4179db86b943af99a0eaacdcae32ea987bdd73f --- /dev/null +++ b/Sources/GoogleDriveFeature/GoogleDriveMetadata.swift @@ -0,0 +1,27 @@ +import GoogleAPIClientForREST_Drive + +public struct GoogleDriveMetadata: Equatable { + public var size: Float + public var identifier: String + public var modifiedDate: Date + + public init( + size: Float, + identifier: String, + modifiedDate: Date + ) { + self.size = size + self.identifier = identifier + self.modifiedDate = modifiedDate + } +} + +extension GoogleDriveMetadata { + init?(withDriveFile file: GTLRDrive_File) { + guard let size = file.size?.floatValue, + let identifier = file.identifier, + let modifiedDate = file.modifiedTime?.date else { return nil } + + self.init(size: size, identifier: identifier, modifiedDate: modifiedDate) + } +} diff --git a/Sources/GoogleDriveFeature/GoogleDriveService.swift b/Sources/GoogleDriveFeature/GoogleDriveService.swift new file mode 100644 index 0000000000000000000000000000000000000000..4ae4ac7457058d11ab3777cd71a4e8843f33458c --- /dev/null +++ b/Sources/GoogleDriveFeature/GoogleDriveService.swift @@ -0,0 +1,335 @@ +import UIKit +import GoogleSignIn +import GTMSessionFetcherFull +import GTMSessionFetcherCore +import GoogleAPIClientForREST_Drive + +public final class GoogleDriveService: GoogleDriveInterface { + private static let scopeFile = "https://www.googleapis.com/auth/drive.file" + private static let scopeAppData = "https://www.googleapis.com/auth/drive.appdata" + + var user: GIDGoogleUser? + + let service: GTLRDriveService = { + let service = GTLRDriveService() + + let path = Bundle.module.path(forResource: "GoogleDrive-Keys", ofType: "plist") + let url = URL(fileURLWithPath: path!) + let keys = try! NSDictionary(contentsOf: url, error: ()) + + service.apiKey = keys["DRIVE_API_KEY"] as? String + return service + }() + + public init() {} + + public func isAuthorized(_ completion: @escaping (Bool) -> Void) { + guard GIDSignIn.sharedInstance.hasPreviousSignIn() else { + return completion(false) + } + + GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in + guard let user = user, let scopes = user.grantedScopes, error == nil else { + return completion(false) + } + + self.user = user + self.service.authorizer = user.authentication.fetcherAuthorizer() + completion(scopes.contains(GoogleDriveService.scopeFile) && scopes.contains(GoogleDriveService.scopeAppData)) + } + } + + public func authorize( + presenting controller: UIViewController, + _ completion: @escaping (Result<Void, Error>) -> Void + ) { + GIDSignIn.sharedInstance.restorePreviousSignIn { [weak self] user, error in + guard let self = self else { return } + + guard error == nil else { + self.signIn(presenting: controller) { + switch $0 { + case .success: + self.authorizeDrive(controller: controller, completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } + + return + } + + guard let user = user else { fatalError() } + + self.user = user + self.service.authorizer = user.authentication.fetcherAuthorizer() + self.authorizeDrive(controller: controller, completion: completion) + } + } + + public func downloadMetadata(_ completion: @escaping (Result<GoogleDriveMetadata?, Error>) -> Void) { + Task { + do { + guard let folder = try await fetchFolder() else { + completion(.success(nil)) + return + } + + _ = try await listFiles(on: folder) + + let backup = try await fetchBackup(at: folder) + completion(.success(backup)) + } catch { + completion(.failure(error)) + } + } + } + + public func downloadBackup( + _ backup: String, + progressCallback: @escaping (Float) -> Void, + _ completion: @escaping (Result<Data, Error>) -> Void + ) { + let query = GTLRDriveQuery_FilesGet.queryForMedia(withFileId: backup) + service.executeQuery(query) { _, file, error in + guard error == nil else { + print("Error on line #\(#line): \(error!.localizedDescription)") + return completion(.failure(error!)) + } + + guard let data = (file as? GTLRDataObject)?.data else { + print("Error on line #\(#line)") + return completion(.failure(NSError())) + } + + completion(.success(data)) + } + } + + public func uploadBackup( + _ file: URL, + _ completion: @escaping (Result<GoogleDriveMetadata, Error>) -> Void + ) { + Task { + do { + var folder = try await fetchFolder() + if folder == nil { folder = try await createFolder() } + let metadata = try await uploadFile(file, to: folder!) + let listMetadata = try await listFiles(on: folder!) + try await cleanup(listMetadata) + completion(.success(metadata)) + } catch { + print("Error on line #\(#line): \(error.localizedDescription)") + completion(.failure(error)) + } + } + } +} + +extension GoogleDriveService { + private func authorizeDrive( + controller: UIViewController, + completion: @escaping (Result<Void, Error>) -> Void + ) { + if let user = user, + let scopes = user.grantedScopes, + scopes.contains(GoogleDriveService.scopeFile), + scopes.contains(GoogleDriveService.scopeAppData) { + return completion(.success(())) + } + + GIDSignIn.sharedInstance.addScopes( + [GoogleDriveService.scopeFile, GoogleDriveService.scopeAppData], + presenting: controller, callback: { user, error in + guard error == nil else { + print("Error on line #\(#line): \(error!.localizedDescription)") + return completion(.failure(error!)) + } + + guard let user = user else { fatalError() } + self.user = user + completion(.success(())) + } + ) + } + + private func signIn( + presenting controller: UIViewController, + completion: @escaping (Result<Void, Error>) -> Void + ) { + GIDSignIn.sharedInstance.signIn( + with: GIDConfiguration(clientID: "662236151640-30i07ubg6ukodg15u0bnpk322p030u3j.apps.googleusercontent.com"), + presenting: controller, + callback: { user, error in + guard error == nil else { + print("Error on line #\(#line): \(error!.localizedDescription)") + return completion(.failure(error!)) + } + + guard let user = user else { fatalError() } + + self.user = user + self.service.authorizer = user.authentication.fetcherAuthorizer() + completion(.success(())) + } + ) + } + + private func fetchFolder() async throws -> String? { + let query = GTLRDriveQuery_FilesList.query() + query.q = "mimeType = 'application/vnd.google-apps.folder' and name = 'backup'" + query.spaces = "appDataFolder" + query.fields = "nextPageToken, files(id, name)" + + return try await withCheckedThrowingContinuation { continuation in + service.executeQuery(query) { _, result, error in + if let error = error { + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + let item = (result as? GTLRDrive_FileList)?.files?.first + continuation.resume(returning: item?.identifier) + } + } + } + + private func fetchBackup(at folder: String) async throws -> GoogleDriveMetadata? { + let query = GTLRDriveQuery_FilesList.query() + query.q = "'\(folder)' in parents and name = 'backup.xxm'" + query.spaces = "appDataFolder" + query.fields = "nextPageToken, files(id, size, name, modifiedTime)" + + return try await withCheckedThrowingContinuation { continuation in + service.executeQuery(query) { _, result, error in + if let error = error { + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + var metadata: GoogleDriveMetadata? = nil + + if let file = (result as? GTLRDrive_FileList)?.files?.first, + let size = file.size, + let id = file.identifier, + let date = file.modifiedTime?.date { + metadata = GoogleDriveMetadata(size: size.floatValue, identifier: id, modifiedDate: date) + } + + continuation.resume(returning: metadata) + } + } + } + + private func createFolder() async throws -> String { + let file = GTLRDrive_File() + file.name = "backup" + file.parents = ["appDataFolder"] + file.mimeType = "application/vnd.google-apps.folder" + + let query = GTLRDriveQuery_FilesCreate.query(withObject: file, uploadParameters: nil) + + return try await withCheckedThrowingContinuation { continuation in + service.executeQuery(query) { _, result, error in + if let error = error { + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + guard let identifier = (result as? GTLRDrive_File)?.identifier else { + let errorTitle = "Couldn't create backup folder but no error was passed (?)" + let error = NSError(domain: errorTitle, code: 0, userInfo: [NSLocalizedDescriptionKey: errorTitle]) + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + continuation.resume(returning: identifier) + } + } + } + + private func uploadFile( + _ fileURL: URL, + to folder: String + ) async throws -> GoogleDriveMetadata { + + let file = GTLRDrive_File() + file.name = "backup.xxm" + file.parents = [folder] + file.mimeType = "application/octet-stream" + + let params = GTLRUploadParameters(fileURL: fileURL, mimeType: file.mimeType!) + let query = GTLRDriveQuery_FilesCreate.query(withObject: file, uploadParameters: params) + query.fields = "id, size, modifiedTime" + + return try await withCheckedThrowingContinuation { continuation in + service.executeQuery(query) { _, result, error in + if let error = error { + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + guard let driveFile = (result as? GTLRDrive_File), + let size = driveFile.size, + let id = driveFile.identifier, + let date = driveFile.modifiedTime?.date else { + let errorTitle = "Couldn't upload file but no error was passed (?)" + let error = NSError(domain: errorTitle, code: 0, userInfo: [NSLocalizedDescriptionKey: errorTitle]) + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + continuation.resume(returning: .init(size: size.floatValue, identifier: id, modifiedDate: date)) + } + } + } + + private func listFiles(on folder: String) async throws -> [GoogleDriveMetadata] { + let query = GTLRDriveQuery_FilesList.query() + query.q = "'\(folder)' in parents" + query.spaces = "appDataFolder" + query.fields = "nextPageToken, files(id, modifiedTime, size, name)" + + return try await withCheckedThrowingContinuation { continuation in + service.executeQuery(query) { _, result, error in + if let error = error { + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + guard let files = (result as? GTLRDrive_FileList)?.files else { + continuation.resume(returning: []) + return + } + + let metadataList = files.compactMap(GoogleDriveMetadata.init(withDriveFile:)) + continuation.resume(returning: metadataList) + } + } + } + + private func cleanup(_ files: [GoogleDriveMetadata]) async throws { + let latestBackup = files.max { $0.modifiedDate < $1.modifiedDate } + let identifiers = files.filter { $0 != latestBackup }.map(\.identifier) + let query = GTLRBatchQuery(queries: identifiers.map(GTLRDriveQuery_FilesDelete.query(withFileId:))) + + return try await withCheckedThrowingContinuation { continuation in + service.executeQuery(query) { _, _, error in + if let error = error { + print("Error on line #\(#line): \(error.localizedDescription)") + continuation.resume(throwing: error) + return + } + + continuation.resume(returning: ()) + } + } + } +} diff --git a/Sources/GoogleDriveFeature/GoogleDriveServiceMock.swift b/Sources/GoogleDriveFeature/GoogleDriveServiceMock.swift new file mode 100644 index 0000000000000000000000000000000000000000..cf7625355cbc778fca29eb245586079b1601db6f --- /dev/null +++ b/Sources/GoogleDriveFeature/GoogleDriveServiceMock.swift @@ -0,0 +1,40 @@ +import UIKit + +public final class GoogleDriveServiceMock: GoogleDriveInterface { + public init() {} + + public func isAuthorized(_ completion: @escaping (Bool) -> Void) { + completion(true) + } + + public func uploadBackup(_: URL, _ completion: @escaping (Result<GoogleDriveMetadata, Error>) -> Void) { + completion(.success(.init(size: 23.toBytes(), identifier: "", modifiedDate: Date()))) + } + + public func downloadMetadata(_ completion: @escaping (Result<GoogleDriveMetadata?, Error>) -> Void) { + completion(.success(.init(size: 23.toBytes(), identifier: "", modifiedDate: Date()))) + } + + public func authorize(presenting: UIViewController, _ completion: @escaping (Result<Void, Error>) -> Void) { + completion(.success(())) + } + + public func downloadBackup( + _: String, + progressCallback: @escaping (Float) -> Void, + _ completion: @escaping (Result<Data, Error>) -> Void + ) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { progressCallback(3.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { progressCallback(7.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) { progressCallback(12.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { progressCallback(15.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { progressCallback(16.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { progressCallback(19.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 2.1) { progressCallback(22.toBytes()) } + DispatchQueue.main.asyncAfter(deadline: .now() + 2.4) { completion(.success(Data())) } + } +} + +private extension Int { + func toBytes() -> Float { Float(self) * 1000000.0 } +} diff --git a/Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist b/Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist new file mode 100644 index 0000000000000000000000000000000000000000..614bac60d3fd5014a33613ee3e83c72656d0854d --- /dev/null +++ b/Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>DRIVE_API_KEY</key> + <string></string> +</dict> +</plist> diff --git a/Sources/Integration/Callbacks.swift b/Sources/Integration/Callbacks.swift index 5f658bc3ce6b2bbdb427153f8f45a7554a0e2429..ed9d917cf8ff0469015a0dfa4db869a450666bfe 100644 --- a/Sources/Integration/Callbacks.swift +++ b/Sources/Integration/Callbacks.swift @@ -102,7 +102,7 @@ final class RoundCallback: NSObject, BindingsRoundCompletionCallbackProtocol { } func eventCallback(_ rid: Int, success: Bool, timedOut: Bool) { - log(string: "Add/Confirm RoundCallback:\nid: \(rid)\nSuccessfull: \(success)\nTimed out: \(timedOut)", type: .info) + log(string: ">>> Add/Confirm RoundCallback:\nid: \(rid)\nSuccessfull: \(success)\nTimed out: \(timedOut)", type: .info) callback(success && !timedOut) } } @@ -203,17 +203,14 @@ final class LookupCallback: NSObject, BindingsLookupCallbackProtocol { } func callback(_ contact: BindingsContact?, error: String?) { - guard let contact = contact else { - if let error = error { - if !error.isEmpty { - callback(.failure(NSError.create(error).friendly())) - } - } - + if let error = error, !error.isEmpty { + callback(.failure(NSError.create(error).friendly())) return } - callback(.success(contact)) + if let contact = contact { + callback(.success(contact)) + } } } @@ -255,3 +252,44 @@ final class OutgoingTransferProgressCallback: NSObject, BindingsFileTransferSent callback(completed, sent, arrived, total, err) } } + +final class UpdateBackupCallback: NSObject, BindingsUpdateBackupFuncProtocol { + let callback: (Data) -> Void + + init(_ callback: @escaping (Data) -> Void) { + self.callback = callback + super.init() + } + + func updateBackup(_ encryptedBackup: Data?) { + guard let data = encryptedBackup else { return } + callback(data) + } +} + +final class ResetCallback: NSObject, BindingsAuthResetNotificationCallbackProtocol { + let callback: (BindingsContact) -> Void + + init(_ callback: @escaping (BindingsContact) -> Void) { + self.callback = callback + super.init() + } + + func callback(_ requestor: BindingsContact?) { + guard let requestor = requestor else { return } + callback(requestor) + } +} + +final class RestoreContactsCallback: NSObject, BindingsRestoreContactsUpdaterProtocol { + let callback: (Int, Int, Int, String?) -> Void + + init(_ callback: @escaping (Int, Int, Int, String?) -> Void) { + self.callback = callback + super.init() + } + + func restoreContactsCallback(_ numFound: Int, numRestored: Int, total: Int, err: String?) { + callback(numFound, numRestored, total, err) + } +} diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift index 22d4a68f2167c6d5a9e09919bae19b076c7621fd..690f6132f7ed344d74dcde0014778fc464f71dce 100644 --- a/Sources/Integration/Client.swift +++ b/Sources/Integration/Client.swift @@ -3,42 +3,65 @@ import Models import Combine import Defaults import Foundation +import Bindings public class Client { @KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool let bindings: BindingsInterface + var backupManager: BackupInterface? var dummyManager: DummyTrafficManaging? var groupManager: GroupManagerInterface? var userDiscovery: UserDiscoveryInterface? var transferManager: TransferManagerInterface? + var backup: AnyPublisher<Data, Never> { backupSubject.eraseToAnyPublisher() } var network: AnyPublisher<Bool, Never> { networkSubject.eraseToAnyPublisher() } + var resets: AnyPublisher<Contact, Never> { resetsSubject.eraseToAnyPublisher() } var messages: AnyPublisher<Message, Never> { messagesSubject.eraseToAnyPublisher() } var requests: AnyPublisher<Contact, Never> { requestsSubject.eraseToAnyPublisher() } var events: AnyPublisher<BackendEvent, Never> { eventsSubject.eraseToAnyPublisher() } + var requestsSent: AnyPublisher<Contact, Never> { requestsSentSubject.eraseToAnyPublisher() } var confirmations: AnyPublisher<Contact, Never> { confirmationsSubject.eraseToAnyPublisher() } var groupMessages: AnyPublisher<GroupMessage, Never> { groupMessagesSubject.eraseToAnyPublisher() } var incomingTransfers: AnyPublisher<FileTransfer, Never> { transfersSubject.eraseToAnyPublisher() } var groupRequests: AnyPublisher<(Group, [Data], String?), Never> { groupRequestsSubject.eraseToAnyPublisher() } + private let backupSubject = PassthroughSubject<Data, Never>() private let networkSubject = PassthroughSubject<Bool, Never>() + private let resetsSubject = PassthroughSubject<Contact, Never>() private let requestsSubject = PassthroughSubject<Contact, Never>() private let messagesSubject = PassthroughSubject<Message, Never>() private let eventsSubject = PassthroughSubject<BackendEvent, Never>() + private let requestsSentSubject = PassthroughSubject<Contact, Never>() private let confirmationsSubject = PassthroughSubject<Contact, Never>() private let transfersSubject = PassthroughSubject<FileTransfer, Never>() private let groupMessagesSubject = PassthroughSubject<GroupMessage, Never>() private let groupRequestsSubject = PassthroughSubject<(Group, [Data], String?), Never>() + private var isBackupInitialization = false + private var isBackupInitializationCompleted = false + // MARK: Lifecycle - init(_ bindings: BindingsInterface) { + init( + _ bindings: BindingsInterface, + fromBackup: Bool, + email: String?, + phone: String? + ) { self.bindings = bindings + self.isBackupInitialization = fromBackup do { try registerListenersAndStart() - try instantiateUserDiscovery() + + if fromBackup { + try instantiateUserDiscoveryFromBackup(email: email, phone: phone) + } else { + try instantiateUserDiscovery() + } + try instantiateTransferManager() try instantiateDummyTrafficManager() updatePreImage() @@ -47,18 +70,81 @@ public class Client { } } - // MARK: Public + public func listenBackup() { + backupManager = nil + backupManager = bindings.listenBackups { [weak backupSubject] in + backupSubject?.send($0) + } + } + + public func addJson(_ string: String) { + guard let backupManager = backupManager else { return } + backupManager.addJson(string) + } - private func registerListenersAndStart() throws { - bindings.listenNetworkUpdates { [weak networkSubject] in - networkSubject?.send($0) + public func stopListeningBackup() { + guard let backupManager = backupManager else { return } + try? backupManager.stop() + self.backupManager = nil + } + + public func restoreContacts(fromBackup backup: Data) { + var totalPendingRestoration: Int = 0 + + let report = bindings.restore( + ids: backup, + using: userDiscovery!) { [weak self] in + guard let self = self else { return } + + switch $0 { + case .success(var contact): + contact.status = .requested + self.requestsSentSubject.send(contact) + print(">>> Restored \(contact.username). Setting status as requested") + case .failure(let error): + print(">>> \(error.localizedDescription)") + } + } restoreCallback: { numFound, numRestored, total, errorString in + totalPendingRestoration = total + let results = + """ + >>> Results from within closure of RestoreContacts: + - numFound: \(numFound) + - numRestored: \(numRestored) + - total: \(total) + - errorString: \(errorString) + """ + print(results) + } + + guard totalPendingRestoration > 0 else { fatalError("Total is zero, why called restore contacts?") } + + guard report.lenRestored() == totalPendingRestoration else { + print(">>> numRestored \(report.lenRestored()) is != than the total (\(totalPendingRestoration)). Going on recursion...\nnumFailed: \(report.lenFailed())\n\(report.getRestoreContactsError())") + restoreContacts(fromBackup: backup) + return } + isBackupInitializationCompleted = true + } + + private func registerListenersAndStart() throws { + bindings.listenNetworkUpdates { [weak networkSubject] in networkSubject?.send($0) } + bindings.listenRequests { [weak self] in guard let self = self else { return } - self.requestsSubject.send($0) - } confirmations: { [weak confirmationsSubject] in + + if self.isBackupInitialization { + if self.isBackupInitializationCompleted { + self.requestsSubject.send($0) + } + } else { + self.requestsSubject.send($0) + } + } _: { [weak confirmationsSubject] in confirmationsSubject?.send($0) + } _: { [weak resetsSubject] in + resetsSubject?.send($0) } bindings.listenEvents { [weak eventsSubject] in @@ -115,6 +201,13 @@ public class Client { } } + private func instantiateUserDiscoveryFromBackup(email: String?, phone: String?) throws { + retry(max: 4, retryStrategy: .delay(seconds: 1)) { [weak self] in + guard let self = self else { return } + self.userDiscovery = try self.bindings.generateUDFromBackup(email: email, phone: phone) + } + } + private func instantiateDummyTrafficManager() throws { dummyManager = try bindings.generateDummyTraficManager() } diff --git a/Sources/Integration/Implementations/Bindings.swift b/Sources/Integration/Implementations/Bindings.swift index f6a5fc67754e8c60211cc3edd5beeeb361763f1e..e9766771e889f1df7725c7b55295a9ac9a113249 100644 --- a/Sources/Integration/Implementations/Bindings.swift +++ b/Sources/Integration/Implementations/Bindings.swift @@ -49,6 +49,16 @@ extension BindingsClient: BindingsInterface { log(string: string, type: .bindings) } + public func resetSessionWith(_ recipient: Data) { + var int: Int = 0 + + do { + try resetSession(recipient, meMarshaled: meMarshalled, message: "", ret0_: &int) + } catch { + print(">>> \(error.localizedDescription)") + } + } + public func verify(marshaled: Data, verifiedMarshaled: Data) throws -> Bool { var bool: ObjCBool = false try verifyOwnership(marshaled, verifiedMarshaled: verifiedMarshaled, ret0_: &bool) @@ -181,12 +191,14 @@ extension BindingsClient: BindingsInterface { return BindingsGetVersion() }() + public static let new: ClientNew = BindingsNewClient + + public static let fromBackup: ClientFromBackup = BindingsNewClientFromBackup + public static let secret: (Int) -> Data? = BindingsGenerateSecret public static let login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = BindingsLogin - public static let newClient: (String?, String?, Data?, String?, NSErrorPointer) -> Bool = BindingsNewClient - public static func updateNDF( for env: NetworkEnvironment, _ completion: @escaping (Result<Data?, Error>) -> Void @@ -246,14 +258,14 @@ extension BindingsClient: BindingsInterface { registerErrorCallback(BindingsError()) guard status == 0 else { - log(string: "Network is not ready yet. Let's give it a second...", type: .error) + log(string: ">>> Network is not ready yet. Let's give it a second...", type: .error) sleep(1) startNetwork() return } try! startNetworkFollower(10000) - log(string: "Starting the network...", type: .info) + log(string: ">>> Starting the network...", type: .info) } /// (Tries) to stop the network @@ -528,22 +540,79 @@ extension BindingsClient: BindingsInterface { throw error.friendly() } - /// Instantiates user discovery - /// - /// - Returns: An instance of *UD (User discovery)* - /// - /// - Throws: `UDError.noInstance` if no error was thrown - /// but also no instance was created - /// + public func generateUDFromBackup(email: String?, phone: String?) throws -> UserDiscoveryInterface { + var error: NSError? + + let paramEmail = email != nil ? "E\(email!)" : nil + let paramPhone = phone != nil ? "P\(phone!)" : nil + + let udb = BindingsNewUserDiscoveryFromBackup(self, paramEmail, paramPhone, &error) + + /// Alternate udb + +// guard let certPath = Bundle.module.path(forResource: "ud.elixxir.io", ofType: "crt") else { +// fatalError("Couldn't retrieve cert.") +// } +// +// guard let contactFilePath = Bundle.module.path(forResource: "udContact-test", ofType: "bin") else { +// fatalError("Couldn't retrieve cert.") +// } +// +// try! udb!.setAlternative( +// "18.198.117.203:11420".data(using: .utf8), +// cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), +// contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) +// ) + + guard let error = error else { return udb! } + throw error.friendly() + } + public func generateUD() throws -> UserDiscoveryInterface { log(type: .crumbs) var error: NSError? let udb = BindingsNewUserDiscovery(self, &error) + /// Alternate udb + +// guard let certPath = Bundle.module.path(forResource: "ud.elixxir.io", ofType: "crt") else { +// fatalError("Couldn't retrieve cert.") +// } +// +// guard let contactFilePath = Bundle.module.path(forResource: "udContact-test", ofType: "bin") else { +// fatalError("Couldn't retrieve cert.") +// } +// +// try! udb!.setAlternative( +// "18.198.117.203:11420".data(using: .utf8), +// cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), +// contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) +// ) + guard let error = error else { return udb! } throw error.friendly() } + + public func restore( + ids: Data, + using ud: UserDiscoveryInterface, + lookupCallback: @escaping (Result<Contact, Error>) -> Void, + restoreCallback: @escaping (Int, Int, Int, String?) -> Void + ) -> RestoreReportType { + let restoreCb = RestoreContactsCallback(restoreCallback) + + let lookupCb = LookupCallback { + switch $0 { + case .success(let contact): + lookupCallback(.success(.init(with: contact, status: .stranger))) + case .failure(let error): + lookupCallback(.failure(error)) + } + } + + return BindingsRestoreContactsFromBackup(ids, self, ud as? BindingsUserDiscovery, lookupCb, restoreCb)! + } } extension BindingsContact { @@ -585,7 +654,6 @@ extension BindingsSendReport: E2ESendReportType { public var roundURL: String { getRoundURL() } } - public protocol DummyTrafficManaging { var status: Bool { get } func setStatus(status: Bool) @@ -600,3 +668,7 @@ extension BindingsDummyTraffic: DummyTrafficManaging { try? setStatus(status) } } + +extension BindingsBackup: BackupInterface {} + +extension BindingsRestoreContactsReport: RestoreReportType {} diff --git a/Sources/Integration/Implementations/UserDiscovery.swift b/Sources/Integration/Implementations/UserDiscovery.swift index 98e584e7d8096df3aca0a053b82ac1b2c833944e..b88afa0d8585ef62648eb0af7fe86160fff18b8c 100644 --- a/Sources/Integration/Implementations/UserDiscovery.swift +++ b/Sources/Integration/Implementations/UserDiscovery.swift @@ -1,3 +1,4 @@ +import Retry import Models import Bindings import Foundation @@ -9,23 +10,20 @@ extension BindingsUserDiscovery: UserDiscoveryInterface { case .success(let contact): completion(.success(.init(with: contact, status: .stranger))) case .failure(let error): - log(string: "UD.lookup 4E2E callback failed:\n\(error.localizedDescription)", type: .error) completion(.failure(error)) } } - do { - try lookup(forUserId, callback: callback, timeoutMS: 20000) - } catch { + retry(max: 10, retryStrategy: .delay(seconds: 1)) { [weak self] in + guard let self = self else { return } + try self.lookup(forUserId, callback: callback, timeoutMS: 20000) + }.finalCatch { error in log(string: "UD.lookup 4E2E failed:\n\(error.localizedDescription)", type: .error) completion(.failure(error.friendly())) } } - public func lookup( - idList: [Data], - _ completion: @escaping (Result<[LookupResult], Error>) -> Void - ) { + public func lookup(idList: [Data], _ completion: @escaping (Result<[Contact], Error>) -> Void) { let list = BindingsIdList() idList.forEach { try? list.add($0) } @@ -40,16 +38,16 @@ extension BindingsUserDiscovery: UserDiscoveryInterface { guard let contacts = contactList else { return } let count = contacts.len() - var results = [LookupResult]() + var results = [Contact]() for index in 0..<count { guard let contact = try? contacts.get(index), let marshal = try? contact.marshal(), - let username = try? self.retrieve(from: marshal, fact: .username) else { + ((try? self.retrieve(from: marshal, fact: .username) != nil) != nil) else { log(string: "Skipping", type: .error); continue } - results.append(.init(id: contact.getID()!, username: username)) + results.append(Contact(with: contact, status: .stranger)) } completion(.success(results)) @@ -103,7 +101,7 @@ extension BindingsUserDiscovery: UserDiscoveryInterface { let confirmationId = addFact(bindingsFact?.stringify(), error: &otherError) if let otherError = otherError { - completion(.failure(otherError.friendly())) + completion(.failure(otherError)) return } diff --git a/Sources/Integration/Interfaces/BindingsInterface.swift b/Sources/Integration/Interfaces/BindingsInterface.swift index 9d60f2cbe28876c8fc2dc15403b825e5de1c2180..e9d913af958664feb10e0746f0687dcf17dd4064 100644 --- a/Sources/Integration/Interfaces/BindingsInterface.swift +++ b/Sources/Integration/Interfaces/BindingsInterface.swift @@ -1,5 +1,6 @@ import Models import Foundation +import Combine public enum MessageDeliveryStatus { case sent @@ -7,9 +8,13 @@ public enum MessageDeliveryStatus { case timedout } +public typealias DeliveryResult = (Data?, Bool, Bool, Data?) + public typealias BackendEvent = (Int, String?, String?, String?) -public typealias DeliveryResult = (Data?, Bool, Bool, Data?) +public typealias ClientNew = (String?, String?, Data?, String?, NSErrorPointer) -> Bool + +public typealias ClientFromBackup = (String?, String?, Data?, Data?, Data?, NSErrorPointer) -> Data? public typealias NotificationEvaluation = (String?, String?, NSErrorPointer) -> NotificationManyReportProtocol? @@ -20,6 +25,20 @@ public protocol E2ESendReportType { var roundURL: String { get } } +public protocol BackupInterface { + func stop() throws + func addJson(_: String?) +} + +public protocol RestoreReportType { + func lenFailed() -> Int + func lenRestored() -> Int + func getErrorAt(_: Int) -> String + func getFailedAt(_: Int) -> Data? + func getRestoreContactsError() -> String + func getRestoredAt(_: Int) -> Data? +} + public protocol BindingsInterface { // MARK: Properties @@ -48,7 +67,9 @@ public protocol BindingsInterface { static var login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? { get } - static var newClient: (String?, String?, Data?, String?, NSErrorPointer) -> Bool { get } + static var new: ClientNew { get } + + static var fromBackup: ClientFromBackup { get } static func updateNDF(for: NetworkEnvironment, _: @escaping (Result<Data?, Error>) -> Void) @@ -74,6 +95,8 @@ public protocol BindingsInterface { func compress(image: Data, _: @escaping(Result<Data, Error>) -> Void) + func resetSessionWith(_: Data) + func listen( report: Data, _: @escaping (Result<MessageDeliveryStatus, Error>) -> Void @@ -98,6 +121,8 @@ public protocol BindingsInterface { func generateUD() throws -> UserDiscoveryInterface + func generateUDFromBackup(email: String?, phone: String?) throws -> UserDiscoveryInterface + // MARK: FileTransfer func generateTransferManager( @@ -112,9 +137,12 @@ public protocol BindingsInterface { func listenMessages(_: @escaping (Message) -> Void) throws + func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface + func listenRequests( - _: @escaping (Contact) -> Void, - confirmations: @escaping (Contact) -> Void + _ requests: @escaping (Contact) -> Void, + _ confirmations: @escaping (Contact) -> Void, + _ resets: @escaping (Contact) -> Void ) func listenPreImageUpdates() @@ -127,4 +155,11 @@ public protocol BindingsInterface { func listenNetworkUpdates(_: @escaping (Bool) -> Void) func removeContact(_ data: Data) throws + + func restore( + ids: Data, + using: UserDiscoveryInterface, + lookupCallback: @escaping (Result<Contact, Error>) -> Void, + restoreCallback: @escaping (Int, Int, Int, String?) -> Void + ) -> RestoreReportType } diff --git a/Sources/Integration/Interfaces/UserDiscoveryInterface.swift b/Sources/Integration/Interfaces/UserDiscoveryInterface.swift index 116cf5072955f5ba432287c6d0d9dc0724e5785d..07398985fd1a25e500bb9015832d18e07226b57b 100644 --- a/Sources/Integration/Interfaces/UserDiscoveryInterface.swift +++ b/Sources/Integration/Interfaces/UserDiscoveryInterface.swift @@ -20,7 +20,7 @@ public protocol UserDiscoveryInterface { func search(fact: String, _: @escaping (Result<Contact, Error>) -> Void) throws - func lookup(idList: [Data], _: @escaping (Result<[LookupResult], Error>) -> Void) + func lookup(idList: [Data], _: @escaping (Result<[Contact], Error>) -> Void) func register(_: FactType, value: String, _: @escaping (Result<String?, Error>) -> Void) } diff --git a/Sources/Integration/Listeners.swift b/Sources/Integration/Listeners.swift index 32a6291244e598843585003dd24c468cefcbc5f9..cd81bc446f79b0d1e5b0339cae546f9191b58467 100644 --- a/Sources/Integration/Listeners.swift +++ b/Sources/Integration/Listeners.swift @@ -3,6 +3,8 @@ import Shared import Bindings import Foundation +import Combine + public extension BindingsClient { static func listenLogs() { let callback = LogCallback { log(string: $0 ?? "", type: .bindings) } @@ -20,6 +22,12 @@ public extension BindingsClient { registerPreimageCallback(receptionId, pin: callback) } + func listenBackups(_ callback: @escaping (Data) -> Void) -> BackupInterface { + var error: NSError? + let backup = BindingsInitializeBackup("", UpdateBackupCallback(callback), self, &error) + return backup! + } + func listenMessages(_ callback: @escaping (Message) -> Void) throws { let zeroBytes = [UInt8](repeating: 0, count: 33) @@ -34,11 +42,13 @@ public extension BindingsClient { func listenRequests( _ requests: @escaping (Contact) -> Void, - confirmations: @escaping (Contact) -> Void + _ confirmations: @escaping (Contact) -> Void, + _ resets: @escaping (Contact) -> Void ) { - let requestCallback = RequestCallback { requests(Contact(with: $0, status: .verificationInProgress)) } + let resetCallback = ResetCallback { resets(Contact(with: $0, status: .friend)) } let confirmCallback = ConfirmationCallback { confirmations(Contact(with: $0, status: .friend)) } - registerAuthCallbacks(requestCallback, confirm: confirmCallback, reset: nil) + let requestCallback = RequestCallback { requests(Contact(with: $0, status: .verificationInProgress)) } + registerAuthCallbacks(requestCallback, confirm: confirmCallback, reset: resetCallback) } func listenNetworkUpdates(_ callback: @escaping (Bool) -> Void) { @@ -49,7 +59,7 @@ public extension BindingsClient { do { try registerEventCallback("EventListener", myObj: EventCallback(completion)) } catch { - log(string: "Event listener failed: \(error.localizedDescription)", type: .error) + log(string: ">>> Event listener failed: \(error.localizedDescription)", type: .error) } } @@ -79,7 +89,7 @@ public extension BindingsClient { try roundList.get(index, ret0_: &integer) roundIds.append(integer) } catch { - log(string: "Error inspecting round list:\n\(error.localizedDescription)", type: .error) + log(string: ">>> Error inspecting round list:\n\(error.localizedDescription)", type: .error) } } } diff --git a/Sources/Integration/Logging.swift b/Sources/Integration/Logging.swift index edc3692e5cb9b6f47d81875ec94008221b37043f..c37e60b32c1261112930efe2a84248a74290fe3b 100644 --- a/Sources/Integration/Logging.swift +++ b/Sources/Integration/Logging.swift @@ -21,7 +21,7 @@ final class BindingsError: NSObject, BindingsClientErrorProtocol { extension Error { func friendly() -> NSError { - log(string: "Switching to friendly error from: \(localizedDescription)", type: .error) + log(string: ">>> Switching to friendly error from: \(localizedDescription)", type: .error) let error = BindingsErrorStringToUserFriendlyMessage(localizedDescription) if error.hasPrefix("UR") { diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift index cf1800b0c722925c25db1f2674df0ca527258be9..9d2c8acc3a34b210567bb90cdc9dcbb797d8f14d 100644 --- a/Sources/Integration/Mocks/BindingsMock.swift +++ b/Sources/Integration/Mocks/BindingsMock.swift @@ -35,12 +35,11 @@ public final class BindingsMock: BindingsInterface { public static let version: String = "MOCK" - public static var login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = { - _,_,_,_ in BindingsMock() - } - public static var newClient: (String?, String?, Data?, String?, NSErrorPointer) -> Bool = { - _,_,_,_,_ in true - } + public static var new: ClientNew = { _,_,_,_,_ in true } + + public static var fromBackup: ClientFromBackup = { _,_,_,_,_,_ in Data() } + + public static var login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = { _,_,_,_ in BindingsMock() } public func meMarshalled(_: String, email: String?, phone: String?) -> Data { meMarshalled @@ -70,6 +69,11 @@ public final class BindingsMock: BindingsInterface { public func generateUD() throws -> UserDiscoveryInterface { UserDiscoveryMock() } + public func generateUDFromBackup( + email: String?, + phone: String? + ) throws -> UserDiscoveryInterface { UserDiscoveryMock() } + public func generateTransferManager( _: @escaping (Data, String?, String?, Data?) -> Void ) throws -> TransferManagerInterface { @@ -80,6 +84,8 @@ public final class BindingsMock: BindingsInterface { public func listenMessages(_: @escaping (Message) -> Void) throws {} + public func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface { fatalError() } + public func listenNetworkUpdates(_: @escaping (Bool) -> Void) {} public func confirm(_: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { @@ -129,9 +135,12 @@ public final class BindingsMock: BindingsInterface { public func removeContact(_ data: Data) throws {} + public func resetSessionWith(_: Data) {} + public func listenRequests( _ requests: @escaping (Contact) -> Void, - confirmations: @escaping (Contact) -> Void + _ confirmations: @escaping (Contact) -> Void, + _ resets: @escaping (Contact) -> Void ) { requestsSubject.sink(receiveValue: requests).store(in: &cancellables) confirmationsSubject.sink(receiveValue: confirmations).store(in: &cancellables) @@ -144,6 +153,15 @@ public final class BindingsMock: BindingsInterface { GroupManagerMock() } + public func restore( + ids: Data, + using ud: UserDiscoveryInterface, + lookupCallback: @escaping (Result<Contact, Error>) -> Void, + restoreCallback: @escaping (Int, Int, Int, String?) -> Void + ) -> RestoreReportType { + fatalError() + } + public static func updateNDF(for: NetworkEnvironment, _ completion: @escaping (Result<Data?, Error>) -> Void) { completion(.success(Data())) } diff --git a/Sources/Integration/Mocks/UserDiscoveryMock.swift b/Sources/Integration/Mocks/UserDiscoveryMock.swift index b23a612e07ff3f7dde71b68e50b417496155c6e1..69cd094e0eae4ed5c281d5cc3058ad7e94945dec 100644 --- a/Sources/Integration/Mocks/UserDiscoveryMock.swift +++ b/Sources/Integration/Mocks/UserDiscoveryMock.swift @@ -9,7 +9,7 @@ final class UserDiscoveryMock: UserDiscoveryInterface { func confirm(code: String, id: String) throws {} - func lookup(idList: [Data], _: @escaping (Result<[LookupResult], Error>) -> Void) {} + func lookup(idList: [Data], _: @escaping (Result<[Contact], Error>) -> Void) {} func retrieve(from: Data, fact: FactType) throws -> String? { fact.description } @@ -21,5 +21,22 @@ final class UserDiscoveryMock: UserDiscoveryInterface { completion(.success("#CONFIRMATION_CODE_FOR \(value)")) } - func lookup(forUserId: Data, _: @escaping (Result<Contact, Error>) -> Void) {} + func lookup( + forUserId: Data, + _ completion: @escaping (Result<Contact, Error>) -> Void + ) { + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + completion(.success(.init( + photo: nil, + userId: "mock_username".data(using: .utf8)!, + email: nil, + phone: nil, + status: .stranger, + marshaled: "mock_username".data(using: .utf8)!, + username: "mock_username", + nickname: "mock_nickname", + createdAt: Date() + ))) + } + } } diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift index 1fbde8f3906abfc7a23e65c8ec4cb5ec679628f7..3a8b20a3f35d4cd211e7f01612a3d14e18976f3d 100644 --- a/Sources/Integration/Session/Session+Contacts.swift +++ b/Sources/Integration/Session/Session+Contacts.swift @@ -162,8 +162,6 @@ extension Session { contactToOperate.status = success ? .requested : .requestFailed contactToOperate = try self.dbManager.save(contactToOperate) - - log(string: "Successfully added \(title)", type: .info) case .failure(let error): contactToOperate.status = .requestFailed diff --git a/Sources/Integration/Session/Session+Group.swift b/Sources/Integration/Session/Session+Group.swift index 4abeefe4041bd9aa3358533570959350df469b07..115bd669c39af44a9a17e63a3e0a49313787bfca 100644 --- a/Sources/Integration/Session/Session+Group.swift +++ b/Sources/Integration/Session/Session+Group.swift @@ -169,9 +169,9 @@ extension Session { ud.lookup(idList: ids) { switch $0 { - case .success(let result): + case .success(let contacts): strangers.forEach { stranger in - if let found = result.first(where: { lookup in lookup.id == stranger.userId }) { + if let found = contacts.first(where: { contact in contact.userId == stranger.userId }) { var updatedStranger = stranger updatedStranger.username = found.username updatedStrangers.append(updatedStranger) @@ -180,7 +180,6 @@ extension Session { DispatchQueue.main.async { updatedStrangers.forEach { - do { try self.dbManager.save($0) } catch { diff --git a/Sources/Integration/Session/Session+UD.swift b/Sources/Integration/Session/Session+UD.swift index 4046dde176002267e1df84101de6f31dd00b423a..82c1e427b6312ae5ab647e3fb2783388ba4368d6 100644 --- a/Sources/Integration/Session/Session+UD.swift +++ b/Sources/Integration/Session/Session+UD.swift @@ -61,5 +61,7 @@ extension Session { } else { phone = confirmation.content } + + updateFactsOnBackup() } } diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift index 8de6347227c5f8c0acb92b1e32d06a67dda8b1ed..5cdb481b0040f90c52210d27e92bf8ffb7e66d4a 100644 --- a/Sources/Integration/Session/Session.swift +++ b/Sources/Integration/Session/Session.swift @@ -5,9 +5,26 @@ import Combine import Defaults import Database import Foundation +import BackupFeature import NetworkMonitor import DependencyInjection +struct BackupParameters: Codable { + var email: String? + var phone: String? + var username: String +} + +struct BackupReport: Codable { + var contactIds: [String] + var parameters: String + + private enum CodingKeys: String, CodingKey { + case parameters = "Params" + case contactIds = "RestoredContacts" + } +} + public final class Session: SessionType { @KeyObject(.theme, defaultValue: nil) var theme: String? @KeyObject(.email, defaultValue: nil) var email: String? @@ -25,6 +42,7 @@ public final class Session: SessionType { @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications: Bool @KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool + @Dependency var backupService: BackupService @Dependency var networkMonitor: NetworkMonitoring public let client: Client @@ -97,11 +115,37 @@ public final class Session: SessionType { .eraseToAnyPublisher() } + public init(backupFile: Data, ndf: String) throws { + let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking + let (client, backupData) = try network.newClientFromBackup(data: backupFile, ndf: ndf) + self.client = client + dbManager = GRDBDatabaseManager() + + let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!) + + if !report.parameters.isEmpty { + let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8)) + + username = params.username + phone = params.phone + email = params.email + } + + try continueInitialization() + + if !report.contactIds.isEmpty { + client.restoreContacts(fromBackup: try! JSONSerialization.data(withJSONObject: report.contactIds)) + } + } + public init(ndf: String) throws { let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking self.client = try network.newClient(ndf: ndf) - dbManager = GRDBDatabaseManager() + try continueInitialization() + } + + private func continueInitialization() throws { try dbManager.setup() setupBindings() @@ -118,12 +162,7 @@ public final class Session: SessionType { pendingVerificationUsers.forEach { var contact = $0 contact.status = .verificationFailed - - do { - _ = try dbManager.save(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } + _ = try? dbManager.save(contact) } } } @@ -133,42 +172,22 @@ public final class Session: SessionType { } public func deleteMyself() throws { - log(string: "Will start deleting account process", type: .crumbs) - - guard let username = username, let ud = client.userDiscovery else { - log(string: "Failed deleting account. No username or UD", type: .error) - return - } - - do { - try unregisterNotifications() - } catch { - log(string: "Failed to unregister for notifications", type: .error) - } + guard let username = username, let ud = client.userDiscovery else { return } + try? unregisterNotifications() try ud.deleteMyself(username) - log(string: "Deleted myself from User Discovery", type: .info) stop() - log(string: "Requested network stop", type: .crumbs) - cleanUp() } private func cleanUp() { retry(max: 10, retryStrategy: .delay(seconds: 1)) { [unowned self] in - guard self.hasRunningTasks == false else { - let string = "Tried to clean up database and defaults but network hasn't stopped yet. Sleeping for a second..." - log(string: string, type: .error) - throw NSError.create("") - } + guard self.hasRunningTasks == false else { throw NSError.create("") } }.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") } dbManager.drop() - log(string: "Dropped database", type: .info) - FileManager.xxCleanup() - log(string: "Wiped disk", type: .info) email = nil phone = nil @@ -185,8 +204,6 @@ public final class Session: SessionType { icognitoKeyboard = false pushNotifications = false inappnotifications = true - - log(string: "Wiped defaults", type: .info) } public func forceFailMessages() { @@ -194,12 +211,7 @@ public final class Session: SessionType { pendingE2E.forEach { var message = $0 message.status = .failedToSend - - do { - try dbManager.save(message) - } catch { - log(string: error.localizedDescription, type: .error) - } + _ = try? dbManager.save(message) } } @@ -207,12 +219,7 @@ public final class Session: SessionType { pendingGroupMessages.forEach { var message = $0 message.status = .failed - - do { - try dbManager.save(message) - } catch { - log(string: error.localizedDescription, type: .error) - } + _ = try? dbManager.save(message) } } } @@ -220,27 +227,17 @@ public final class Session: SessionType { private func registerUnfinishedTransfers() { guard let unfinisheds: [Message] = try? dbManager.fetch(.sendingAttachment), !unfinisheds.isEmpty else { return } - log(string: "There are unfinished transfers from the last session. Re-registering their upload progress", type: .crumbs) - for var message in unfinisheds { - guard let tid = message.payload.attachment?.transferId else { - log(string: "Impossible to resume a transfer that had no TID", type: .error) - return - } + guard let tid = message.payload.attachment?.transferId else { return } do { try client.transferManager?.listenUploadFromTransfer(with: tid) { completed, sent, arrived, total, error in if completed { message.status = .sent message.payload.attachment?.progress = 1.0 - log(string: "FT Up finished", type: .info) if let transfer: FileTransfer = try? self.dbManager.fetch(.withTID(tid)).first { - do { - try self.dbManager.delete(transfer) - } catch { - log(string: error.localizedDescription, type: .error) - } + try? self.dbManager.delete(transfer) } } else { if let error = error { @@ -249,28 +246,40 @@ public final class Session: SessionType { } else { let progress = Float(arrived)/Float(total) message.payload.attachment?.progress = progress - log(string: "FT Up: \(progress)", type: .crumbs) return } } - do { - _ = try self.dbManager.save(message) - } catch { - log(string: error.localizedDescription, type: .error) - } + _ = try? self.dbManager.save(message) } } catch { - log(string: "An error occurred when trying to register unfinished FT: \(error.localizedDescription). Switching it to 'sent'", type: .error) message.status = .sent + _ = try? self.dbManager.save(message) + } + } + } - do { - _ = try self.dbManager.save(message) - } catch { - log(string: error.localizedDescription, type: .error) - } + func updateFactsOnBackup() { + struct BackupParameters: Codable { + var email: String? + var phone: String? + var username: String + + var jsonFormat: String { + let data = try! JSONEncoder().encode(self) + let json = String(data: data, encoding: .utf8) + return json! } } + + let params = BackupParameters( + email: email, + phone: phone, + username: username! + ).jsonFormat + + client.addJson(params) + backupService.performBackupIfAutomaticIsEnabled() } private func setupBindings() { @@ -284,26 +293,51 @@ public final class Session: SessionType { } verify(contact: request) - } + }.store(in: &cancellables) + + client.requestsSent + .sink { [unowned self] in _ = try? dbManager.save($0) } .store(in: &cancellables) - client.groupMessages + client.backup + .throttle(for: .seconds(5), scheduler: DispatchQueue.main, latest: true) + .sink { [unowned self] in backupService.updateBackup(data: $0) } + .store(in: &cancellables) + + client.resets .sink { [unowned self] in - do { - _ = try dbManager.save($0) - } catch { - log(string: "Failed to save an incoming group message: \(error.localizedDescription)", type: .error) - } + /// This will get called when my contact restore its contact. + /// TODO: Hold a record on the chat that this contact restored. + /// + var contact = $0 + contact.status = .friend + _ = try? dbManager.save(contact) }.store(in: &cancellables) - client.messages + backupService.settingsPublisher + .map { $0.enabledService != nil } + .removeDuplicates() .sink { [unowned self] in - do { - _ = try dbManager.save($0) - } catch { - log(string: "Failed to save an incoming direct message: \(error.localizedDescription)", type: .error) + if $0 == true { + client.listenBackup() + updateFactsOnBackup() + } else { + client.stopListeningBackup() } - }.store(in: &cancellables) + } + .store(in: &cancellables) + + networkMonitor.statusPublisher + .sink { print($0) } + .store(in: &cancellables) + + client.groupMessages + .sink { [unowned self] in _ = try? dbManager.save($0) } + .store(in: &cancellables) + + client.messages + .sink { [unowned self] in _ = try? dbManager.save($0) } + .store(in: &cancellables) client.network .sink { [unowned self] in networkMonitor.update($0) } @@ -324,14 +358,9 @@ public final class Session: SessionType { client.confirmations .sink { [unowned self] in - guard var contact: Contact = try? dbManager.fetch(.withUserId($0.userId)).first else { return } - - contact.status = .friend - - do { - try dbManager.save(contact) - } catch { - log(string: error.localizedDescription, type: .error) + if var contact: Contact = try? dbManager.fetch(.withUserId($0.userId)).first { + contact.status = .friend + _ = try? dbManager.save(contact) } }.store(in: &cancellables) } diff --git a/Sources/Integration/XXNetwork.swift b/Sources/Integration/XXNetwork.swift index 26ffe4d36e7c9f131dd42d4f5fe496467467159d..4d6ee69040d61cf04d42f39924fc6d1182960276 100644 --- a/Sources/Integration/XXNetwork.swift +++ b/Sources/Integration/XXNetwork.swift @@ -15,18 +15,15 @@ public protocol XXNetworking { func purgeFiles() func updateErrors() func newClient(ndf: String) throws -> Client - func loadClient(with: Data) throws -> Client func updateNDF(_: @escaping (Result<String, Error>) -> Void) + func newClientFromBackup(data: Data, ndf: String) throws -> (Client, Data?) + func loadClient(with: Data, fromBackup: Bool, email: String?, phone: String?) throws -> Client } public struct XXNetwork<B: BindingsInterface> { - // MARK: Injected - @Dependency private var logger: XXLogger @Dependency private var keychain: KeychainHandling - // MARK: Lifecycle - public init() {} } @@ -64,6 +61,30 @@ extension XXNetwork: XXNetworking { FileManager.xxCleanup() } + public func newClientFromBackup(data: Data, ndf: String) throws -> (Client, Data?) { + var error: NSError? + + let password = B.secret(32)! + try keychain.store(password: password) + + let backupData = B.fromBackup(ndf, FileManager.xxPath, password, nil, data, &error) + if let error = error { throw error } + + var email: String? + var phone: String? + + let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!) + + if !report.parameters.isEmpty { + let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8)) + phone = params.phone + email = params.email + } + + let client = try loadClient(with: password, fromBackup: true, email: email, phone: phone) + return (client, backupData) + } + public func newClient(ndf: String) throws -> Client { var password: Data! @@ -73,7 +94,7 @@ extension XXNetwork: XXNetworking { password = B.secret(32) try keychain.store(password: password) - _ = B.newClient(ndf, FileManager.xxPath, password, nil, &error) + _ = B.new(ndf, FileManager.xxPath, password, nil, &error) if let error = error { throw error } } else { guard let secret = try keychain.getPassword() else { @@ -83,10 +104,15 @@ extension XXNetwork: XXNetworking { password = secret } - return try loadClient(with: password) + return try loadClient(with: password, fromBackup: false, email: nil, phone: nil) } - public func loadClient(with secret: Data) throws -> Client { + public func loadClient( + with secret: Data, + fromBackup: Bool, + email: String?, + phone: String? + ) throws -> Client { var error: NSError? let bindings = B.login(FileManager.xxPath, secret, "", &error) if let error = error { throw error } @@ -95,11 +121,10 @@ extension XXNetwork: XXNetworking { defaults.set(bindings!.receptionId.base64EncodedString(), forKey: "receptionId") } - return Client(bindings!) + return Client(bindings!, fromBackup: fromBackup, email: email, phone: phone) } } - extension NetworkEnvironment { var url: String { switch self { diff --git a/Sources/Models/Backup.swift b/Sources/Models/Backup.swift new file mode 100644 index 0000000000000000000000000000000000000000..a3518af44998b729a0c66f0c347d26d295634898 --- /dev/null +++ b/Sources/Models/Backup.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct Backup: Equatable, Codable { + public var id: String + public var date: Date + public var size: Float + + public init( + id: String, + date: Date, + size: Float + ) { + self.id = id + self.date = date + self.size = size + } +} diff --git a/Sources/Models/BackupSettings.swift b/Sources/Models/BackupSettings.swift new file mode 100644 index 0000000000000000000000000000000000000000..1ec2883fc6119882f5008d26b86bb65ed54962c4 --- /dev/null +++ b/Sources/Models/BackupSettings.swift @@ -0,0 +1,51 @@ +import Foundation + +public struct BackupSettings: Equatable, Codable { + public var wifiOnlyBackup: Bool + public var automaticBackups: Bool + public var enabledService: CloudService? + public var connectedServices: Set<CloudService> + public var backups: [CloudService: Backup] + + public init( + wifiOnlyBackup: Bool = false, + automaticBackups: Bool = false, + enabledService: CloudService? = nil, + connectedServices: Set<CloudService> = [], + backups: [CloudService: Backup] = [:] + ) { + self.wifiOnlyBackup = wifiOnlyBackup + self.automaticBackups = automaticBackups + self.enabledService = enabledService + self.connectedServices = connectedServices + self.backups = backups + } + + public func toData() -> Data { + (try? PropertyListEncoder().encode(self)) ?? Data() + } + + public init(fromData data: Data) { + let settings = try? PropertyListDecoder().decode(BackupSettings.self, from: data) + self.init( + wifiOnlyBackup: settings?.wifiOnlyBackup ?? false, + automaticBackups: settings?.automaticBackups ?? false, + enabledService: settings?.enabledService, + connectedServices: settings?.connectedServices ?? [], + backups: settings?.backups ?? [:] + ) + } +} + +public struct RestoreSettings { + public var backup: Backup? + public var cloudService: CloudService + + public init( + backup: Backup? = nil, + cloudService: CloudService + ) { + self.backup = backup + self.cloudService = cloudService + } +} diff --git a/Sources/Models/CloudService.swift b/Sources/Models/CloudService.swift new file mode 100644 index 0000000000000000000000000000000000000000..5daba7238d74388dac591b63220dbbc8b1c9f911 --- /dev/null +++ b/Sources/Models/CloudService.swift @@ -0,0 +1,5 @@ +public enum CloudService: Equatable, Codable { + case drive + case icloud + case dropbox +} diff --git a/Sources/NetworkMonitor/MockNetworkMonitor.swift b/Sources/NetworkMonitor/MockNetworkMonitor.swift index 46df25e22a2803146ae68827fb2c4e0c14a51cc5..d84355e2a5c90bbdbee48820a7d6a11f51613e0b 100644 --- a/Sources/NetworkMonitor/MockNetworkMonitor.swift +++ b/Sources/NetworkMonitor/MockNetworkMonitor.swift @@ -2,11 +2,28 @@ import Combine public struct MockNetworkMonitor: NetworkMonitoring { private let statusRelay = PassthroughSubject<NetworkStatus, Never>() - public var statusPublisher: AnyPublisher<NetworkStatus, Never> { statusRelay.eraseToAnyPublisher() } - public var xxStatus: NetworkStatus { .available } + public var connType: AnyPublisher<ConnectionType, Never> { + Just(.wifi).eraseToAnyPublisher() + } - public init() {} - public func start() {} - public func update(_ status: Bool) {} + public var statusPublisher: AnyPublisher<NetworkStatus, Never> { + statusRelay.eraseToAnyPublisher() + } + + public var xxStatus: NetworkStatus { + .available + } + + public init() { + // TODO + } + + public func start() { + // TODO + } + + public func update(_ status: Bool) { + // TODO + } } diff --git a/Sources/NetworkMonitor/NetworkMonitor.swift b/Sources/NetworkMonitor/NetworkMonitor.swift index 07effb7889066c2aaf1c57fecf194f41549a6742..ab805ca524e337bc8961c9dac719ac79d74f72f2 100644 --- a/Sources/NetworkMonitor/NetworkMonitor.swift +++ b/Sources/NetworkMonitor/NetworkMonitor.swift @@ -2,6 +2,7 @@ import Network import Combine +import Foundation public enum NetworkStatus: Equatable { case unknown @@ -10,11 +11,19 @@ public enum NetworkStatus: Equatable { case internetNotAvailable } +public enum ConnectionType { + case wifi + case ethernet + case cellular + case unknown +} + public protocol NetworkMonitoring { func start() func update(_ status: Bool) var xxStatus: NetworkStatus { get } + var connType: AnyPublisher<ConnectionType, Never> { get } var statusPublisher: AnyPublisher<NetworkStatus, Never> { get } } @@ -24,11 +33,16 @@ public struct NetworkMonitor: NetworkMonitoring { private var monitor = NWPathMonitor() private let isXXAvailableRelay = CurrentValueSubject<Bool?, Never>(nil) private let isInternetAvailableRelay = CurrentValueSubject<Bool?, Never>(nil) + private let connTypeSubject = PassthroughSubject<ConnectionType, Never>() public var xxStatus: NetworkStatus { isXXAvailableRelay.value == true ? .available : .xxNotAvailable } + public var connType: AnyPublisher<ConnectionType, Never> { + connTypeSubject.eraseToAnyPublisher() + } + public var statusPublisher: AnyPublisher<NetworkStatus, Never> { isInternetAvailableRelay.combineLatest(isXXAvailableRelay) .map { (isInternetAvailable, isXXAvailable) -> NetworkStatus in @@ -50,7 +64,8 @@ public struct NetworkMonitor: NetworkMonitoring { } public func start() { - monitor.pathUpdateHandler = { [weak isInternetAvailableRelay] in + monitor.pathUpdateHandler = { [weak isInternetAvailableRelay, weak connTypeSubject] in + connTypeSubject?.send(checkConnectionTypeForPath($0)) isInternetAvailableRelay?.send($0.status == .satisfied) } @@ -60,4 +75,16 @@ public struct NetworkMonitor: NetworkMonitoring { public func update(_ status: Bool) { isXXAvailableRelay.send(status) } + + private func checkConnectionTypeForPath(_ path: NWPath) -> ConnectionType { + if path.usesInterfaceType(.wifi) { + return .wifi + } else if path.usesInterfaceType(.wiredEthernet) { + return .ethernet + } else if path.usesInterfaceType(.cellular) { + return .cellular + } + + return .unknown + } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift index 34b242b905bb20a0c1d4641f52d87dd40a827f1b..3ab0b1633f363bb0f3ba0743357fc79fa83a0f2b 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift @@ -8,7 +8,7 @@ import DependencyInjection import ScrollViewController import Models -final class OnboardingEmailConfirmationController: UIViewController { +public final class OnboardingEmailConfirmationController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -21,7 +21,7 @@ final class OnboardingEmailConfirmationController: UIViewController { private var popupCancellables = Set<AnyCancellable>() private let viewModel: OnboardingEmailConfirmationViewModel - init( + public init( _ confirmation: AttributeConfirmation, _ completion: @escaping (UIViewController) -> Void ) { @@ -32,13 +32,13 @@ final class OnboardingEmailConfirmationController: UIViewController { required init?(coder: NSCoder) { nil } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() navigationItem.backButtonTitle = " " diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index a36c5ab29a85ec1afe104b221b8007efe7349685..9997793da6a7803b491ff4a3b0abc57814432a62 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -7,7 +7,7 @@ import Combine import DependencyInjection import ScrollViewController -final class OnboardingEmailController: UIViewController { +public final class OnboardingEmailController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -19,13 +19,13 @@ final class OnboardingEmailController: UIViewController { private let viewModel = OnboardingEmailViewModel() private var popupCancellables = Set<AnyCancellable>() - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() navigationItem.backButtonTitle = " " @@ -70,7 +70,13 @@ final class OnboardingEmailController: UIViewController { .sink { [unowned self] in viewModel.clearUp() coordinator.toEmailConfirmation(with: $0, from: self) { controller in - coordinator.toSuccess(isEmail: true, from: controller) + let successModel = OnboardingSuccessModel( + title: Localized.Onboarding.Success.Email.title, + subtitle: nil, + nextController: coordinator.toPhone(from:) + ) + + coordinator.toSuccess(with: successModel, from: controller) } }.store(in: &cancellables) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift b/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift index 4f061c26757abac89e60ed7acc139ee282afb479..7c384baaee94b9f88bb2402d9f9bccf3f14e459a 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift @@ -119,7 +119,7 @@ public final class OnboardingLaunchController: UIViewController { if let notNowTitle = updateModel.notNowTitle { let notNow = CapsuleButton() - notNow.set(style: .simplestColored, title: notNowTitle) + notNow.set(style: .simplestColoredRed, title: notNowTitle) notNow.publisher(for: .touchUpInside) .sink { [unowned self] in diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift index 1ace1449b88a86e7441cd22650f5be6097129349..c92ee37dd8379cc1bd4fbfb61a790fa35788dd86 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift @@ -8,7 +8,7 @@ import DependencyInjection import ScrollViewController import Models -final class OnboardingPhoneConfirmationController: UIViewController { +public final class OnboardingPhoneConfirmationController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -21,7 +21,7 @@ final class OnboardingPhoneConfirmationController: UIViewController { private var popupCancellables = Set<AnyCancellable>() private let viewModel: OnboardingPhoneConfirmationViewModel - init( + public init( _ confirmation: AttributeConfirmation, _ completion: @escaping (UIViewController) -> Void ) { @@ -32,13 +32,13 @@ final class OnboardingPhoneConfirmationController: UIViewController { required init?(coder: NSCoder) { nil } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() navigationItem.backButtonTitle = " " diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index 9f44854db75ef7b8f7bed40e7d0967fe6ae54edf..5fde1a32e78599330fb50a28f558933e13803726 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -7,7 +7,7 @@ import Combine import DependencyInjection import ScrollViewController -final class OnboardingPhoneController: UIViewController { +public final class OnboardingPhoneController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -19,13 +19,13 @@ final class OnboardingPhoneController: UIViewController { private let viewModel = OnboardingPhoneViewModel() private var popupCancellables = Set<AnyCancellable>() - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() navigationItem.backButtonTitle = " " @@ -90,7 +90,13 @@ final class OnboardingPhoneController: UIViewController { .sink { [unowned self] in viewModel.clearUp() coordinator.toPhoneConfirmation(with: $0, from: self) { controller in - coordinator.toSuccess(isEmail: false, from: controller) + let successModel = OnboardingSuccessModel( + title: Localized.Onboarding.Success.Phone.title, + subtitle: nil, + nextController: coordinator.toChats(from:) + ) + + coordinator.toSuccess(with: successModel, from: controller) } }.store(in: &cancellables) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index 6263e43f4ccdc2c644e37644b81d4ee69d606239..2ed639b116b86ab36b2cdf8924b63626bd653d0c 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -5,7 +5,7 @@ import Shared import Combine import DependencyInjection -final class OnboardingStartController: UIViewController { +public final class OnboardingStartController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: OnboardingCoordinating @@ -14,23 +14,23 @@ final class OnboardingStartController: UIViewController { private let ndf: String private var cancellables = Set<AnyCancellable>() - override func loadView() { + public override func loadView() { view = screenView } - init(_ ndf: String) { + public init(_ ndf: String) { self.ndf = ndf super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { nil } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLayoutSubviews() { + public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let gradient = CAGradientLayer() @@ -48,7 +48,7 @@ final class OnboardingStartController: UIViewController { screenView.layer.insertSublayer(gradient, at: 0) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() screenView.startButton.publisher(for: .touchUpInside) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift b/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift index 4ee799a4d4db4dc94e2c6b71cedd65d672157c6e..a260afa9a7c69369f0bf9a9510c851ebe2e20313 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift @@ -6,31 +6,37 @@ import Combine import Countries import DependencyInjection -final class OnboardingSuccessController: UIViewController { +public struct OnboardingSuccessModel { + var title: String + var subtitle: String? + var nextController: (UIViewController) -> Void +} + +public final class OnboardingSuccessController: UIViewController { @Dependency private var coordinator: OnboardingCoordinating lazy private var screenView = OnboardingSuccessView() - - private let isEmail: Bool private var cancellables = Set<AnyCancellable>() - override func loadView() { + private var model: OnboardingSuccessModel + + public override func loadView() { view = screenView } - init(_ isEmail: Bool) { - self.isEmail = isEmail + public init(_ model: OnboardingSuccessModel) { + self.model = model super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { nil } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLayoutSubviews() { + public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let gradient = CAGradientLayer() @@ -48,23 +54,15 @@ final class OnboardingSuccessController: UIViewController { screenView.layer.insertSublayer(gradient, at: 0) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() - let type = isEmail ? - Localized.Onboarding.Email.input : - Localized.Onboarding.Phone.input - - screenView.set(type: type.components(separatedBy: " ").first!) + screenView.setTitle(model.title) + screenView.setSubtitle(model.subtitle) screenView.nextButton .publisher(for: .touchUpInside) - .sink { [unowned self] in - if isEmail { - coordinator.toPhone(from: self) - } else { - coordinator.toChats(from: self) - } - }.store(in: &cancellables) + .sink { [unowned self] in model.nextController(self) } + .store(in: &cancellables) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index e43a0a69527de944a2992cf1fd8572cba4eaa525..635cb70388ba061b96c07e0f97553186abad843c 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -7,7 +7,7 @@ import Combine import DependencyInjection import ScrollViewController -final class OnboardingUsernameController: UIViewController { +public final class OnboardingUsernameController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -15,24 +15,26 @@ final class OnboardingUsernameController: UIViewController { lazy private var screenView = OnboardingUsernameView() lazy private var scrollViewController = ScrollViewController() + private let ndf: String private var cancellables = Set<AnyCancellable>() private let viewModel: OnboardingUsernameViewModel! private var popupCancellables = Set<AnyCancellable>() - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } - init(_ ndf: String) { + public init(_ ndf: String) { + self.ndf = ndf self.viewModel = OnboardingUsernameViewModel(ndf: ndf) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { nil } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupScrollView() setupBindings() @@ -68,6 +70,12 @@ final class OnboardingUsernameController: UIViewController { .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) + screenView.restoreView.restoreButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in coordinator.toRestoreList(with: ndf, from: self) } + .store(in: &cancellables) + screenView.inputField.returnPublisher .sink { [unowned self] in if screenView.nextButton.isEnabled { diff --git a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift index d994fd60815655360919fb4a35df0b888dbd6546..5efe0d7e0aa23e6c228ddb6c1c7443187155b0b6 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift @@ -6,7 +6,7 @@ import Combine import Defaults import DependencyInjection -final class OnboardingWelcomeController: UIViewController { +public final class OnboardingWelcomeController: UIViewController { @KeyObject(.username, defaultValue: "") var username: String @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -16,17 +16,17 @@ final class OnboardingWelcomeController: UIViewController { private var cancellables = Set<AnyCancellable>() private var popupCancellables = Set<AnyCancellable>() - override func loadView() { + public override func loadView() { view = screenView } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupBindings() diff --git a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift index 485a89d7ec62d51ca0133f94c5d50bdbe82ff70b..0d12a9e0f63c9bbeda543d51a84634ddc133d063 100644 --- a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift +++ b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift @@ -4,6 +4,8 @@ import Models import Countries import Presentation +public typealias AttributeControllerClosure = (UIViewController) -> Void + public protocol OnboardingCoordinating { func toChats(from: UIViewController) func toEmail(from: UIViewController) @@ -11,9 +13,11 @@ public protocol OnboardingCoordinating { func toWelcome(from: UIViewController) func toStart(with: String, from: UIViewController) func toUsername(with: String, from: UIViewController) - func toSuccess(isEmail: Bool, from: UIViewController) func toPopup(_: UIViewController, from: UIViewController) + func toSuccess(with: OnboardingSuccessModel, from: UIViewController) + func toRestoreList(with: String, from: UIViewController) + func toEmailConfirmation( with: AttributeConfirmation, from: UIViewController, @@ -33,118 +37,114 @@ public protocol OnboardingCoordinating { } public struct OnboardingCoordinator: OnboardingCoordinating { - public init(chatListFactory: @escaping () -> UIViewController) { - self.chatListFactory = chatListFactory - } - - var pusher: Presenting = PushPresenter() - var replacer: Presenting = ReplacePresenter() + var pushPresenter: Presenting = PushPresenter() var bottomPresenter: Presenting = BottomPresenter() - - // MARK: Factories - - var chatListFactory: () -> UIViewController - - var welcomeFactory: () -> UIViewController - = OnboardingWelcomeController.init + var replacePresenter: Presenting = ReplacePresenter() var emailFactory: () -> UIViewController - = OnboardingEmailController.init - var phoneFactory: () -> UIViewController - = OnboardingPhoneController.init - + var welcomeFactory: () -> UIViewController + var chatListFactory: () -> UIViewController var startFactory: (String) -> UIViewController - = OnboardingStartController.init(_:) - var usernameFactory: (String) -> UIViewController - = OnboardingUsernameController.init(_:) - - var successFactory: (Bool) -> UIViewController - = OnboardingSuccessController.init(_:) - + var restoreListFactory: (String) -> UIViewController + var successFactory: (OnboardingSuccessModel) -> UIViewController var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - = CountryListController.init(_:) - - var phoneConfirmationFactory: (AttributeConfirmation, @escaping (UIViewController) -> Void) -> UIViewController - = OnboardingPhoneConfirmationController.init(_:_:) - - var emailConfirmationFactory: (AttributeConfirmation, @escaping (UIViewController) -> Void) -> UIViewController - = OnboardingEmailConfirmationController.init(_:_:) + var phoneConfirmationFactory: (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController + var emailConfirmationFactory: (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController + + public init( + emailFactory: @escaping () -> UIViewController, + phoneFactory: @escaping () -> UIViewController, + welcomeFactory: @escaping () -> UIViewController, + chatListFactory: @escaping () -> UIViewController, + startFactory: @escaping (String) -> UIViewController, + usernameFactory: @escaping (String) -> UIViewController, + restoreListFactory: @escaping (String) -> UIViewController, + successFactory: @escaping (OnboardingSuccessModel) -> UIViewController, + countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController, + phoneConfirmationFactory: @escaping (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController, + emailConfirmationFactory: @escaping (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController + ) { + self.emailFactory = emailFactory + self.phoneFactory = phoneFactory + self.startFactory = startFactory + self.welcomeFactory = welcomeFactory + self.usernameFactory = usernameFactory + self.chatListFactory = chatListFactory + self.restoreListFactory = restoreListFactory + self.successFactory = successFactory + self.countriesFactory = countriesFactory + self.phoneConfirmationFactory = phoneConfirmationFactory + self.emailConfirmationFactory = emailConfirmationFactory + } } public extension OnboardingCoordinator { - func toSuccess( - isEmail: Bool, - from parent: UIViewController - ) { - let screen = successFactory(isEmail) - replacer.present(screen, from: parent) + func toEmail(from parent: UIViewController) { + let screen = emailFactory() + replacePresenter.present(screen, from: parent) } - func toEmailConfirmation( - with confirmation: AttributeConfirmation, - from parent: UIViewController, - completion: @escaping (UIViewController) -> Void - ) { - let screen = emailConfirmationFactory(confirmation, completion) - pusher.present(screen, from: parent) + func toPhone(from parent: UIViewController) { + let screen = phoneFactory() + replacePresenter.present(screen, from: parent) } - func toPhoneConfirmation( - with confirmation: AttributeConfirmation, - from parent: UIViewController, - completion: @escaping (UIViewController) -> Void - ) { - let screen = phoneConfirmationFactory(confirmation, completion) - pusher.present(screen, from: parent) + func toChats(from parent: UIViewController) { + let screen = chatListFactory() + replacePresenter.present(screen, from: parent) } - func toEmail(from parent: UIViewController) { - let screen = emailFactory() - replacer.present(screen, from: parent) + func toWelcome(from parent: UIViewController) { + let screen = welcomeFactory() + replacePresenter.present(screen, from: parent) } - func toPhone(from parent: UIViewController) { - let screen = phoneFactory() - replacer.present(screen, from: parent) + func toRestoreList(with ndf: String, from parent: UIViewController) { + let screen = restoreListFactory(ndf) + pushPresenter.present(screen, from: parent) } - func toCountries( - from parent: UIViewController, - _ onChoose: @escaping (Country) -> Void - ) { - let screen = countriesFactory(onChoose) - pusher.present(screen, from: parent) + func toSuccess(with model: OnboardingSuccessModel, from parent: UIViewController) { + let screen = successFactory(model) + replacePresenter.present(screen, from: parent) + } + + func toStart(with ndf: String, from parent: UIViewController) { + let screen = startFactory(ndf) + replacePresenter.present(screen, from: parent) } func toUsername(with ndf: String, from parent: UIViewController) { let screen = usernameFactory(ndf) - replacer.present(screen, from: parent) + replacePresenter.present(screen, from: parent) } - func toChats(from parent: UIViewController) { - let screen = chatListFactory() - replacer.present(screen, from: parent) + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) } - func toStart( - with ndf: String, - from parent: UIViewController - ) { - let screen = startFactory(ndf) - replacer.present(screen, from: parent) + func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { + let screen = countriesFactory(onChoose) + pushPresenter.present(screen, from: parent) } - func toPopup( - _ popup: UIViewController, - from parent: UIViewController + func toEmailConfirmation( + with confirmation: AttributeConfirmation, + from parent: UIViewController, + completion: @escaping (UIViewController) -> Void ) { - bottomPresenter.present(popup, from: parent) + let screen = emailConfirmationFactory(confirmation, completion) + pushPresenter.present(screen, from: parent) } - func toWelcome(from parent: UIViewController) { - let screen = welcomeFactory() - replacer.present(screen, from: parent) + func toPhoneConfirmation( + with confirmation: AttributeConfirmation, + from parent: UIViewController, + completion: @escaping (UIViewController) -> Void + ) { + let screen = phoneConfirmationFactory(confirmation, completion) + pushPresenter.present(screen, from: parent) } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift index 34ac05f9a62f6244297a2e40a607be6f8e4cd9e4..9992881ee768ac0010f57b0f506ff62fbc1f48e1 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift @@ -8,6 +8,7 @@ import Permissions import VersionChecking import CombineSchedulers import DependencyInjection +import DropboxFeature struct UpdatePopupModel { let body: String @@ -28,6 +29,7 @@ final class OnboardingLaunchViewModel { @Dependency private var network: XXNetworking @Dependency private var versioning: VersionChecker @Dependency private var permissions: PermissionHandling + @Dependency private var dropboxService: DropboxInterface // MARK: Properties @@ -61,7 +63,7 @@ final class OnboardingLaunchViewModel { updateRelay.send(.init( body: "There is a new version available that enhance the current performance and usability.", updateTitle: "Update", - updateStyle: .simplestColored, + updateStyle: .simplestColoredRed, notNowTitle: "Not now", appUrl: info.appUrl )) @@ -107,6 +109,7 @@ final class OnboardingLaunchViewModel { guard network.hasClient == true else { hudRelay.send(.none) usernameRelay.send(ndf) + dropboxService.unlink() return } @@ -114,6 +117,7 @@ final class OnboardingLaunchViewModel { network.purgeFiles() hudRelay.send(.none) usernameRelay.send(ndf) + dropboxService.unlink() return } diff --git a/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift b/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift index 04f0a9bb2117915942c918105a3ad4dfdaa583ad..7e02167c2786cb11c4fcc04d6ad4930b9fab92e2 100644 --- a/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift @@ -4,6 +4,7 @@ import Shared final class OnboardingSuccessView: UIView { let iconImageView = UIImageView() let titleLabel = UILabel() + let subtitleLabel = UILabel() let nextButton = CapsuleButton() init() { @@ -13,8 +14,13 @@ final class OnboardingSuccessView: UIView { iconImageView.image = Asset.onboardingSuccess.image nextButton.set(style: .white, title: Localized.Onboarding.Success.action) + subtitleLabel.numberOfLines = 0 + subtitleLabel.textColor = Asset.neutralWhite.color + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + addSubview(iconImageView) addSubview(titleLabel) + addSubview(subtitleLabel) addSubview(nextButton) iconImageView.snp.makeConstraints { make in @@ -28,6 +34,12 @@ final class OnboardingSuccessView: UIView { make.right.equalToSuperview().offset(-90) } + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(30) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-90) + } + nextButton.snp.makeConstraints { make in make.left.equalToSuperview().offset(24) make.right.equalToSuperview().offset(-24) @@ -37,14 +49,12 @@ final class OnboardingSuccessView: UIView { required init?(coder: NSCoder) { nil } - func set(type: String) { + func setTitle(_ title: String) { let paragraph = NSMutableParagraphStyle() paragraph.alignment = .left paragraph.lineHeightMultiple = 1.1 - let attrString = NSMutableAttributedString( - string: Localized.Onboarding.Success.title(type.lowercased()) - ) + let attrString = NSMutableAttributedString(string: title) attrString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 39.0)) attrString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) @@ -58,4 +68,8 @@ final class OnboardingSuccessView: UIView { titleLabel.numberOfLines = 0 titleLabel.attributedText = attrString } + + func setSubtitle(_ subtitle: String?) { + subtitleLabel.text = subtitle + } } diff --git a/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift b/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift new file mode 100644 index 0000000000000000000000000000000000000000..f03437558a9157fd6846ce76436f1ed1c4d8822b --- /dev/null +++ b/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift @@ -0,0 +1,47 @@ +import UIKit +import Shared + +final class OnboardingUsernameRestoreView: UIView { + let titleLabel = UILabel() + let restoreButton = CapsuleButton() + let separatorView = UIView() + + init() { + super.init(frame: .zero) + + titleLabel.text = Localized.Onboarding.Username.Restore.title + restoreButton.set(style: .seeThrough, title: Localized.Onboarding.Username.Restore.action) + + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .center + titleLabel.font = Fonts.Mulish.bold.font(size: 24) + + addSubview(titleLabel) + addSubview(restoreButton) + addSubview(separatorView) + + separatorView.backgroundColor = Asset.neutralLine.color + + separatorView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.height.equalTo(1) + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(separatorView.snp.bottom).offset(40) + make.left.equalToSuperview().offset(20) + make.right.equalToSuperview().offset(-20) + } + + restoreButton.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(34) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-40) + make.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift b/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift index 6ed0744f1186cce25d4fbf7a1106f5d66372e5a6..32ec1e43e312aee7880f1003c9e3f4ec0f18e899 100644 --- a/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift @@ -7,6 +7,7 @@ final class OnboardingUsernameView: UIView { let subtitleView = TextWithInfoView() let inputField = InputField() let nextButton = CapsuleButton() + let restoreView = OnboardingUsernameRestoreView() var didTapInfo: (() -> Void)? @@ -31,6 +32,7 @@ final class OnboardingUsernameView: UIView { addSubview(subtitleView) addSubview(inputField) addSubview(nextButton) + addSubview(restoreView) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(30) @@ -54,6 +56,12 @@ final class OnboardingUsernameView: UIView { make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) make.left.equalToSuperview().offset(40) make.right.equalToSuperview().offset(-40) + } + + restoreView.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(nextButton.snp.bottom).offset(30) + make.left.equalToSuperview() + make.right.equalToSuperview() make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } } diff --git a/Sources/Popup/StackItems/PopupButton.swift b/Sources/Popup/StackItems/PopupButton.swift index cecdcc3547b015ee126cf4410c1dd5ac7ff6687c..0715ceef3985ff1f11fd524996bfb00338dbfbd9 100644 --- a/Sources/Popup/StackItems/PopupButton.swift +++ b/Sources/Popup/StackItems/PopupButton.swift @@ -3,8 +3,6 @@ import Shared import Combine public final class PopupButton: PopupStackItem { - // MARK: Properties - let font: UIFont? let title: String? let color: UIColor? @@ -18,8 +16,6 @@ public final class PopupButton: PopupStackItem { public var action: AnyPublisher<Void, Never> { actionSubject.eraseToAnyPublisher() } - // MARK: Lifecycle - public init( title: String? = nil, font: UIFont? = Fonts.Mulish.regular.font(size: 12.0), @@ -36,8 +32,6 @@ public final class PopupButton: PopupStackItem { self.accessibility = accessibility } - // MARK: Builder - public func makeView() -> UIView { cancellables.removeAll() @@ -59,3 +53,72 @@ public final class PopupButton: PopupStackItem { } } } + +public final class PopupRadioButton: PopupStackItem { + let radioView = UIView() + let titleLabel = UILabel() + let radioInnerView = UIView() + + public var spacingAfter: CGFloat? = 10 + private var cancellables = Set<AnyCancellable>() + private let actionSubject = PassthroughSubject<Void, Never>() + + public var action: AnyPublisher<Void, Never> { actionSubject.eraseToAnyPublisher() } + + public init( + title: String, + isSelected: Bool + ) { + titleLabel.text = title + titleLabel.textColor = Asset.neutralDark.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + radioView.layer.cornerRadius = 11.0 + radioInnerView.layer.cornerRadius = 3 + radioView.isUserInteractionEnabled = false + + if isSelected { + radioView.layer.borderWidth = 0.0 + radioView.backgroundColor = Asset.brandLight.color + radioView.layer.borderColor = Asset.brandLight.color.cgColor + radioInnerView.backgroundColor = Asset.neutralWhite.color + } else { + radioView.layer.borderWidth = 1.0 + radioView.backgroundColor = Asset.neutralSecondary.color + radioView.layer.borderColor = Asset.neutralLine.color.cgColor + radioInnerView.backgroundColor = .clear + } + } + + public func makeView() -> UIView { + cancellables.removeAll() + + let view = UIControl() + view.addSubview(titleLabel) + view.addSubview(radioView) + radioView.addSubview(radioInnerView) + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(42) + make.centerY.equalToSuperview() + } + + radioView.snp.makeConstraints { make in + make.right.equalTo(titleLabel.snp.left).offset(-12) + make.width.height.equalTo(20) + make.centerY.equalToSuperview() + make.bottom.equalToSuperview().offset(-5) + } + + radioInnerView.snp.makeConstraints { make in + make.width.height.equalTo(6) + make.center.equalToSuperview() + } + + view.publisher(for: .touchUpInside) + .sink { [weak self] in self?.actionSubject.send() } + .store(in: &cancellables) + + return view + } +} diff --git a/Sources/Popup/StackItems/PopupEmptyView.swift b/Sources/Popup/StackItems/PopupEmptyView.swift index 96d4031450fb0a51feaca3f466365fb9a8488612..bc32b728c6a5cc3e9bc723465f8487036786aafa 100644 --- a/Sources/Popup/StackItems/PopupEmptyView.swift +++ b/Sources/Popup/StackItems/PopupEmptyView.swift @@ -1,11 +1,16 @@ import UIKit public final class PopupEmptyView: PopupStackItem { - // MARK: Lifecycle + private var height: CGFloat - public init() {} + public init(height: CGFloat) { + self.height = height + } - // MARK: Builder + public func makeView() -> UIView { + let view = UIView() + view.snp.makeConstraints { $0.height.equalTo(height) } - public func makeView() -> UIView { UIView() } + return view + } } diff --git a/Sources/Popup/StackItems/PopupStackView.swift b/Sources/Popup/StackItems/PopupStackView.swift index 56271d01ccef4b43eab59023185d73d901151219..a705e8e284ee4a50514270d0c910204b1defa572 100644 --- a/Sources/Popup/StackItems/PopupStackView.swift +++ b/Sources/Popup/StackItems/PopupStackView.swift @@ -2,8 +2,6 @@ import UIKit import Shared public final class PopupStackView: PopupStackItem { - // MARK: Properties - let views: [UIView] let spacing: CGFloat let axis: NSLayoutConstraint.Axis @@ -11,8 +9,6 @@ public final class PopupStackView: PopupStackItem { public var spacingAfter: CGFloat? = 10 - // MARK: Lifecycle - public init( axis: NSLayoutConstraint.Axis = .horizontal, spacing: CGFloat = 10, @@ -25,8 +21,6 @@ public final class PopupStackView: PopupStackItem { self.distribution = distribution } - // MARK: Builder - public func makeView() -> UIView { let stack = UIStackView() stack.axis = axis diff --git a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift index 5cbaff29877d60b4bec21a31dd93bef12e6a855b..d909662aa961fda594217d71d4dd940bd0cf80af 100644 --- a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift @@ -9,7 +9,7 @@ import ScrollViewController public typealias ControllerClosure = (UIViewController, AttributeConfirmation) -> Void -final class ProfileCodeController: UIViewController { +public final class ProfileCodeController: UIViewController { @Dependency private var hud: HUDType lazy private var screenView = ProfileCodeView() @@ -20,13 +20,16 @@ final class ProfileCodeController: UIViewController { private var cancellables = Set<AnyCancellable>() lazy private var viewModel = ProfileCodeViewModel(confirmation) - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.navigationBar .customize(backgroundColor: Asset.neutralWhite.color) } - init(_ confirmation: AttributeConfirmation, _ completion: @escaping ControllerClosure) { + public init( + _ confirmation: AttributeConfirmation, + _ completion: @escaping ControllerClosure + ) { self.completion = completion self.confirmation = confirmation super.init(nibName: nil, bundle: nil) @@ -34,9 +37,8 @@ final class ProfileCodeController: UIViewController { required init?(coder: NSCoder) { nil } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() - setupNavigationBar() setupScrollView() setupBindings() diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift index f5fb58c57450d93ae2b3ad8150964ed1e61c24ae..fb85221449bea9ecd89b1a9507db4182b7365622 100644 --- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift @@ -6,7 +6,7 @@ import Theme import DependencyInjection import ScrollViewController -final class ProfileEmailController: UIViewController { +public final class ProfileEmailController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: ProfileCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -17,14 +17,14 @@ final class ProfileEmailController: UIViewController { private let viewModel = ProfileEmailViewModel() private var cancellables = Set<AnyCancellable>() - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar .customize(backgroundColor: Asset.neutralWhite.color) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupScrollView() diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift index 5d2a91eab29a6354e6367d7d03e3747564603c3b..b6ddaffb5574bdb44f70994fe2ce2c7b0e98b3c0 100644 --- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift +++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift @@ -8,7 +8,7 @@ import ScrollViewController #warning("TODO: Merge ProfilePhoneController/ProfileEmailController") -final class ProfilePhoneController: UIViewController { +public final class ProfilePhoneController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: ProfileCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -19,14 +19,14 @@ final class ProfilePhoneController: UIViewController { private let viewModel = ProfilePhoneViewModel() private var cancellables = Set<AnyCancellable>() - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.darkContent) navigationController?.navigationBar .customize(backgroundColor: Asset.neutralWhite.color) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupScrollView() @@ -108,8 +108,6 @@ final class ProfilePhoneController: UIViewController { .store(in: &cancellables) } - // MARK: ObjC - @objc private func didTapBack() { navigationController?.popViewController(animated: true) } diff --git a/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift b/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift index ff2615f2dd8db88395d86c667d23691b774f4085..d3eee0e16d403bb07d590a950c3fe50dba3f1bf5 100644 --- a/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift +++ b/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift @@ -25,61 +25,43 @@ public protocol ProfileCoordinating { } public struct ProfileCoordinator: ProfileCoordinating { - public init() {} - - var pusher: Presenting = PushPresenter() - var presenter: Presenting = ModalPresenter() + var pushPresenter: Presenting = PushPresenter() + var modalPresenter: Presenting = ModalPresenter() var bottomPresenter: Presenting = BottomPresenter() - // MARK: Factories - var emailFactory: () -> UIViewController - = ProfileEmailController.init - var phoneFactory: () -> UIViewController - = ProfilePhoneController.init - var imagePickerFactory: () -> UIImagePickerController - = UIImagePickerController.init - - var permissionFactory: () -> RequestPermissionController = RequestPermissionController.init - + var permissionFactory: () -> RequestPermissionController var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - = CountryListController.init(_:) - var codeFactory: (AttributeConfirmation, @escaping ControllerClosure) -> UIViewController - = ProfileCodeController.init(_:_:) -} - -public extension ProfileCoordinator { - func toPermission(type: PermissionType, from parent: UIViewController) { - let screen = permissionFactory() - screen.setup(type: type) - pusher.present(screen, from: parent) - } - func toPopup( - _ popup: UIViewController, - from parent: UIViewController + public init( + emailFactory: @escaping () -> UIViewController, + phoneFactory: @escaping () -> UIViewController, + imagePickerFactory: @escaping () -> UIImagePickerController, + permissionFactory: @escaping () -> RequestPermissionController, // ⚠️ + countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController, + codeFactory: @escaping (AttributeConfirmation, @escaping ControllerClosure) -> UIViewController ) { - bottomPresenter.present(popup, from: parent) - } - - func toPhotos(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = true - presenter.present(screen, from: parent) + self.codeFactory = codeFactory + self.emailFactory = emailFactory + self.phoneFactory = phoneFactory + self.countriesFactory = countriesFactory + self.permissionFactory = permissionFactory + self.imagePickerFactory = imagePickerFactory } +} +public extension ProfileCoordinator { func toEmail(from parent: UIViewController) { let screen = emailFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toPhone(from parent: UIViewController) { let screen = phoneFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toCode( @@ -88,14 +70,28 @@ public extension ProfileCoordinator { _ completion: @escaping ControllerClosure ) { let screen = codeFactory(confirmation, completion) - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } - func toCountries( - from parent: UIViewController, - _ onChoose: @escaping (Country) -> Void - ) { + func toPermission(type: PermissionType, from parent: UIViewController) { + let screen = permissionFactory() + screen.setup(type: type) + pushPresenter.present(screen, from: parent) + } + + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) + } + + func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { let screen = countriesFactory(onChoose) - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) + } + + func toPhotos(from parent: UIViewController) { + let screen = imagePickerFactory() + screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) + screen.allowsEditing = true + modalPresenter.present(screen, from: parent) } } diff --git a/Sources/RequestsFeature/Controllers/VerifyingController.swift b/Sources/RequestsFeature/Controllers/VerifyingController.swift index cc154f62b1ac21348f5db835f6f87ecc003dbddd..65d20075b80e55aa309d86fa9e59f22524e52cef 100644 --- a/Sources/RequestsFeature/Controllers/VerifyingController.swift +++ b/Sources/RequestsFeature/Controllers/VerifyingController.swift @@ -1,16 +1,16 @@ import UIKit import Combine -final class VerifyingController: UIViewController { +public final class VerifyingController: UIViewController { lazy private var screenView = VerifyingView() private var cancellables = Set<AnyCancellable>() - override func loadView() { + public override func loadView() { view = screenView } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() screenView.action.publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) diff --git a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift b/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift index f48524969d5ef640c751bb88fae1241a72a17012..2ef069e020f5f36c25b51699596dd89966181cf0 100644 --- a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift +++ b/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift @@ -6,46 +6,37 @@ import ContactFeature public protocol RequestsCoordinating { func toSearch(from: UIViewController) + func toVerifying(from: UIViewController) func toContact(_: Contact, from: UIViewController) func toNickname(from: UIViewController, prefilled: String, _: @escaping StringClosure) - func toVerifying(from: UIViewController) } public struct RequestsCoordinator: RequestsCoordinating { - public init(searchFactory: @escaping () -> UIViewController) { - self.searchFactory = searchFactory - } - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() + var pushPresenter: Presenting = PushPresenter() var bottomPresenter: Presenting = BottomPresenter() - // MARK: Factories - var searchFactory: () -> UIViewController - - var verifyingFactory: () -> UIViewController = VerifyingController.init - + var verifyingFactory: () -> UIViewController var contactFactory: (Contact) -> UIViewController - = ContactController.init(_:) - var nicknameFactory: (String, @escaping StringClosure) -> UIViewController - = NickameController.init(prefilled:_:) + + public init( + searchFactory: @escaping () -> UIViewController, + verifyingFactory: @escaping () -> UIViewController, + contactFactory: @escaping (Contact) -> UIViewController, + nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController + ) { + self.searchFactory = searchFactory + self.contactFactory = contactFactory + self.nicknameFactory = nicknameFactory + self.verifyingFactory = verifyingFactory + } } public extension RequestsCoordinator { func toSearch(from parent: UIViewController) { let screen = searchFactory() - pusher.present(screen, from: parent) - } - - func toContact( - _ contact: Contact, - from parent: UIViewController - ) { - let screen = contactFactory(contact) - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } func toNickname( @@ -61,4 +52,9 @@ public extension RequestsCoordinator { let screen = verifyingFactory() bottomPresenter.present(screen, from: parent) } + + func toContact(_ contact: Contact, from parent: UIViewController) { + let screen = contactFactory(contact) + pushPresenter.present(screen, from: parent) + } } diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift new file mode 100644 index 0000000000000000000000000000000000000000..1d87c70e5735f920d1b5b01f5c7aadfa17fb04eb --- /dev/null +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -0,0 +1,118 @@ +import UIKit +import Models +import Shared +import Popup +import Combine +import DependencyInjection + +public final class RestoreController: UIViewController { + @Dependency private var coordinator: RestoreCoordinating + + lazy private var screenView = RestoreView() + + private let viewModel: RestoreViewModel + private var cancellables = Set<AnyCancellable>() + private var popupCancellables = Set<AnyCancellable>() + + public init(_ ndf: String, _ settings: RestoreSettings) { + viewModel = .init(ndf: ndf, settings: settings) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + presentWarning() + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let title = UILabel() + title.text = Localized.Restore.header + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + + let back = UIButton.back() + back.addTarget(self, action: #selector(didTapBack), for: .touchUpInside) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [back, title]) + ) + } + + private func setupBindings() { + viewModel.step + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + screenView.updateFor(step: $0) + + if $0 == .done { + coordinator.toSuccess(from: self) + } + }.store(in: &cancellables) + + screenView.backButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in didTapBack() } + .store(in: &cancellables) + + screenView.cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in didTapBack() } + .store(in: &cancellables) + + screenView.restoreButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapRestore() } + .store(in: &cancellables) + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } +} + +extension RestoreController { + private func presentWarning() { + let actionButton = CapsuleButton() + actionButton.set( + style: .brandColored, + title: Localized.Restore.Warning.action + ) + + let popup = BottomPopup(with: [ + PopupLabel( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.Restore.Warning.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + PopupLabelAttributed( + text: Localized.Restore.Warning.subtitle, + spacingAfter: 37 + ), + PopupStackView(views: [actionButton]) + ]) + + actionButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { + popup.dismiss(animated: true) { [weak self] in + guard let self = self else { return } + self.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + coordinator.toPopup(popup, from: self) + } +} diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift new file mode 100644 index 0000000000000000000000000000000000000000..c4e31d9daf904c9f2ddd59fe225aec49beb63e58 --- /dev/null +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -0,0 +1,124 @@ +import HUD +import Popup +import Shared +import UIKit +import Combine +import DependencyInjection + +public final class RestoreListController: UIViewController { + @Dependency private var hud: HUDType + @Dependency private var coordinator: RestoreCoordinating + + lazy private var screenView = RestoreListView() + + private let ndf: String + private let viewModel = RestoreListViewModel() + private var cancellables = Set<AnyCancellable>() + private var popupCancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + presentWarning() + } + + public init(_ ndf: String) { + self.ndf = ndf + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let back = UIButton.back() + back.addTarget(self, action: #selector(didTapBack), for: .touchUpInside) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [back]) + ) + } + + private func setupBindings() { + viewModel.hud + .receive(on: DispatchQueue.main) + .sink { [hud] in hud.update(with: $0) } + .store(in: &cancellables) + + viewModel.didFetchBackup + .receive(on: DispatchQueue.main) + .sink { [unowned self] in coordinator.toRestore(using: ndf, with: $0, from: self) } + .store(in: &cancellables) + + screenView.cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in didTapBack() } + .store(in: &cancellables) + + screenView.driveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapCloud(.drive, from: self) } + .store(in: &cancellables) + + screenView.icloudButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapCloud(.icloud, from: self) } + .store(in: &cancellables) + + screenView.dropboxButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapCloud(.dropbox, from: self) } + .store(in: &cancellables) + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } +} + +extension RestoreListController { + private func presentWarning() { + let actionButton = CapsuleButton() + actionButton.set( + style: .brandColored, + title: Localized.Restore.Warning.action + ) + + let popup = BottomPopup(with: [ + PopupLabel( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.Restore.Warning.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + PopupLabelAttributed( + text: Localized.Restore.Warning.subtitle, + spacingAfter: 37 + ), + PopupStackView(views: [actionButton]) + ]) + + actionButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { + popup.dismiss(animated: true) { [weak self] in + guard let self = self else { return } + self.popupCancellables.removeAll() + } + }.store(in: &popupCancellables) + + coordinator.toPopup(popup, from: self) + } +} diff --git a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift new file mode 100644 index 0000000000000000000000000000000000000000..a0624385c1d0143c5826c358f7081ada3573f8b1 --- /dev/null +++ b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift @@ -0,0 +1,44 @@ +import UIKit +import Combine +import DependencyInjection + +public final class RestoreSuccessController: UIViewController { + @Dependency private var coordinator: RestoreCoordinating + + lazy private var screenView = RestoreSuccessView() + private var cancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let gradient = CAGradientLayer() + gradient.colors = [ + UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, + UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, + UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, + UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor + ] + + gradient.startPoint = CGPoint(x: 0, y: 0) + gradient.endPoint = CGPoint(x: 1, y: 1) + + gradient.frame = screenView.bounds + screenView.layer.insertSublayer(gradient, at: 0) + } + + private func setupBindings() { + screenView.nextButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in coordinator.toChats(from: self) } + .store(in: &cancellables) + } +} diff --git a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..2ade73355fa9b2b3de55cac81fa3b11b9b37621b --- /dev/null +++ b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift @@ -0,0 +1,55 @@ +import UIKit +import Models +import Presentation + +public protocol RestoreCoordinating { + func toChats(from: UIViewController) + func toSuccess(from: UIViewController) + func toPopup(_: UIViewController, from: UIViewController) + func toRestore(using: String, with: RestoreSettings, from: UIViewController) +} + +public struct RestoreCoordinator: RestoreCoordinating { + var pushPresenter: Presenting = PushPresenter() + var bottomPresenter: Presenting = BottomPresenter() + var replacePresenter: Presenting = ReplacePresenter() + + var successFactory: () -> UIViewController + var chatListFactory: () -> UIViewController + var restoreFactory: (String, RestoreSettings) -> UIViewController + + public init( + successFactory: @escaping () -> UIViewController, + chatListFactory: @escaping () -> UIViewController, + restoreFactory: @escaping (String, RestoreSettings) -> UIViewController + ) { + self.successFactory = successFactory + self.restoreFactory = restoreFactory + self.chatListFactory = chatListFactory + } +} + +public extension RestoreCoordinator { + func toRestore( + using ndf: String, + with settings: RestoreSettings, + from parent: UIViewController + ) { + let screen = restoreFactory(ndf, settings) + pushPresenter.present(screen, from: parent) + } + + func toChats(from parent: UIViewController) { + let screen = chatListFactory() + replacePresenter.present(screen, from: parent) + } + + func toSuccess(from parent: UIViewController) { + let screen = successFactory() + replacePresenter.present(screen, from: parent) + } + + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) + } +} diff --git a/Sources/RestoreFeature/Service/MockRestoreService.swift b/Sources/RestoreFeature/Service/MockRestoreService.swift new file mode 100644 index 0000000000000000000000000000000000000000..1a013434211580df1c99ac1fa3842ab0fc797d59 --- /dev/null +++ b/Sources/RestoreFeature/Service/MockRestoreService.swift @@ -0,0 +1,30 @@ +import UIKit +import Models +import Combine +import Foundation +import GoogleDriveFeature +import DependencyInjection + +public struct RestoreServiceMock: RestoreServiceType { + public var inProgress: AnyPublisher<Void, Never> { + fatalError() + } + + public var settings: AnyPublisher<RestoreSettings, Never> { + fatalError() + } + + public init() {} + + public func didSelectBackup(at url: URL) {} + + public func authorize(service: CloudService, from: UIViewController) {} + + public func download( + from settings: RestoreSettings, + progress: @escaping RestoreProgress, + whenFinished: @escaping RestoreDownloadFinished + ) { + fatalError() + } +} diff --git a/Sources/RestoreFeature/Service/RestoreService.swift b/Sources/RestoreFeature/Service/RestoreService.swift new file mode 100644 index 0000000000000000000000000000000000000000..9bd5b80e169d75d89806e716116b4781a080f691 --- /dev/null +++ b/Sources/RestoreFeature/Service/RestoreService.swift @@ -0,0 +1,38 @@ +//import UIKit +//import Models +//import Combine +// +//import DependencyInjection +// +//public struct RestoreService: RestoreServiceType { +// +// +// +// @Dependency private var coordinator: RestoreCoordinating +// +// public var inProgress: AnyPublisher<Void, Never> { inProgressSubject.eraseToAnyPublisher() } +// public var settings: AnyPublisher<RestoreSettings, Never> { settingsSubject.eraseToAnyPublisher() } +// +// private let inProgressSubject = PassthroughSubject<Void, Never>() +// private let settingsSubject = PassthroughSubject<RestoreSettings, Never>() +// +// private var cancellables = Set<AnyCancellable>() +// +// public init() {} +// +// public func authorize(service: CloudService, from controller: UIViewController) { +// } +// } +// +// public func download( +// from settings: RestoreSettings, +// progress: @escaping RestoreProgress, +// whenFinished: @escaping RestoreDownloadFinished +// ) { +// drive.downloadBackup( +// settings.backup!.id, +// progressCallback: progress, +// whenFinished +// ) +// } +//} diff --git a/Sources/RestoreFeature/Service/RestoreServiceType.swift b/Sources/RestoreFeature/Service/RestoreServiceType.swift new file mode 100644 index 0000000000000000000000000000000000000000..78a32e9f6d9e199d78fa9c17a4a06b7ca1de51f3 --- /dev/null +++ b/Sources/RestoreFeature/Service/RestoreServiceType.swift @@ -0,0 +1,20 @@ +import UIKit +import Models +import Combine + +public typealias RestoreProgress = (Float) -> Void +public typealias RestoreDownloadFinished = (Result<Data, Error>) -> Void + +public protocol RestoreServiceType { + var inProgress: AnyPublisher<Void, Never> { get } + + var settings: AnyPublisher<RestoreSettings, Never> { get } + + func authorize(service: CloudService, from: UIViewController) + + func download( + from settings: RestoreSettings, + progress: @escaping RestoreProgress, + whenFinished: @escaping RestoreDownloadFinished + ) +} diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..88d507c17b55eabdfd7ea24c8e22d8a3a609468d --- /dev/null +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -0,0 +1,120 @@ +import HUD +import UIKit +import Models +import Shared +import Combine +import BackupFeature +import DependencyInjection + +import iCloudFeature +import DropboxFeature +import GoogleDriveFeature + +final class RestoreListViewModel { + @Dependency private var icloud: iCloudInterface + @Dependency private var dropbox: DropboxInterface + @Dependency private var drive: GoogleDriveInterface + + var hud: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() } + var didFetchBackup: AnyPublisher<RestoreSettings, Never> { backupSubject.eraseToAnyPublisher() } + + private var dropboxAuthCancellable: AnyCancellable? + + private let hudSubject = PassthroughSubject<HUDStatus, Never>() + private let backupSubject = PassthroughSubject<RestoreSettings, Never>() + + func didTapCloud(_ cloudService: CloudService, from parent: UIViewController) { + switch cloudService { + case .drive: + didRequestDriveAuthorization(from: parent) + case .icloud: + didRequestICloudAuthorization() + case .dropbox: + didRequestDropboxAuthorization(from: parent) + } + } + + private func didRequestDriveAuthorization(from controller: UIViewController) { + drive.authorize(presenting: controller) { authResult in + switch authResult { + case .success: + self.hudSubject.send(.on) + self.drive.downloadMetadata { downloadResult in + switch downloadResult { + case .success(let metadata): + var backup: Backup? + + if let metadata = metadata { + backup = .init(id: metadata.identifier, date: metadata.modifiedDate, size: metadata.size) + } + + self.hudSubject.send(.none) + self.backupSubject.send(RestoreSettings(backup: backup, cloudService: .drive)) + + case .failure(let error): + self.hudSubject.send(.error(.init(with: error))) + } + } + case .failure(let error): + self.hudSubject.send(.error(.init(with: error))) + } + } + } + + private func didRequestICloudAuthorization() { + if icloud.isAuthorized() { + self.hudSubject.send(.on) + + icloud.downloadMetadata { result in + switch result { + case .success(let metadata): + var backup: Backup? + + if let metadata = metadata { + backup = .init(id: metadata.path, date: metadata.modifiedDate, size: metadata.size) + } + + self.hudSubject.send(.none) + self.backupSubject.send(RestoreSettings(backup: backup, cloudService: .icloud)) + case .failure(let error): + self.hudSubject.send(.error(.init(with: error))) + } + } + } else { + /// This could be an alert controller asking if user wants to enable/deeplink + /// + icloud.openSettings() + } + } + + private func didRequestDropboxAuthorization(from controller: UIViewController) { + dropboxAuthCancellable = dropbox.authorize(presenting: controller) + .receive(on: DispatchQueue.main) + .sink { [unowned self] authResult in + switch authResult { + case .success(let bool): + guard bool == true else { return } + + self.hudSubject.send(.on) + dropbox.downloadMetadata { metadataResult in + switch metadataResult { + case .success(let metadata): + var backup: Backup? + + if let metadata = metadata { + backup = .init(id: metadata.path, date: metadata.modifiedDate, size: metadata.size) + } + + self.hudSubject.send(.none) + self.backupSubject.send(RestoreSettings(backup: backup, cloudService: .dropbox)) + + case .failure(let error): + self.hudSubject.send(.error(.init(with: error))) + } + } + case .failure(let error): + self.hudSubject.send(.error(.init(with: error))) + } + } + } +} diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..24a3cfb6a91b001547f410882d2cfadd619623af --- /dev/null +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -0,0 +1,130 @@ +import UIKit +import Models +import Shared +import Combine +import Defaults +import Foundation +import Integration +import BackupFeature +import DependencyInjection + +import iCloudFeature +import DropboxFeature +import GoogleDriveFeature + +enum RestorationStep { + case idle(CloudService, Backup?) + case downloading(Float, Float) + case failDownload(Error) + case parsingData + case done +} + +extension RestorationStep: Equatable { + static func ==(lhs: RestorationStep, rhs: RestorationStep) -> Bool { + switch (lhs, rhs) { + case (.done, .done): + return true + case let (.failDownload(a), .failDownload(b)): + return a.localizedDescription == b.localizedDescription + case let (.downloading(a, b), .downloading(c, d)): + return a == c && b == d + case (.idle, _), (.downloading, _), (.parsingData, _), + (.done, _), (.failDownload, _): + return false + } + } +} + +final class RestoreViewModel { + @Dependency private var iCloudService: iCloudInterface + @Dependency private var dropboxService: DropboxInterface + @Dependency private var googleService: GoogleDriveInterface + + @KeyObject(.username, defaultValue: nil) var username: String? + + var step: AnyPublisher<RestorationStep, Never> { stepRelay.eraseToAnyPublisher() } + + private let ndf: String + private let settings: RestoreSettings + private let stepRelay: CurrentValueSubject<RestorationStep, Never> + + init(ndf: String, settings: RestoreSettings) { + self.ndf = ndf + self.settings = settings + self.stepRelay = .init(.idle(settings.cloudService, settings.backup)) + } + + func didTapRestore() { + guard let backup = settings.backup else { fatalError() } + + stepRelay.send(.downloading(0.0, backup.size)) + + switch settings.cloudService { + case .drive: + downloadBackupForDrive(backup) + case .dropbox: + downloadBackupForDropbox(backup) + case .icloud: + downloadBackupForiCloud(backup) + } + } + + private func downloadBackupForDropbox(_ backup: Backup) { + dropboxService.downloadBackup(backup.id) { [weak self] in + guard let self = self else { return } + self.stepRelay.send(.downloading(backup.size, backup.size)) + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } + } + } + + private func downloadBackupForiCloud(_ backup: Backup) { + iCloudService.downloadBackup(backup.id) { [weak self] in + guard let self = self else { return } + self.stepRelay.send(.downloading(backup.size, backup.size)) + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } + } + } + + private func downloadBackupForDrive(_ backup: Backup) { + googleService.downloadBackup(backup.id) { [weak self] in + if let stepRelay = self?.stepRelay { + stepRelay.send(.downloading($0, backup.size)) + } + } _: { [weak self] in + guard let self = self else { return } + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } + } + } + + private func continueRestoring(data: Data) { + stepRelay.send(.parsingData) + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + let session = try! Session(backupFile: data, ndf: self.ndf) + DependencyInjection.Container.shared.register(session as SessionType) + + self.stepRelay.send(.done) + } + } +} diff --git a/Sources/RestoreFeature/Views/RestoreDetailsView.swift b/Sources/RestoreFeature/Views/RestoreDetailsView.swift new file mode 100644 index 0000000000000000000000000000000000000000..55f18c4d2befcd5dcfd9fa296fe7f6c888fd1aee --- /dev/null +++ b/Sources/RestoreFeature/Views/RestoreDetailsView.swift @@ -0,0 +1,56 @@ +import UIKit +import Shared + +final class RestoreDetailsView: UIView { + let separatorView = UIView() + let imageView = UIImageView() + let titleLabel = UILabel() + + let stackView = UIStackView() + let dateView = DetailRowButton() + let sizeView = DetailRowButton() + + init() { + super.init(frame: .zero) + separatorView.backgroundColor = Asset.neutralLine.color + + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + titleLabel.textColor = Asset.neutralActive.color + + stackView.axis = .vertical + stackView.spacing = 22 + stackView.addArrangedSubview(dateView) + stackView.addArrangedSubview(sizeView) + + addSubview(separatorView) + addSubview(imageView) + addSubview(titleLabel) + addSubview(stackView) + + separatorView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview().offset(25) + make.right.equalToSuperview().offset(-25) + make.height.equalTo(1) + } + + imageView.snp.makeConstraints { make in + make.top.equalTo(separatorView.snp.bottom).offset(40) + make.left.equalToSuperview().offset(24) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalTo(imageView) + make.left.equalToSuperview().offset(92) + } + + stackView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(20) + make.left.equalTo(titleLabel) + make.right.equalToSuperview().offset(-40) + make.bottom.lessThanOrEqualToSuperview().offset(-20) + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/RestoreFeature/Views/RestoreListView.swift b/Sources/RestoreFeature/Views/RestoreListView.swift new file mode 100644 index 0000000000000000000000000000000000000000..26d937db458bc12f164141a32c665f1ac57ee12c --- /dev/null +++ b/Sources/RestoreFeature/Views/RestoreListView.swift @@ -0,0 +1,123 @@ +import UIKit +import Shared + +final class RestoreListView: UIView { + let titleLabel = UILabel() + let stackView = UIStackView() + let firstSubtitleLabel = UILabel() + let secondSubtitleLabel = UILabel() + let driveButton = RowButton() + let icloudButton = RowButton() + let dropboxButton = RowButton() + let cancelButton = CapsuleButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupTitle(Localized.Restore.List.title) + setupSubtitle(Localized.Restore.List.firstSubtitle) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attrString = NSMutableAttributedString( + string: Localized.Restore.List.secondSubtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ] + ) + + secondSubtitleLabel.numberOfLines = 0 + secondSubtitleLabel.attributedText = attrString + + icloudButton.setup(title: Localized.Backup.iCloud, icon: Asset.restoreIcloud.image) + dropboxButton.setup(title: Localized.Backup.dropbox, icon: Asset.restoreDropbox.image) + driveButton.setup(title: Localized.Backup.googleDrive, icon: Asset.restoreDrive.image) + + cancelButton.set(style: .seeThrough, title: Localized.Restore.List.cancel) + + stackView.axis = .vertical + stackView.addArrangedSubview(driveButton) + stackView.addArrangedSubview(icloudButton) + stackView.addArrangedSubview(dropboxButton) + + addSubview(titleLabel) + addSubview(firstSubtitleLabel) + addSubview(secondSubtitleLabel) + addSubview(stackView) + addSubview(cancelButton) + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide).offset(15) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + firstSubtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(8) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + secondSubtitleLabel.snp.makeConstraints { make in + make.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-41) + } + + stackView.snp.makeConstraints { make in + make.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + } + + cancelButton.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-40) + make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) + } + } + + required init?(coder: NSCoder) { nil } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1 + + attString.addAttribute(.paragraphStyle, value: paragraph) + attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) + attString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 34.0) as Any) + + attString.addAttributes(attributes: [ + .font: Fonts.Mulish.bold.font(size: 34.0) as Any, + .foregroundColor: Asset.brandPrimary.color + ], betweenCharacters: "#") + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attString + } + + private func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSAttributedString( + string: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + firstSubtitleLabel.numberOfLines = 0 + firstSubtitleLabel.attributedText = attString + } +} diff --git a/Sources/RestoreFeature/Views/RestoreProgressView.swift b/Sources/RestoreFeature/Views/RestoreProgressView.swift new file mode 100644 index 0000000000000000000000000000000000000000..95a471f02bf53bf936ca1cd30d98217bc3afde7c --- /dev/null +++ b/Sources/RestoreFeature/Views/RestoreProgressView.swift @@ -0,0 +1,87 @@ +import UIKit +import Shared + +final class RestoreProgressView: UIView { + let progressBarFull = UIView() + let progressBarFiller = UIView() + let progressLabel = UILabel() + let warningLabel = UILabel() + let descriptiveProgressLabel = UILabel() + + init() { + super.init(frame: .zero) + warningLabel.textColor = Asset.neutralDisabled.color + progressLabel.textColor = Asset.neutralDisabled.color + descriptiveProgressLabel.textColor = Asset.neutralDisabled.color + + warningLabel.font = Fonts.Mulish.regular.font(size: 14.0) + progressLabel.font = Fonts.Mulish.regular.font(size: 14.0) + descriptiveProgressLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + descriptiveProgressLabel.textAlignment = .center + + progressBarFull.backgroundColor = Asset.neutralLine.color + progressBarFiller.backgroundColor = Asset.brandPrimary.color + progressBarFull.layer.masksToBounds = true + progressBarFull.layer.cornerRadius = 4 + + warningLabel.numberOfLines = 0 + descriptiveProgressLabel.numberOfLines = 0 + warningLabel.text = "This may take up to 5 mins, please don’t close the app and don’t put in background and don’t close your phone screen" + + addSubview(progressBarFull) + addSubview(progressLabel) + addSubview(warningLabel) + addSubview(descriptiveProgressLabel) + progressBarFull.addSubview(progressBarFiller) + + descriptiveProgressLabel.snp.makeConstraints { make in + make.top.greaterThanOrEqualToSuperview() + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + make.bottom.equalTo(progressBarFull.snp.top).offset(-15) + } + + progressBarFull.snp.makeConstraints { make in + make.top.greaterThanOrEqualToSuperview() + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + make.centerY.equalToSuperview() + make.height.equalTo(8) + } + + progressBarFiller.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.width.equalTo(0) + make.bottom.equalToSuperview() + } + + progressLabel.snp.makeConstraints { make in + make.top.equalTo(progressBarFull.snp.bottom).offset(15) + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + } + + warningLabel.snp.makeConstraints { make in + make.top.equalTo(progressLabel.snp.bottom).offset(15) + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + make.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func update(downloaded: Float, total: Float) { + let totalkb = String(format: "%.1f kb", total/1000) + let downloadedKb = String(format: "%.1f kb", downloaded/1000) + let percent = String(format: "%.0f", downloaded/total * 100) + + progressLabel.text = "Downloaded \(downloadedKb) of \(totalkb) (\(percent)%)" + + progressBarFiller.snp.updateConstraints { make in + make.width.equalTo(CGFloat(downloaded/total) * progressBarFull.frame.size.width) + } + } +} diff --git a/Sources/RestoreFeature/Views/RestoreSuccessView.swift b/Sources/RestoreFeature/Views/RestoreSuccessView.swift new file mode 100644 index 0000000000000000000000000000000000000000..94f318951e8d3f447d72a2d2188a37aca398b84f --- /dev/null +++ b/Sources/RestoreFeature/Views/RestoreSuccessView.swift @@ -0,0 +1,78 @@ +import UIKit +import Shared + +final class RestoreSuccessView: UIView { + let iconImageView = UIImageView() + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let nextButton = CapsuleButton() + + init() { + super.init(frame: .zero) + + iconImageView.contentMode = .center + iconImageView.image = Asset.onboardingSuccess.image + nextButton.set(style: .white, title: Localized.Onboarding.Success.action) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.textColor = Asset.neutralWhite.color + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + addSubview(iconImageView) + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(nextButton) + + iconImageView.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide).offset(40) + make.left.equalToSuperview().offset(40) + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(iconImageView.snp.bottom).offset(40) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-90) + } + + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(30) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-90) + } + + nextButton.snp.makeConstraints { make in + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalToSuperview().offset(-60) + } + + setTitle(Localized.Restore.Success.title) + setSubtitle(Localized.Restore.Success.subtitle) + } + + required init?(coder: NSCoder) { nil } + + private func setTitle(_ title: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.1 + + let attrString = NSMutableAttributedString(string: title) + + attrString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 39.0)) + attrString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) + + attrString.addAttribute( + name: .foregroundColor, + value: Asset.neutralBody.color, + betweenCharacters: "#" + ) + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attrString + } + + private func setSubtitle(_ subtitle: String?) { + subtitleLabel.text = subtitle + } +} diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift new file mode 100644 index 0000000000000000000000000000000000000000..cb7f0b7dcde49e40faa4872dcb99c88e6e3199f0 --- /dev/null +++ b/Sources/RestoreFeature/Views/RestoreView.swift @@ -0,0 +1,165 @@ +import UIKit +import Shared +import Models + +final class RestoreView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let detailsView = RestoreDetailsView() + let progressView = RestoreProgressView() + + let bottomStackView = UIStackView() + let backButton = CapsuleButton() + let cancelButton = CapsuleButton() + let restoreButton = CapsuleButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + subtitleLabel.numberOfLines = 0 + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + titleLabel.textColor = Asset.neutralDark.color + subtitleLabel.textColor = Asset.neutralDark.color + + restoreButton.set(style: .brandColored, title: Localized.Restore.Found.restore) + cancelButton.set(style: .simplestColoredBrand, title: Localized.Restore.Found.cancel) + backButton.set(style: .seeThrough, title: Localized.Restore.NotFound.back) + + bottomStackView.axis = .vertical + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(detailsView) + addSubview(progressView) + addSubview(bottomStackView) + + bottomStackView.addArrangedSubview(restoreButton) + bottomStackView.addArrangedSubview(cancelButton) + bottomStackView.addArrangedSubview(backButton) + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide).offset(20) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-38) + } + + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(20) + make.left.equalToSuperview().offset(38) + make.right.equalToSuperview().offset(-38) + } + + detailsView.snp.makeConstraints { make in + make.top.equalTo(subtitleLabel.snp.bottom).offset(40) + make.left.equalToSuperview() + make.right.equalToSuperview() + } + + progressView.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(detailsView.snp.bottom) + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.lessThanOrEqualTo(bottomStackView.snp.top) + } + + bottomStackView.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-40) + make.bottom.equalTo(safeAreaLayoutGuide).offset(-20) + } + } + + required init?(coder: NSCoder) { nil } + + func updateFor(step: RestorationStep) { + switch step { + case .idle(let cloudService, let backup): + guard let backup = backup else { + showNoBackupForCloud(named: cloudService.name()) + return + } + + showBackup(backup, fromCloud: cloudService) + + case .downloading(let downloaded, let total): + restoreButton.isHidden = true + cancelButton.isHidden = true + progressView.isHidden = false + + progressView.update(downloaded: downloaded, total: total) + + case .failDownload(let error): + progressView.descriptiveProgressLabel.text = error.localizedDescription + + case .parsingData: + progressView.descriptiveProgressLabel.text = "Parsing backup data" + + case .done: + progressView.descriptiveProgressLabel.text = "Done" + } + } + + private func showBackup(_ backup: Backup, fromCloud cloud: CloudService) { + titleLabel.text = Localized.Restore.Found.title + subtitleLabel.text = Localized.Restore.Found.subtitle + + detailsView.titleLabel.text = cloud.name() + detailsView.imageView.image = cloud.asset() + + detailsView.dateView.setup( + title: Localized.Restore.Found.date, + value: backup.date.backupStyle(), + hasArrow: false + ) + + detailsView.sizeView.setup( + title: Localized.Restore.Found.size, + value: String(format: "%.1f kb", backup.size/1000), + hasArrow: false + ) + + detailsView.isHidden = false + backButton.isHidden = true + restoreButton.isHidden = false + cancelButton.isHidden = false + progressView.isHidden = true + } + + private func showNoBackupForCloud(named cloud: String) { + titleLabel.text = Localized.Restore.NotFound.title + subtitleLabel.text = Localized.Restore.NotFound.subtitle(cloud) + + restoreButton.isHidden = true + cancelButton.isHidden = true + detailsView.isHidden = true + backButton.isHidden = false + progressView.isHidden = true + } +} + +private extension CloudService { + func name() -> String { + switch self { + case .drive: + return Localized.Backup.googleDrive + case .icloud: + return Localized.Backup.iCloud + case .dropbox: + return Localized.Backup.dropbox + } + } + + func asset() -> UIImage { + switch self { + case .drive: + return Asset.restoreDrive.image + case .icloud: + return Asset.restoreIcloud.image + case .dropbox: + return Asset.restoreDropbox.image + } + } +} diff --git a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift b/Sources/ScanFeature/Coordinator/ScanCoordinator.swift index 3993baffaf0fb1e47dde0c305a854a647c92d321..782d5b62980f165f27aa813be6294e6cdac221e3 100644 --- a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift +++ b/Sources/ScanFeature/Coordinator/ScanCoordinator.swift @@ -11,52 +11,42 @@ public protocol ScanCoordinating { } public struct ScanCoordinator { - public init( - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController - ) { - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - } - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() + var pushPresenter: Presenting = PushPresenter() var bottomPresenter: Presenting = BottomPresenter() - var replacer: Presenting = ReplacePresenter(mode: .replaceLast) - - // MARK: Factories + var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast) var contactsFactory: () -> UIViewController var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - = ContactController.init(_:) -} -extension ScanCoordinator: ScanCoordinating { - public func toPopup( - _ popup: UIViewController, - from parent: UIViewController + public init( + contactsFactory: @escaping () -> UIViewController, + requestsFactory: @escaping () -> UIViewController, + contactFactory: @escaping (Contact) -> UIViewController ) { - bottomPresenter.present(popup, from: parent) + self.contactFactory = contactFactory + self.contactsFactory = contactsFactory + self.requestsFactory = requestsFactory } +} +extension ScanCoordinator: ScanCoordinating { public func toRequests(from parent: UIViewController) { let screen = requestsFactory() - replacer.present(screen, from: parent) + replacePresenter.present(screen, from: parent) } public func toContacts(from parent: UIViewController) { let screen = contactsFactory() - replacer.present(screen, from: parent) + replacePresenter.present(screen, from: parent) } - public func toContact( - _ contact: Contact, - from parent: UIViewController - ) { + public func toContact(_ contact: Contact, from parent: UIViewController) { let screen = contactFactory(contact) - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) + } + + public func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) } } diff --git a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift index b62ec70f805c0a3104c558b97b3f533bcbd64bb5..0bb1448c964c26e399979a4a47a2f8ab98f5dec8 100644 --- a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift +++ b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift @@ -2,52 +2,41 @@ import UIKit import Models import Countries import Presentation -import ContactFeature public protocol SearchCoordinating { func toContact(_: Contact, from: UIViewController) func toPopup(_: UIViewController, from: UIViewController) - func toCountries(from: UIViewController, _ onChoose: @escaping (Country) -> Void) + func toCountries(from: UIViewController, _: @escaping (Country) -> Void) } public struct SearchCoordinator { - public init() {} - - // MARK: Presenters - - var pusher: Presenting = PushPresenter() + var pushPresenter: Presenting = PushPresenter() var bottomPresenter: Presenting = BottomPresenter() - // MARK: Factories - var contactFactory: (Contact) -> UIViewController - = ContactController.init(_:) - var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - = CountryListController.init(_:) + + public init( + contactFactory: @escaping (Contact) -> UIViewController, + countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController + ) { + self.contactFactory = contactFactory + self.countriesFactory = countriesFactory + } } extension SearchCoordinator: SearchCoordinating { - public func toContact( - _ contact: Contact, - from parent: UIViewController - ) { + public func toContact(_ contact: Contact, from parent: UIViewController) { let screen = contactFactory(contact) - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } - public func toCountries( - from parent: UIViewController, - _ onChoose: @escaping (Country) -> Void - ) { - let screen = countriesFactory(onChoose) - pusher.present(screen, from: parent) + public func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) } - public func toPopup( - _ popup: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(popup, from: parent) + public func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { + let screen = countriesFactory(onChoose) + pushPresenter.present(screen, from: parent) } } diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index eff3a3b4663af6871ffe74863389820162d80fb9..24f7ba5dea09f3f2cca6389607eaff6c840d8a96 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -7,7 +7,7 @@ import Defaults import ScrollViewController import DependencyInjection -final class AccountDeleteController: UIViewController { +public final class AccountDeleteController: UIViewController { @KeyObject(.username, defaultValue: "") var username: String @Dependency private var hud: HUDType @@ -20,13 +20,13 @@ final class AccountDeleteController: UIViewController { private var cancellables = Set<AnyCancellable>() private var popupCancellables = Set<AnyCancellable>() - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.navigationBar .customize(backgroundColor: Asset.neutralWhite.color) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupScrollView() diff --git a/Sources/SettingsFeature/Controllers/AdvancedController.swift b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift similarity index 74% rename from Sources/SettingsFeature/Controllers/AdvancedController.swift rename to Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift index e3df6d4ef0f8ead60698e6de4d2a5dbb37b43584..1645d25a32e6325eb902a122f2633cabdfd0cab7 100644 --- a/Sources/SettingsFeature/Controllers/AdvancedController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift @@ -3,25 +3,25 @@ import Shared import Combine import DependencyInjection -final class AdvancedController: UIViewController { +public final class SettingsAdvancedController: UIViewController { @Dependency private var coordinator: SettingsCoordinating - lazy private var screenView = AdvancedView() + lazy private var screenView = SettingsAdvancedView() - private let viewModel = AdvancedViewModel() private var cancellables = Set<AnyCancellable>() + private let viewModel = SettingsAdvancedViewModel() - override func loadView() { + public override func loadView() { view = screenView } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.navigationBar .customize(backgroundColor: Asset.neutralWhite.color) } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupBindings() @@ -46,31 +46,31 @@ final class AdvancedController: UIViewController { } private func setupBindings() { - viewModel.sharePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toActivityController(with: [$0], from: self) } - .store(in: &cancellables) - - screenView.downloadLogs + screenView.downloadLogsButton .publisher(for: .touchUpInside) .sink { [weak viewModel] in viewModel?.didTapDownloadLogs() } .store(in: &cancellables) - screenView.logs.switcherView + screenView.logRecordingSwitcher.switcherView .publisher(for: .valueChanged) .sink { [weak viewModel] in viewModel?.didToggleRecordLogs() } .store(in: &cancellables) - screenView.crashes.switcherView + screenView.crashReportingSwitcher.switcherView .publisher(for: .valueChanged) .sink { [weak viewModel] in viewModel?.didToggleCrashReporting() } .store(in: &cancellables) + viewModel.sharePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in coordinator.toActivityController(with: [$0], from: self) } + .store(in: &cancellables) + viewModel.state .removeDuplicates() .sink { [unowned self] state in - screenView.logs.switcherView.setOn(state.isRecordingLogs, animated: true) - screenView.crashes.switcherView.setOn(state.isCrashReporting, animated: true) + screenView.logRecordingSwitcher.switcherView.setOn(state.isRecordingLogs, animated: true) + screenView.crashReportingSwitcher.switcherView.setOn(state.isCrashReporting, animated: true) }.store(in: &cancellables) } diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift index 8426272f70a0b8ebc0e5709ff7f89ada52724958..fcf0aa2525e00479333eaaad5bb7e99c43c14ffb 100644 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsController.swift @@ -52,11 +52,6 @@ public final class SettingsController: UIViewController { .customize(backgroundColor: Asset.neutralWhite.color) } - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.didAppear() - } - public override func viewDidLoad() { super.viewDidLoad() @@ -100,15 +95,6 @@ public final class SettingsController: UIViewController { .sink { [hud] in hud.update(with: $0) } .store(in: &cancellables) - viewModel.infoPopupPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - presentInfo( - title: Localized.Settings.InfoPopUp.Privacy.title, - subtitle: Localized.Settings.InfoPopUp.Privacy.subtitle - ) - }.store(in: &cancellables) - screenView.inAppNotifications.switcherView .publisher(for: .valueChanged) .sink { [weak viewModel] in viewModel?.didToggleInAppNotifications() } @@ -139,7 +125,7 @@ public final class SettingsController: UIViewController { .sink { [weak viewModel] in viewModel?.didToggleBiometrics() } .store(in: &cancellables) - screenView.privacyPolicy + screenView.privacyPolicyButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in @@ -152,7 +138,7 @@ public final class SettingsController: UIViewController { } }.store(in: &cancellables) - screenView.disclosures + screenView.disclosuresButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in @@ -165,14 +151,19 @@ public final class SettingsController: UIViewController { } }.store(in: &cancellables) - screenView.delete + screenView.deleteButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toAccountDelete(from: self) - }.store(in: &cancellables) + .sink { [unowned self] in coordinator.toDelete(from: self) } + .store(in: &cancellables) + + screenView.accountBackupButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in coordinator.toBackup(from: self) } + .store(in: &cancellables) - screenView.advanced + screenView.advancedButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in coordinator.toAdvanced(from: self) } diff --git a/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift b/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift index de2df5e82f5a9994cfc84482c6e5ead6756a5291..6a3bf13774762095d79cacd7ac0e9e264871fc6f 100644 --- a/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift +++ b/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift @@ -3,54 +3,58 @@ import Shared import Presentation public protocol SettingsCoordinating { + func toBackup(from: UIViewController) + func toDelete(from: UIViewController) func toAdvanced(from: UIViewController) - func toAccountDelete(from: UIViewController) func toPopup(_: UIViewController, from: UIViewController) func toActivityController(with: [Any], from: UIViewController) } public struct SettingsCoordinator: SettingsCoordinating { - public init() {} - - // MARK: Presenters + public init( + backupFactory: @escaping () -> UIViewController, + advancedFactory: @escaping () -> UIViewController, + accountDeleteFactory: @escaping () -> UIViewController + ) { + self.backupFactory = backupFactory + self.advancedFactory = advancedFactory + self.accountDeleteFactory = accountDeleteFactory + } - var pusher: Presenting = PushPresenter() - var presenter: Presenting = ModalPresenter() + var pushPresenter: Presenting = PushPresenter() + var modalPresenter: Presenting = ModalPresenter() var bottomPresenter: Presenting = BottomPresenter() - // MARK: Factories - - var advancedFactory: () -> UIViewController = AdvancedController.init - - var accountDeleteFactory: () -> UIViewController = AccountDeleteController.init + var backupFactory: () -> UIViewController + var advancedFactory: () -> UIViewController + var accountDeleteFactory: () -> UIViewController var activityControllerFactory: ([Any]) -> UIViewController = { UIActivityViewController(activityItems: $0, applicationActivities: nil) } } public extension SettingsCoordinator { - func toPopup( - _ popup: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(popup, from: parent) - } - func toAdvanced(from parent: UIViewController) { let screen = advancedFactory() - pusher.present(screen, from: parent) + pushPresenter.present(screen, from: parent) } - func toActivityController( - with items: [Any], - from parent: UIViewController - ) { - let screen = activityControllerFactory(items) - presenter.present(screen, from: parent) + func toDelete(from parent: UIViewController) { + let screen = accountDeleteFactory() + pushPresenter.present(screen, from: parent) } - func toAccountDelete(from parent: UIViewController) { - let screen = accountDeleteFactory() - pusher.present(screen, from: parent) + func toBackup(from parent: UIViewController) { + let screen = backupFactory() + pushPresenter.present(screen, from: parent) + } + + func toPopup(_ popup: UIViewController, from parent: UIViewController) { + bottomPresenter.present(popup, from: parent) + } + + func toActivityController(with items: [Any], from parent: UIViewController) { + let screen = activityControllerFactory(items) + modalPresenter.present(screen, from: parent) } } diff --git a/Sources/SettingsFeature/ViewModels/AdvancedViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift similarity index 97% rename from Sources/SettingsFeature/ViewModels/AdvancedViewModel.swift rename to Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift index 34b266a6c3020c33aea7c2a411ee76926686bf65..ed304656ddc9bf88a262bb01beb8c9aeb29194c9 100644 --- a/Sources/SettingsFeature/ViewModels/AdvancedViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift @@ -10,7 +10,7 @@ struct AdvancedViewState: Equatable { var isCrashReporting = false } -final class AdvancedViewModel { +final class SettingsAdvancedViewModel { @KeyObject(.recordingLogs, defaultValue: true) var isRecordingLogs: Bool @KeyObject(.crashReporting, defaultValue: true) var isCrashReporting: Bool diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index 0e916ec9b54d3c221d6207bafa4dff4bcd460e87..a90685459ed3895ec092639512af84d11dff949d 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -25,7 +25,6 @@ final class SettingsViewModel { @Dependency private var pushHandler: PushHandling @Dependency private var permissions: PermissionHandling - @KeyObject(.openedSettingsFirstTime, defaultValue: true) var isFirstTime: Bool @KeyObject(.dummyTrafficOn, defaultValue: false) var isDummyTrafficOn: Bool @KeyObject(.biometrics, defaultValue: false) private var biometrics @KeyObject(.hideAppList, defaultValue: false) private var hideAppList @@ -38,12 +37,6 @@ final class SettingsViewModel { var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - var infoPopupPublisher: AnyPublisher<Void, Never> { - infoPopupSubject.eraseToAnyPublisher() - } - - private let infoPopupSubject = PassthroughSubject<Void, Never>() - var state: AnyPublisher<SettingsViewState, Never> { stateRelay.eraseToAnyPublisher() } private let stateRelay = CurrentValueSubject<SettingsViewState, Never>(.init()) @@ -57,12 +50,6 @@ final class SettingsViewModel { stateRelay.value.isDummyTrafficOn = isDummyTrafficOn } - func didAppear() { - guard isFirstTime else { return } - isFirstTime = false - infoPopupSubject.send() - } - func didToggleBiometrics() { biometricAuthentication(enable: !biometrics) } @@ -159,19 +146,3 @@ final class SettingsViewModel { } } } - -/* - - - case .appCancel: The app canceled authentication by invalidating the LAContext - - case .authenticationFailed: The user did not provide valid credentials - - case .invalidContext: The LAContext was invalid - - case .notInteractive: Interaction was not allowed so the authentication failed - - case .passcodeNotSet: The user has not set a passcode on this device - - case .systemCancel: The system canceled authentication for example to show another app - - case .userCancel: The user canceled the authentication dialog - - case .userFallback: The user selected to use a fallback authentication method - - case .biometryLockout: Too many failed attempts locked biometric authentication - - case .biometryNotAvailable: The user's device does not support biometric authentication - - case .biometryNotEnrolled: The user has not configured biometric authentication - - */ diff --git a/Sources/SettingsFeature/Views/AccountDeleteView.swift b/Sources/SettingsFeature/Views/AccountDeleteView.swift index 2dcc28e37f4a1a7db402e7cd46d5ca4206347379..e4aef7f5642929216a84e9e7b889ec0d3fa095ff 100644 --- a/Sources/SettingsFeature/Views/AccountDeleteView.swift +++ b/Sources/SettingsFeature/Views/AccountDeleteView.swift @@ -53,7 +53,7 @@ final class AccountDeleteView: UIView { confirmButton.setStyle(.red) confirmButton.isEnabled = false confirmButton.setTitle(Localized.Settings.Delete.delete, for: .normal) - cancelButton.setStyle(.simplestColored) + cancelButton.setStyle(.simplestColoredRed) cancelButton.setTitle(Localized.Settings.Delete.cancel, for: .normal) stackView.spacing = 12 diff --git a/Sources/SettingsFeature/Views/AdvancedView.swift b/Sources/SettingsFeature/Views/AdvancedView.swift deleted file mode 100644 index 5baaac01cd6e25a961c3676361534b432505f301..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/Views/AdvancedView.swift +++ /dev/null @@ -1,48 +0,0 @@ -import UIKit -import Shared - -final class AdvancedView: UIView { - let stack = UIStackView() - let downloadLogs = UIButton() - let logs = SettingsSwitcher() - let crashes = SettingsSwitcher() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - backgroundColor = Asset.neutralWhite.color - downloadLogs.setImage(Asset.settingsDownload.image, for: .normal) - - logs.set( - title: Localized.Settings.Advanced.Logs.title, - text: Localized.Settings.Advanced.Logs.description, - icon: Asset.settingsLogs.image, - extraAction: downloadLogs - ) - - crashes.set( - title: Localized.Settings.Advanced.Crashes.title, - text: Localized.Settings.Advanced.Crashes.description, - icon: Asset.settingsCrash.image, - separator: false - ) - - stack.spacing = 20 - stack.axis = .vertical - stack.addArrangedSubview(logs) - stack.addArrangedSubview(crashes) - - addSubview(stack) - - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - } -} diff --git a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e7f96ee81c740660545ef29d402c419046a8fbb6 --- /dev/null +++ b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift @@ -0,0 +1,46 @@ +import UIKit +import Shared + +final class SettingsAdvancedView: UIView { + let stackView = UIStackView() + let downloadLogsButton = UIButton() + let logRecordingSwitcher = SettingsSwitcher() + let crashReportingSwitcher = SettingsSwitcher() + + init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralWhite.color + downloadLogsButton.setImage(Asset.settingsDownload.image, for: .normal) + + logRecordingSwitcher.set( + title: Localized.Settings.Advanced.Logs.title, + text: Localized.Settings.Advanced.Logs.description, + icon: Asset.settingsLogs.image, + extraAction: downloadLogsButton + ) + + crashReportingSwitcher.set( + title: Localized.Settings.Advanced.Crashes.title, + text: Localized.Settings.Advanced.Crashes.description, + icon: Asset.settingsCrash.image + ) + + stackView.axis = .vertical + stackView.addArrangedSubview(logRecordingSwitcher) + stackView.addArrangedSubview(crashReportingSwitcher) + + stackView.setCustomSpacing(20, after: logRecordingSwitcher) + stackView.setCustomSpacing(10, after: crashReportingSwitcher) + + addSubview(stackView) + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(24) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SettingsFeature/Views/SettingsView.swift b/Sources/SettingsFeature/Views/SettingsView.swift index 646dfbc565cf215f7f010f568ad5c9732d40f0de..8f17926c9bc885d82d5625d83eb7cfdf1eae6807 100644 --- a/Sources/SettingsFeature/Views/SettingsView.swift +++ b/Sources/SettingsFeature/Views/SettingsView.swift @@ -21,11 +21,12 @@ final class SettingsView: UIView { let hideActiveApp = SettingsSwitcher() let icognitoKeyboard = SettingsInfoSwitcher() - let otherStack = UIStackView() - let privacyPolicy = RowButton() - let disclosures = RowButton() - let advanced = RowButton() - let delete = RowButton() + let otherStackView = UIStackView() + let privacyPolicyButton = RowButton() + let disclosuresButton = RowButton() + let advancedButton = RowButton() + let accountBackupButton = RowButton() + let deleteButton = RowButton() let didTap: (InfoTapped) -> Void @@ -132,38 +133,46 @@ final class SettingsView: UIView { } private func setupOtherStack() { - privacyPolicy.set( + privacyPolicyButton.setup( title: Localized.Settings.privacyPolicy, icon: Asset.settingsPrivacy.image, separator: false ) - disclosures.set( + disclosuresButton.setup( title: Localized.Settings.disclosures, icon: Asset.settingsFolder.image ) - advanced.set( + advancedButton.setup( title: Localized.Settings.advanced, icon: Asset.settingsAdvanced.image ) - delete.set( + accountBackupButton.setup( + title: Localized.Settings.Advanced.AccountBackup.title, + icon: Asset.settingsAdvanced.image, + style: .clean, + separator: false + ) + + deleteButton.setup( title: Localized.Settings.delete, icon: Asset.settingsDelete.image, style: .delete, separator: false ) - otherStack.axis = .vertical - otherStack.addArrangedSubview(privacyPolicy) - otherStack.addArrangedSubview(disclosures) - otherStack.addArrangedSubview(advanced) - otherStack.addArrangedSubview(delete) + otherStackView.axis = .vertical + otherStackView.addArrangedSubview(privacyPolicyButton) + otherStackView.addArrangedSubview(disclosuresButton) + otherStackView.addArrangedSubview(accountBackupButton) + otherStackView.addArrangedSubview(advancedButton) + otherStackView.addArrangedSubview(deleteButton) - addSubview(otherStack) + addSubview(otherStackView) - otherStack.snp.makeConstraints { make in + otherStackView.snp.makeConstraints { make in make.top.equalTo(chatStack.snp.bottom).offset(15) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) diff --git a/Sources/Shared/AutoGenerated/Assets.swift b/Sources/Shared/AutoGenerated/Assets.swift index fd77d73250c3199e63ff4550827a05d278f01ef5..12a0ace416e24834f4c2089132bc2b773b74f75c 100644 --- a/Sources/Shared/AutoGenerated/Assets.swift +++ b/Sources/Shared/AutoGenerated/Assets.swift @@ -21,6 +21,7 @@ public typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name public enum Asset { + public static let backupSuccess = ImageAsset(name: "backup_success") public static let chatAudioCloseSpeaker = ImageAsset(name: "chat_audio_close_speaker") public static let chatAudioOpenSpeaker = ImageAsset(name: "chat_audio_open_speaker") public static let chatAudioPause = ImageAsset(name: "chat_audio_pause") @@ -87,6 +88,10 @@ public enum Asset { public static let requestsAccept = ImageAsset(name: "requests_accept") public static let requestsReceivedPlaceholder = ImageAsset(name: "requests_received_placeholder") public static let requestsReject = ImageAsset(name: "requests_reject") + public static let restoreDrive = ImageAsset(name: "restore_drive") + public static let restoreDropbox = ImageAsset(name: "restore_dropbox") + public static let restoreIcloud = ImageAsset(name: "restore_icloud") + public static let restoreSuccess = ImageAsset(name: "restore_success") public static let scanEmail = ImageAsset(name: "scan_email") public static let scanError = ImageAsset(name: "scan_error") public static let scanPhone = ImageAsset(name: "scan_phone") diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index 084fba8be2bca104ef28af65c1917237172eb1cf..4acbf48e1c5824279a950c20dde174079ce18550 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -172,6 +172,41 @@ public enum Localized { } } + public enum Backup { + /// Dropbox + public static let dropbox = Localized.tr("Localizable", "backup.dropbox") + /// Google Drive + public static let googleDrive = Localized.tr("Localizable", "backup.googleDrive") + /// Account Backup + public static let header = Localized.tr("Localizable", "backup.header") + /// iCloud + public static let iCloud = Localized.tr("Localizable", "backup.iCloud") + /// Back up your account to a cloud storage service, you can restore it along with your contacts when you reinstall xx messenger on another device. + public static let subtitle = Localized.tr("Localizable", "backup.subtitle") + public enum Config { + /// Backup now + public static let backupNow = Localized.tr("Localizable", "backup.config.backupNow") + /// Content backed up in %@ is not protected by xx network end-to-end encryption. + public static func disclaimer(_ p1: Any) -> String { + return Localized.tr("Localizable", "backup.config.disclaimer", String(describing: p1)) + } + /// Backup to %@ + public static func frequency(_ p1: Any) -> String { + return Localized.tr("Localizable", "backup.config.frequency", String(describing: p1)) + } + /// Backup over + public static let infrastructure = Localized.tr("Localizable", "backup.config.infrastructure") + /// LATEST BACKUP + public static let latestBackup = Localized.tr("Localizable", "backup.config.latestBackup") + /// Backup settings + public static let title = Localized.tr("Localizable", "backup.config.title") + } + public enum Setup { + /// Setup your #backup service#. + public static let title = Localized.tr("Localizable", "backup.setup.title") + } + } + public enum Chat { /// Cancel public static let cancel = Localized.tr("Localizable", "chat.cancel") @@ -619,9 +654,13 @@ public enum Localized { public enum Success { /// Next public static let action = Localized.tr("Localizable", "onboarding.success.action") - /// Your #%@# has been successfully #added#. - public static func title(_ p1: Any) -> String { - return Localized.tr("Localizable", "onboarding.success.title", String(describing: p1)) + public enum Email { + /// Your #email# has been successfully #added#. + public static let title = Localized.tr("Localizable", "onboarding.success.email.title") + } + public enum Phone { + /// Your #phone# has been successfully #added#. + public static let title = Localized.tr("Localizable", "onboarding.success.phone.title") } } public enum Username { @@ -639,6 +678,12 @@ public enum Localized { /// Your Username public static let title = Localized.tr("Localizable", "onboarding.username.info.title") } + public enum Restore { + /// Restore From Backup + public static let action = Localized.tr("Localizable", "onboarding.username.restore.action") + /// Already have an account? + public static let title = Localized.tr("Localizable", "onboarding.username.restore.title") + } } public enum Welcome { /// Yes, continue @@ -750,6 +795,61 @@ public enum Localized { } } + public enum Restore { + /// Account restore + public static let header = Localized.tr("Localizable", "restore.header") + public enum Found { + /// Cancel + public static let cancel = Localized.tr("Localizable", "restore.found.cancel") + /// BACKUP DATE + public static let date = Localized.tr("Localizable", "restore.found.date") + /// Next + public static let next = Localized.tr("Localizable", "restore.found.next") + /// Restore account + public static let restore = Localized.tr("Localizable", "restore.found.restore") + /// FILE SIZE + public static let size = Localized.tr("Localizable", "restore.found.size") + /// Restore your contacts from the following backup. + public static let subtitle = Localized.tr("Localizable", "restore.found.subtitle") + /// Backup found + public static let title = Localized.tr("Localizable", "restore.found.title") + } + public enum List { + /// Cancel + public static let cancel = Localized.tr("Localizable", "restore.list.cancel") + /// Restore your account from a previous backup. You’ll be able to have access to all your contacts. + public static let firstSubtitle = Localized.tr("Localizable", "restore.list.firstSubtitle") + /// Select the cloud storage service you previously used to create a backup. + public static let secondSubtitle = Localized.tr("Localizable", "restore.list.secondSubtitle") + /// Restore your #account#. + public static let title = Localized.tr("Localizable", "restore.list.title") + } + public enum NotFound { + /// Go back + public static let back = Localized.tr("Localizable", "restore.notFound.back") + /// No account backup was found in %@ + public static func subtitle(_ p1: Any) -> String { + return Localized.tr("Localizable", "restore.notFound.subtitle", String(describing: p1)) + } + /// Backup not found + public static let title = Localized.tr("Localizable", "restore.notFound.title") + } + public enum Success { + /// You now have access to all your contacts. + public static let subtitle = Localized.tr("Localizable", "restore.success.subtitle") + /// Your #account# has been successfully #restored#. + public static let title = Localized.tr("Localizable", "restore.success.title") + } + public enum Warning { + /// I understand + public static let action = Localized.tr("Localizable", "restore.warning.action") + /// xx messenger account can only run on a single device at a time. Using the same account on multiple devices may permanently damage your account and make it impossible to converse with your contacts + public static let subtitle = Localized.tr("Localizable", "restore.warning.subtitle") + /// Warning + public static let title = Localized.tr("Localizable", "restore.warning.title") + } + } + public enum Scan { /// Go to contact public static let contact = Localized.tr("Localizable", "scan.contact") @@ -825,6 +925,10 @@ public enum Localized { public enum Advanced { /// Advanced Settings public static let title = Localized.tr("Localizable", "settings.advanced.title") + public enum AccountBackup { + /// Account Backup + public static let title = Localized.tr("Localizable", "settings.advanced.accountBackup.title") + } public enum Crashes { /// Automatically sends anonymous reports containing crash data public static let description = Localized.tr("Localizable", "settings.advanced.crashes.description") diff --git a/Sources/Shared/Extensions/Date.swift b/Sources/Shared/Extensions/Date.swift index 5ffdaacf22dcc4b98246da2b1e243d8cf29bfa49..dd0fc91aaa80cfb18e028721d1ae617fd311354b 100644 --- a/Sources/Shared/Extensions/Date.swift +++ b/Sources/Shared/Extensions/Date.swift @@ -25,6 +25,17 @@ public extension Date { return formatter.string(for: self) ?? "" } + func backupStyle() -> String { + let formatter = DateFormatter() + formatter.dateFormat = DateFormatter.dateFormat( + fromTemplate: "MMM d, YYYY - h:mm", + options: 0, + locale: Locale(identifier: "en_US") + ) + + return formatter.string(from: self) + } + static var asTimestamp: Int { Int(Date().timeIntervalSince1970).toNano() } diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..73c00596a7fca3f3d4bdd64053b69d86745f9e10 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..21dbcf85660641cd948448a96694c50b6f35b39c --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Group 512227.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2cd77a744520d2830992a96604be01dfb3e925d4 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf @@ -0,0 +1,113 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +32.000000 16.000000 m +32.000000 7.163445 24.836555 0.000000 16.000000 0.000000 c +7.163444 0.000000 0.000000 7.163445 0.000000 16.000000 c +0.000000 24.836555 7.163444 32.000000 16.000000 32.000000 c +24.836555 32.000000 32.000000 24.836555 32.000000 16.000000 c +h +W* +n +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.929167 0.929167 0.929167 scn +31.000000 16.000000 m +31.000000 7.715729 24.284271 1.000000 16.000000 1.000000 c +16.000000 -1.000000 l +25.388842 -1.000000 33.000000 6.611158 33.000000 16.000000 c +31.000000 16.000000 l +h +16.000000 1.000000 m +7.715729 1.000000 1.000000 7.715729 1.000000 16.000000 c +-1.000000 16.000000 l +-1.000000 6.611158 6.611159 -1.000000 16.000000 -1.000000 c +16.000000 1.000000 l +h +1.000000 16.000000 m +1.000000 24.284271 7.715729 31.000000 16.000000 31.000000 c +16.000000 33.000000 l +6.611159 33.000000 -1.000000 25.388840 -1.000000 16.000000 c +1.000000 16.000000 l +h +16.000000 31.000000 m +24.284271 31.000000 31.000000 24.284271 31.000000 16.000000 c +33.000000 16.000000 l +33.000000 25.388840 25.388842 33.000000 16.000000 33.000000 c +16.000000 31.000000 l +h +f +n +Q +Q +q +1.000000 0.000000 -0.000000 1.000000 8.634277 10.403870 cm +0.172549 0.752941 0.411765 scn +14.731392 10.017323 m +4.714046 -0.000023 l +0.000000 4.714022 l +1.175000 5.889021 l +4.714046 2.358311 l +13.556392 11.192322 l +14.731392 10.017323 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1393 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 32.000000 32.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001483 00000 n +0000001506 00000 n +0000001679 00000 n +0000001753 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1812 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..73c00596a7fca3f3d4bdd64053b69d86745f9e10 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a46bd03786ee3e3e3c937ae63fa938e5fb56e780 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf @@ -0,0 +1,119 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 19.556641 15.999268 cm +0.678431 0.709804 0.741176 scn +18.188225 0.000000 m +18.264029 0.132510 l +22.032003 6.602953 l +22.260807 6.993652 l +21.801113 6.993652 l +4.226277 6.993652 l +4.071191 6.993652 l +3.995387 6.861826 l +0.227413 0.390699 l +0.000000 0.000000 l +0.458304 0.000000 l +18.033140 0.000000 l +18.188225 0.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 24.521484 24.497009 cm +0.678431 0.709804 0.741176 scn +8.203583 15.502991 m +8.048495 15.502308 l +0.458305 15.472254 l +0.000000 15.470204 l +0.230196 15.080189 l +9.017959 0.130486 l +9.094460 0.000025 l +9.249547 0.000025 l +16.837650 0.032127 l +17.296650 0.034177 l +17.067152 0.422827 l +8.279387 15.371847 l +8.203583 15.502991 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 16.758179 cm +0.678431 0.709804 0.741176 scn +0.077891 6.829033 m +3.900804 0.388641 l +4.131000 -0.000008 l +4.359804 0.390007 l +13.147567 15.339723 l +13.225458 15.469501 l +13.147567 15.602011 l +9.325349 22.041037 l +9.094459 22.429688 l +8.864959 22.039671 l +0.077195 7.090637 l +0.000000 6.960177 l +0.077891 6.829033 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1138 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 56.000000 56.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001228 00000 n +0000001251 00000 n +0000001424 00000 n +0000001498 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1557 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..deb5a44b9e70a37a4dd49b15a309788bf25d4075 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Icon-3.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ad3060905eb2a0aba3f6190ff57813c85bb580df --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf @@ -0,0 +1,95 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +-1.000000 -0.000000 -0.000000 1.000000 43.000000 15.000000 cm +0.678431 0.709804 0.741176 scn +7.336956 25.000000 m +0.000000 20.290787 l +7.336956 15.651003 l +14.674046 20.290787 l +7.336956 25.000000 l +h +22.010607 25.000000 m +14.674046 20.290787 l +22.010870 15.651003 l +29.347828 20.290787 l +22.010607 25.000000 l +h +0.000000 10.941789 m +7.336956 6.232576 l +14.674046 10.941789 l +7.336956 15.651003 l +0.000000 10.941789 l +h +22.010607 15.651003 m +14.674046 10.941789 l +22.010870 6.232576 l +29.347828 10.941789 l +22.010607 15.651003 l +h +7.336956 4.708551 m +14.674046 0.000000 l +22.010870 4.709213 l +14.674046 9.348997 l +7.336956 4.708551 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 672 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 56.000000 56.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000762 00000 n +0000000784 00000 n +0000000957 00000 n +0000001031 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1090 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..902114534e813c60b003270ace57ee2b9b436fc4 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Icon-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ccb74736c630534a457087fc953d992bf86d3f7c --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf @@ -0,0 +1,77 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 12.000000 18.000000 cm +0.678431 0.709804 0.741176 scn +25.522648 0.000000 m +6.264725 0.000000 l +2.784380 0.000000 0.000000 2.758663 0.000000 6.206862 c +0.000000 8.850319 1.624321 11.034307 4.060546 11.953918 c +4.176483 14.252605 6.032679 16.091656 8.352796 16.091656 c +9.177963 16.093571 9.985262 15.853600 10.673082 15.401948 c +12.181294 18.275476 15.081611 20.000000 18.330082 20.000000 c +22.970999 20.000000 26.798986 16.321558 26.914923 11.838881 c +29.583193 11.149343 31.555841 8.735620 31.555841 5.976959 c +31.555841 2.758831 28.887398 0.000170 25.522989 0.000170 c +25.522648 0.000000 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 667 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 56.000000 56.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000757 00000 n +0000000779 00000 n +0000000952 00000 n +0000001026 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1085 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..21dbcf85660641cd948448a96694c50b6f35b39c --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Group 512227.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2cd77a744520d2830992a96604be01dfb3e925d4 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf @@ -0,0 +1,113 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +32.000000 16.000000 m +32.000000 7.163445 24.836555 0.000000 16.000000 0.000000 c +7.163444 0.000000 0.000000 7.163445 0.000000 16.000000 c +0.000000 24.836555 7.163444 32.000000 16.000000 32.000000 c +24.836555 32.000000 32.000000 24.836555 32.000000 16.000000 c +h +W* +n +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.929167 0.929167 0.929167 scn +31.000000 16.000000 m +31.000000 7.715729 24.284271 1.000000 16.000000 1.000000 c +16.000000 -1.000000 l +25.388842 -1.000000 33.000000 6.611158 33.000000 16.000000 c +31.000000 16.000000 l +h +16.000000 1.000000 m +7.715729 1.000000 1.000000 7.715729 1.000000 16.000000 c +-1.000000 16.000000 l +-1.000000 6.611158 6.611159 -1.000000 16.000000 -1.000000 c +16.000000 1.000000 l +h +1.000000 16.000000 m +1.000000 24.284271 7.715729 31.000000 16.000000 31.000000 c +16.000000 33.000000 l +6.611159 33.000000 -1.000000 25.388840 -1.000000 16.000000 c +1.000000 16.000000 l +h +16.000000 31.000000 m +24.284271 31.000000 31.000000 24.284271 31.000000 16.000000 c +33.000000 16.000000 l +33.000000 25.388840 25.388842 33.000000 16.000000 33.000000 c +16.000000 31.000000 l +h +f +n +Q +Q +q +1.000000 0.000000 -0.000000 1.000000 8.634277 10.403870 cm +0.172549 0.752941 0.411765 scn +14.731392 10.017323 m +4.714046 -0.000023 l +0.000000 4.714022 l +1.175000 5.889021 l +4.714046 2.358311 l +13.556392 11.192322 l +14.731392 10.017323 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1393 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 32.000000 32.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001483 00000 n +0000001506 00000 n +0000001679 00000 n +0000001753 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1812 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json index 558dd2cf607944359cad5cf13c3e04cde1feb188..300616c824959c152991ea6b10c670a5cbe0ee0f 100644 --- a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "Icon-32.pdf", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 1f51e0785c1c916600938ecf7d7ce85423892a67..152ed8db26af658560e757a240688efbd9d8cd4f 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -428,6 +428,34 @@ = "Enable crash reporting"; "settings.advanced.crashes.description" = "Automatically sends anonymous reports containing crash data"; +"settings.advanced.accountBackup.title" += "Account Backup"; + +"backup.header" += "Account Backup"; +"backup.setup.title" += "Setup your #backup service#."; +"backup.config.title" += "Backup settings"; +"backup.subtitle" += "Back up your account to a cloud storage service, you can restore it along with only your contacts when you reinstall xx Messenger on another device."; +"backup.config.backupNow" += "Backup now"; +"backup.config.disclaimer" += "Content backed up in %@ is not protected by xx network end-to-end encryption."; +"backup.config.latestBackup" += "LATEST BACKUP"; +"backup.config.frequency" += "Backup to %@"; +"backup.config.infrastructure" += "Backup over"; + +"backup.iCloud" += "iCloud"; +"backup.dropbox" += "Dropbox"; +"backup.googleDrive" += "Google Drive"; // Settings - Delete Account @@ -517,6 +545,10 @@ = "Your Username"; "onboarding.username.info.subtitle" = "Your chosen username will be registered with the #User Discovery Service# allowing your public keys to be accessible to anyone who knows your username. They will then be able to send a request to create an authenticated channel with you. You will then be able to reject unwanted requests."; +"onboarding.username.restore.title" += "Already have an account?"; +"onboarding.username.restore.action" += "Restore From Backup"; // OnboardingFeature - Email @@ -588,8 +620,10 @@ // OnboardingFeature - Success -"onboarding.success.title" -= "Your #%@# has been successfully #added#."; +"onboarding.success.email.title" += "Your #email# has been successfully #added#."; +"onboarding.success.phone.title" += "Your #phone# has been successfully #added#."; "onboarding.success.action" = "Next"; @@ -615,6 +649,49 @@ "onboarding.welcome.info.subtitle" = "Registration is completely optional. When registering an email or phone number, they will be evaluated by twilio, a 3rd party partner. Afterwards, salted hashes will be registered in #User Discovery# to allow other uses to search for you using the registered data completely privately."; +"restore.warning.title" += "Warning"; +"restore.warning.subtitle" += "xx messenger account can only run on a single device at a time. Using the same account on multiple devices may permanently damage your account and make it impossible to converse with your contacts"; +"restore.warning.action" += "I understand"; + +"restore.list.title" += "Restore your #account#."; +"restore.list.firstSubtitle" += "Restore your account from a previous backup. You’ll be able to have access to all your contacts."; +"restore.list.secondSubtitle" += "Select the cloud storage service you previously used to create a backup."; +"restore.list.cancel" += "Cancel"; + +"restore.header" += "Account restore"; +"restore.found.title" += "Backup found"; +"restore.found.subtitle" += "Restore your contacts from the following backup."; +"restore.found.date" += "BACKUP DATE"; +"restore.found.size" += "FILE SIZE"; +"restore.found.restore" += "Restore account"; +"restore.found.cancel" += "Cancel"; +"restore.found.next" += "Next"; +"restore.notFound.title" += "Backup not found"; +"restore.notFound.subtitle" += "No account backup was found in %@"; +"restore.notFound.back" += "Go back"; +"restore.success.title" += "Your #account# has been successfully #restored#."; +"restore.success.subtitle" += "You now have access to all your contacts."; + // Shared "shared.search.placeholder" diff --git a/Sources/Shared/Views/CapsuleButton.swift b/Sources/Shared/Views/CapsuleButton.swift index 492bea9a6d424200c68c1514eccebb3cb23f6dff..96ef2e0963ec5ee20cc2b470f9a58c194847f936 100644 --- a/Sources/Shared/Views/CapsuleButton.swift +++ b/Sources/Shared/Views/CapsuleButton.swift @@ -65,13 +65,21 @@ public extension CapsuleButtonStyle { disabledTitleColor: Asset.brandPrimary.color.withAlphaComponent(0.5) ) - static let simplestColored = CapsuleButtonStyle( + static let simplestColoredRed = CapsuleButtonStyle( fill: .color(UIColor.clear), borderWidth: 0, borderColor: nil, titleColor: Asset.accentDanger.color, disabledTitleColor: Asset.brandDefault.color.withAlphaComponent(0.5) ) + + static let simplestColoredBrand = CapsuleButtonStyle( + fill: .color(UIColor.clear), + borderWidth: 0, + borderColor: nil, + titleColor: Asset.brandPrimary.color, + disabledTitleColor: Asset.brandDefault.color.withAlphaComponent(0.5) + ) } public final class CapsuleButton: UIButton { diff --git a/Sources/Shared/Views/DetailRowButton.swift b/Sources/Shared/Views/DetailRowButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..132d499ac0e0223be1f9333cbd6e1643cfff7157 --- /dev/null +++ b/Sources/Shared/Views/DetailRowButton.swift @@ -0,0 +1,48 @@ +import UIKit + +public final class DetailRowButton: UIControl { + let titleLabel = UILabel() + let valueLabel = UILabel() + let rowIndicator = UIImageView() + + public init() { + super.init(frame: .zero) + + rowIndicator.contentMode = .center + rowIndicator.image = Asset.settingsDisclosure.image + + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + valueLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + titleLabel.textColor = Asset.neutralWeak.color + valueLabel.textColor = Asset.neutralActive.color + + addSubview(titleLabel) + addSubview(valueLabel) + addSubview(rowIndicator) + + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + } + + valueLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(4) + make.left.equalToSuperview() + make.bottom.equalToSuperview() + } + + rowIndicator.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + public func setup(title: String, value: String, hasArrow: Bool = true) { + titleLabel.text = title + valueLabel.text = value + rowIndicator.isHidden = !hasArrow + } +} diff --git a/Sources/Shared/Views/RowButton.swift b/Sources/Shared/Views/RowButton.swift index 0f6ab3887d1d240ba7a74ef2f81c437454c8a68a..689a705af98ce2364251f7d1d654de6eea3bd799 100644 --- a/Sources/Shared/Views/RowButton.swift +++ b/Sources/Shared/Views/RowButton.swift @@ -35,10 +35,7 @@ public final class RowButton: UIControl { stack.spacing = 10 stack.addArrangedSubview(icon) stack.addArrangedSubview(title.pinning(at: .left(0))) - stack.addArrangedSubview( - accessory - .pinning(at: .top(10)) - ) + stack.addArrangedSubview(accessory.pinning(at: .top(10))) addSubview(stack) addSubview(separator) @@ -62,7 +59,7 @@ public final class RowButton: UIControl { required init?(coder: NSCoder) { nil } - public func set( + public func setup( title: String, icon: UIImage, style: RowButtonStyle = .clean, diff --git a/Sources/Shared/Views/RowSwitchableButton.swift b/Sources/Shared/Views/RowSwitchableButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..38136c3ef7f656e8df8907ef6b5a23d461b1adee --- /dev/null +++ b/Sources/Shared/Views/RowSwitchableButton.swift @@ -0,0 +1,88 @@ +import UIKit + +public enum RowSwitchableButtonState { + case disclosure + case switcher(Bool) +} + +public final class RowSwitchableButton: UIControl { + public let title = UILabel() + public let icon = UIImageView() + public let separator = UIView() + + public let switcher = UISwitch() + public let disclosureIcon = UIImageView() + + public init() { + super.init(frame: .zero) + + icon.contentMode = .center + title.font = Fonts.Mulish.semiBold.font(size: 14.0) + separator.backgroundColor = Asset.neutralLine.color + title.textColor = Asset.neutralActive.color + disclosureIcon.image = Asset.settingsDisclosure.image + switcher.onTintColor = Asset.brandLight.color + + addSubview(icon) + addSubview(title) + addSubview(disclosureIcon) + addSubview(switcher) + addSubview(separator) + + icon.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.left.equalToSuperview().offset(36) + make.bottom.equalToSuperview().offset(-20) + } + + title.snp.makeConstraints { make in + make.left.equalTo(icon.snp.right).offset(25) + make.centerY.equalTo(icon) + } + + disclosureIcon.snp.makeConstraints { make in + make.centerY.equalTo(icon) + make.right.equalToSuperview().offset(-48) + } + + switcher.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-25) + make.centerY.equalTo(icon) + } + + separator.snp.makeConstraints { make in + make.height.equalTo(1) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalToSuperview() + } + } + + public required init?(coder: NSCoder) { nil } + + public func setup( + title: String, + icon: UIImage, + state: RowSwitchableButtonState = .disclosure, + separator: Bool = true + ) { + self.icon.image = icon + self.title.text = title + + switch state { + case .disclosure: + switcher.isHidden = true + disclosureIcon.isHidden = false + + case .switcher(let bool): + switcher.isOn = bool + switcher.isHidden = false + disclosureIcon.isHidden = true + } + + guard separator == true else { + self.separator.removeFromSuperview() + return + } + } +} diff --git a/Sources/iCloudFeature/iCloudInterface.swift b/Sources/iCloudFeature/iCloudInterface.swift new file mode 100644 index 0000000000000000000000000000000000000000..41b3153667013967f53fab9eb01d95a99293297c --- /dev/null +++ b/Sources/iCloudFeature/iCloudInterface.swift @@ -0,0 +1,13 @@ +import Foundation + +public protocol iCloudInterface { + func openSettings() + + func isAuthorized() -> Bool + + func downloadMetadata(_: @escaping (Result<iCloudMetadata?, Error>) -> Void) + + func uploadBackup(_: URL, _: @escaping (Result<iCloudMetadata, Error>) -> Void) + + func downloadBackup(_: String, _: @escaping (Result<Data, Error>) -> Void) +} diff --git a/Sources/iCloudFeature/iCloudMetadata.swift b/Sources/iCloudFeature/iCloudMetadata.swift new file mode 100644 index 0000000000000000000000000000000000000000..9aa70514badbef94033ec24bc965f49b23567e21 --- /dev/null +++ b/Sources/iCloudFeature/iCloudMetadata.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct iCloudMetadata: Equatable { + public var size: Float + public var path: String + public var modifiedDate: Date + + public init( + path: String, + size: Float, + modifiedDate: Date + ) { + self.path = path + self.size = size + self.modifiedDate = modifiedDate + } +} diff --git a/Sources/iCloudFeature/iCloudService.swift b/Sources/iCloudFeature/iCloudService.swift new file mode 100644 index 0000000000000000000000000000000000000000..2ccf20ba26357c9e3966b88fe17f9941fd7525d4 --- /dev/null +++ b/Sources/iCloudFeature/iCloudService.swift @@ -0,0 +1,87 @@ +import UIKit +import FilesProvider + +public struct iCloudService: iCloudInterface { + private let documentsProvider = CloudFileProvider(containerId: "iCloud.xxm-cloud", scope: .data) + + public init() {} + + public func isAuthorized() -> Bool { + FileManager.default.ubiquityIdentityToken != nil + } + + public func openSettings() { + if let url = URL(string: "App-Prefs:root=CASTLE"), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + public func downloadMetadata(_ completion: @escaping (Result<iCloudMetadata?, Error>) -> Void) { + guard let documentsProvider = documentsProvider else { fatalError() } + + documentsProvider.contentsOfDirectory(path: "/", completionHandler: { contents, error in + guard error == nil else { + print(">>> [iCloud] downloadMetadata got error: \(error!.localizedDescription)") + completion(.failure(error!)) + return + } + + print(contents) + + if let file = contents.first(where: { $0.name == "backup.xxm" }) { + completion(.success(.init( + path: file.path, + size: Float(file.size), + modifiedDate: file.modifiedDate! + ))) + } else { + completion(.success(nil)) + } + }) + } + + public func uploadBackup(_ url: URL, _ completion: @escaping (Result<iCloudMetadata, Error>) -> Void) { + guard let documentsProvider = documentsProvider else { fatalError() } + + do { + let data = try Data(contentsOf: url) + + documentsProvider.writeContents(path: "backup.xxm", contents: data, overwrite: true) { error in + guard error == nil else { + print(">>> [iCloud] uploadBackup got error: \(error!.localizedDescription)") + completion(.failure(error!)) + return + } + + completion(.success(.init( + path: "backup.xxm", + size: Float(data.count), + modifiedDate: Date() + ))) + } + } catch { + completion(.failure(error)) + } + } + + public func downloadBackup( + _ path: String, + _ completion: @escaping (Result<Data, Error>) -> Void + ) { + guard let documentsProvider = documentsProvider else { fatalError() } + + documentsProvider.contents(path: path, completionHandler: { contents, error in + guard error == nil else { + print(">>> [iCloud] downloadBackup got error: \(error!.localizedDescription)") + completion(.failure(error!)) + return + } + + if let contents = contents { + completion(.success(contents)) + } else { + completion(.failure(NSError(domain: "Backup file is invalid", code: 0))) + } + }) + } +} diff --git a/Sources/iCloudFeature/iCloudServiceMock.swift b/Sources/iCloudFeature/iCloudServiceMock.swift new file mode 100644 index 0000000000000000000000000000000000000000..f0bcfca66108e297f5d04de4fb3e5dc0705ae097 --- /dev/null +++ b/Sources/iCloudFeature/iCloudServiceMock.swift @@ -0,0 +1,39 @@ +import Foundation + +public struct iCloudServiceMock: iCloudInterface { + public init() { + // TODO + } + + public func openSettings() { + // TODO + } + + public func isAuthorized() -> Bool { + true + } + + public func downloadBackup( + _: String, + _: @escaping (Result<Data, Error>) -> Void + ) { + // TODO + } + + public func uploadBackup( + _: URL, + _: @escaping (Result<iCloudMetadata, Error>) -> Void + ) { + // TODO + } + + public func downloadMetadata( + _ completion: @escaping (Result<iCloudMetadata?, Error>) -> Void + ) { + completion(.success(.init( + path: "/", + size: 1230000000.0, + modifiedDate: Date() + ))) + } +} diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings index e9720c545e7eceba5d6946f435854e0019614dc2..132638347cca07890832d2d4cc9508a49422aa95 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h index 5a5d5963442bdd4d0b36d2f4540903d62aacde94..209b34eb81ea47e4d05a9163fa87b2151842d99e 100644 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h +++ b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h @@ -12,6 +12,7 @@ @class BindingsBackup; +@class BindingsBackupReport; @class BindingsClient; @class BindingsContact; @class BindingsContactList; @@ -45,8 +46,8 @@ @class BindingsAuthConfirmCallback; @protocol BindingsAuthRequestCallback; @class BindingsAuthRequestCallback; -@protocol BindingsAuthResetCallback; -@class BindingsAuthResetCallback; +@protocol BindingsAuthResetNotificationCallback; +@class BindingsAuthResetNotificationCallback; @protocol BindingsClientError; @class BindingsClientError; @protocol BindingsEventCallbackFunctionObject; @@ -98,7 +99,7 @@ - (void)callback:(BindingsContact* _Nullable)requestor; @end -@protocol BindingsAuthResetCallback <NSObject> +@protocol BindingsAuthResetNotificationCallback <NSObject> - (void)callback:(BindingsContact* _Nullable)requestor; @end @@ -193,6 +194,10 @@ - (nonnull instancetype)initWithRef:(_Nonnull id)ref; - (nonnull instancetype)init; +/** + * AddJson stores a passed in json string in the backup structure + */ +- (void)addJson:(NSString* _Nullable)json; /** * IsBackupRunning returns true if the backup has been initialized and is running. Returns false if it has been stopped. @@ -205,6 +210,17 @@ storage. To enable backups again, call InitializeBackup. - (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; @end +@interface BindingsBackupReport : NSObject <goSeqRefInterface> { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID + +@property (nonatomic) NSString* _Nonnull params; +@end + /** * BindingsClient wraps the api.Client, implementing additional functions to support the gomobile Client interface @@ -282,7 +298,7 @@ Running - 2000 Stopping - 3000 */ - (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetCallback> _Nullable)reset; +- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; /** * RegisterClientErrorCallback registers the callback to handle errors from the long running threads controlled by StartNetworkFollower and StopNetworkFollower @@ -675,11 +691,6 @@ the period. The period is specified in milliseconds. */ - (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Resend resends a file if sending fails. This function should only be called -if the interfaces.SentProgressCallback returns an error. - */ -- (BOOL)resend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; /** * Send sends a file to the recipient. The sender must have an E2E relationship with the recipient. @@ -1140,6 +1151,10 @@ for determining which IDs restored, which failed, and why. * GetFailedAt returns the failed ID at index */ - (NSData* _Nullable)getFailedAt:(long)index; +/** + * GetRestoreContactsError returns an error string. Empty if no error. + */ +- (NSString* _Nonnull)getRestoreContactsError; /** * GetRestoredAt returns the restored ID at index */ @@ -1259,6 +1274,28 @@ for the life of the program. This must be called while start network follower is running. */ - (nullable instancetype)init:(BindingsClient* _Nullable)client; +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; /** * AddFact adds a fact for the user to user discovery. Will only succeed if the user is already registered and the system does not have the fact currently @@ -1270,20 +1307,6 @@ associated with, a code will be sent. This confirmation ID needs to be called along with the code to finalize the fact. */ - (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * BackUpMissingFacts adds a registered fact to the Store object and saves -it to storage. It can take in both an email or a phone number, passed into -the function in that order. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. - */ -- (BOOL)backUpMissingFacts:(NSString* _Nullable)email phone:(NSString* _Nullable)phone error:(NSError* _Nullable* _Nullable)error; /** * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from AddFact while the code will come over the associated communications system @@ -1523,7 +1546,7 @@ FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* * NewClientFromBackup constructs a new Client from an encrypted backup. The backup is decrypted using the backupPassphrase. On success a successful client creation, the function will return a JSON encoded list of the E2E partners -contained in the backup. +contained in the backup and a json-encoded string of the parameters stored in the backup */ FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); @@ -1597,6 +1620,29 @@ This must be called while start network follower is running. */ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); + /** * NotificationsForMe Check if a notification received is for me It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, @@ -1604,6 +1650,7 @@ a Type, and a source. These are as follows: TYPE SOURCE DESCRIPTION "default" recipient user ID A message with no association "request" sender user ID A channel request has been received + "reset" sender user ID A channel reset has been received "confirm" sender user ID A channel request has been accepted "silent" sender user ID A message which should not be notified on "e2e" sender user ID reception of an E2E message @@ -1618,8 +1665,17 @@ FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotific */ FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); -// skipped function RestoreContactsFromBackup with unsupported parameter or return types - +/** + * RestoreContactsFromBackup takes as input the jason output of the +`NewClientFromBackup` function, unmarshals it into IDs, looks up +each ID in user discovery, and initiates a session reset request. +This function will not return until every id in the list has been sent a +request. It should be called again and again until it completes. +xxDK users should not use this function. This function is used by +the mobile phone apps and are not intended to be part of the xxDK. It +should be treated as internal functions specific to the phone apps. + */ +FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); /** * ResumeBackup starts the backup processes back up with a new callback after it @@ -1678,7 +1734,7 @@ FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, @class BindingsAuthRequestCallback; -@class BindingsAuthResetCallback; +@class BindingsAuthResetNotificationCallback; @class BindingsClientError; @@ -1750,7 +1806,7 @@ request * AuthRequestCallback notifies the register whenever they receive an auth request */ -@interface BindingsAuthResetCallback : NSObject <goSeqRefInterface, BindingsAuthResetCallback> { +@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { } @property(strong, readonly) _Nonnull id _ref; diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings index e9720c545e7eceba5d6946f435854e0019614dc2..132638347cca07890832d2d4cc9508a49422aa95 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h index 5a5d5963442bdd4d0b36d2f4540903d62aacde94..209b34eb81ea47e4d05a9163fa87b2151842d99e 100644 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h @@ -12,6 +12,7 @@ @class BindingsBackup; +@class BindingsBackupReport; @class BindingsClient; @class BindingsContact; @class BindingsContactList; @@ -45,8 +46,8 @@ @class BindingsAuthConfirmCallback; @protocol BindingsAuthRequestCallback; @class BindingsAuthRequestCallback; -@protocol BindingsAuthResetCallback; -@class BindingsAuthResetCallback; +@protocol BindingsAuthResetNotificationCallback; +@class BindingsAuthResetNotificationCallback; @protocol BindingsClientError; @class BindingsClientError; @protocol BindingsEventCallbackFunctionObject; @@ -98,7 +99,7 @@ - (void)callback:(BindingsContact* _Nullable)requestor; @end -@protocol BindingsAuthResetCallback <NSObject> +@protocol BindingsAuthResetNotificationCallback <NSObject> - (void)callback:(BindingsContact* _Nullable)requestor; @end @@ -193,6 +194,10 @@ - (nonnull instancetype)initWithRef:(_Nonnull id)ref; - (nonnull instancetype)init; +/** + * AddJson stores a passed in json string in the backup structure + */ +- (void)addJson:(NSString* _Nullable)json; /** * IsBackupRunning returns true if the backup has been initialized and is running. Returns false if it has been stopped. @@ -205,6 +210,17 @@ storage. To enable backups again, call InitializeBackup. - (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; @end +@interface BindingsBackupReport : NSObject <goSeqRefInterface> { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID + +@property (nonatomic) NSString* _Nonnull params; +@end + /** * BindingsClient wraps the api.Client, implementing additional functions to support the gomobile Client interface @@ -282,7 +298,7 @@ Running - 2000 Stopping - 3000 */ - (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetCallback> _Nullable)reset; +- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; /** * RegisterClientErrorCallback registers the callback to handle errors from the long running threads controlled by StartNetworkFollower and StopNetworkFollower @@ -675,11 +691,6 @@ the period. The period is specified in milliseconds. */ - (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Resend resends a file if sending fails. This function should only be called -if the interfaces.SentProgressCallback returns an error. - */ -- (BOOL)resend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; /** * Send sends a file to the recipient. The sender must have an E2E relationship with the recipient. @@ -1140,6 +1151,10 @@ for determining which IDs restored, which failed, and why. * GetFailedAt returns the failed ID at index */ - (NSData* _Nullable)getFailedAt:(long)index; +/** + * GetRestoreContactsError returns an error string. Empty if no error. + */ +- (NSString* _Nonnull)getRestoreContactsError; /** * GetRestoredAt returns the restored ID at index */ @@ -1259,6 +1274,28 @@ for the life of the program. This must be called while start network follower is running. */ - (nullable instancetype)init:(BindingsClient* _Nullable)client; +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; /** * AddFact adds a fact for the user to user discovery. Will only succeed if the user is already registered and the system does not have the fact currently @@ -1270,20 +1307,6 @@ associated with, a code will be sent. This confirmation ID needs to be called along with the code to finalize the fact. */ - (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * BackUpMissingFacts adds a registered fact to the Store object and saves -it to storage. It can take in both an email or a phone number, passed into -the function in that order. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. - */ -- (BOOL)backUpMissingFacts:(NSString* _Nullable)email phone:(NSString* _Nullable)phone error:(NSError* _Nullable* _Nullable)error; /** * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from AddFact while the code will come over the associated communications system @@ -1523,7 +1546,7 @@ FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* * NewClientFromBackup constructs a new Client from an encrypted backup. The backup is decrypted using the backupPassphrase. On success a successful client creation, the function will return a JSON encoded list of the E2E partners -contained in the backup. +contained in the backup and a json-encoded string of the parameters stored in the backup */ FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); @@ -1597,6 +1620,29 @@ This must be called while start network follower is running. */ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); + /** * NotificationsForMe Check if a notification received is for me It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, @@ -1604,6 +1650,7 @@ a Type, and a source. These are as follows: TYPE SOURCE DESCRIPTION "default" recipient user ID A message with no association "request" sender user ID A channel request has been received + "reset" sender user ID A channel reset has been received "confirm" sender user ID A channel request has been accepted "silent" sender user ID A message which should not be notified on "e2e" sender user ID reception of an E2E message @@ -1618,8 +1665,17 @@ FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotific */ FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); -// skipped function RestoreContactsFromBackup with unsupported parameter or return types - +/** + * RestoreContactsFromBackup takes as input the jason output of the +`NewClientFromBackup` function, unmarshals it into IDs, looks up +each ID in user discovery, and initiates a session reset request. +This function will not return until every id in the list has been sent a +request. It should be called again and again until it completes. +xxDK users should not use this function. This function is used by +the mobile phone apps and are not intended to be part of the xxDK. It +should be treated as internal functions specific to the phone apps. + */ +FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); /** * ResumeBackup starts the backup processes back up with a new callback after it @@ -1678,7 +1734,7 @@ FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, @class BindingsAuthRequestCallback; -@class BindingsAuthResetCallback; +@class BindingsAuthResetNotificationCallback; @class BindingsClientError; @@ -1750,7 +1806,7 @@ request * AuthRequestCallback notifies the register whenever they receive an auth request */ -@interface BindingsAuthResetCallback : NSObject <goSeqRefInterface, BindingsAuthResetCallback> { +@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { } @property(strong, readonly) _Nonnull id _ref; diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings index e9720c545e7eceba5d6946f435854e0019614dc2..132638347cca07890832d2d4cc9508a49422aa95 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h index 5a5d5963442bdd4d0b36d2f4540903d62aacde94..209b34eb81ea47e4d05a9163fa87b2151842d99e 100644 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h +++ b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h @@ -12,6 +12,7 @@ @class BindingsBackup; +@class BindingsBackupReport; @class BindingsClient; @class BindingsContact; @class BindingsContactList; @@ -45,8 +46,8 @@ @class BindingsAuthConfirmCallback; @protocol BindingsAuthRequestCallback; @class BindingsAuthRequestCallback; -@protocol BindingsAuthResetCallback; -@class BindingsAuthResetCallback; +@protocol BindingsAuthResetNotificationCallback; +@class BindingsAuthResetNotificationCallback; @protocol BindingsClientError; @class BindingsClientError; @protocol BindingsEventCallbackFunctionObject; @@ -98,7 +99,7 @@ - (void)callback:(BindingsContact* _Nullable)requestor; @end -@protocol BindingsAuthResetCallback <NSObject> +@protocol BindingsAuthResetNotificationCallback <NSObject> - (void)callback:(BindingsContact* _Nullable)requestor; @end @@ -193,6 +194,10 @@ - (nonnull instancetype)initWithRef:(_Nonnull id)ref; - (nonnull instancetype)init; +/** + * AddJson stores a passed in json string in the backup structure + */ +- (void)addJson:(NSString* _Nullable)json; /** * IsBackupRunning returns true if the backup has been initialized and is running. Returns false if it has been stopped. @@ -205,6 +210,17 @@ storage. To enable backups again, call InitializeBackup. - (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; @end +@interface BindingsBackupReport : NSObject <goSeqRefInterface> { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID + +@property (nonatomic) NSString* _Nonnull params; +@end + /** * BindingsClient wraps the api.Client, implementing additional functions to support the gomobile Client interface @@ -282,7 +298,7 @@ Running - 2000 Stopping - 3000 */ - (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetCallback> _Nullable)reset; +- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; /** * RegisterClientErrorCallback registers the callback to handle errors from the long running threads controlled by StartNetworkFollower and StopNetworkFollower @@ -675,11 +691,6 @@ the period. The period is specified in milliseconds. */ - (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Resend resends a file if sending fails. This function should only be called -if the interfaces.SentProgressCallback returns an error. - */ -- (BOOL)resend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; /** * Send sends a file to the recipient. The sender must have an E2E relationship with the recipient. @@ -1140,6 +1151,10 @@ for determining which IDs restored, which failed, and why. * GetFailedAt returns the failed ID at index */ - (NSData* _Nullable)getFailedAt:(long)index; +/** + * GetRestoreContactsError returns an error string. Empty if no error. + */ +- (NSString* _Nonnull)getRestoreContactsError; /** * GetRestoredAt returns the restored ID at index */ @@ -1259,6 +1274,28 @@ for the life of the program. This must be called while start network follower is running. */ - (nullable instancetype)init:(BindingsClient* _Nullable)client; +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; /** * AddFact adds a fact for the user to user discovery. Will only succeed if the user is already registered and the system does not have the fact currently @@ -1270,20 +1307,6 @@ associated with, a code will be sent. This confirmation ID needs to be called along with the code to finalize the fact. */ - (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * BackUpMissingFacts adds a registered fact to the Store object and saves -it to storage. It can take in both an email or a phone number, passed into -the function in that order. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. - */ -- (BOOL)backUpMissingFacts:(NSString* _Nullable)email phone:(NSString* _Nullable)phone error:(NSError* _Nullable* _Nullable)error; /** * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from AddFact while the code will come over the associated communications system @@ -1523,7 +1546,7 @@ FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* * NewClientFromBackup constructs a new Client from an encrypted backup. The backup is decrypted using the backupPassphrase. On success a successful client creation, the function will return a JSON encoded list of the E2E partners -contained in the backup. +contained in the backup and a json-encoded string of the parameters stored in the backup */ FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); @@ -1597,6 +1620,29 @@ This must be called while start network follower is running. */ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); + /** * NotificationsForMe Check if a notification received is for me It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, @@ -1604,6 +1650,7 @@ a Type, and a source. These are as follows: TYPE SOURCE DESCRIPTION "default" recipient user ID A message with no association "request" sender user ID A channel request has been received + "reset" sender user ID A channel reset has been received "confirm" sender user ID A channel request has been accepted "silent" sender user ID A message which should not be notified on "e2e" sender user ID reception of an E2E message @@ -1618,8 +1665,17 @@ FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotific */ FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); -// skipped function RestoreContactsFromBackup with unsupported parameter or return types - +/** + * RestoreContactsFromBackup takes as input the jason output of the +`NewClientFromBackup` function, unmarshals it into IDs, looks up +each ID in user discovery, and initiates a session reset request. +This function will not return until every id in the list has been sent a +request. It should be called again and again until it completes. +xxDK users should not use this function. This function is used by +the mobile phone apps and are not intended to be part of the xxDK. It +should be treated as internal functions specific to the phone apps. + */ +FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); /** * ResumeBackup starts the backup processes back up with a new callback after it @@ -1678,7 +1734,7 @@ FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, @class BindingsAuthRequestCallback; -@class BindingsAuthResetCallback; +@class BindingsAuthResetNotificationCallback; @class BindingsClientError; @@ -1750,7 +1806,7 @@ request * AuthRequestCallback notifies the register whenever they receive an auth request */ -@interface BindingsAuthResetCallback : NSObject <goSeqRefInterface, BindingsAuthResetCallback> { +@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { } @property(strong, readonly) _Nonnull id _ref; diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings index 62d3a3ad56dc1c079e800f5aa915479f08533760..70d29b99750c2894dd0cde34438a187dd60ea325 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h index 5a5d5963442bdd4d0b36d2f4540903d62aacde94..209b34eb81ea47e4d05a9163fa87b2151842d99e 100644 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h +++ b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h @@ -12,6 +12,7 @@ @class BindingsBackup; +@class BindingsBackupReport; @class BindingsClient; @class BindingsContact; @class BindingsContactList; @@ -45,8 +46,8 @@ @class BindingsAuthConfirmCallback; @protocol BindingsAuthRequestCallback; @class BindingsAuthRequestCallback; -@protocol BindingsAuthResetCallback; -@class BindingsAuthResetCallback; +@protocol BindingsAuthResetNotificationCallback; +@class BindingsAuthResetNotificationCallback; @protocol BindingsClientError; @class BindingsClientError; @protocol BindingsEventCallbackFunctionObject; @@ -98,7 +99,7 @@ - (void)callback:(BindingsContact* _Nullable)requestor; @end -@protocol BindingsAuthResetCallback <NSObject> +@protocol BindingsAuthResetNotificationCallback <NSObject> - (void)callback:(BindingsContact* _Nullable)requestor; @end @@ -193,6 +194,10 @@ - (nonnull instancetype)initWithRef:(_Nonnull id)ref; - (nonnull instancetype)init; +/** + * AddJson stores a passed in json string in the backup structure + */ +- (void)addJson:(NSString* _Nullable)json; /** * IsBackupRunning returns true if the backup has been initialized and is running. Returns false if it has been stopped. @@ -205,6 +210,17 @@ storage. To enable backups again, call InitializeBackup. - (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; @end +@interface BindingsBackupReport : NSObject <goSeqRefInterface> { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID + +@property (nonatomic) NSString* _Nonnull params; +@end + /** * BindingsClient wraps the api.Client, implementing additional functions to support the gomobile Client interface @@ -282,7 +298,7 @@ Running - 2000 Stopping - 3000 */ - (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetCallback> _Nullable)reset; +- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; /** * RegisterClientErrorCallback registers the callback to handle errors from the long running threads controlled by StartNetworkFollower and StopNetworkFollower @@ -675,11 +691,6 @@ the period. The period is specified in milliseconds. */ - (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Resend resends a file if sending fails. This function should only be called -if the interfaces.SentProgressCallback returns an error. - */ -- (BOOL)resend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; /** * Send sends a file to the recipient. The sender must have an E2E relationship with the recipient. @@ -1140,6 +1151,10 @@ for determining which IDs restored, which failed, and why. * GetFailedAt returns the failed ID at index */ - (NSData* _Nullable)getFailedAt:(long)index; +/** + * GetRestoreContactsError returns an error string. Empty if no error. + */ +- (NSString* _Nonnull)getRestoreContactsError; /** * GetRestoredAt returns the restored ID at index */ @@ -1259,6 +1274,28 @@ for the life of the program. This must be called while start network follower is running. */ - (nullable instancetype)init:(BindingsClient* _Nullable)client; +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; /** * AddFact adds a fact for the user to user discovery. Will only succeed if the user is already registered and the system does not have the fact currently @@ -1270,20 +1307,6 @@ associated with, a code will be sent. This confirmation ID needs to be called along with the code to finalize the fact. */ - (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * BackUpMissingFacts adds a registered fact to the Store object and saves -it to storage. It can take in both an email or a phone number, passed into -the function in that order. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. - */ -- (BOOL)backUpMissingFacts:(NSString* _Nullable)email phone:(NSString* _Nullable)phone error:(NSError* _Nullable* _Nullable)error; /** * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from AddFact while the code will come over the associated communications system @@ -1523,7 +1546,7 @@ FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* * NewClientFromBackup constructs a new Client from an encrypted backup. The backup is decrypted using the backupPassphrase. On success a successful client creation, the function will return a JSON encoded list of the E2E partners -contained in the backup. +contained in the backup and a json-encoded string of the parameters stored in the backup */ FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); @@ -1597,6 +1620,29 @@ This must be called while start network follower is running. */ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); + /** * NotificationsForMe Check if a notification received is for me It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, @@ -1604,6 +1650,7 @@ a Type, and a source. These are as follows: TYPE SOURCE DESCRIPTION "default" recipient user ID A message with no association "request" sender user ID A channel request has been received + "reset" sender user ID A channel reset has been received "confirm" sender user ID A channel request has been accepted "silent" sender user ID A message which should not be notified on "e2e" sender user ID reception of an E2E message @@ -1618,8 +1665,17 @@ FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotific */ FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); -// skipped function RestoreContactsFromBackup with unsupported parameter or return types - +/** + * RestoreContactsFromBackup takes as input the jason output of the +`NewClientFromBackup` function, unmarshals it into IDs, looks up +each ID in user discovery, and initiates a session reset request. +This function will not return until every id in the list has been sent a +request. It should be called again and again until it completes. +xxDK users should not use this function. This function is used by +the mobile phone apps and are not intended to be part of the xxDK. It +should be treated as internal functions specific to the phone apps. + */ +FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); /** * ResumeBackup starts the backup processes back up with a new callback after it @@ -1678,7 +1734,7 @@ FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, @class BindingsAuthRequestCallback; -@class BindingsAuthResetCallback; +@class BindingsAuthResetNotificationCallback; @class BindingsClientError; @@ -1750,7 +1806,7 @@ request * AuthRequestCallback notifies the register whenever they receive an auth request */ -@interface BindingsAuthResetCallback : NSObject <goSeqRefInterface, BindingsAuthResetCallback> { +@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { } @property(strong, readonly) _Nonnull id _ref; diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings index 62d3a3ad56dc1c079e800f5aa915479f08533760..70d29b99750c2894dd0cde34438a187dd60ea325 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h index 5a5d5963442bdd4d0b36d2f4540903d62aacde94..209b34eb81ea47e4d05a9163fa87b2151842d99e 100644 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h @@ -12,6 +12,7 @@ @class BindingsBackup; +@class BindingsBackupReport; @class BindingsClient; @class BindingsContact; @class BindingsContactList; @@ -45,8 +46,8 @@ @class BindingsAuthConfirmCallback; @protocol BindingsAuthRequestCallback; @class BindingsAuthRequestCallback; -@protocol BindingsAuthResetCallback; -@class BindingsAuthResetCallback; +@protocol BindingsAuthResetNotificationCallback; +@class BindingsAuthResetNotificationCallback; @protocol BindingsClientError; @class BindingsClientError; @protocol BindingsEventCallbackFunctionObject; @@ -98,7 +99,7 @@ - (void)callback:(BindingsContact* _Nullable)requestor; @end -@protocol BindingsAuthResetCallback <NSObject> +@protocol BindingsAuthResetNotificationCallback <NSObject> - (void)callback:(BindingsContact* _Nullable)requestor; @end @@ -193,6 +194,10 @@ - (nonnull instancetype)initWithRef:(_Nonnull id)ref; - (nonnull instancetype)init; +/** + * AddJson stores a passed in json string in the backup structure + */ +- (void)addJson:(NSString* _Nullable)json; /** * IsBackupRunning returns true if the backup has been initialized and is running. Returns false if it has been stopped. @@ -205,6 +210,17 @@ storage. To enable backups again, call InitializeBackup. - (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; @end +@interface BindingsBackupReport : NSObject <goSeqRefInterface> { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID + +@property (nonatomic) NSString* _Nonnull params; +@end + /** * BindingsClient wraps the api.Client, implementing additional functions to support the gomobile Client interface @@ -282,7 +298,7 @@ Running - 2000 Stopping - 3000 */ - (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetCallback> _Nullable)reset; +- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; /** * RegisterClientErrorCallback registers the callback to handle errors from the long running threads controlled by StartNetworkFollower and StopNetworkFollower @@ -675,11 +691,6 @@ the period. The period is specified in milliseconds. */ - (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Resend resends a file if sending fails. This function should only be called -if the interfaces.SentProgressCallback returns an error. - */ -- (BOOL)resend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; /** * Send sends a file to the recipient. The sender must have an E2E relationship with the recipient. @@ -1140,6 +1151,10 @@ for determining which IDs restored, which failed, and why. * GetFailedAt returns the failed ID at index */ - (NSData* _Nullable)getFailedAt:(long)index; +/** + * GetRestoreContactsError returns an error string. Empty if no error. + */ +- (NSString* _Nonnull)getRestoreContactsError; /** * GetRestoredAt returns the restored ID at index */ @@ -1259,6 +1274,28 @@ for the life of the program. This must be called while start network follower is running. */ - (nullable instancetype)init:(BindingsClient* _Nullable)client; +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; /** * AddFact adds a fact for the user to user discovery. Will only succeed if the user is already registered and the system does not have the fact currently @@ -1270,20 +1307,6 @@ associated with, a code will be sent. This confirmation ID needs to be called along with the code to finalize the fact. */ - (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * BackUpMissingFacts adds a registered fact to the Store object and saves -it to storage. It can take in both an email or a phone number, passed into -the function in that order. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. - */ -- (BOOL)backUpMissingFacts:(NSString* _Nullable)email phone:(NSString* _Nullable)phone error:(NSError* _Nullable* _Nullable)error; /** * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from AddFact while the code will come over the associated communications system @@ -1523,7 +1546,7 @@ FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* * NewClientFromBackup constructs a new Client from an encrypted backup. The backup is decrypted using the backupPassphrase. On success a successful client creation, the function will return a JSON encoded list of the E2E partners -contained in the backup. +contained in the backup and a json-encoded string of the parameters stored in the backup */ FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); @@ -1597,6 +1620,29 @@ This must be called while start network follower is running. */ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); + /** * NotificationsForMe Check if a notification received is for me It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, @@ -1604,6 +1650,7 @@ a Type, and a source. These are as follows: TYPE SOURCE DESCRIPTION "default" recipient user ID A message with no association "request" sender user ID A channel request has been received + "reset" sender user ID A channel reset has been received "confirm" sender user ID A channel request has been accepted "silent" sender user ID A message which should not be notified on "e2e" sender user ID reception of an E2E message @@ -1618,8 +1665,17 @@ FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotific */ FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); -// skipped function RestoreContactsFromBackup with unsupported parameter or return types - +/** + * RestoreContactsFromBackup takes as input the jason output of the +`NewClientFromBackup` function, unmarshals it into IDs, looks up +each ID in user discovery, and initiates a session reset request. +This function will not return until every id in the list has been sent a +request. It should be called again and again until it completes. +xxDK users should not use this function. This function is used by +the mobile phone apps and are not intended to be part of the xxDK. It +should be treated as internal functions specific to the phone apps. + */ +FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); /** * ResumeBackup starts the backup processes back up with a new callback after it @@ -1678,7 +1734,7 @@ FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, @class BindingsAuthRequestCallback; -@class BindingsAuthResetCallback; +@class BindingsAuthResetNotificationCallback; @class BindingsClientError; @@ -1750,7 +1806,7 @@ request * AuthRequestCallback notifies the register whenever they receive an auth request */ -@interface BindingsAuthResetCallback : NSObject <goSeqRefInterface, BindingsAuthResetCallback> { +@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { } @property(strong, readonly) _Nonnull id _ref; diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings index 62d3a3ad56dc1c079e800f5aa915479f08533760..70d29b99750c2894dd0cde34438a187dd60ea325 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h index 5a5d5963442bdd4d0b36d2f4540903d62aacde94..209b34eb81ea47e4d05a9163fa87b2151842d99e 100644 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h +++ b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h @@ -12,6 +12,7 @@ @class BindingsBackup; +@class BindingsBackupReport; @class BindingsClient; @class BindingsContact; @class BindingsContactList; @@ -45,8 +46,8 @@ @class BindingsAuthConfirmCallback; @protocol BindingsAuthRequestCallback; @class BindingsAuthRequestCallback; -@protocol BindingsAuthResetCallback; -@class BindingsAuthResetCallback; +@protocol BindingsAuthResetNotificationCallback; +@class BindingsAuthResetNotificationCallback; @protocol BindingsClientError; @class BindingsClientError; @protocol BindingsEventCallbackFunctionObject; @@ -98,7 +99,7 @@ - (void)callback:(BindingsContact* _Nullable)requestor; @end -@protocol BindingsAuthResetCallback <NSObject> +@protocol BindingsAuthResetNotificationCallback <NSObject> - (void)callback:(BindingsContact* _Nullable)requestor; @end @@ -193,6 +194,10 @@ - (nonnull instancetype)initWithRef:(_Nonnull id)ref; - (nonnull instancetype)init; +/** + * AddJson stores a passed in json string in the backup structure + */ +- (void)addJson:(NSString* _Nullable)json; /** * IsBackupRunning returns true if the backup has been initialized and is running. Returns false if it has been stopped. @@ -205,6 +210,17 @@ storage. To enable backups again, call InitializeBackup. - (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; @end +@interface BindingsBackupReport : NSObject <goSeqRefInterface> { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID + +@property (nonatomic) NSString* _Nonnull params; +@end + /** * BindingsClient wraps the api.Client, implementing additional functions to support the gomobile Client interface @@ -282,7 +298,7 @@ Running - 2000 Stopping - 3000 */ - (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetCallback> _Nullable)reset; +- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; /** * RegisterClientErrorCallback registers the callback to handle errors from the long running threads controlled by StartNetworkFollower and StopNetworkFollower @@ -675,11 +691,6 @@ the period. The period is specified in milliseconds. */ - (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Resend resends a file if sending fails. This function should only be called -if the interfaces.SentProgressCallback returns an error. - */ -- (BOOL)resend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; /** * Send sends a file to the recipient. The sender must have an E2E relationship with the recipient. @@ -1140,6 +1151,10 @@ for determining which IDs restored, which failed, and why. * GetFailedAt returns the failed ID at index */ - (NSData* _Nullable)getFailedAt:(long)index; +/** + * GetRestoreContactsError returns an error string. Empty if no error. + */ +- (NSString* _Nonnull)getRestoreContactsError; /** * GetRestoredAt returns the restored ID at index */ @@ -1259,6 +1274,28 @@ for the life of the program. This must be called while start network follower is running. */ - (nullable instancetype)init:(BindingsClient* _Nullable)client; +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; /** * AddFact adds a fact for the user to user discovery. Will only succeed if the user is already registered and the system does not have the fact currently @@ -1270,20 +1307,6 @@ associated with, a code will be sent. This confirmation ID needs to be called along with the code to finalize the fact. */ - (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * BackUpMissingFacts adds a registered fact to the Store object and saves -it to storage. It can take in both an email or a phone number, passed into -the function in that order. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. - */ -- (BOOL)backUpMissingFacts:(NSString* _Nullable)email phone:(NSString* _Nullable)phone error:(NSError* _Nullable* _Nullable)error; /** * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from AddFact while the code will come over the associated communications system @@ -1523,7 +1546,7 @@ FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* * NewClientFromBackup constructs a new Client from an encrypted backup. The backup is decrypted using the backupPassphrase. On success a successful client creation, the function will return a JSON encoded list of the E2E partners -contained in the backup. +contained in the backup and a json-encoded string of the parameters stored in the backup */ FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); @@ -1597,6 +1620,29 @@ This must be called while start network follower is running. */ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); +/** + * NewUserDiscoveryFromBackup returns a new user discovery object. It +wil set up the manager with the backup data. Pass into it the backed up +facts, one email and phone number each. This will add the registered facts +to the backed Store. Any one of these fields may be empty, +however both fields being empty will cause an error. Any other fact that is not +an email or phone number will return an error. You may only add a fact for the +accepted types once each. If you attempt to back up a fact type that has already +been backed up, an error will be returned. Anytime an error is returned, it means +the backup was not successful. +NOTE: Do not use this as a direct store operation. This feature is intended to add facts +to a backend store that have ALREADY BEEN REGISTERED on the account. +THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +Only call this once. It must be called after StartNetworkFollower +is called and will fail if the network has never been contacted. +This function technically has a memory leak because it causes both sides of +the bindings to think the other is in charge of the client object. +In general this is not an issue because the client object should exist +for the life of the program. +This must be called while start network follower is running. + */ +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); + /** * NotificationsForMe Check if a notification received is for me It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, @@ -1604,6 +1650,7 @@ a Type, and a source. These are as follows: TYPE SOURCE DESCRIPTION "default" recipient user ID A message with no association "request" sender user ID A channel request has been received + "reset" sender user ID A channel reset has been received "confirm" sender user ID A channel request has been accepted "silent" sender user ID A message which should not be notified on "e2e" sender user ID reception of an E2E message @@ -1618,8 +1665,17 @@ FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotific */ FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); -// skipped function RestoreContactsFromBackup with unsupported parameter or return types - +/** + * RestoreContactsFromBackup takes as input the jason output of the +`NewClientFromBackup` function, unmarshals it into IDs, looks up +each ID in user discovery, and initiates a session reset request. +This function will not return until every id in the list has been sent a +request. It should be called again and again until it completes. +xxDK users should not use this function. This function is used by +the mobile phone apps and are not intended to be part of the xxDK. It +should be treated as internal functions specific to the phone apps. + */ +FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); /** * ResumeBackup starts the backup processes back up with a new callback after it @@ -1678,7 +1734,7 @@ FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, @class BindingsAuthRequestCallback; -@class BindingsAuthResetCallback; +@class BindingsAuthResetNotificationCallback; @class BindingsClientError; @@ -1750,7 +1806,7 @@ request * AuthRequestCallback notifies the register whenever they receive an auth request */ -@interface BindingsAuthResetCallback : NSObject <goSeqRefInterface, BindingsAuthResetCallback> { +@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { } @property(strong, readonly) _Nonnull id _ref; diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fa572d8e1c912a0a5c75c9c9cabc13c6e36157c..d7628f469871f2e1ed53e79524cddf88345b8ac8 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,24 @@ "version": "0.20200225.4" } }, + { + "package": "Alamofire", + "repositoryURL": "https://github.com/Alamofire/Alamofire.git", + "state": { + "branch": null, + "revision": "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864", + "version": "5.5.0" + } + }, + { + "package": "AppAuth", + "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", + "state": { + "branch": null, + "revision": "01131d68346c8ae552961c768d583c715fbe1410", + "version": "1.4.0" + } + }, { "package": "BoringSSL-GRPC", "repositoryURL": "https://github.com/firebase/boringssl-SwiftPM.git", @@ -64,6 +82,15 @@ "version": "1.2.0" } }, + { + "package": "FilesProvider", + "repositoryURL": "https://github.com/amosavian/FileProvider.git", + "state": { + "branch": null, + "revision": "abf68a62541a4193c8d106367ddb3648e8ab693f", + "version": "0.26.0" + } + }, { "package": "Firebase", "repositoryURL": "https://github.com/firebase/firebase-ios-sdk.git", @@ -73,6 +100,15 @@ "version": "8.10.0" } }, + { + "package": "GoogleAPIClientForREST", + "repositoryURL": "https://github.com/google/google-api-objectivec-client-for-rest", + "state": { + "branch": null, + "revision": "22e0bb02729d60db396e8b90d8189313cd86ba53", + "version": "1.6.0" + } + }, { "package": "GoogleAppMeasurement", "repositoryURL": "https://github.com/google/GoogleAppMeasurement.git", @@ -91,6 +127,15 @@ "version": "9.1.2" } }, + { + "package": "GoogleSignIn", + "repositoryURL": "https://github.com/google/GoogleSignIn-iOS", + "state": { + "branch": null, + "revision": "60ca2bfd218ccb194a746a79b41d9d50eb7e3af0", + "version": "6.1.0" + } + }, { "package": "GoogleUtilities", "repositoryURL": "https://github.com/google/GoogleUtilities.git", @@ -127,6 +172,15 @@ "version": "1.7.0" } }, + { + "package": "GTMAppAuth", + "repositoryURL": "https://github.com/google/GTMAppAuth.git", + "state": { + "branch": null, + "revision": "40f4103fb52109032c05599a0c39ad43edbdf80a", + "version": "1.2.2" + } + }, { "package": "KeychainAccess", "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", @@ -271,6 +325,15 @@ "version": "1.9.5" } }, + { + "package": "SwiftyDropbox", + "repositoryURL": "https://github.com/dropbox/SwiftyDropbox.git", + "state": { + "branch": null, + "revision": "7af87d903be1cf0af0e76e0394d992943055894e", + "version": "8.2.1" + } + }, { "package": "xctest-dynamic-overlay", "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",