From 8c9a3ee0f661de8917ddc17bce22c6bc77dcedff Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 24 Nov 2023 14:03:53 +0100 Subject: [PATCH 01/28] implement new flow --- Core/UIViewControllerExtension.swift | 8 +- DuckDuckGo.xcodeproj/project.pbxproj | 5 +- DuckDuckGo/SettingsViewController.swift | 2 +- .../Sync-Recover-128.imageset/Contents.json | 15 ++ .../Sync-Recover-128.svg | 20 ++ .../Sync-Server-128.imageset/Contents.json | 15 ++ .../Sync-Server-128.svg | 18 ++ ...cSettingsViewController+PDFRendering.swift | 10 + ...cSettingsViewController+SyncDelegate.swift | 36 ++-- DuckDuckGo/SyncSettingsViewController.swift | 19 +- .../ViewModels/SaveRecoveryKeyViewModel.swift | 8 +- .../ViewModels/ScanOrPasteCodeViewModel.swift | 15 +- .../ViewModels/SyncSettingsViewModel.swift | 3 + .../SyncUI/Views/DeviceConnectedView.swift | 105 +--------- .../SyncUI/Views/Internal/UserText.swift | 10 +- .../SyncUI/Views/PreparingToSync.swift | 47 +++++ .../SyncUI/Views/RecoverSyncedData.swift | 68 +++++++ .../SyncUI/Views/SaveRecoveryKeyView.swift | 34 ++-- .../SyncUI/Views/ScanOrPasteCodeView.swift | 188 +++++++++++++----- .../SyncUI/Views/SyncSettingsView.swift | 133 ++++++++++--- .../Sources/SyncUI/Views/SyncWithServer.swift | 77 +++++++ 21 files changed, 605 insertions(+), 231 deletions(-) create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Sync-Recover-128.svg create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Sync-Server-128.svg create mode 100644 LocalPackages/SyncUI/Sources/SyncUI/Views/PreparingToSync.swift create mode 100644 LocalPackages/SyncUI/Sources/SyncUI/Views/RecoverSyncedData.swift create mode 100644 LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWithServer.swift diff --git a/Core/UIViewControllerExtension.swift b/Core/UIViewControllerExtension.swift index f37bca411b..4e91074719 100644 --- a/Core/UIViewControllerExtension.swift +++ b/Core/UIViewControllerExtension.swift @@ -50,11 +50,15 @@ extension UIViewController { present(controller: shareController, fromButtonItem: buttonItem) } - public func presentShareSheet(withItems activityItems: [Any], fromView sourceView: UIView, atPoint point: Point? = nil, completion: UIActivityViewController.CompletionWithItemsHandler? = nil) { + public func presentShareSheet(withItems activityItems: [Any], fromView sourceView: UIView, atPoint point: Point? = nil, overrideInterfaceStyle: UIUserInterfaceStyle? = nil, completion: UIActivityViewController.CompletionWithItemsHandler? = nil) { let activities = buildActivities() let shareController = UIActivityViewController(activityItems: activityItems, applicationActivities: activities) shareController.completionWithItemsHandler = completion - shareController.overrideUserInterfaceStyle() + if let overrideInterfaceStyle { + shareController.overrideUserInterfaceStyle = overrideInterfaceStyle + } else { + shareController.overrideUserInterfaceStyle() + } shareController.excludedActivityTypes = [.markupAsPDF] present(controller: shareController, fromView: sourceView, atPoint: point) } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 41659805c7..1bc8cba999 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8102,11 +8102,13 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -8115,6 +8117,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Development - App"; SWIFT_VERSION = 5.0; }; diff --git a/DuckDuckGo/SettingsViewController.swift b/DuckDuckGo/SettingsViewController.swift index 4820200190..a02dd31774 100644 --- a/DuckDuckGo/SettingsViewController.swift +++ b/DuckDuckGo/SettingsViewController.swift @@ -113,7 +113,7 @@ class SettingsViewController: UITableViewController { } private var shouldShowSyncCell: Bool { - return featureFlagger.isFeatureOn(.sync) + return true } private var shouldShowTextSizeCell: Bool { diff --git a/DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Contents.json b/DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Contents.json new file mode 100644 index 0000000000..19881bb976 --- /dev/null +++ b/DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Sync-Recover-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Sync-Recover-128.svg b/DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Sync-Recover-128.svg new file mode 100644 index 0000000000..b0ace196dd --- /dev/null +++ b/DuckDuckGo/SyncAssets.xcassets/Sync-Recover-128.imageset/Sync-Recover-128.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Contents.json b/DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Contents.json new file mode 100644 index 0000000000..ca8f234f0d --- /dev/null +++ b/DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Sync-Server-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Sync-Server-128.svg b/DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Sync-Server-128.svg new file mode 100644 index 0000000000..c9a50a0589 --- /dev/null +++ b/DuckDuckGo/SyncAssets.xcassets/Sync-Server-128.imageset/Sync-Server-128.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift b/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift index fe164242f4..48b9d5f11d 100644 --- a/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift +++ b/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift @@ -37,6 +37,16 @@ extension SyncSettingsViewController { } } + func shareCode(_ code: String) { + + navigationController?.visibleViewController?.presentShareSheet(withItems: [code], + fromView: view, + overrideInterfaceStyle: .dark) { [weak self] _, success, _, _ in + guard success else { return } + self?.navigationController?.visibleViewController?.dismiss(animated: true) + } + } + } private class RecoveryCodeItem: NSObject, UIActivityItemSource { diff --git a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift index 082b312ace..8be85c7323 100644 --- a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift +++ b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift @@ -52,10 +52,12 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { func createAccountAndStartSyncing(optionsViewModel: SyncSettingsViewModel) { Task { @MainActor in do { + self.dismissPresentedViewController() + self.showPreparingSync() try await syncService.createAccount(deviceName: deviceName, deviceType: deviceType) self.rootView.model.syncEnabled(recoveryCode: recoveryCode) self.refreshDevices() - self.showDeviceConnected([], optionsModel: optionsViewModel, isSingleSetUp: true, shouldShowOptions: false) + navigationController?.topViewController?.dismiss(animated: true, completion: showRecoveryPDF) } catch { handleError(error) } @@ -65,11 +67,11 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { @MainActor func handleError(_ error: Error) { // Work out how to handle this properly later - assertionFailure(error.localizedDescription) +// assertionFailure(error.localizedDescription) } func showSyncWithAnotherDevice() { - collectCode(showConnectMode: syncService.account == nil) + collectCode(showConnectMode: true) } func showSyncWithAnotherDeviceEnterText() { @@ -77,35 +79,39 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { } func showRecoverData() { + dismissPresentedViewController() collectCode(showConnectMode: false) } - func showDeviceConnected(_ devices: [SyncSettingsViewModel.Device], optionsModel: SyncSettingsViewModel, isSingleSetUp: Bool, shouldShowOptions: Bool) { - let model = SaveRecoveryKeyViewModel(key: recoveryCode) { [weak self] in - self?.shareRecoveryPDF() - } + func showDeviceConnected() { let controller = UIHostingController( - rootView: DeviceConnectedView(model, - optionsViewModel: optionsModel, - devices: devices, - isSingleSetUp: isSingleSetUp, - shouldShowOptions: shouldShowOptions)) + rootView: DeviceConnectedView()) navigationController?.present(controller, animated: true) { [weak self] in self?.rootView.model.syncEnabled(recoveryCode: self!.recoveryCode) - self?.refreshDevices() +// self?.refreshDevices() } } + func showPreparingSync() { + let controller = UIHostingController(rootView: PreparingToSync()) + navigationController?.present(controller, animated: true) + } + + @MainActor func showRecoveryPDF() { let model = SaveRecoveryKeyViewModel(key: recoveryCode) { [weak self] in self?.shareRecoveryPDF() + } onDismiss: { + self.showDeviceConnected() } let controller = UIHostingController(rootView: SaveRecoveryKeyView(model: model)) - navigationController?.present(controller, animated: true) + navigationController?.present(controller, animated: true) { [weak self] in + self?.rootView.model.syncEnabled(recoveryCode: self!.recoveryCode) + } } private func collectCode(showConnectMode: Bool, showEnterTextCode: Bool = false) { - let model = ScanOrPasteCodeViewModel(showConnectMode: showConnectMode) + let model = ScanOrPasteCodeViewModel(showConnectMode: showConnectMode, recoveryCode: recoveryCode) model.delegate = self var controller: UIHostingController diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index 6bba63102b..e0a0d508dd 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -186,19 +186,21 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { } func loginAndShowDeviceConnected(recoveryKey: SyncCode.RecoveryKey, isActiveSyncDevice: Bool) async throws { - let knownDevices = Set(self.rootView.model.devices.map { $0.id }) +// let knownDevices = Set(self.rootView.model.devices.map { $0.id }) let registeredDevices = try await syncService.login(recoveryKey, deviceName: deviceName, deviceType: deviceType) mapDevices(registeredDevices) - dismissPresentedViewController() - let devices = self.rootView.model.devices.filter { !knownDevices.contains($0.id) && !$0.isThisDevice } - let isSecondDevice = devices.count == 1 - showDeviceConnected(devices, optionsModel: self.rootView.model, isSingleSetUp: false, shouldShowOptions: isActiveSyncDevice && isSecondDevice) + navigationController?.topViewController?.dismiss(animated: true, completion: showRecoveryPDF) +// let devices = self.rootView.model.devices.filter { !knownDevices.contains($0.id) && !$0.isThisDevice } +// let isSecondDevice = devices.count == 1 +// showDeviceConnected(devices, optionsModel: self.rootView.model, isSingleSetUp: false, shouldShowOptions: isActiveSyncDevice && isSecondDevice) } func startPolling() { Task { @MainActor in do { if let recoveryKey = try await connector?.pollForRecoveryKey() { + dismissPresentedViewController() + showPreparingSync() try await loginAndShowDeviceConnected(recoveryKey: recoveryKey, isActiveSyncDevice: false) } else { // Likely cancelled elsewhere @@ -220,6 +222,7 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { try await loginAndShowDeviceConnected(recoveryKey: recoveryKey, isActiveSyncDevice: true) return true } else if let connectKey = syncCode.connect { + showPreparingSync() if syncService.account == nil { try await syncService.createAccount(deviceName: deviceName, deviceType: deviceType) rootView.model.syncEnabled(recoveryCode: recoveryCode) @@ -234,11 +237,7 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { .prefix(1) .sink { [weak self] devices in guard let self else { return } - self.showDeviceConnected( - devices.filter { !$0.isThisDevice }, - optionsModel: self.rootView.model, - isSingleSetUp: false, - shouldShowOptions: devices.count == 2) + self.showDeviceConnected() self.rootView.model.isSyncingDevices = false }.store(in: &cancellables) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift index ced8831904..0e696b8885 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift @@ -24,14 +24,20 @@ public class SaveRecoveryKeyViewModel: ObservableObject { let key: String let showRecoveryPDFAction: () -> Void + let onDismiss: () -> Void - public init(key: String, showRecoveryPDFAction: @escaping () -> Void) { + public init(key: String, showRecoveryPDFAction: @escaping () -> Void, onDismiss: @escaping () -> Void) { self.key = key self.showRecoveryPDFAction = showRecoveryPDFAction + self.onDismiss = onDismiss } func copyKey() { UIPasteboard.general.string = key } + func dismissed() { + onDismiss() + } + } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ScanOrPasteCodeViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ScanOrPasteCodeViewModel.swift index 7c5d1f181a..80894ce495 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ScanOrPasteCodeViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ScanOrPasteCodeViewModel.swift @@ -31,6 +31,7 @@ public protocol ScanOrPasteCodeViewModelDelegate: AnyObject { func codeCollectionCancelled() func gotoSettings() + func shareCode(_ code: String) } @@ -62,12 +63,16 @@ public class ScanOrPasteCodeViewModel: ObservableObject { public weak var delegate: ScanOrPasteCodeViewModelDelegate? - var showQRCodeModel: ShowQRCodeViewModel? + var showQRCodeModel: ShowQRCodeViewModel let showConnectMode: Bool + let recoveryCode: String? - public init(showConnectMode: Bool) { + public init(showConnectMode: Bool, recoveryCode: String?) { self.showConnectMode = showConnectMode + self.recoveryCode = recoveryCode + showQRCodeModel = ShowQRCodeViewModel() + showQRCodeModel.code = recoveryCode } func codeScanned(_ code: String) async -> Bool { @@ -106,11 +111,15 @@ public class ScanOrPasteCodeViewModel: ObservableObject { let model = ShowQRCodeViewModel() showQRCodeModel = model Task { @MainActor in - showQRCodeModel?.code = await delegate?.startConnectMode() + showQRCodeModel.code = await delegate?.startConnectMode() } return model } + func showShareCodeSheet() { + delegate?.shareCode(showQRCodeModel.code ?? "") + } + func endConnectMode() { self.delegate?.endConnectMode() } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift index 3e13d1c9a1..b2c1b8da66 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift @@ -160,4 +160,7 @@ public class SyncSettingsViewModel: ObservableObject { delegate?.launchAutofillViewController() } + public func recoverSyncDataPressed() { + delegate?.showRecoverData() + } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift index 2adec5fc5c..6152d00f1a 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift @@ -24,53 +24,9 @@ import DesignResourcesKit public struct DeviceConnectedView: View { - @Environment(\.verticalSizeClass) var verticalSizeClass + @Environment(\.presentationMode) var presentation - var isCompact: Bool { - verticalSizeClass == .compact - } - let isSingleSetUp: Bool - let shouldShowOptions: Bool - @State var showRecoveryPDF = false - - let saveRecoveryKeyViewModel: SaveRecoveryKeyViewModel - @ObservedObject var optionsViewModel: SyncSettingsViewModel - let devices: [SyncSettingsViewModel.Device] - - public init(_ saveRecoveryKeyViewModel: SaveRecoveryKeyViewModel, optionsViewModel: SyncSettingsViewModel, devices: [SyncSettingsViewModel.Device], isSingleSetUp: Bool, shouldShowOptions: Bool) { - self.saveRecoveryKeyViewModel = saveRecoveryKeyViewModel - self.devices = devices - self.optionsViewModel = optionsViewModel - self.isSingleSetUp = isSingleSetUp - self.shouldShowOptions = shouldShowOptions - } - - var title: String { - if isSingleSetUp { - return UserText.syngleDeviceConnectedTitle - } - return UserText.deviceSyncedTitle - } - - var message: String { - if isSingleSetUp { - return UserText.firstDeviceSyncedMessage - } - if devices.count == 1 { - return UserText.deviceSyncedMessage - } - return UserText.multipleDevicesSyncedMessage - } - - var devicesOnMessageText: String { - if devices.isEmpty { - return "" - } - if devices.count == 1 { - return devices[0].name - } - return "\(devices.count + 1) " + UserText.wordDevices - } + public init() {} @ViewBuilder func deviceSyncedView() -> some View { @@ -79,73 +35,26 @@ public struct DeviceConnectedView: View { Image("Sync-Start-128") .padding(.bottom, 20) - Text(title) + Text("Your Data is Synced!") .daxTitle1() .padding(.bottom, 24) - - Text("\(message) \(Text(devicesOnMessageText).bold())") - .multilineTextAlignment(.center) - - if shouldShowOptions { - options() - } } .padding(.horizontal, 20) } foregroundContent: { Button { - withAnimation { - self.showRecoveryPDF = true - } + presentation.wrappedValue.dismiss() } label: { - Text(UserText.nextButton) + Text(UserText.doneButton) } .buttonStyle(PrimaryButtonStyle()) .frame(maxWidth: 360) .padding(.horizontal, 30) } - .padding(.top, isCompact ? 0 : 56) .padding(.bottom) } - @ViewBuilder - func options() -> some View { - VStack { - Spacer(minLength: 71) - Text(UserText.options.uppercased()) - .daxFootnoteRegular() - Toggle(isOn: $optionsViewModel.isUnifiedFavoritesEnabled) { - HStack(spacing: 16) { - Image("SyncAllDevices") - VStack(alignment: .leading) { - Text(UserText.unifiedFavoritesTitle) - .foregroundColor(.primary) - .daxBodyRegular() - Text(UserText.unifiedFavoritesInstruction) - .daxCaption() - .foregroundColor(.secondary) - } - } - } - .padding(16) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(.black.opacity(0.01)) - ) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(.black.opacity(0.2), lineWidth: 0.2) - ) - } - } - public var body: some View { - if showRecoveryPDF { - SaveRecoveryKeyView(model: saveRecoveryKeyViewModel) - .transition(.move(edge: .trailing)) - } else { - deviceSyncedView() - .transition(.move(edge: .leading)) - } + deviceSyncedView() + .transition(.move(edge: .leading)) } - } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift index 1fc5c3535b..156f9dacb7 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift @@ -23,7 +23,7 @@ import Foundation // Localise these later, when feature is closer to exernal release struct UserText { - static let syncWithAnotherDeviceTitle = "Sync with Another Device" + static let syncWithAnotherDeviceTitle = "Begin Syncing" static let syncWithAnotherDeviceMessage = "Securely sync bookmarks and Logins between your devices." static let recoveryMessage = "If you lose access to your devices, you will need this code to recover your synced data." @@ -39,14 +39,14 @@ struct UserText { static let recoverYourData = "Recover Your Data" static let options = "Options" - static let unifiedFavoritesTitle = "Share Favorites" - static let unifiedFavoritesInstruction = "Use the same favorites on all devices. Leave off to keep mobile and desktop favorites separate." + static let unifiedFavoritesTitle = "Unify Favorites " + static let unifiedFavoritesInstruction = "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate." static let syncSettingsFooter = "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the decryption key." - static let connectDeviceInstructions = "Go to Settings > Sync & Back Up in the DuckDuckGo App on a different device and scan the QR code to sync." + static let connectDeviceInstructions = "Go to Settings › Sync & Backup in the DuckDuckGo Browser on a another device and select \n”Sync with Another Device.”" - static let recoveryModeInstructions = "Scan the QR code from your Recovery PDF or in the DuckDuckGo app under Settings > Sync & Back Up on a signed-in device." + static let recoveryModeInstructions = "Scan the QR code on your Recovery PDF, or another synced device, to recover your data." static let validatingCode = "Validating code" static let validatingCodeFailed = "Invalid code." diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/PreparingToSync.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/PreparingToSync.swift new file mode 100644 index 0000000000..a3178a3fe5 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/PreparingToSync.swift @@ -0,0 +1,47 @@ +// +// PreparingToSync.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI +import DesignResourcesKit + + +public struct PreparingToSync: View { + + public init() {} + + public var body: some View { + UnderflowContainer { + VStack(spacing: 0) { + Image("Sync-Start-128") + .padding(.bottom, 20) + + Text("Preparing to Sync") + .daxTitle1() + .padding(.bottom, 24) + + Text("We are getting things set up. \nIt won't take long") + .multilineTextAlignment(.center) + } + .padding(.horizontal, 20) + } foregroundContent: { + Text("Connecting…") + } + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/RecoverSyncedData.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/RecoverSyncedData.swift new file mode 100644 index 0000000000..69c87961ba --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/RecoverSyncedData.swift @@ -0,0 +1,68 @@ +// +// RecoverSyncedData.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI +import DesignResourcesKit + +public struct RecoverSyncedData: View { + + @ObservedObject public var model: SyncSettingsViewModel + var onCancel: () -> Void + + public init(model: SyncSettingsViewModel, onCancel: @escaping () -> Void) { + self.model = model + self.onCancel = onCancel + } + + public var body: some View { + UnderflowContainer { + VStack(spacing: 0) { + HStack { + Button(action: onCancel, label: { + Text("Cancel") + .foregroundColor(.primary) + }) + Spacer() + } + .frame(height: 56) + Image("Sync-Recover-128") + .padding(.bottom, 20) + + Text("Recover Synced Data") + .daxTitle1() + .multilineTextAlignment(.center) + .padding(.bottom, 24) + + Text("To restore your synced data, you'll need the Recover") + .multilineTextAlignment(.center) + } + .padding(.horizontal, 20) + } foregroundContent: { + Button { + model.recoverSyncDataPressed() + } label: { + Text("Get Started") + } + .buttonStyle(PrimaryButtonStyle()) + .frame(maxWidth: 360) + .padding(.horizontal, 30) + } + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift index b2206716a4..aaa10ede2c 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift @@ -38,26 +38,25 @@ public struct SaveRecoveryKeyView: View { @ViewBuilder func recoveryInfo() -> some View { - ZStack { VStack(spacing: 26) { HStack(spacing: 16) { - QRCodeView(string: model.key, size: 94, style: .dark) + QRCodeView(string: model.key, size: 64, style: .dark) Text(model.key) .fontWeight(.light) .lineSpacing(1.6) - .lineLimit(5) + .lineLimit(3) .applyKerning(2) .truncationMode(.tail) .monospaceSystemFont(ofSize: 16) .frame(maxWidth: .infinity) } + buttons() } .padding(.top, 20) .padding(.horizontal, 20) .padding(.bottom, 12) - } - .background(RoundedRectangle(cornerRadius: 10).foregroundColor(.black.opacity(0.03))) + .background(RoundedRectangle(cornerRadius: 10).foregroundColor(.black.opacity(0.03))) } @ViewBuilder @@ -66,7 +65,12 @@ public struct SaveRecoveryKeyView: View { Button("Save as PDF") { model.showRecoveryPDFAction() } - .buttonStyle(PrimaryButtonStyle(compact: isCompact)) + .buttonStyle(SecondaryButtonStyle(compact: isCompact)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .inset(by: 0.5) + .stroke(.blue, lineWidth: 1) + ) Button(UserText.copyCode) { model.copyKey() @@ -77,16 +81,7 @@ public struct SaveRecoveryKeyView: View { .inset(by: 0.5) .stroke(.blue, lineWidth: 1) ) - - Button { - presentation.wrappedValue.dismiss() - } label: { - Text(UserText.notNowButton) - } - .buttonStyle(SecondaryButtonStyle(compact: isCompact)) } - .frame(maxWidth: 360) - .padding(.horizontal, 30) } @ViewBuilder @@ -121,7 +116,14 @@ public struct SaveRecoveryKeyView: View { UnderflowContainer { mainContent() } foregroundContent: { - buttons() + Button { + presentation.wrappedValue.dismiss() + model.onDismiss() + } label: { + Text(UserText.nextButton) + } + .buttonStyle(PrimaryButtonStyle(compact: isCompact)) + .padding(.horizontal, 20) } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ScanOrPasteCodeView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ScanOrPasteCodeView.swift index 5c8a08af55..796c4c57d7 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ScanOrPasteCodeView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ScanOrPasteCodeView.swift @@ -24,6 +24,9 @@ import DesignResourcesKit public struct ScanOrPasteCodeView: View { @ObservedObject var model: ScanOrPasteCodeViewModel + @State var qrCodeModel = ShowQRCodeViewModel() + + @State private var isShareSheetPresented: Bool = false public init(model: ScanOrPasteCodeViewModel) { self.model = model @@ -120,14 +123,11 @@ public struct ScanOrPasteCodeView: View { @ViewBuilder func instructions() -> some View { - + Text(model.showConnectMode ? UserText.connectDeviceInstructions : UserText.recoveryModeInstructions) - .lineLimit(nil) + .daxFootnoteRegular() .multilineTextAlignment(.center) - .daxCaption() - .foregroundColor(.white.opacity(0.6)) - .padding(.top, 20) - + .padding(.horizontal, 16) } @ViewBuilder @@ -192,70 +192,160 @@ public struct ScanOrPasteCodeView: View { .stroke(lineWidth: 8) .foregroundColor(isInvalidCode ? .red.opacity(0.6) : .white.opacity(0.8)) .rotationEffect(.degrees(degrees), anchor: .center) - .frame(width: 300, height: 300) + .frame(width: 250, height: 250) } } } } public var body: some View { - GeometryReader { g in - ZStack(alignment: .top) { - fullscreenCameraBackground() - - VStack(spacing: 0) { - Rectangle() // Also acts as the blur for the camera - .fill(.black) - .frame(height: g.safeAreaInsets.top) - - ZStack { - // Background in case fullscreen camera view doesn't work - if !model.showCamera { - Rectangle().fill(Color.black) - } - - cameraViewPort() - .frame(width: g.size.width, height: g.size.width) - .frame(maxHeight: g.size.height - 300) - - Group { - cameraPermissionDenied() - cameraUnavailable() - } - .padding(.horizontal, 0) - } + VStack(spacing: 10) { + VStack(spacing: 10) { + if model.showConnectMode { + Text("Scan QR Code") + .daxTitle2() + } + instructions() + } + .padding(.top, 10) + GeometryReader { g in + ZStack(alignment: .top) { + fullscreenCameraBackground() - ZStack { + VStack(spacing: 0) { Rectangle() // Also acts as the blur for the camera .fill(.black) - .regularMaterialBackground() + .frame(height: g.safeAreaInsets.top) - VStack(spacing: 0) { - Section { - instructions() - .padding(.horizontal, 20) + ZStack { + // Background in case fullscreen camera view doesn't work + if !model.showCamera { + Rectangle().fill(Color.black) } - List { - buttons() +// cameraViewPort() +// .frame(width: g.size.width, height: g.size.width) +// .frame(maxHeight: g.size.height) + + Group { + cameraPermissionDenied() + cameraUnavailable() + } + .padding(.horizontal, 0) + + VStack { + Spacer() + Text("Point camera at QR code to sync") + .padding(.vertical, 8) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 56) + .fill(.clear) + .background(BlurView(style: .light)) + .cornerRadius(20) + ) + .daxCaption() } - .ignoresSafeArea() - .disableScrolling() + .padding(.bottom, 12) } - .frame(maxWidth: Constants.maxFullScreenWidth) + } + .ignoresSafeArea() + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel", action: model.cancel) + .foregroundColor(Color.white) + } + ToolbarItem(placement: .navigationBarTrailing) { + NavigationLink("Manually Enter Code", destination: { + PasteCodeView(model: model) + }) + .foregroundColor(Color(designSystemColor: .accent)) } } - .ignoresSafeArea() } - .navigationTitle("Scan QR Code") - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel", action: model.cancel) - .foregroundColor(Color.white) + } + if model.showConnectMode { + VStack(spacing: 8) { + HStack(alignment: .top, spacing: 20) { + QRCodeView(string: qrCodeModel.code ?? "", size: 120) + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Mobile-to-Mobile?") + .daxBodyBold() + .fixedSize() + Spacer() + Image("SyncDeviceType_phone") + .padding(2) + .background( + RoundedRectangle(cornerRadius: 2) + .fill(Color(designSystemColor: .lines)) + ) + } + Text("Scan this code with another device to sync.") + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) + } + } + .padding(20) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(designSystemColor: .panel)) + ) + .padding(20) + HStack { + Text("can't scan?") + Text("Share Text Code") + .foregroundColor(Color(designSystemColor: .accent)) + .onTapGesture { + model.showShareCodeSheet() + } + // .sheet(isPresented: $isShareSheetPresented, content: { + // ShareSheet(activityItems: [qrCodeModel.code ?? ""]) + // .frame(height: UIScreen.main.bounds.height / 2) + // }) + } + } + .padding(.bottom, 40) + .onAppear { + if let recoveryCode = model.recoveryCode { + self.qrCodeModel.code = recoveryCode + } else { + self.qrCodeModel = model.startConnectMode() } } + } else { + Rectangle().fill(Color.black) + .frame(maxHeight: 274) } } + + struct ShareSheet: UIViewControllerRepresentable { + let activityItems: [Any] + + func makeUIViewController(context: Context) -> UIViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + return controller + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + } + } + + + struct BlurView: UIViewRepresentable { + var style: UIBlurEffect.Style + + func makeUIView(context: Context) -> UIVisualEffectView { + return UIVisualEffectView(effect: UIBlurEffect(style: style)) + } + + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + uiView.effect = UIBlurEffect(style: style) + } + } + } private struct RoundedCorner: Shape { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift index f2fff4bf6c..a73997f41d 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift @@ -25,6 +25,8 @@ public struct SyncSettingsView: View { @ObservedObject public var model: SyncSettingsViewModel let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() + @State var isSyncWithSetUpSheetVisible = false + @State var isRecoverSyncedDataSheetVisible = false public init(model: SyncSettingsViewModel) { self.model = model @@ -40,7 +42,7 @@ public struct SyncSettingsView: View { } } else { List { - workInProgress() +// workInProgress() if model.isSyncEnabled { @@ -56,7 +58,7 @@ public struct SyncSettingsView: View { devices() - syncNewDevice() +// syncNewDevice() OptionsView(isUnifiedFavoritesEnabled: $model.isUnifiedFavoritesEnabled) .onAppear(perform: { @@ -70,12 +72,14 @@ public struct SyncSettingsView: View { } else { syncWithAnotherDeviceView() + + otherOptions() + +// singleDeviceSetUpView() - singleDeviceSetUpView() - - recoverYourDataView() +// recoverYourDataView() - footerView() +// footerView() } } .navigationTitle(UserText.syncTitle) @@ -166,27 +170,86 @@ extension SyncSettingsView { @ViewBuilder func syncWithAnotherDeviceView() -> some View { Section { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(UserText.syncWithAnotherDeviceTitle) - .daxBodyBold() - Text(UserText.syncWithAnotherDeviceMessage) - .daxBodyRegular() - } - Spacer() + VStack(spacing: 8) { Image("Sync-Pair-96") - - } - Button(UserText.scanQRCode) { - model.scanQRCode() - } - Button(UserText.enterTextCode) { - model.showEnterTextView() + Text(UserText.syncWithAnotherDeviceTitle) + .daxTitle3() + Text(UserText.syncWithAnotherDeviceMessage) + .daxBodyRegular() + .multilineTextAlignment(.center) + .foregroundColor(Color(designSystemColor: .textPrimary)) + Button(action: { + model.scanQRCode() + }, label: { + Text("Sync With Another Device") + .daxButton() + .foregroundColor(.white) + .frame(maxWidth: 310) + .frame(height: 50) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(designSystemColor: .accent)) + ) + }) + .padding(.vertical, 16) } + + // HStack { + // VStack(alignment: .leading, spacing: 4) { + // Text(UserText.syncWithAnotherDeviceTitle) + // .daxBodyBold() + // Text(UserText.syncWithAnotherDeviceMessage) + // .daxBodyRegular() + // } + // Spacer() + // Image("Sync-Pair-96") + // + // } + // Button(UserText.scanQRCode) { +// model.scanQRCode() + // } + // Button(UserText.enterTextCode) { + // model.showEnterTextView() + // } + } footer: { + Text("Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.") + .daxFootnoteRegular() + .multilineTextAlignment(.center) + } + } + + @ViewBuilder + func otherOptions() -> some View { + Section { + Text("Sync and Back Up This Device") + .daxBodyRegular() + .foregroundColor(Color(designSystemColor: .accent)) + .onTapGesture { + isSyncWithSetUpSheetVisible = true + } + .sheet(isPresented: $isSyncWithSetUpSheetVisible, content: { + SyncWithServer(model: model, onCancel: { + isSyncWithSetUpSheetVisible = false + }) + }) + Text("Recover Synced Data") + .daxBodyRegular() + .foregroundColor(Color(designSystemColor: .accent)) + .onTapGesture { + isRecoverSyncedDataSheetVisible = true + } + .sheet(isPresented: $isRecoverSyncedDataSheetVisible, content: { + RecoverSyncedData(model: model, onCancel: { + isRecoverSyncedDataSheetVisible = false + }) + }) + } header: { + Text("Other Options") } } } + // Sync Enabled Views extension SyncSettingsView { @ViewBuilder @@ -267,6 +330,9 @@ extension SyncSettingsView { } } } + Button("Sync With Another Device") { + model.scanQRCode() + } } header: { Text(UserText.connectedDevicesTitle) } @@ -304,6 +370,16 @@ extension SyncSettingsView { model.disableSync() } } + } header: { + HStack(alignment: .center) { + Text("Sync Enabled") + Circle() + .fill(.green) + .frame(width: 8) + } + } footer: { + Text("Bookmarks and passwords are currently synced across your devices.") + .multilineTextAlignment(.leading) } } @@ -363,15 +439,12 @@ public struct OptionsView: View { public var body: some View { Section { Toggle(isOn: $isUnifiedFavoritesEnabled) { - HStack(spacing: 16) { - Image("SyncAllDevices") - VStack(alignment: .leading) { - Text(UserText.unifiedFavoritesTitle) - .foregroundColor(.primary) - Text(UserText.unifiedFavoritesInstruction) - .daxBodyRegular() - .foregroundColor(.secondary) - } + VStack(alignment: .leading, spacing: 5) { + Text(UserText.unifiedFavoritesTitle) + .foregroundColor(.primary) + Text(UserText.unifiedFavoritesInstruction) + .daxBodyRegular() + .foregroundColor(.secondary) } } } header: { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWithServer.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWithServer.swift new file mode 100644 index 0000000000..1d8a0e5368 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWithServer.swift @@ -0,0 +1,77 @@ +// +// SyncWithServer.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DuckUI +import DesignResourcesKit + +public struct SyncWithServer: View { + + @ObservedObject public var model: SyncSettingsViewModel + var onCancel: () -> Void + + public init(model: SyncSettingsViewModel, onCancel: @escaping () -> Void) { + self.model = model + self.onCancel = onCancel + } + + public var body: some View { + UnderflowContainer { + VStack(spacing: 0) { + HStack { + Button(action: onCancel, label: { + Text("Cancel") + .foregroundColor(.primary) + }) + Spacer() + } + .frame(height: 56) + Image("Sync-Server-128") + .padding(.bottom, 20) + + Text("Sync and Back Up This Device") + .daxTitle1() + .multilineTextAlignment(.center) + .padding(.bottom, 24) + VStack(spacing: 10) { + Text("This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.") + .multilineTextAlignment(.center) + + Text("The encryption key is only stored on your device, DuckDuckGo cannot access it.") + .multilineTextAlignment(.center) + } + } + .padding(.horizontal, 20) + } foregroundContent: { + VStack(spacing: 8) { + Button { + model.startSyncPressed() + } label: { + Text("Turn on Sync & Backup") + } + .buttonStyle(PrimaryButtonStyle()) + .frame(maxWidth: 360) + .padding(.horizontal, 30) + Text("You can sync with your other devices later.") + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + } + } + } +} From 9e554ab19169a4c76d803c27720dccaadd6229f1 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 24 Nov 2023 15:58:45 +0100 Subject: [PATCH 02/28] clean up Sync set up and sync enabled page --- DuckDuckGo/Base.lproj/Settings.storyboard | 42 ++-- DuckDuckGo/SettingsViewController.swift | 4 +- ...cSettingsViewController+SyncDelegate.swift | 2 +- DuckDuckGo/SyncSettingsViewController.swift | 1 - DuckDuckGo/UserText.swift | 2 +- .../ViewModels/SyncSettingsViewModel.swift | 4 - .../SyncUI/Views/Internal/ShowCodeView.swift | 46 ++-- .../SyncUI/Views/Internal/UserText.swift | 65 ++++-- .../SyncUI/Views/SyncSettingsView.swift | 208 ++++-------------- 9 files changed, 136 insertions(+), 238 deletions(-) diff --git a/DuckDuckGo/Base.lproj/Settings.storyboard b/DuckDuckGo/Base.lproj/Settings.storyboard index 1d4eb7fa61..63439e5402 100644 --- a/DuckDuckGo/Base.lproj/Settings.storyboard +++ b/DuckDuckGo/Base.lproj/Settings.storyboard @@ -104,7 +104,7 @@ -