diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index e2cd020d69..7c15bd0bab 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -57,7 +57,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { } @Published var shouldShowErrorMessage: Bool = false - @Published private(set) var errorMessage: String? + @Published private(set) var syncErrorMessage: SyncErrorMessage? @Published var isCreatingAccount: Bool = false @@ -120,7 +120,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { } .store(in: &cancellables) - $errorMessage + $syncErrorMessage .map { $0 != nil } .receive(on: DispatchQueue.main) .assign(to: \.shouldShowErrorMessage, onWeaklyHeld: self) @@ -179,7 +179,8 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncBookmarksPaused.rawValue) UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncCredentialsPaused.rawValue) } catch { - errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToDeleteData, description: error.localizedDescription) } } } @@ -328,7 +329,8 @@ extension SyncPreferences: ManagementDialogModelDelegate { UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncBookmarksPaused.rawValue) UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncCredentialsPaused.rawValue) } catch { - managementDialogModel.errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToDeleteData, description: error.localizedDescription) } } } @@ -338,9 +340,11 @@ extension SyncPreferences: ManagementDialogModelDelegate { do { self.devices = [] let devices = try await syncService.updateDeviceName(name) + managementDialogModel.endFlow() mapDevices(devices) } catch { - managementDialogModel.errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToUpdateDeviceName, description: error.localizedDescription) } } } @@ -380,7 +384,8 @@ extension SyncPreferences: ManagementDialogModelDelegate { Pixel.fire(.syncSignupDirect) presentDialog(for: .saveRecoveryCode(recoveryCode ?? "")) } catch { - managementDialogModel.errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToSync, description: error.localizedDescription) } } } @@ -405,7 +410,8 @@ extension SyncPreferences: ManagementDialogModelDelegate { } } catch { if syncService.account == nil { - managementDialogModel.errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToSync, description: error.localizedDescription) } } } @@ -420,7 +426,8 @@ extension SyncPreferences: ManagementDialogModelDelegate { Task { @MainActor in do { guard let syncCode = try? SyncCode.decodeBase64String(recoveryCode) else { - managementDialogModel.errorMessage = "Invalid code" + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .invalidCode, description: "") return } presentDialog(for: .prepareToSync) @@ -447,11 +454,13 @@ extension SyncPreferences: ManagementDialogModelDelegate { // The UI will update when the devices list changes. } else { - managementDialogModel.errorMessage = "Invalid code" + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .invalidCode, description: "") return } } catch { - managementDialogModel.errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToSync, description: error.localizedDescription) } } } @@ -481,7 +490,8 @@ extension SyncPreferences: ManagementDialogModelDelegate { do { try data.writeFileWithProgress(to: location) } catch { - managementDialogModel.errorMessage = String(describing: error) + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableCreateRecoveryPDF, description: error.localizedDescription) } } @@ -495,8 +505,8 @@ extension SyncPreferences: ManagementDialogModelDelegate { refreshDevices() managementDialogModel.endFlow() } catch { - managementDialogModel.errorMessage = String(describing: error) - } + managementDialogModel.syncErrorMessage + = SyncErrorMessage(type: .unableToRemoveDevice, description: error.localizedDescription) } } } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index 9b26bbae9e..7360356841 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -777,9 +777,9 @@ final class PasswordManagementViewController: NSViewController { } menu.items = [ - createMenuItem(title: UserText.pmNewCard, action: #selector(createNewCreditCard), imageName: "CreditCardGlyph"), createMenuItem(title: UserText.pmNewLogin, action: #selector(createNewLogin), imageName: "LoginGlyph"), - createMenuItem(title: UserText.pmNewIdentity, action: #selector(createNewIdentity), imageName: "IdentityGlyph") + createMenuItem(title: UserText.pmNewIdentity, action: #selector(createNewIdentity), imageName: "IdentityGlyph"), + createMenuItem(title: UserText.pmNewCard, action: #selector(createNewCreditCard), imageName: "CreditCardGlyph"), ] return menu diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementDialogModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementDialogModel.swift index 2265e212e0..eaf0aa2808 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementDialogModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementDialogModel.swift @@ -40,12 +40,12 @@ public final class ManagementDialogModel: ObservableObject { public var codeToDisplay: String? @Published public var shouldShowErrorMessage: Bool = false - @Published public var errorMessage: String? + @Published public var syncErrorMessage: SyncErrorMessage? public weak var delegate: ManagementDialogModelDelegate? public init() { - shouldShowErrorMessageCancellable = $errorMessage + shouldShowErrorMessageCancellable = $syncErrorMessage .map { $0 != nil } .receive(on: DispatchQueue.main) .sink { [weak self] hasError in diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift index a1218f2153..89cb583ab2 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift @@ -23,7 +23,7 @@ public protocol ManagementViewModel: ObservableObject { var isSyncEnabled: Bool { get } var isCreatingAccount: Bool { get } var shouldShowErrorMessage: Bool { get set } - var errorMessage: String? { get } + var syncErrorMessage: SyncErrorMessage? { get } var isSyncBookmarksPaused: Bool { get } var isSyncCredentialsPaused: Bool { get } @@ -48,3 +48,49 @@ public protocol ManagementViewModel: ObservableObject { func recoverDataPressed() func turnOffSyncPressed() } + +public enum SyncErrorType { + case unableToSync + case unableToGetDevices + case unableToUpdateDeviceName + case unableToTurnSyncOff + case unableToDeleteData + case unableToRemoveDevice + case invalidCode + case unableCreateRecoveryPDF + + var title: String { + return UserText.syncErrorAlertTitle + } + + var description: String { + switch self { + case .unableToSync: + return UserText.unableToSyncDescription + case .unableToGetDevices: + return UserText.unableToGetDevicesDescription + case .unableToUpdateDeviceName: + return UserText.unableToUpdateDeviceNameDescription + case .unableToTurnSyncOff: + return UserText.unableToTurnSyncOffDescription + case .unableToDeleteData: + return UserText.unableToDeleteDataDescription + case .unableToRemoveDevice: + return UserText.unableToRemoveDeviceDescription + case .invalidCode: + return UserText.invalidCodeDescription + case .unableCreateRecoveryPDF: + return UserText.unableCreateRecoveryPdfDescription + } + } +} + +public struct SyncErrorMessage { + var type: SyncErrorType + var errorDescription: String + + public init(type: SyncErrorType, description: String) { + self.type = type + self.errorDescription = description + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/DeviceDetailsView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/DeviceDetailsView.swift index 017021da64..dc3b53f8b1 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/DeviceDetailsView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/DeviceDetailsView.swift @@ -26,6 +26,7 @@ struct DeviceDetailsView: View { let device: SyncDevice @State var deviceName = "" + @State private var isLoading = false var canSave: Bool { !deviceName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && @@ -35,39 +36,42 @@ struct DeviceDetailsView: View { func submit() { guard canSave else { return } model.delegate?.updateDeviceName(deviceName) - model.endFlow() } var body: some View { - SyncDialog { - VStack(spacing: 20) { - SyncUIViews.TextHeader(text: UserText.deviceDetailsTitle) - - HStack { - Text(UserText.deviceDetailsLabel) - .font(.system(size: 13, weight: .semibold)) - TextField(UserText.deviceDetailsPrompt, text: $deviceName, onCommit: submit) - .textFieldStyle(RoundedBorderTextFieldStyle()) + if isLoading { + ProgressView() + .padding() + } else { + SyncDialog { + VStack(spacing: 20) { + SyncUIViews.TextHeader(text: UserText.deviceDetailsTitle) + HStack { + Text(UserText.deviceDetailsLabel) + .font(.system(size: 13, weight: .semibold)) + TextField(UserText.deviceDetailsPrompt, text: $deviceName, onCommit: submit) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + .padding(.horizontal, 10) + .padding(.vertical, 14.5) + .roundedBorder() } - .padding(.horizontal, 10) - .padding(.vertical, 14.5) - .roundedBorder() - } - } buttons: { - Button(UserText.cancel) { - model.endFlow() + } buttons: { + Button(UserText.cancel) { + model.endFlow() + } + .buttonStyle(DismissActionButtonStyle()) + Button(UserText.ok) { + submit() + isLoading = true + } + .disabled(!canSave) + .buttonStyle(DefaultActionButtonStyle(enabled: canSave)) } - .buttonStyle(DismissActionButtonStyle()) - Button(UserText.ok) { - submit() + .frame(width: 360, height: 178) + .onAppear { + deviceName = device.name } - .disabled(!canSave) - .buttonStyle(DefaultActionButtonStyle(enabled: canSave)) - - } - .frame(width: 360, height: 178) - .onAppear { - deviceName = device.name } } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SaveRecoveryPDFView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SaveRecoveryPDFView.swift index e4b965bd0c..61e93944d3 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SaveRecoveryPDFView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SaveRecoveryPDFView.swift @@ -31,17 +31,14 @@ struct SaveRecoveryPDFView: View { SyncUIViews.TextDetailMultiline(text: UserText.recoveryPDFExplanation) } VStack(alignment: .leading, spacing: 20) { - HStack { - QRCode(string: code, size: CGSize(width: 56, height: 56)) - Text(code) - .kerning(2) - .multilineTextAlignment(.leading) - .lineSpacing(5) - .lineLimit(3) - .font(Font.custom("SF Mono", size: 12)) - .fixedSize(horizontal: false, vertical: true) - } - .frame(width: 340) + Text(code) + .kerning(2) + .multilineTextAlignment(.leading) + .lineSpacing(5) + .lineLimit(3) + .font(Font.custom("SF Mono", size: 12)) + .fixedSize(horizontal: false, vertical: true) + .frame(width: 340) HStack { Button { viewModel.delegate?.copyCode() diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementDialog.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementDialog.swift index 34e25dc3af..6af2621ee1 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementDialog.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementDialog.swift @@ -36,6 +36,19 @@ public struct ManagementDialog: View { @ObservedObject public var model: ManagementDialogModel @ObservedObject public var recoveryCodeModel: RecoveryCodeViewModel + var errorTitle: String { + return model.syncErrorMessage?.type.title ?? "Sync Error" + } + + var errorDescription: String { + guard let typeDescription = model.syncErrorMessage?.type.description, + let errorDescription = model.syncErrorMessage?.errorDescription + else { + return "" + } + return typeDescription + "\n" + errorDescription + } + public init(model: ManagementDialogModel, recoveryCodeModel: RecoveryCodeViewModel = .init()) { self.model = model self.recoveryCodeModel = recoveryCodeModel @@ -45,9 +58,11 @@ public struct ManagementDialog: View { content .alert(isPresented: $model.shouldShowErrorMessage) { Alert( - title: Text("Unable to turn on Sync"), - message: Text(model.errorMessage ?? "An error occurred"), - dismissButton: .default(Text(UserText.ok)) + title: Text(errorTitle), + message: Text(errorDescription), + dismissButton: .default(Text(UserText.ok)) { + model.endFlow() + } ) } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView.swift index feb18ccd3a..11072dd02f 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView.swift @@ -51,8 +51,5 @@ public struct ManagementView: View where ViewModel: ManagementViewMod } } } - .alert(isPresented: $model.shouldShowErrorMessage) { - Alert(title: Text("Unable to turn on Sync"), message: Text(model.errorMessage ?? "An error occurred"), dismissButton: .default(Text(UserText.ok))) - } } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index 215a904155..928aa1123f 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -144,6 +144,15 @@ enum UserText { static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to manage bookmarks") static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords...", comment: "Button title for sync credentials limits exceeded warning to manage logins") + static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync Error", comment: "Title for sync error alert") + static let unableToSyncDescription = NSLocalizedString("alert.unable-to-sync-description", value: "Unable to sync.", comment: "Description for unable to sync error") + static let unableToGetDevicesDescription = NSLocalizedString("alert.unable-to-get-devices-description", value: "Unable to retrieve the list of connected devices.", comment: "Description for unable to get devices error") + static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", value: "Unable to update the name of the device.", comment: "Description for unable to update device name error") + static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", value: "Unable to turn sync off.", comment: "Description for unable to turn sync off error") + static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") + static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", value: "Unable to remove the specified device from the synchronized devices.", comment: "Description for unable to remove device error") + static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", value: "The code used is invalid.", comment: "Description for invalid code error") + static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", value: "There was a problem creating the recovery PDF.", comment: "Description for unable to create recovery pdf error") static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog")