Skip to content

Commit d2dcea8

Browse files
committed
Support scanning URL-based sync setup codes
1 parent 2ada4fb commit d2dcea8

11 files changed

+189
-96
lines changed

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

+61-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,67 @@ 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+
156157
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 {
158+
val decodedCode = code.decodeB64()
159+
160+
canParseAsRecoveryCode(decodedCode)?.let {
161+
if (wasInUrl) {
162+
throw IllegalArgumentException("cdr Sync: Recovery code found inside a URL which is not acceptable")
163+
} else {
164+
Recovery(it)
165+
}
166+
}
167+
?: canParseAsExchangeCode(decodedCode)?.let { Exchange(it) }
168+
?: canParseAsConnectCode(decodedCode)?.let { Connect(it) }
169+
?: Unknown(code)
170+
}.onSuccess {
171+
Timber.i("cdr Sync: code type is ${it.javaClass.simpleName}. was inside url: $wasInUrl")
172+
}.getOrElse {
165173
Timber.e(it, "Failed to decode code")
166-
}.getOrDefault(UNKNOWN)
174+
Unknown(code)
175+
}
167176
}
168177

178+
private fun canParseAsRecoveryCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery
179+
private fun canParseAsExchangeCode(decodedCode: String) = Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey
180+
private fun canParseAsConnectCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect
181+
169182
private fun onInvitationCodeReceived(invitationCode: InvitationCode): Result<Boolean> {
170183
// Sync: InviteFlow - B (https://app.asana.com/0/72649045549333/1209571867429615)
171184
Timber.d("Sync-exchange: InviteFlow - B. code is an exchange code $invitationCode")
@@ -596,7 +609,8 @@ class AppSyncAccountRepository @Inject constructor(
596609
}
597610

598611
is Success -> {
599-
val loginResult = processCode(stringCode)
612+
val codeType = getCodeType(stringCode)
613+
val loginResult = processCode(codeType)
600614
if (loginResult is Error) {
601615
syncPixels.fireUserSwitchedLoginError()
602616
}
@@ -882,11 +896,11 @@ enum class AccountErrorCodes(val code: Int) {
882896
EXCHANGE_FAILED(56),
883897
}
884898

885-
enum class CodeType {
886-
RECOVERY,
887-
CONNECT,
888-
EXCHANGE,
889-
UNKNOWN,
899+
sealed interface CodeType {
900+
data class Recovery(val code: RecoveryCode) : CodeType
901+
data class Connect(val code: ConnectCode) : CodeType
902+
data class Exchange(val code: InvitationCode) : CodeType
903+
data class Unknown(val code: String) : CodeType
890904
}
891905

892906
sealed class Result<out R> {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ 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 -> {
106106
if (codeType == EXCHANGE) {
107107
pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, code = pastedCode)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ 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
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
185185
viewModelScope.launch(dispatchers.io()) {
186186
val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey
187187
val codeType = syncAccountRepository.getCodeType(qrCode)
188-
when (val result = syncAccountRepository.processCode(qrCode)) {
188+
when (val result = syncAccountRepository.processCode(codeType)) {
189189
is Error -> {
190190
Timber.w("Sync: error processing code ${result.reason}")
191191
emitError(result, qrCode)

0 commit comments

Comments
 (0)