From 2bb06063c7dcb11aefa6497358b73a3b95006943 Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 4 Nov 2025 15:51:01 -0600 Subject: [PATCH] PM-27817: Consolidate totp parsing with TotpUriUtils (#6122) --- .../feature/qrcodescan/QrCodeScanViewModel.kt | 93 +--------- .../qrcodescan/QrCodeScanViewModelTest.kt | 173 +----------------- .../feature/qrcodescan/QrCodeScanViewModel.kt | 121 ++++-------- .../qrcodescan/QrCodeScanViewModelTest.kt | 19 +- .../ui/platform/util/TotpUriUtils.kt | 7 + 5 files changed, 69 insertions(+), 344 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModel.kt index 1b1e7983da..de6f0da5b4 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModel.kt @@ -1,21 +1,14 @@ package com.x8bit.bitwarden.ui.vault.feature.qrcodescan -import android.net.Uri import com.bitwarden.ui.platform.base.BaseViewModel +import com.bitwarden.ui.platform.util.getTotpDataOrNull import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -private const val ALGORITHM = "algorithm" -private const val DIGITS = "digits" -private const val PERIOD = "period" -private const val SECRET = "secret" -private const val TOTP_CODE_PREFIX = "otpauth://totp" - /** - * Handles [QrCodeScanAction], - * and launches [QrCodeScanEvent] for the [QrCodeScanScreen]. + * Handles [QrCodeScanAction] and launches [QrCodeScanEvent] for the [QrCodeScanScreen]. */ @HiltViewModel class QrCodeScanViewModel @Inject constructor( @@ -33,83 +26,25 @@ class QrCodeScanViewModel @Inject constructor( } private fun handleCloseClick() { - sendEvent( - QrCodeScanEvent.NavigateBack, - ) + sendEvent(QrCodeScanEvent.NavigateBack) } private fun handleManualEntryTextClick() { - sendEvent( - QrCodeScanEvent.NavigateToManualCodeEntry, - ) + sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry) } // For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) { - var result: TotpCodeResult = TotpCodeResult.Success(action.qrCode) - val scannedCode = action.qrCode - - if (scannedCode.isBlank() || !scannedCode.startsWith(TOTP_CODE_PREFIX)) { - vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) - sendEvent(QrCodeScanEvent.NavigateBack) - return - } - - val scannedCodeUri = Uri.parse(scannedCode) - val secretValue = scannedCodeUri.getQueryParameter(SECRET) - if (secretValue == null || !secretValue.isBase32()) { - vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) - sendEvent(QrCodeScanEvent.NavigateBack) - return - } - - val values = scannedCodeUri.queryParameterNames - if (!areParametersValid(scannedCode, values)) { - result = TotpCodeResult.CodeScanningError() - } - - vaultRepository.emitTotpCodeResult(result) + val qrCode = action.qrCode + qrCode + .getTotpDataOrNull() + ?.let { vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(code = qrCode)) } + ?: run { vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) } sendEvent(QrCodeScanEvent.NavigateBack) } private fun handleCameraErrorReceive() { - sendEvent( - QrCodeScanEvent.NavigateToManualCodeEntry, - ) - } - - @Suppress("NestedBlockDepth", "MagicNumber") - private fun areParametersValid(scannedCode: String, parameters: Set): Boolean { - parameters.forEach { parameter -> - Uri.parse(scannedCode).getQueryParameter(parameter)?.let { value -> - when (parameter) { - DIGITS -> { - val digit = value.toInt() - if (digit > 10 || digit < 1) { - return false - } - } - - PERIOD -> { - val period = value.toInt() - if (period < 1) { - return false - } - } - - ALGORITHM -> { - val lowercaseAlgo = value.lowercase() - if (lowercaseAlgo != "sha1" && - lowercaseAlgo != "sha256" && - lowercaseAlgo != "sha512" - ) { - return false - } - } - } - } - } - return true + sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry) } } @@ -154,11 +89,3 @@ sealed class QrCodeScanAction { */ data object CameraSetupErrorReceive : QrCodeScanAction() } - -/** - * Checks if a string is using base32 digits. - */ -private fun String.isBase32(): Boolean { - val regex = ("^[A-Za-z2-7]+=*$").toRegex() - return regex.matches(this) -} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModelTest.kt index af781be123..c7ae00af47 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanViewModelTest.kt @@ -1,9 +1,9 @@ package com.x8bit.bitwarden.ui.vault.feature.qrcodescan -import android.net.Uri import app.cash.turbine.test import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.util.getTotpDataOrNull import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import io.mockk.every @@ -27,16 +27,15 @@ class QrCodeScanViewModelTest : BaseViewModelTest() { every { totpCodeFlow } returns totpTestCodeFlow every { emitTotpCodeResult(any()) } just runs } - private val uriMock = mockk() @BeforeEach fun setup() { - mockkStatic(Uri::class) + mockkStatic(String::getTotpDataOrNull) } @AfterEach fun tearDown() { - unmockkStatic(Uri::class) + unmockkStatic(String::getTotpDataOrNull) } @Test @@ -76,32 +75,11 @@ class QrCodeScanViewModelTest : BaseViewModelTest() { } @Test - fun `QrCodeScan should emit new code and NavigateBack with a valid code with all values`() = - runTest { - setupMockUri() - - val validCode = - "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&algorithm=sha256&digits=8&period=60" - val viewModel = createViewModel() - val result = TotpCodeResult.Success(validCode) - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(validCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit new code and NavigateBack without optional values`() = runTest { - setupMockUri( - queryParameterNames = setOf(SECRET), - ) - - val validCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP" + fun `QrCodeScanReceive with valid code should emit new code and NavigateBack`() = runTest { val viewModel = createViewModel() + val validCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP" val result = TotpCodeResult.Success(validCode) + every { validCode.getTotpDataOrNull() } returns mockk() viewModel.eventFlow.test { viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(validCode)) @@ -112,60 +90,11 @@ class QrCodeScanViewModelTest : BaseViewModelTest() { } @Test - fun `QrCodeScan should emit failure result and NavigateBack with invalid algorithm`() = - runTest { - setupMockUri(algorithm = "SHA-224") - - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&algorithm=sha224" - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit failure result and NavigateBack with invalid digits`() = runTest { - setupMockUri(digits = "11") - - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&digits=11" - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit failure result and NavigateBack with invalid period`() = runTest { - setupMockUri(period = "0") - - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&period=0" - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit failure result without correct prefix`() = runTest { - + fun `QrCodeScanReceive with invalid totp should emit failure result`() = runTest { val viewModel = createViewModel() val result = TotpCodeResult.CodeScanningError() val invalidCode = "nototpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP" + every { invalidCode.getTotpDataOrNull() } returns null viewModel.eventFlow.test { viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) @@ -175,94 +104,8 @@ class QrCodeScanViewModelTest : BaseViewModelTest() { } } - @Test - fun `QrCodeScan should emit failure result with non base32 secret`() = runTest { - setupMockUri(secret = "JBSWY3dpeHPK3PXP1") - - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP1" - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit failure result and NavigateBack without Secret`() = runTest { - setupMockUri(secret = null) - - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "otpauth://totp/Test:me" - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit failure result and NavigateBack if secret is empty`() = runTest { - setupMockUri(secret = "") - - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "otpauth://totp/Test:me?secret= " - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - @Test - fun `QrCodeScan should emit failure result and NavigateBack if code is empty`() = runTest { - val viewModel = createViewModel() - val result = TotpCodeResult.CodeScanningError() - val invalidCode = "" - - viewModel.eventFlow.test { - viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode)) - - verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) } - assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) - } - } - - private fun setupMockUri( - secret: String? = "JBSWY3dpeHPK3PXP", - algorithm: String = "SHA256", - digits: String = "8", - period: String = "60", - queryParameterNames: Set = setOf( - ALGORITHM, PERIOD, DIGITS, SECRET, - ), - ) { - every { Uri.parse(any()) } returns uriMock - every { uriMock.getQueryParameter(SECRET) } returns secret - every { uriMock.getQueryParameter(ALGORITHM) } returns algorithm - every { uriMock.getQueryParameter(DIGITS) } returns digits - every { uriMock.getQueryParameter(PERIOD) } returns period - every { uriMock.queryParameterNames } returns queryParameterNames - } - private fun createViewModel(): QrCodeScanViewModel = QrCodeScanViewModel( vaultRepository = vaultRepository, ) - - companion object { - private const val ALGORITHM = "algorithm" - private const val DIGITS = "digits" - private const val PERIOD = "period" - private const val SECRET = "secret" - } } diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModel.kt index 4dbf1e77e3..3cc496d6f0 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModel.kt @@ -1,9 +1,7 @@ package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan -import android.net.Uri import android.os.Parcelable -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase +import androidx.core.net.toUri import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult @@ -12,15 +10,14 @@ import com.bitwarden.authenticator.data.platform.repository.SettingsRepository import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager import com.bitwarden.ui.platform.base.BaseViewModel -import com.bitwarden.ui.platform.base.util.isBase32 +import com.bitwarden.ui.platform.util.getTotpDataOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.update import kotlinx.parcelize.Parcelize import javax.inject.Inject /** - * Handles [QrCodeScanAction], - * and launches [QrCodeScanEvent] for the [QrCodeScanScreen]. + * Handles [QrCodeScanAction] and launches [QrCodeScanEvent] for the [QrCodeScanScreen]. */ @HiltViewModel @Suppress("TooManyFunctions") @@ -77,15 +74,11 @@ class QrCodeScanViewModel @Inject constructor( } private fun handleCloseClick() { - sendEvent( - QrCodeScanEvent.NavigateBack, - ) + sendEvent(QrCodeScanEvent.NavigateBack) } private fun handleManualEntryTextClick() { - sendEvent( - QrCodeScanEvent.NavigateToManualCodeEntry, - ) + sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry) } private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) { @@ -103,96 +96,48 @@ class QrCodeScanViewModel @Inject constructor( // For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters private fun handleTotpUriReceive(scannedCode: String) { - val result = TotpCodeResult.TotpCodeScan(scannedCode) - val scannedCodeUri = Uri.parse(scannedCode) - val secretValue = scannedCodeUri - .getQueryParameter(TotpCodeManager.SECRET_PARAM) - .orEmpty() - .toUpperCase(Locale.current) + scannedCode + .getTotpDataOrNull() + ?.let { + val result = TotpCodeResult.TotpCodeScan(code = scannedCode) + if (authenticatorRepository.sharedCodesStateFlow.value.isSyncWithBitwardenEnabled) { + when (settingsRepository.defaultSaveOption) { + DefaultSaveOption.BITWARDEN_APP -> { + saveCodeToBitwardenAndNavigateBack(result = result) + } - if (secretValue.isEmpty() || !secretValue.isBase32()) { - authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError) - sendEvent(QrCodeScanEvent.NavigateBack) - return - } - - val values = scannedCodeUri.queryParameterNames - // If the parameters are not valid, - if (!areParametersValid(scannedCode, values)) { - authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError) - sendEvent(QrCodeScanEvent.NavigateBack) - return - } - if (authenticatorRepository.sharedCodesStateFlow.value.isSyncWithBitwardenEnabled) { - when (settingsRepository.defaultSaveOption) { - DefaultSaveOption.BITWARDEN_APP -> saveCodeToBitwardenAndNavigateBack(result) - DefaultSaveOption.LOCAL -> saveCodeLocallyAndNavigateBack(result) - - DefaultSaveOption.NONE -> { - pendingSuccessfulScan = result - mutableStateFlow.update { - it.copy( - dialog = QrCodeScanState.DialogState.ChooseSaveLocation, - ) + DefaultSaveOption.LOCAL -> saveCodeLocallyAndNavigateBack(result = result) + DefaultSaveOption.NONE -> { + pendingSuccessfulScan = result + mutableStateFlow.update { + it.copy(dialog = QrCodeScanState.DialogState.ChooseSaveLocation) + } + } } + } else { + // Syncing with Bitwarden not enabled, save code locally: + saveCodeLocallyAndNavigateBack(result = result) } } - } else { - // Syncing with Bitwarden not enabled, save code locally: - saveCodeLocallyAndNavigateBack(result) - } + ?: run { + authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError) + sendEvent(QrCodeScanEvent.NavigateBack) + } } private fun handleGoogleExportUriReceive(scannedCode: String) { - val uri = Uri.parse(scannedCode) + val uri = scannedCode.toUri() val encodedData = uri.getQueryParameter(TotpCodeManager.DATA_PARAM) - val result: TotpCodeResult = if (encodedData.isNullOrEmpty()) { - TotpCodeResult.CodeScanningError + if (encodedData.isNullOrEmpty()) { + authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError) } else { - TotpCodeResult.GoogleExportScan(encodedData) + authenticatorRepository.emitTotpCodeResult(TotpCodeResult.GoogleExportScan(encodedData)) } - authenticatorRepository.emitTotpCodeResult(result) sendEvent(QrCodeScanEvent.NavigateBack) } private fun handleCameraErrorReceive() { - sendEvent( - QrCodeScanEvent.NavigateToManualCodeEntry, - ) - } - - @Suppress("NestedBlockDepth", "ReturnCount", "MagicNumber") - private fun areParametersValid(scannedCode: String, parameters: Set): Boolean { - parameters.forEach { parameter -> - Uri.parse(scannedCode).getQueryParameter(parameter)?.let { value -> - when (parameter) { - TotpCodeManager.DIGITS_PARAM -> { - val digit = value.toInt() - if (digit > 10 || digit < 1) { - return false - } - } - - TotpCodeManager.PERIOD_PARAM -> { - val period = value.toInt() - if (period < 1) { - return false - } - } - - TotpCodeManager.ALGORITHM_PARAM -> { - val lowercaseAlgo = value.lowercase() - if (lowercaseAlgo != "sha1" && - lowercaseAlgo != "sha256" && - lowercaseAlgo != "sha512" - ) { - return false - } - } - } - } - } - return true + sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry) } private fun saveCodeToBitwardenAndNavigateBack(result: TotpCodeResult.TotpCodeScan) { diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModelTest.kt index 81914661e9..0ed1948bf3 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanViewModelTest.kt @@ -9,6 +9,7 @@ import com.bitwarden.authenticator.data.platform.repository.SettingsRepository import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.util.getTotpDataOrNull import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -37,13 +38,20 @@ class QrCodeScanViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - mockkStatic(Uri::parse) + mockkStatic( + String::getTotpDataOrNull, + Uri::parse, + ) + every { VALID_TOTP_CODE.getTotpDataOrNull() } returns mockk() every { Uri.parse(VALID_TOTP_CODE) } returns VALID_TOTP_URI } @AfterEach fun teardown() { - unmockkStatic(Uri::parse) + unmockkStatic( + String::getTotpDataOrNull, + Uri::parse, + ) } @Test @@ -242,13 +250,8 @@ class QrCodeScanViewModelTest : BaseViewModelTest() { every { authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError) } just runs - val invalidUri: Uri = mockk { - every { getQueryParameter("secret") } returns "SECRET" - every { queryParameterNames } returns setOf("digits") - every { getQueryParameter("digits") } returns "100" - } val invalidQrCode = "otpauth://totp/secret=SECRET" - every { Uri.parse(invalidQrCode) } returns invalidUri + every { invalidQrCode.getTotpDataOrNull() } returns null viewModel.eventFlow.test { viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidQrCode)) assertEquals(QrCodeScanEvent.NavigateBack, awaitItem()) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TotpUriUtils.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TotpUriUtils.kt index db0dadf4d5..298ed66ae4 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TotpUriUtils.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TotpUriUtils.kt @@ -1,6 +1,7 @@ package com.bitwarden.ui.platform.util import android.net.Uri +import androidx.core.net.toUri import com.bitwarden.ui.platform.base.util.isBase32 import com.bitwarden.ui.platform.model.TotpData @@ -12,6 +13,12 @@ private const val PARAM_NAME_ISSUER: String = "issuer" private const val PARAM_NAME_PERIOD: String = "period" private const val PARAM_NAME_SECRET: String = "secret" +/** + * Checks if the given [String] contains valid data for a TOTP. The [TotpData] will be returned + * when the correct data is present or `null` if data is invalid or missing. + */ +fun String.getTotpDataOrNull(): TotpData? = this.toUri().getTotpDataOrNull() + /** * Checks if the given [Uri] contains valid data for a TOTP. The [TotpData] will be returned when * the correct data is present or `null` if data is invalid or missing.