From 635cc120c97aac074f34ab9dfa34b96841d49649 Mon Sep 17 00:00:00 2001 From: Ramsey Smith <142836716+ramsey-livefront@users.noreply.github.com> Date: Wed, 11 Oct 2023 09:37:03 -0600 Subject: [PATCH] BIT-764: Chrome Custom Tabs for hcpatcha verification (#105) --- README.md | 5 +++++ app/build.gradle.kts | 1 + .../network/util/LoginResultExtensions.kt | 6 +++--- .../ui/auth/feature/login/LoginScreen.kt | 4 +++- .../ui/auth/feature/login/LoginViewModel.kt | 8 ++++---- .../ui/platform/base/util/IntentHandler.kt | 12 ++++++++++++ .../network/util/LoginResultExtensionsTest.kt | 18 ++++++++++-------- .../ui/auth/feature/login/LoginScreenTest.kt | 12 ++++++------ .../auth/feature/login/LoginViewModelTest.kt | 12 ++++++------ gradle/libs.versions.toml | 2 ++ 10 files changed, 52 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 88205047a0..93a9a0b1cf 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,11 @@ The following is a list of all third-party dependencies included as part of the - Purpose: Supplementary Android Compose features. - License: Apache 2.0 +- **AndroidX Browser** + - https://developer.android.com/jetpack/androidx/releases/browser + - Purpose: Displays webpages with the user's default browser. + - License: Apache 2.0 + - **Core SplashScreen** - https://developer.android.com/jetpack/androidx/releases/core - Purpose: Backwards compatible SplashScreen API implementation. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5860a55c0..8865f9da81 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,6 +76,7 @@ android { dependencies { implementation(libs.androidx.activity.compose) + implementation(libs.androidx.browser) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.animation) implementation(libs.androidx.compose.material3) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensions.kt index 90aa871e66..fcb1cb053a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensions.kt @@ -13,9 +13,9 @@ private const val CAPTCHA_HOST: String = "captcha-callback" private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST" /** - * Generates an [Intent] to display a CAPTCHA challenge for Bitwarden authentication. + * Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication. */ -fun LoginResult.CaptchaRequired.generateIntentForCaptcha(): Intent { +fun LoginResult.CaptchaRequired.generateUriForCaptcha(): Uri { val json = buildJsonObject { put(key = "siteKey", value = captchaId) put(key = "locale", value = Locale.getDefault().toString()) @@ -32,7 +32,7 @@ fun LoginResult.CaptchaRequired.generateIntentForCaptcha(): Intent { val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8") val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" + "?data=$base64Data&parent=$parentParam&v=1" - return Intent(Intent.ACTION_VIEW, Uri.parse(url)) + return Uri.parse(url) } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt index 57f19a1a66..cfb3f3704e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt @@ -49,7 +49,9 @@ fun LoginScreen( EventsEffect(viewModel = viewModel) { event -> when (event) { LoginEvent.NavigateBack -> onNavigateBack() - is LoginEvent.NavigateToCaptcha -> intentHandler.startActivity(intent = event.intent) + is LoginEvent.NavigateToCaptcha -> { + intentHandler.startCustomTabsActivity(uri = event.uri) + } is LoginEvent.ShowToast -> { Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt index c04eda5329..88dba9be12 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt @@ -2,14 +2,14 @@ package com.x8bit.bitwarden.ui.auth.feature.login -import android.content.Intent +import android.net.Uri import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult -import com.x8bit.bitwarden.data.auth.datasource.network.util.generateIntentForCaptcha +import com.x8bit.bitwarden.data.auth.datasource.network.util.generateUriForCaptcha import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -84,7 +84,7 @@ class LoginViewModel @Inject constructor( mutableStateFlow.update { it.copy(loadingDialogState = LoadingDialogState.Hidden) } sendEvent( event = LoginEvent.NavigateToCaptcha( - intent = loginResult.generateIntentForCaptcha(), + uri = loginResult.generateUriForCaptcha(), ), ) } @@ -201,7 +201,7 @@ sealed class LoginEvent { /** * Navigates to the captcha verification screen. */ - data class NavigateToCaptcha(val intent: Intent) : LoginEvent() + data class NavigateToCaptcha(val uri: Uri) : LoginEvent() /** * Shows a toast with the given [message]. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt index 5895bb2759..9c2dcad979 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt @@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.base.util import android.content.Context import android.content.Intent +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent /** * A utility class for simplifying the handling of Android Intents within a given context. @@ -14,4 +16,14 @@ class IntentHandler(private val context: Context) { fun startActivity(intent: Intent) { context.startActivity(intent) } + + /** + * Start a Custom Tabs Activity using the provided [Uri]. + */ + fun startCustomTabsActivity(uri: Uri) { + CustomTabsIntent + .Builder() + .build() + .launchUrl(context, uri) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensionsTest.kt index 3f3da136b0..aaf186972c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/LoginResultExtensionsTest.kt @@ -3,23 +3,25 @@ package com.x8bit.bitwarden.data.auth.datasource.network.util import android.content.Intent import android.net.Uri import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult +import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals import org.junit.Test -class LoginResultExtensionsTest { +class LoginResultExtensionsTest : BaseComposeTest() { @Test - fun `generateIntentForCaptcha should return valid Intent`() { + fun `generateIntentForCaptcha should return valid Uri`() { val captchaRequired = LoginResult.CaptchaRequired("testCaptchaId") - val intent = captchaRequired.generateIntentForCaptcha() + val actualUri = captchaRequired.generateUriForCaptcha() val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" + - "?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGkxZGQiLCJsb2NhbGUiOiJlbl9VUyJ9" + - "&parent=bitwarden%3A%2F%2Fcaptcha-callback&v=1" - val expectedIntent = Intent(Intent.ACTION_VIEW, Uri.parse(expectedUrl)) - assertEquals(expectedIntent.action, intent.action) - assertEquals(expectedIntent.data, intent.data) + "?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGFJZCIsImxvY2FsZSI6ImVuX1VTIiwiY2Fsb" + + "GJhY2tVcmkiOiJiaXR3YXJkZW46Ly9jYXB0Y2hhLWNhbGxiYWNrIiwiY2FwdGNoYVJlcXVp" + + "cmVkVGV4dCI6IkNhcHRjaGEgcmVxdWlyZWQifQ==&parent=bitwarden%3A%2F%2F" + + "captcha-callback&v=1" + val expectedUri = Uri.parse(expectedUrl) + assertEquals(expectedUri, actualUri) } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt index 2e07455278..a1b44b90b6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt @@ -1,6 +1,6 @@ package com.x8bit.bitwarden.ui.auth.feature.login -import android.content.Intent +import android.net.Uri import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.filter import androidx.compose.ui.test.filterToOne @@ -200,13 +200,13 @@ class LoginScreenTest : BaseComposeTest() { } @Test - fun `NavigateToCaptcha should call intentHandler startActivity`() { + fun `NavigateToCaptcha should call intentHandler startCustomTabsActivity`() { val intentHandler = mockk(relaxed = true) { - every { startActivity(any()) } returns Unit + every { startCustomTabsActivity(any()) } returns Unit } - val mockIntent = mockk() + val mockUri = mockk() val viewModel = mockk(relaxed = true) { - every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockIntent)) + every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockUri)) every { stateFlow } returns MutableStateFlow( LoginState( emailAddress = "", @@ -225,6 +225,6 @@ class LoginScreenTest : BaseComposeTest() { viewModel = viewModel, ) } - verify { intentHandler.startActivity(mockIntent) } + verify { intentHandler.startCustomTabsActivity(mockUri) } } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index 9af0e7f41a..dc1c2b2d0c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -1,12 +1,12 @@ package com.x8bit.bitwarden.ui.auth.feature.login -import android.content.Intent +import android.net.Uri import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult -import com.x8bit.bitwarden.data.auth.datasource.network.util.generateIntentForCaptcha +import com.x8bit.bitwarden.data.auth.datasource.network.util.generateUriForCaptcha import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -174,12 +174,12 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `LoginButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() = runTest { - val mockkIntent = mockk() + val mockkUri = mockk() every { LoginResult .CaptchaRequired(captchaId = "mock_captcha_id") - .generateIntentForCaptcha() - } returns mockkIntent + .generateUriForCaptcha() + } returns mockkUri val authRepository = mockk { coEvery { login("test@gmail.com", "", captchaToken = null) } returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id") @@ -192,7 +192,7 @@ class LoginViewModelTest : BaseViewModelTest() { viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.LoginButtonClick) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) - assertEquals(LoginEvent.NavigateToCaptcha(intent = mockkIntent), awaitItem()) + assertEquals(LoginEvent.NavigateToCaptcha(uri = mockkUri), awaitItem()) } coVerify { authRepository.login(email = "test@gmail.com", password = "", captchaToken = null) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe748d1d46..0caca03267 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ minSdk = "28" accompanist = "0.30.1" androidGradlePlugin = "8.1.0" androidxActivity = "1.7.2" +androidxBrowser = "1.6.0" androidxComposeBom = "2023.09.02" # TODO: Once the Material3 color scheme changes are no longer in alpha, we should remove this # individual dependency version and use the Compose BOM version (BIT-702). @@ -46,6 +47,7 @@ zxing = "3.5.2" [libraries] # Format: - androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } +androidx-browser = {module = "androidx.browser:browser", version.ref = "androidxBrowser"} androidx-compose-animation = { module = "androidx.compose.animation:animation" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidxComposeBom" } androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidxComposeMaterial3" }