Skip to content

Support scanning URL-based sync setup codes #5957

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

Open
wants to merge 1 commit into
base: feature/craig/sync_barcode_url_support
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.EXCHANGE_FAILED
import com.duckduckgo.sync.impl.AccountErrorCodes.GENERIC_ERROR
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
import com.duckduckgo.sync.impl.CodeType.UNKNOWN
import com.duckduckgo.sync.impl.CodeType.Connect
import com.duckduckgo.sync.impl.CodeType.Exchange
import com.duckduckgo.sync.impl.CodeType.Recovery
import com.duckduckgo.sync.impl.CodeType.Unknown
import com.duckduckgo.sync.impl.ExchangeResult.*
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.ui.qrcode.SyncBarcodeUrl
import com.duckduckgo.sync.store.*
import com.squareup.anvil.annotations.*
import com.squareup.moshi.*
Expand All @@ -54,7 +58,7 @@ interface SyncAccountRepository {
fun isSyncSupported(): Boolean
fun createAccount(): Result<Boolean>
fun isSignedIn(): Boolean
fun processCode(stringCode: String): Result<Boolean>
fun processCode(code: CodeType): Result<Boolean>
fun getAccountInfo(): AccountInfo
fun logout(deviceId: String): Result<Boolean>
fun deleteAccount(): Result<Boolean>
Expand Down Expand Up @@ -114,58 +118,72 @@ class AppSyncAccountRepository @Inject constructor(
}
}

override fun processCode(stringCode: String): Result<Boolean> {
val decodedCode: String? = kotlin.runCatching {
return@runCatching stringCode.decodeB64()
}.getOrNull()
if (decodedCode == null) {
Timber.w("Failed while b64 decoding barcode; barcode is unusable")
return Error(code = INVALID_CODE.code, reason = "Failed to decode code")
}

kotlin.runCatching {
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery
}.getOrNull()?.let {
Timber.d("Sync: code is a recovery code")
return login(it)
}
override fun processCode(code: CodeType): Result<Boolean> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now takes in the parsed code so we don’t have to parse it from JSON twice any more

when (code) {
is Recovery -> {
Timber.d("Sync: code is a recovery code")
return login(code.code)
}

kotlin.runCatching {
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect
}.getOrNull()?.let {
Timber.d("Sync: code is a connect code")
return connectDevice(it)
}
is Connect -> {
Timber.d("Sync: code is a connect code")
return connectDevice(code.code)
}

kotlin.runCatching {
Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey
}.getOrNull()?.let {
if (!syncFeature.exchangeKeysToSyncWithAnotherDevice().isEnabled()) {
Timber.w("Sync: Scanned exchange code type but exchanging keys to sync with another device is disabled")
return@let null
is Exchange -> {
if (!syncFeature.exchangeKeysToSyncWithAnotherDevice().isEnabled()) {
Timber.w("Sync: Scanned exchange code type but exchanging keys to sync with another device is disabled")
} else {
return onInvitationCodeReceived(code.code)
}
}

return onInvitationCodeReceived(it)
else -> {
Timber.d("Sync: code type unknown")
}
}

Timber.e("Sync: code is not supported")
Timber.e("Sync: code type (${code.javaClass.simpleName}) is not supported")
return Error(code = INVALID_CODE.code, reason = "Failed to decode code")
}

override fun getCodeType(stringCode: String): CodeType {
// check first if it's a URL which contains the code
val (code, wasInUrl) = kotlin.runCatching {
SyncBarcodeUrl.parseUrl(stringCode)?.webSafeB64EncodedCode?.removeUrlSafetyToRestoreB64()
?.let { Pair(it, true) }
?: Pair(stringCode, false)
}.getOrDefault(Pair(stringCode, false))

if (wasInUrl && syncFeature.canScanUrlBasedSyncSetupBarcodes().isEnabled().not()) {
Timber.e("Feature to allow scanning URL-based sync setup codes is disabled")
return Unknown(code)
}

return kotlin.runCatching {
val decodedCode = stringCode.decodeB64()
when {
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery != null -> CodeType.RECOVERY
Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect != null -> CodeType.CONNECT
Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey != null -> CodeType.EXCHANGE
else -> UNKNOWN
}
}.onFailure {
val decodedCode = code.decodeB64()

canParseAsRecoveryCode(decodedCode)?.let {
if (wasInUrl) {
throw IllegalArgumentException("Sync: Recovery code found inside a URL which is not acceptable")
} else {
Recovery(it)
}
}
?: canParseAsExchangeCode(decodedCode)?.let { Exchange(it) }
?: canParseAsConnectCode(decodedCode)?.let { Connect(it) }
?: Unknown(code)
}.onSuccess {
Timber.i("Sync: code type is ${it.javaClass.simpleName}. was inside url: $wasInUrl")
}.getOrElse {
Timber.e(it, "Failed to decode code")
}.getOrDefault(UNKNOWN)
Unknown(code)
}
}

private fun canParseAsRecoveryCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery
private fun canParseAsExchangeCode(decodedCode: String) = Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey
private fun canParseAsConnectCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect

private fun onInvitationCodeReceived(invitationCode: InvitationCode): Result<Boolean> {
// Sync: InviteFlow - B (https://app.asana.com/0/72649045549333/1209571867429615)
Timber.d("Sync-exchange: InviteFlow - B. code is an exchange code $invitationCode")
Expand Down Expand Up @@ -596,7 +614,8 @@ class AppSyncAccountRepository @Inject constructor(
}

is Success -> {
val loginResult = processCode(stringCode)
val codeType = getCodeType(stringCode)
val loginResult = processCode(codeType)
if (loginResult is Error) {
syncPixels.fireUserSwitchedLoginError()
}
Expand Down Expand Up @@ -882,11 +901,11 @@ enum class AccountErrorCodes(val code: Int) {
EXCHANGE_FAILED(56),
}

enum class CodeType {
RECOVERY,
CONNECT,
EXCHANGE,
UNKNOWN,
sealed interface CodeType {
data class Recovery(val code: RecoveryCode) : CodeType
data class Connect(val code: ConnectCode) : CodeType
data class Exchange(val code: InvitationCode) : CodeType
data class Unknown(val code: String) : CodeType
}

sealed class Result<out R> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ interface SyncFeature {

@Toggle.DefaultValue(false)
fun syncSetupBarcodeIsUrlBased(): Toggle

@Toggle.DefaultValue(true)
fun canScanUrlBasedSyncSetupBarcodes(): Toggle
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
import com.duckduckgo.sync.impl.Clipboard
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
import com.duckduckgo.sync.impl.CodeType
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
import com.duckduckgo.sync.impl.ExchangeResult.Pending
Expand Down Expand Up @@ -101,9 +101,9 @@ class EnterCodeViewModel @Inject constructor(
) {
val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey
val codeType = syncAccountRepository.getCodeType(pastedCode)
when (val result = syncAccountRepository.processCode(pastedCode)) {
when (val result = syncAccountRepository.processCode(codeType)) {
is Result.Success -> {
if (codeType == EXCHANGE) {
if (codeType is CodeType.Exchange) {
pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, code = pastedCode)
} else {
onLoginSuccess(previousPrimaryKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
import com.duckduckgo.sync.impl.Clipboard
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
import com.duckduckgo.sync.impl.CodeType.Exchange
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
import com.duckduckgo.sync.impl.ExchangeResult.Pending
Expand Down Expand Up @@ -178,13 +178,13 @@ class SyncConnectViewModel @Inject constructor(
fun onQRCodeScanned(qrCode: String) {
viewModelScope.launch(dispatchers.io()) {
val codeType = syncAccountRepository.getCodeType(qrCode)
when (val result = syncAccountRepository.processCode(qrCode)) {
when (val result = syncAccountRepository.processCode(codeType)) {
is Error -> {
processError(result)
}

is Success -> {
if (codeType == EXCHANGE) {
if (codeType is Exchange) {
pollForRecoveryKey()
} else {
syncPixels.fireLoginPixel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ constructor(

fun onQRScanned(contents: String) {
viewModelScope.launch(dispatchers.io()) {
val result = syncAccountRepository.processCode(contents)
val codeType = syncAccountRepository.getCodeType(contents)
val result = syncAccountRepository.processCode(codeType)
if (result is Error) {
command.send(Command.ShowMessage("$result"))
}
Expand All @@ -222,7 +223,8 @@ constructor(

fun onConnectQRScanned(contents: String) {
viewModelScope.launch(dispatchers.io()) {
val result = syncAccountRepository.processCode(contents)
val codeType = syncAccountRepository.getCodeType(contents)
val result = syncAccountRepository.processCode(codeType)
when (result) {
is Error -> {
command.send(Command.ShowMessage("$result"))
Expand Down Expand Up @@ -291,7 +293,8 @@ constructor(
private suspend fun authFlow(
pastedCode: String,
) {
val result = syncAccountRepository.processCode(pastedCode)
val codeType = syncAccountRepository.getCodeType(pastedCode)
val result = syncAccountRepository.processCode(codeType)
when (result) {
is Result.Success -> command.send(Command.LoginSuccess)
is Result.Error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CONNECT_FAILED
import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
import com.duckduckgo.sync.impl.CodeType
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
import com.duckduckgo.sync.impl.ExchangeResult.Pending
Expand Down Expand Up @@ -88,13 +88,13 @@ class SyncLoginViewModel @Inject constructor(
fun onQRCodeScanned(qrCode: String) {
viewModelScope.launch(dispatchers.io()) {
val codeType = syncAccountRepository.getCodeType(qrCode)
when (val result = syncAccountRepository.processCode(qrCode)) {
when (val result = syncAccountRepository.processCode(codeType)) {
is Error -> {
processError(result)
}

is Success -> {
if (codeType == EXCHANGE) {
if (codeType is CodeType.Exchange) {
pollForRecoveryKey()
} else {
syncPixels.fireLoginPixel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
import com.duckduckgo.sync.impl.Clipboard
import com.duckduckgo.sync.impl.CodeType.EXCHANGE
import com.duckduckgo.sync.impl.CodeType
import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired
import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn
import com.duckduckgo.sync.impl.ExchangeResult.Pending
Expand Down Expand Up @@ -178,14 +178,14 @@ class SyncWithAnotherActivityViewModel @Inject constructor(
viewModelScope.launch(dispatchers.io()) {
val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey
val codeType = syncAccountRepository.getCodeType(qrCode)
when (val result = syncAccountRepository.processCode(qrCode)) {
when (val result = syncAccountRepository.processCode(codeType)) {
is Error -> {
Timber.w("Sync: error processing code ${result.reason}")
emitError(result, qrCode)
}

is Success -> {
if (codeType == EXCHANGE) {
if (codeType is CodeType.Exchange) {
pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, qrCode = qrCode)
} else {
onLoginSuccess(previousPrimaryKey)
Expand Down
Loading
Loading