Skip to content

Commit d40f734

Browse files
committed
Support scanning URL-based sync setup codes
1 parent 860b063 commit d40f734

12 files changed

+207
-106
lines changed

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt

+66-47
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.EXCHANGE_FAILED
3333
import com.duckduckgo.sync.impl.AccountErrorCodes.GENERIC_ERROR
3434
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
3535
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
36-
import com.duckduckgo.sync.impl.CodeType.UNKNOWN
36+
import com.duckduckgo.sync.impl.CodeType.Connect
37+
import com.duckduckgo.sync.impl.CodeType.Exchange
38+
import com.duckduckgo.sync.impl.CodeType.Recovery
39+
import com.duckduckgo.sync.impl.CodeType.Unknown
3740
import com.duckduckgo.sync.impl.ExchangeResult.*
3841
import com.duckduckgo.sync.impl.Result.Error
3942
import com.duckduckgo.sync.impl.Result.Success
4043
import com.duckduckgo.sync.impl.pixels.*
44+
import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeUrl
4145
import com.duckduckgo.sync.store.*
4246
import com.squareup.anvil.annotations.*
4347
import com.squareup.moshi.*
@@ -54,7 +58,7 @@ interface SyncAccountRepository {
5458
fun isSyncSupported(): Boolean
5559
fun createAccount(): Result<Boolean>
5660
fun isSignedIn(): Boolean
57-
fun processCode(stringCode: String): Result<Boolean>
61+
fun processCode(code: CodeType): Result<Boolean>
5862
fun getAccountInfo(): AccountInfo
5963
fun logout(deviceId: String): Result<Boolean>
6064
fun deleteAccount(): Result<Boolean>
@@ -114,58 +118,72 @@ class AppSyncAccountRepository @Inject constructor(
114118
}
115119
}
116120

117-
override fun processCode(stringCode: String): Result<Boolean> {
118-
val decodedCode: String? = kotlin.runCatching {
119-
return@runCatching stringCode.decodeB64()
120-
}.getOrNull()
121-
if (decodedCode == null) {
122-
Timber.w("Failed while b64 decoding barcode; barcode is unusable")
123-
return Error(code = INVALID_CODE.code, reason = "Failed to decode code")
124-
}
125-
126-
kotlin.runCatching {
127-
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery
128-
}.getOrNull()?.let {
129-
Timber.d("Sync: code is a recovery code")
130-
return login(it)
131-
}
121+
override fun processCode(code: CodeType): Result<Boolean> {
122+
when (code) {
123+
is Recovery -> {
124+
Timber.d("Sync: code is a recovery code")
125+
return login(code.code)
126+
}
132127

133-
kotlin.runCatching {
134-
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect
135-
}.getOrNull()?.let {
136-
Timber.d("Sync: code is a connect code")
137-
return connectDevice(it)
138-
}
128+
is Connect -> {
129+
Timber.d("Sync: code is a connect code")
130+
return connectDevice(code.code)
131+
}
139132

140-
kotlin.runCatching {
141-
Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey
142-
}.getOrNull()?.let {
143-
if (!syncFeature.exchangeKeysToSyncWithAnotherDevice().isEnabled()) {
144-
Timber.w("Sync: Scanned exchange code type but exchanging keys to sync with another device is disabled")
145-
return@let null
133+
is Exchange -> {
134+
if (!syncFeature.exchangeKeysToSyncWithAnotherDevice().isEnabled()) {
135+
Timber.w("Sync: Scanned exchange code type but exchanging keys to sync with another device is disabled")
136+
} else {
137+
return onInvitationCodeReceived(code.code)
138+
}
146139
}
147140

148-
return onInvitationCodeReceived(it)
141+
else -> {
142+
Timber.d("Sync: code type unknown")
143+
}
149144
}
150-
151-
Timber.e("Sync: code is not supported")
145+
Timber.e("Sync: code type (${code.javaClass.simpleName}) is not supported")
152146
return Error(code = INVALID_CODE.code, reason = "Failed to decode code")
153147
}
154148

155149
override fun getCodeType(stringCode: String): CodeType {
150+
// check first if it's a URL which contains the code
151+
val (code, wasInUrl) = kotlin.runCatching {
152+
SyncBarcodeUrl.parseUrl(stringCode)?.webSafeB64EncodedCode?.removeUrlSafetyToRestoreB64()
153+
?.let { Pair(it, true) }
154+
?: Pair(stringCode, false)
155+
}.getOrDefault(Pair(stringCode, false))
156+
157+
if (wasInUrl && syncFeature.canScanUrlBasedSyncSetupBarcodes().isEnabled().not()) {
158+
Timber.e("Feature to allow scanning URL-based sync setup codes is disabled")
159+
return Unknown(code)
160+
}
161+
156162
return kotlin.runCatching {
157-
val decodedCode = stringCode.decodeB64()
158-
when {
159-
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery != null -> CodeType.RECOVERY
160-
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect != null -> CodeType.CONNECT
161-
Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey != null -> CodeType.EXCHANGE
162-
else -> UNKNOWN
163-
}
164-
}.onFailure {
163+
val decodedCode = code.decodeB64()
164+
165+
canParseAsRecoveryCode(decodedCode)?.let {
166+
if (wasInUrl) {
167+
throw IllegalArgumentException("cdr Sync: Recovery code found inside a URL which is not acceptable")
168+
} else {
169+
Recovery(it)
170+
}
171+
}
172+
?: canParseAsExchangeCode(decodedCode)?.let { Exchange(it) }
173+
?: canParseAsConnectCode(decodedCode)?.let { Connect(it) }
174+
?: Unknown(code)
175+
}.onSuccess {
176+
Timber.i("cdr Sync: code type is ${it.javaClass.simpleName}. was inside url: $wasInUrl")
177+
}.getOrElse {
165178
Timber.e(it, "Failed to decode code")
166-
}.getOrDefault(UNKNOWN)
179+
Unknown(code)
180+
}
167181
}
168182

183+
private fun canParseAsRecoveryCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery
184+
private fun canParseAsExchangeCode(decodedCode: String) = Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey
185+
private fun canParseAsConnectCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect
186+
169187
private fun onInvitationCodeReceived(invitationCode: InvitationCode): Result<Boolean> {
170188
// Sync: InviteFlow - B (https://app.asana.com/0/72649045549333/1209571867429615)
171189
Timber.d("Sync-exchange: InviteFlow - B. code is an exchange code $invitationCode")
@@ -596,7 +614,8 @@ class AppSyncAccountRepository @Inject constructor(
596614
}
597615

598616
is Success -> {
599-
val loginResult = processCode(stringCode)
617+
val codeType = getCodeType(stringCode)
618+
val loginResult = processCode(codeType)
600619
if (loginResult is Error) {
601620
syncPixels.fireUserSwitchedLoginError()
602621
}
@@ -882,11 +901,11 @@ enum class AccountErrorCodes(val code: Int) {
882901
EXCHANGE_FAILED(56),
883902
}
884903

885-
enum class CodeType {
886-
RECOVERY,
887-
CONNECT,
888-
EXCHANGE,
889-
UNKNOWN,
904+
sealed interface CodeType {
905+
data class Recovery(val code: RecoveryCode) : CodeType
906+
data class Connect(val code: ConnectCode) : CodeType
907+
data class Exchange(val code: InvitationCode) : CodeType
908+
data class Unknown(val code: String) : CodeType
890909
}
891910

892911
sealed class Result<out R> {

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt

+3
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,7 @@ interface SyncFeature {
5656

5757
@Toggle.DefaultValue(false)
5858
fun syncSetupBarcodeIsUrlBased(): Toggle
59+
60+
@Toggle.DefaultValue(true)
61+
fun canScanUrlBasedSyncSetupBarcodes(): Toggle
5962
}

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
2828
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
2929
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
3030
import com.duckduckgo.sync.impl.Clipboard
31-
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
31+
import com.duckduckgo.sync.impl.CodeType
3232
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
3333
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
3434
import com.duckduckgo.sync.impl.ExchangeResult.Pending
@@ -101,9 +101,9 @@ class EnterCodeViewModel @Inject constructor(
101101
) {
102102
val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey
103103
val codeType = syncAccountRepository.getCodeType(pastedCode)
104-
when (val result = syncAccountRepository.processCode(pastedCode)) {
104+
when (val result = syncAccountRepository.processCode(codeType)) {
105105
is Result.Success -> {
106-
if (codeType == EXCHANGE) {
106+
if (codeType is CodeType.Exchange) {
107107
pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, code = pastedCode)
108108
} else {
109109
onLoginSuccess(previousPrimaryKey)

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
2929
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
3030
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
3131
import com.duckduckgo.sync.impl.Clipboard
32-
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
32+
import com.duckduckgo.sync.impl.CodeType.Exchange
3333
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
3434
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
3535
import com.duckduckgo.sync.impl.ExchangeResult.Pending
@@ -178,13 +178,13 @@ class SyncConnectViewModel @Inject constructor(
178178
fun onQRCodeScanned(qrCode: String) {
179179
viewModelScope.launch(dispatchers.io()) {
180180
val codeType = syncAccountRepository.getCodeType(qrCode)
181-
when (val result = syncAccountRepository.processCode(qrCode)) {
181+
when (val result = syncAccountRepository.processCode(codeType)) {
182182
is Error -> {
183183
processError(result)
184184
}
185185

186186
is Success -> {
187-
if (codeType == EXCHANGE) {
187+
if (codeType is Exchange) {
188188
pollForRecoveryKey()
189189
} else {
190190
syncPixels.fireLoginPixel()

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncInternalSettingsViewModel.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ constructor(
212212

213213
fun onQRScanned(contents: String) {
214214
viewModelScope.launch(dispatchers.io()) {
215-
val result = syncAccountRepository.processCode(contents)
215+
val codeType = syncAccountRepository.getCodeType(contents)
216+
val result = syncAccountRepository.processCode(codeType)
216217
if (result is Error) {
217218
command.send(Command.ShowMessage("$result"))
218219
}
@@ -222,7 +223,8 @@ constructor(
222223

223224
fun onConnectQRScanned(contents: String) {
224225
viewModelScope.launch(dispatchers.io()) {
225-
val result = syncAccountRepository.processCode(contents)
226+
val codeType = syncAccountRepository.getCodeType(contents)
227+
val result = syncAccountRepository.processCode(codeType)
226228
when (result) {
227229
is Error -> {
228230
command.send(Command.ShowMessage("$result"))
@@ -291,7 +293,8 @@ constructor(
291293
private suspend fun authFlow(
292294
pastedCode: String,
293295
) {
294-
val result = syncAccountRepository.processCode(pastedCode)
296+
val codeType = syncAccountRepository.getCodeType(pastedCode)
297+
val result = syncAccountRepository.processCode(codeType)
295298
when (result) {
296299
is Result.Success -> command.send(Command.LoginSuccess)
297300
is Result.Error -> {

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CONNECT_FAILED
2727
import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
2828
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
2929
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
30-
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
30+
import com.duckduckgo.sync.impl.CodeType
3131
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
3232
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
3333
import com.duckduckgo.sync.impl.ExchangeResult.Pending
@@ -88,13 +88,13 @@ class SyncLoginViewModel @Inject constructor(
8888
fun onQRCodeScanned(qrCode: String) {
8989
viewModelScope.launch(dispatchers.io()) {
9090
val codeType = syncAccountRepository.getCodeType(qrCode)
91-
when (val result = syncAccountRepository.processCode(qrCode)) {
91+
when (val result = syncAccountRepository.processCode(codeType)) {
9292
is Error -> {
9393
processError(result)
9494
}
9595

9696
is Success -> {
97-
if (codeType == EXCHANGE) {
97+
if (codeType is CodeType.Exchange) {
9898
pollForRecoveryKey()
9999
} else {
100100
syncPixels.fireLoginPixel()

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
2929
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
3030
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
3131
import com.duckduckgo.sync.impl.Clipboard
32-
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
32+
import com.duckduckgo.sync.impl.CodeType
3333
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
3434
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
3535
import com.duckduckgo.sync.impl.ExchangeResult.Pending
@@ -178,14 +178,14 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
178178
viewModelScope.launch(dispatchers.io()) {
179179
val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey
180180
val codeType = syncAccountRepository.getCodeType(qrCode)
181-
when (val result = syncAccountRepository.processCode(qrCode)) {
181+
when (val result = syncAccountRepository.processCode(codeType)) {
182182
is Error -> {
183183
Timber.w("Sync: error processing code ${result.reason}")
184184
emitError(result, qrCode)
185185
}
186186

187187
is Success -> {
188-
if (codeType == EXCHANGE) {
188+
if (codeType is CodeType.Exchange) {
189189
pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, qrCode = qrCode)
190190
} else {
191191
onLoginSuccess(previousPrimaryKey)

0 commit comments

Comments
 (0)