diff --git a/sync/sync-impl/lint-baseline.xml b/sync/sync-impl/lint-baseline.xml
index b258727a2108..96f067463a0d 100644
--- a/sync/sync-impl/lint-baseline.xml
+++ b/sync/sync-impl/lint-baseline.xml
@@ -34,6 +34,17 @@
message="Defined here, included via layout/activity_sync.xml => layout/view_sync_disabled.xml defines @+id/syncSetupOtherPlatforms"/>
+
+
+
+
@@ -118,7 +129,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt
index 6128db48fcf2..c5ac8003d753 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt
@@ -74,8 +74,11 @@ class AppSyncAccountRepository @Inject constructor(
private val syncPixels: SyncPixels,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
+ private val syncFeature: SyncFeature,
) : SyncAccountRepository {
+ private val connectedDevicesCached: MutableList = mutableListOf()
+
override fun isSyncSupported(): Boolean {
return syncStore.isEncryptionSupported()
}
@@ -108,9 +111,20 @@ class AppSyncAccountRepository @Inject constructor(
}
private fun login(recoveryCode: RecoveryCode): Result {
+ var wasUserLogout = false
if (isSignedIn()) {
- return Error(code = ALREADY_SIGNED_IN.code, reason = "Already signed in")
- .alsoFireAlreadySignedInErrorPixel()
+ val allowSwitchAccount = syncFeature.seamlessAccountSwitching().isEnabled()
+ val error = Error(code = ALREADY_SIGNED_IN.code, reason = "Already signed in").alsoFireAlreadySignedInErrorPixel()
+ if (allowSwitchAccount && connectedDevicesCached.size == 1) {
+ val thisDeviceId = syncStore.deviceId.orEmpty()
+ val result = logout(thisDeviceId)
+ if (result is Error) {
+ return result
+ }
+ wasUserLogout = true
+ } else {
+ return error
+ }
}
val primaryKey = recoveryCode.primaryKey
@@ -120,6 +134,9 @@ class AppSyncAccountRepository @Inject constructor(
return performLogin(userId, deviceId, deviceName, primaryKey).onFailure {
it.alsoFireLoginErrorPixel()
+ if (wasUserLogout) {
+ syncPixels.fireUserSwitchedLoginError()
+ }
return it.copy(code = LOGIN_FAILED.code)
}
}
@@ -288,6 +305,7 @@ class AppSyncAccountRepository @Inject constructor(
return when (val result = syncApi.getDevices(token)) {
is Error -> {
+ connectedDevicesCached.clear()
result.alsoFireAccountErrorPixel().copy(code = GENERIC_ERROR.code)
}
@@ -315,6 +333,11 @@ class AppSyncAccountRepository @Inject constructor(
}
}.sortedWith { a, b ->
if (a.thisDevice) -1 else 1
+ }.also {
+ connectedDevicesCached.apply {
+ clear()
+ addAll(it)
+ }
},
)
}
@@ -326,8 +349,17 @@ class AppSyncAccountRepository @Inject constructor(
override fun logoutAndJoinNewAccount(stringCode: String): Result {
val thisDeviceId = syncStore.deviceId.orEmpty()
return when (val result = logout(thisDeviceId)) {
- is Error -> result
- is Result.Success -> processCode(stringCode)
+ is Error -> {
+ syncPixels.fireUserSwitchedLogoutError()
+ result
+ }
+ is Result.Success -> {
+ val loginResult = processCode(stringCode)
+ if (loginResult is Error) {
+ syncPixels.fireUserSwitchedLoginError()
+ }
+ loginResult
+ }
}
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
index d2e532722e43..f91c261e0083 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
@@ -45,6 +45,6 @@ interface SyncFeature {
@Toggle.DefaultValue(true)
fun gzipPatchRequests(): Toggle
- @Toggle.DefaultValue(false)
+ @Toggle.DefaultValue(true)
fun seamlessAccountSwitching(): Toggle
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt
index 7fca1dd76f17..620cf2b0eb88 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt
@@ -35,6 +35,13 @@ class SyncPixelParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugi
SyncPixelName.SYNC_GET_OTHER_DEVICES_SCREEN_SHOWN.pixelName to PixelParameter.removeAtb(),
SyncPixelName.SYNC_GET_OTHER_DEVICES_LINK_COPIED.pixelName to PixelParameter.removeAtb(),
SyncPixelName.SYNC_GET_OTHER_DEVICES_LINK_SHARED.pixelName to PixelParameter.removeAtb(),
+
+ SyncPixelName.SYNC_ASK_USER_TO_SWITCH_ACCOUNT.pixelName to PixelParameter.removeAtb(),
+ SyncPixelName.SYNC_USER_ACCEPTED_SWITCHING_ACCOUNT.pixelName to PixelParameter.removeAtb(),
+ SyncPixelName.SYNC_USER_CANCELLED_SWITCHING_ACCOUNT.pixelName to PixelParameter.removeAtb(),
+ SyncPixelName.SYNC_USER_SWITCHED_ACCOUNT.pixelName to PixelParameter.removeAtb(),
+ SyncPixelName.SYNC_USER_SWITCHED_LOGOUT_ERROR.pixelName to PixelParameter.removeAtb(),
+ SyncPixelName.SYNC_USER_SWITCHED_LOGIN_ERROR.pixelName to PixelParameter.removeAtb(),
)
}
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt
index 6a9b55c0fb2b..a52493645079 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt
@@ -81,6 +81,13 @@ interface SyncPixels {
feature: SyncableType,
apiError: Error,
)
+
+ fun fireAskUserToSwitchAccount()
+ fun fireUserAcceptedSwitchingAccount()
+ fun fireUserCancelledSwitchingAccount()
+ fun fireUserSwitchedAccount()
+ fun fireUserSwitchedLogoutError()
+ fun fireUserSwitchedLoginError()
}
@ContributesBinding(AppScope::class)
@@ -255,6 +262,30 @@ class RealSyncPixels @Inject constructor(
}
}
+ override fun fireUserSwitchedAccount() {
+ pixel.fire(SyncPixelName.SYNC_USER_SWITCHED_ACCOUNT)
+ }
+
+ override fun fireAskUserToSwitchAccount() {
+ pixel.fire(SyncPixelName.SYNC_ASK_USER_TO_SWITCH_ACCOUNT)
+ }
+
+ override fun fireUserAcceptedSwitchingAccount() {
+ pixel.fire(SyncPixelName.SYNC_USER_ACCEPTED_SWITCHING_ACCOUNT)
+ }
+
+ override fun fireUserCancelledSwitchingAccount() {
+ pixel.fire(SyncPixelName.SYNC_USER_CANCELLED_SWITCHING_ACCOUNT)
+ }
+
+ override fun fireUserSwitchedLoginError() {
+ pixel.fire(SyncPixelName.SYNC_USER_SWITCHED_LOGIN_ERROR)
+ }
+
+ override fun fireUserSwitchedLogoutError() {
+ pixel.fire(SyncPixelName.SYNC_USER_SWITCHED_LOGOUT_ERROR)
+ }
+
companion object {
private const val SYNC_PIXELS_PREF_FILE = "com.duckduckgo.sync.pixels.v1"
}
@@ -302,6 +333,12 @@ enum class SyncPixelName(override val pixelName: String) : Pixel.PixelName {
SYNC_GET_OTHER_DEVICES_SCREEN_SHOWN("sync_get_other_devices"),
SYNC_GET_OTHER_DEVICES_LINK_COPIED("sync_get_other_devices_copy"),
SYNC_GET_OTHER_DEVICES_LINK_SHARED("sync_get_other_devices_share"),
+ SYNC_ASK_USER_TO_SWITCH_ACCOUNT("sync_ask_user_to_switch_account"),
+ SYNC_USER_ACCEPTED_SWITCHING_ACCOUNT("sync_user_accepted_switching_account"),
+ SYNC_USER_CANCELLED_SWITCHING_ACCOUNT("sync_user_cancelled_switching_account"),
+ SYNC_USER_SWITCHED_ACCOUNT("sync_user_switched_account"),
+ SYNC_USER_SWITCHED_LOGOUT_ERROR("sync_user_switched_logout_error"),
+ SYNC_USER_SWITCHED_LOGIN_ERROR("sync_user_switched_login_error"),
}
object SyncPixelParameters {
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt
index c48dc8757a19..63054c345580 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt
@@ -131,6 +131,7 @@ class EnterCodeActivity : DuckDuckGoActivity() {
}
private fun askUserToSwitchAccount(it: AskToSwitchAccount) {
+ viewModel.onUserAskedToSwitchAccount()
TextAlertDialogBuilder(this)
.setTitle(R.string.sync_dialog_switch_account_header)
.setMessage(R.string.sync_dialog_switch_account_description)
@@ -141,6 +142,10 @@ class EnterCodeActivity : DuckDuckGoActivity() {
override fun onPositiveButtonClicked() {
viewModel.onUserAcceptedJoiningNewAccount(it.encodedStringCode)
}
+
+ override fun onNegativeButtonClicked() {
+ viewModel.onUserCancelledJoiningNewAccount()
+ }
},
).show()
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt
index 7ca9026cadf4..7a505bca3fa2 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt
@@ -35,6 +35,7 @@ import com.duckduckgo.sync.impl.SyncAccountRepository
import com.duckduckgo.sync.impl.SyncFeature
import com.duckduckgo.sync.impl.pixels.SyncPixels
import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.AskToSwitchAccount
+import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.LoginSuccess
import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.ShowError
import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.SwitchAccountSuccess
import javax.inject.*
@@ -90,9 +91,17 @@ class EnterCodeViewModel @Inject constructor(
private suspend fun authFlow(
pastedCode: String,
) {
- val result = syncAccountRepository.processCode(pastedCode)
- when (result) {
- is Result.Success -> command.send(Command.LoginSuccess)
+ val userSignedIn = syncAccountRepository.isSignedIn()
+ when (val result = syncAccountRepository.processCode(pastedCode)) {
+ is Result.Success -> {
+ val commandSuccess = if (userSignedIn) {
+ syncPixels.fireUserSwitchedAccount()
+ SwitchAccountSuccess
+ } else {
+ LoginSuccess
+ }
+ command.send(commandSuccess)
+ }
is Result.Error -> {
processError(result, pastedCode)
}
@@ -128,6 +137,7 @@ class EnterCodeViewModel @Inject constructor(
fun onUserAcceptedJoiningNewAccount(encodedStringCode: String) {
viewModelScope.launch(dispatchers.io()) {
+ syncPixels.fireUserAcceptedSwitchingAccount()
val result = syncAccountRepository.logoutAndJoinNewAccount(encodedStringCode)
if (result is Error) {
when (result.code) {
@@ -143,9 +153,17 @@ class EnterCodeViewModel @Inject constructor(
)
}
} else {
- syncPixels.fireLoginPixel()
+ syncPixels.fireUserSwitchedAccount()
command.send(SwitchAccountSuccess)
}
}
}
+
+ fun onUserCancelledJoiningNewAccount() {
+ syncPixels.fireUserCancelledSwitchingAccount()
+ }
+
+ fun onUserAskedToSwitchAccount() {
+ syncPixels.fireAskUserToSwitchAccount()
+ }
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt
index 46ee532a5673..0c186be3ea44 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt
@@ -136,6 +136,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
fun onQRCodeScanned(qrCode: String) {
viewModelScope.launch(dispatchers.io()) {
+ val userSignedIn = syncAccountRepository.isSignedIn()
when (val result = syncAccountRepository.processCode(qrCode)) {
is Error -> {
emitError(result, qrCode)
@@ -143,7 +144,13 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
is Success -> {
syncPixels.fireLoginPixel()
- command.send(LoginSuccess)
+ val commandSuccess = if (userSignedIn) {
+ syncPixels.fireUserSwitchedAccount()
+ SwitchAccountSuccess
+ } else {
+ LoginSuccess
+ }
+ command.send(commandSuccess)
}
}
}
@@ -175,6 +182,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
fun onUserAcceptedJoiningNewAccount(encodedStringCode: String) {
viewModelScope.launch(dispatchers.io()) {
+ syncPixels.fireUserAcceptedSwitchingAccount()
val result = syncAccountRepository.logoutAndJoinNewAccount(encodedStringCode)
if (result is Error) {
when (result.code) {
@@ -191,6 +199,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
}
} else {
syncPixels.fireLoginPixel()
+ syncPixels.fireUserSwitchedAccount()
command.send(SwitchAccountSuccess)
}
}
@@ -211,4 +220,12 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
}
}
}
+
+ fun onUserCancelledJoiningNewAccount() {
+ syncPixels.fireUserCancelledSwitchingAccount()
+ }
+
+ fun onUserAskedToSwitchAccount() {
+ syncPixels.fireAskUserToSwitchAccount()
+ }
}
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt
index 8883523a1e80..3a51582e64f9 100644
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt
@@ -133,6 +133,7 @@ class SyncWithAnotherDeviceActivity : DuckDuckGoActivity() {
}
private fun askUserToSwitchAccount(it: AskToSwitchAccount) {
+ viewModel.onUserAskedToSwitchAccount()
TextAlertDialogBuilder(this)
.setTitle(R.string.sync_dialog_switch_account_header)
.setMessage(R.string.sync_dialog_switch_account_description)
@@ -143,6 +144,10 @@ class SyncWithAnotherDeviceActivity : DuckDuckGoActivity() {
override fun onPositiveButtonClicked() {
viewModel.onUserAcceptedJoiningNewAccount(it.encodedStringCode)
}
+
+ override fun onNegativeButtonClicked() {
+ viewModel.onUserCancelledJoiningNewAccount()
+ }
},
).show()
}
diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt
index 9bbdc1f225a7..120d523b784c 100644
--- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt
+++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt
@@ -140,7 +140,8 @@ object TestSyncFixtures {
)
val loginSuccessResponse: Response = Response.success(loginResponseBody)
- val listOfDevices = listOf(Device(deviceId = deviceId, deviceName = deviceName, jwIat = "", deviceType = deviceFactor))
+ val aDevice = Device(deviceId = deviceId, deviceName = deviceName, jwIat = "", deviceType = deviceFactor)
+ val listOfDevices = listOf(aDevice)
val deviceResponse = DeviceResponse(DeviceEntries(listOfDevices))
val getDevicesBodySuccessResponse: Response = Response.success(deviceResponse)
val getDevicesBodyErrorResponse: Response = Response.error(
diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt
index 9fd80aba10b7..09c5d17cfc96 100644
--- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt
+++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt
@@ -18,6 +18,9 @@ package com.duckduckgo.sync.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.common.utils.DefaultDispatcherProvider
+import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
+import com.duckduckgo.feature.toggles.api.Toggle.State
+import com.duckduckgo.sync.TestSyncFixtures.aDevice
import com.duckduckgo.sync.TestSyncFixtures.accountCreatedFailDupUser
import com.duckduckgo.sync.TestSyncFixtures.accountCreatedSuccess
import com.duckduckgo.sync.TestSyncFixtures.accountKeys
@@ -65,9 +68,8 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
import com.duckduckgo.sync.impl.Result.Error
import com.duckduckgo.sync.impl.Result.Success
-import com.duckduckgo.sync.impl.pixels.*
+import com.duckduckgo.sync.impl.pixels.SyncPixels
import com.duckduckgo.sync.store.SyncStore
-import java.lang.RuntimeException
import kotlinx.coroutines.test.TestScope
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -94,13 +96,25 @@ class AppSyncAccountRepositoryTest {
private var syncStore: SyncStore = mock()
private var syncEngine: SyncEngine = mock()
private var syncPixels: SyncPixels = mock()
+ private val syncFeature = FakeFeatureToggleFactory.create(SyncFeature::class.java).apply {
+ this.seamlessAccountSwitching().setRawStoredState(State(true))
+ }
private lateinit var syncRepo: SyncAccountRepository
@Before
fun before() {
- syncRepo =
- AppSyncAccountRepository(syncDeviceIds, nativeLib, syncApi, syncStore, syncEngine, syncPixels, TestScope(), DefaultDispatcherProvider())
+ syncRepo = AppSyncAccountRepository(
+ syncDeviceIds,
+ nativeLib,
+ syncApi,
+ syncStore,
+ syncEngine,
+ syncPixels,
+ TestScope(),
+ DefaultDispatcherProvider(),
+ syncFeature,
+ )
}
@Test
@@ -231,6 +245,39 @@ class AppSyncAccountRepositoryTest {
)
}
+ @Test
+ fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenSwitchAccountIfOnly1DeviceConnected() {
+ givenAuthenticatedDevice()
+ givenAccountWithConnectedDevices(1)
+ doAnswer {
+ givenUnauthenticatedDevice() // simulate logout locally
+ logoutSuccess
+ }.`when`(syncApi).logout(token, deviceId)
+ prepareForLoginSuccess()
+
+ val result = syncRepo.processCode(jsonRecoveryKeyEncoded)
+
+ verify(syncApi).logout(token, deviceId)
+ verify(syncApi).login(userId, hashedPassword, deviceId, deviceName, deviceFactor)
+
+ assertTrue(result is Success)
+ }
+
+ @Test
+ fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenReturnErrorIfMultipleDevicesConnected() {
+ givenAuthenticatedDevice()
+ givenAccountWithConnectedDevices(2)
+ doAnswer {
+ givenUnauthenticatedDevice() // simulate logout locally
+ logoutSuccess
+ }.`when`(syncApi).logout(token, deviceId)
+ prepareForLoginSuccess()
+
+ val result = syncRepo.processCode(jsonRecoveryKeyEncoded)
+
+ assertEquals((result as Error).code, ALREADY_SIGNED_IN.code)
+ }
+
@Test
fun whenLogoutAndJoinNewAccountSucceedsThenReturnSuccess() {
givenAuthenticatedDevice()
@@ -573,4 +620,15 @@ class AppSyncAccountRepositoryTest {
EncryptResult(0, it.arguments.first() as String)
}
}
+
+ private fun givenAccountWithConnectedDevices(size: Int) {
+ prepareForEncryption()
+ val listOfDevices = mutableListOf()
+ for (i in 0 until size) {
+ listOfDevices.add(aDevice.copy(deviceId = "device$i"))
+ }
+ whenever(syncApi.getDevices(anyString())).thenReturn(Success(listOfDevices))
+
+ syncRepo.getConnectedDevices() as Success
+ }
}
diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt
index eed3a058b789..4979289f791b 100644
--- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt
+++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt
@@ -47,6 +47,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -173,6 +174,34 @@ internal class EnterCodeViewModelTest {
}
}
+ @Test
+ fun whenSignedInUserScansRecoveryCodeAndLoginSucceedsThenReturnSwitchAccount() = runTest {
+ whenever(syncAccountRepository.isSignedIn()).thenReturn(true)
+ whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true))
+ whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded)
+
+ testee.commands().test {
+ testee.onPasteCodeClicked()
+ val command = awaitItem()
+ assertTrue(command is SwitchAccountSuccess)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest {
+ whenever(syncAccountRepository.isSignedIn()).thenReturn(false)
+ whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true))
+ whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded)
+
+ testee.commands().test {
+ testee.onPasteCodeClicked()
+ val command = awaitItem()
+ assertTrue(command is LoginSuccess)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
@Test
fun whenProcessCodeAndLoginFailsThenShowError() = runTest {
whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded)
diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt
index 6b408e780542..9fd327b4edd6 100644
--- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt
+++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt
@@ -168,6 +168,34 @@ class SyncWithAnotherDeviceViewModelTest {
}
}
+ @Test
+ fun whenSignedInUserScansRecoveryCodeAndLoginSucceedsThenReturnSwitchAccount() = runTest {
+ whenever(syncRepository.isSignedIn()).thenReturn(true)
+ whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true))
+
+ testee.commands().test {
+ testee.onQRCodeScanned(jsonRecoveryKeyEncoded)
+ val command = awaitItem()
+ assertTrue(command is SwitchAccountSuccess)
+ verify(syncPixels, times(1)).fireLoginPixel()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest {
+ whenever(syncRepository.isSignedIn()).thenReturn(false)
+ whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true))
+
+ testee.commands().test {
+ testee.onQRCodeScanned(jsonRecoveryKeyEncoded)
+ val command = awaitItem()
+ assertTrue(command is LoginSuccess)
+ verify(syncPixels, times(1)).fireLoginPixel()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
@Test
fun whenUserScansRecoveryQRCodeAndConnectDeviceFailsThenCommandIsError() = runTest {
whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = LOGIN_FAILED.code))