Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify connecting sync accounts when two exist #3680

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3141,6 +3141,13 @@
EEE11C5F2C7F54AD000ABD7E /* AutofillLoginImportState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE11C5D2C7F54AD000ABD7E /* AutofillLoginImportState.swift */; };
EEE50C292C38249C003DD7FF /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE50C282C38249C003DD7FF /* OptionalExtension.swift */; };
EEE50C2A2C38249C003DD7FF /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE50C282C38249C003DD7FF /* OptionalExtension.swift */; };
EEEFA3402D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */; };
EEEFA3412D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */; };
EEEFA3422D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */; };
EEEFA3432D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */; };
EEEFA3442D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */; };
EEEFA3462D145328006A3F8A /* SyncPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA3452D145322006A3F8A /* SyncPixels.swift */; };
EEEFA3472D145328006A3F8A /* SyncPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEFA3452D145322006A3F8A /* SyncPixels.swift */; };
EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; };
EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; };
F10C99422C7E20A1005568B4 /* Logger+DBPBackgroundAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17E7DDB2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift */; };
Expand Down Expand Up @@ -4977,6 +4984,8 @@
EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mock_login_data_large.csv; sourceTree = "<group>"; };
EEE11C5D2C7F54AD000ABD7E /* AutofillLoginImportState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginImportState.swift; sourceTree = "<group>"; };
EEE50C282C38249C003DD7FF /* OptionalExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalExtension.swift; sourceTree = "<group>"; };
EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTestHelpers.swift; sourceTree = "<group>"; };
EEEFA3452D145322006A3F8A /* SyncPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPixels.swift; sourceTree = "<group>"; };
EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = "<group>"; };
EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = "<group>"; };
F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5857,6 +5866,7 @@
3775913429AB99DA00E26367 /* Sync */ = {
isa = PBXGroup;
children = (
EEEFA3452D145322006A3F8A /* SyncPixels.swift */,
C1935A162C88F9AA001AD72D /* Promotion */,
566B73672BECBF4400FF1959 /* Utilities */,
377D801A2AB47FA1002AF251 /* SettingSyncHandlers */,
Expand Down Expand Up @@ -7602,6 +7612,7 @@
85F69B3A25EDE7F800978E59 /* Common */ = {
isa = PBXGroup;
children = (
EEEFA33F2D142AA6006A3F8A /* CombineTestHelpers.swift */,
4B723E1726B000DC00E14D75 /* TemporaryFileCreator.swift */,
4BBF09222830812900EE1418 /* FileSystemDSL.swift */,
4BBF0924283083EC00EE1418 /* FileSystemDSLTests.swift */,
Expand Down Expand Up @@ -11383,6 +11394,7 @@
56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */,
B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */,
1EC711502D033D200009EB5C /* UserText+NetworkProtection+Shared.swift in Sources */,
EEEFA3472D145328006A3F8A /* SyncPixels.swift in Sources */,
3706FAC4293F65D500E42796 /* PrintingUserScript.swift in Sources */,
1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */,
9D9AE86C2AA76D1B0026E7DC /* LoginItemsManager.swift in Sources */,
Expand Down Expand Up @@ -12325,6 +12337,7 @@
3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */,
3706FE15293F661700E42796 /* PrivacyIconViewModelTests.swift in Sources */,
56A0543C2C20878E007D8FAB /* DuckSchemeHandlerTests.swift in Sources */,
EEEFA3442D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */,
370270C12C78EB13002E44E4 /* HomePageSettingsModelTests.swift in Sources */,
B68412212B6A30680092F66A /* StringExtensionTests.swift in Sources */,
1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */,
Expand Down Expand Up @@ -12595,6 +12608,7 @@
B630E80229C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */,
3706FEA3293F662100E42796 /* CoreDataEncryptionTests.swift in Sources */,
B60C6F8929B1CAB7007BFAA8 /* TestRunHelperInitializer.m in Sources */,
EEEFA3422D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */,
560C6ECE2CCA5BCB00D411E2 /* ContextualDaxDialogFactoryIntegrationTests.swift in Sources */,
560C6ED92CCA5CE700D411E2 /* FindInView.swift in Sources */,
B60C6F8B29B1CAC0007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */,
Expand Down Expand Up @@ -12644,6 +12658,7 @@
1D8B7D6A2A38BF050045C6F6 /* FireproofDomainsStoreMock.swift in Sources */,
B630E7FF29C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */,
7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */,
EEEFA3432D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */,
560C6ECD2CCA5BCB00D411E2 /* ContextualDaxDialogFactoryIntegrationTests.swift in Sources */,
560C6ED82CCA5CE700D411E2 /* FindInView.swift in Sources */,
B60C6F8829B1CAB6007BFAA8 /* TestRunHelperInitializer.m in Sources */,
Expand Down Expand Up @@ -12897,6 +12912,7 @@
9D84E4492CD4EE780046CD8B /* TestNavigationDelegate.swift in Sources */,
9D84E4442CD4EE600046CD8B /* EncryptionKeyStoreMock.swift in Sources */,
9D0668C92CD4F04600D6C9EA /* FireproofDomainsStoreMock.swift in Sources */,
EEEFA3402D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */,
9D84E44A2CD4EE7C0046CD8B /* TestRunHelperInitializer.m in Sources */,
9D0668CC2CD4F06900D6C9EA /* FindInView.swift in Sources */,
);
Expand Down Expand Up @@ -13737,6 +13753,7 @@
B69B503F2726A12500758A2B /* LocalStatisticsStore.swift in Sources */,
B689ECD526C247DB006FB0C5 /* BackForwardListItem.swift in Sources */,
B69B50572727D16900758A2B /* AtbAndVariantCleanup.swift in Sources */,
EEEFA3462D145328006A3F8A /* SyncPixels.swift in Sources */,
AA3D531527A1ED9300074EC1 /* FeedbackWindow.swift in Sources */,
B6685E3F29A606190043D2EE /* WorkspaceProtocol.swift in Sources */,
B6ABC5962B4861D4008343B9 /* FocusableTextField.swift in Sources */,
Expand Down Expand Up @@ -14127,6 +14144,7 @@
4BA1A6FE258C5C1300F6F690 /* EncryptedValueTransformerTests.swift in Sources */,
85F69B3C25EDE81F00978E59 /* URLExtensionTests.swift in Sources */,
4B9292BA2667103100AD2C21 /* BookmarkNodePathTests.swift in Sources */,
EEEFA3412D142AB1006A3F8A /* CombineTestHelpers.swift in Sources */,
562532A02BC069180034D316 /* ZoomPopoverViewModelTests.swift in Sources */,
4B9292C02667103100AD2C21 /* BookmarkManagedObjectTests.swift in Sources */,
373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */,
Expand Down
50 changes: 48 additions & 2 deletions DuckDuckGo/Preferences/Model/SyncPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,11 @@ extension SyncPreferences: ManagementDialogModelDelegate {
do {
try await loginAndShowPresentedDialog(recoveryKey, isRecovery: fromRecoveryScreen)
} catch {
managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToMergeTwoAccounts, description: "")
PixelKit.fire(DebugEvent(GeneralPixel.syncLoginExistingAccountError(error: error)))
if case SyncError.accountAlreadyExists = error {
handleAccountAlreadyExists(recoveryKey)
} else {
managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToSyncToOtherDevice)
}
}
} else if let connectKey = syncCode.connect {
do {
Expand Down Expand Up @@ -755,4 +758,47 @@ extension SyncPreferences: ManagementDialogModelDelegate {
func recoveryCodePasted(_ code: String, fromRecoveryScreen: Bool) {
recoverDevice(recoveryCode: code, fromRecoveryScreen: fromRecoveryScreen)
}

private func handleAccountAlreadyExists(_ recoveryKey: SyncCode.RecoveryKey) {
if devices.count > 1 {
managementDialogModel.shouldShowSwitchAccountsMessage = true
PixelKit.fire(SyncSwitchAccountPixelKitEvent.syncAskUserToSwitchAccount.withoutMacPrefix)
} else {
switchAccounts(recoveryKey: recoveryKey)
managementDialogModel.endFlow()
}
PixelKit.fire(DebugEvent(GeneralPixel.syncLoginExistingAccountError(error: SyncError.accountAlreadyExists)))
}

func userConfirmedSwitchAccounts(recoveryCode: String) {
PixelKit.fire(SyncSwitchAccountPixelKitEvent.syncUserAcceptedSwitchingAccount.withoutMacPrefix)
guard let recoveryKey = try? SyncCode.decodeBase64String(recoveryCode).recovery else {
return
}
switchAccounts(recoveryKey: recoveryKey)
}

private func switchAccounts(recoveryKey: SyncCode.RecoveryKey) {
Task { [weak self] in
guard let self else { return }
do {
try await syncService.disconnect()
} catch {
PixelKit.fire(SyncSwitchAccountPixelKitEvent.syncUserSwitchedLogoutError.withoutMacPrefix)
}

do {
let device = deviceInfo()
let registeredDevices = try await syncService.login(recoveryKey, deviceName: device.name, deviceType: device.type)
await mapDevices(registeredDevices)
} catch {
PixelKit.fire(SyncSwitchAccountPixelKitEvent.syncUserSwitchedLoginError.withoutMacPrefix)
}
PixelKit.fire(SyncSwitchAccountPixelKitEvent.syncUserSwitchedAccount.withoutMacPrefix)
}
}

func switchAccountsCancelled() {
PixelKit.fire(SyncSwitchAccountPixelKitEvent.syncUserCancelledSwitchingAccount.withoutMacPrefix)
}
}
51 changes: 51 additions & 0 deletions DuckDuckGo/Sync/SyncPixels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// SyncPixels.swift
//
// Copyright © 2024 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 PixelKit

enum SyncSwitchAccountPixelKitEvent: PixelKitEventV2 {
case syncAskUserToSwitchAccount
case syncUserAcceptedSwitchingAccount
case syncUserCancelledSwitchingAccount
case syncUserSwitchedAccount
case syncUserSwitchedLogoutError
case syncUserSwitchedLoginError

var name: String {
switch self {
case .syncAskUserToSwitchAccount: return "sync_ask_user_to_switch_account"
case .syncUserAcceptedSwitchingAccount: return "sync_user_accepted_switching_account"
case .syncUserCancelledSwitchingAccount: return "sync_user_cancelled_switching_account"
case .syncUserSwitchedAccount: return "sync_user_switched_account"
case .syncUserSwitchedLogoutError: return "sync_user_switched_logout_error"
case .syncUserSwitchedLoginError: return "sync_user_switched_login_error"
}
}

var parameters: [String: String]? {
nil
}

var error: (any Error)? {
nil
}

var withoutMacPrefix: NonStandardEvent {
NonStandardEvent(self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,42 @@
}
}
},
"alert.sync-switch-account-button" : {
"comment" : "Switch account title in alert",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Switch to a different Sync?"
}
}
}
},
"alert.sync-switch-account-message" : {
"comment" : "Description for switching sync accounts when there's two",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "This device is already synced, are you sure you want to sync it with a different back up or device? Switching won't remove any data already synced to this device."
}
}
}
},
"alert.sync-switch-sync-button" : {
"comment" : "Switch account button in alert",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Switch Sync"
}
}
}
},
"alert.unable-to-authenticate-device" : {
"comment" : "Description for unable to authenticate error",
"extractionState" : "extracted_with_value",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public protocol ManagementDialogModelDelegate: AnyObject {
func enterRecoveryCodePressed()
func copyCode()
func openSystemPasswordSettings()
func userConfirmedSwitchAccounts(recoveryCode: String)
func switchAccountsCancelled()
}

public final class ManagementDialogModel: ObservableObject {
Expand All @@ -42,6 +44,9 @@ public final class ManagementDialogModel: ObservableObject {

@Published public var shouldShowErrorMessage: Bool = false
@Published public var syncErrorMessage: SyncErrorMessage?
@Published public var shouldShowSwitchAccountsMessage: Bool = false

private(set) public var codeToSwitchTo: String?

public weak var delegate: ManagementDialogModelDelegate?

Expand All @@ -55,6 +60,18 @@ public final class ManagementDialogModel: ObservableObject {
}

public func endFlow() {
if shouldShowSwitchAccountsMessage {
delegate?.switchAccountsCancelled()
}
doEndFlow()
}

public func userConfirmedSwitchAccounts(recoveryCode: String) {
delegate?.userConfirmedSwitchAccounts(recoveryCode: recoveryCode)
doEndFlow()
}

private func doEndFlow() {
syncErrorMessage?.type.onButtonPressed(delegate: delegate)
syncErrorMessage = nil
currentDialog = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ public struct SyncErrorMessage {
var type: SyncErrorType
var errorDescription: String

public init(type: SyncErrorType, description: String) {
public init(type: SyncErrorType, description: String? = nil) {
self.type = type
self.errorDescription = description
self.errorDescription = description ?? type.description
}
}
12 changes: 12 additions & 0 deletions LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public struct ManagementDialog: View {
}
)
}
.alert(isPresented: $model.shouldShowSwitchAccountsMessage) {
Alert(
title: Text(UserText.syncAlertSwitchAccountTitle),
message: Text(UserText.syncAlertSwitchAccountMessage),
primaryButton: .default(Text(UserText.syncAlertSwitchAccountButton)) {
model.userConfirmedSwitchAccounts(recoveryCode: recoveryCodeModel.recoveryCode)
},
secondaryButton: .cancel {
model.endFlow()
}
)
}
}

@ViewBuilder var content: some View {
Expand Down
4 changes: 4 additions & 0 deletions LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ enum UserText {
static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", bundle: Bundle.module, value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error")
static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", bundle: Bundle.module, value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error")

public static let syncAlertSwitchAccountTitle = NSLocalizedString("alert.sync-switch-account-button", value: "Switch to a different Sync?", comment: "Switch account title in alert")
public static let syncAlertSwitchAccountMessage = NSLocalizedString("alert.sync-switch-account-message", value: "This device is already synced, are you sure you want to sync it with a different back up or device? Switching won't remove any data already synced to this device.", comment: "Description for switching sync accounts when there's two")
public static let syncAlertSwitchAccountButton = NSLocalizedString("alert.sync-switch-sync-button", value: "Switch Sync", comment: "Switch account button in alert")

static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", bundle: Bundle.module, value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog")
static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", bundle: Bundle.module, 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")
static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", bundle: Bundle.module, value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching")
Expand Down
Loading
Loading