mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 13:57:03 -06:00
BIT-764: Chrome Custom Tabs for hcpatcha verification (#105)
This commit is contained in:
parent
5a2a2f93f3
commit
635cc120c9
@ -62,6 +62,11 @@ The following is a list of all third-party dependencies included as part of the
|
|||||||
- Purpose: Supplementary Android Compose features.
|
- Purpose: Supplementary Android Compose features.
|
||||||
- License: Apache 2.0
|
- 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**
|
- **Core SplashScreen**
|
||||||
- https://developer.android.com/jetpack/androidx/releases/core
|
- https://developer.android.com/jetpack/androidx/releases/core
|
||||||
- Purpose: Backwards compatible SplashScreen API implementation.
|
- Purpose: Backwards compatible SplashScreen API implementation.
|
||||||
|
|||||||
@ -76,6 +76,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.browser)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.animation)
|
implementation(libs.androidx.compose.animation)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
|||||||
@ -13,9 +13,9 @@ private const val CAPTCHA_HOST: String = "captcha-callback"
|
|||||||
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
|
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 {
|
val json = buildJsonObject {
|
||||||
put(key = "siteKey", value = captchaId)
|
put(key = "siteKey", value = captchaId)
|
||||||
put(key = "locale", value = Locale.getDefault().toString())
|
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 parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
|
||||||
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||||
"?data=$base64Data&parent=$parentParam&v=1"
|
"?data=$base64Data&parent=$parentParam&v=1"
|
||||||
return Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
return Uri.parse(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -49,7 +49,9 @@ fun LoginScreen(
|
|||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
LoginEvent.NavigateBack -> onNavigateBack()
|
LoginEvent.NavigateBack -> onNavigateBack()
|
||||||
is LoginEvent.NavigateToCaptcha -> intentHandler.startActivity(intent = event.intent)
|
is LoginEvent.NavigateToCaptcha -> {
|
||||||
|
intentHandler.startCustomTabsActivity(uri = event.uri)
|
||||||
|
}
|
||||||
is LoginEvent.ShowToast -> {
|
is LoginEvent.ShowToast -> {
|
||||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.login
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
import android.content.Intent
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
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.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.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
@ -84,7 +84,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
mutableStateFlow.update { it.copy(loadingDialogState = LoadingDialogState.Hidden) }
|
mutableStateFlow.update { it.copy(loadingDialogState = LoadingDialogState.Hidden) }
|
||||||
sendEvent(
|
sendEvent(
|
||||||
event = LoginEvent.NavigateToCaptcha(
|
event = LoginEvent.NavigateToCaptcha(
|
||||||
intent = loginResult.generateIntentForCaptcha(),
|
uri = loginResult.generateUriForCaptcha(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -201,7 +201,7 @@ sealed class LoginEvent {
|
|||||||
/**
|
/**
|
||||||
* Navigates to the captcha verification screen.
|
* 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].
|
* Shows a toast with the given [message].
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.base.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
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.
|
* 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) {
|
fun startActivity(intent: Intent) {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a Custom Tabs Activity using the provided [Uri].
|
||||||
|
*/
|
||||||
|
fun startCustomTabsActivity(uri: Uri) {
|
||||||
|
CustomTabsIntent
|
||||||
|
.Builder()
|
||||||
|
.build()
|
||||||
|
.launchUrl(context, uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,23 +3,25 @@ package com.x8bit.bitwarden.data.auth.datasource.network.util
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
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.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LoginResultExtensionsTest {
|
class LoginResultExtensionsTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `generateIntentForCaptcha should return valid Intent`() {
|
fun `generateIntentForCaptcha should return valid Uri`() {
|
||||||
val captchaRequired = LoginResult.CaptchaRequired("testCaptchaId")
|
val captchaRequired = LoginResult.CaptchaRequired("testCaptchaId")
|
||||||
val intent = captchaRequired.generateIntentForCaptcha()
|
val actualUri = captchaRequired.generateUriForCaptcha()
|
||||||
val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||||
"?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGkxZGQiLCJsb2NhbGUiOiJlbl9VUyJ9" +
|
"?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGFJZCIsImxvY2FsZSI6ImVuX1VTIiwiY2Fsb" +
|
||||||
"&parent=bitwarden%3A%2F%2Fcaptcha-callback&v=1"
|
"GJhY2tVcmkiOiJiaXR3YXJkZW46Ly9jYXB0Y2hhLWNhbGxiYWNrIiwiY2FwdGNoYVJlcXVp" +
|
||||||
val expectedIntent = Intent(Intent.ACTION_VIEW, Uri.parse(expectedUrl))
|
"cmVkVGV4dCI6IkNhcHRjaGEgcmVxdWlyZWQifQ==&parent=bitwarden%3A%2F%2F" +
|
||||||
assertEquals(expectedIntent.action, intent.action)
|
"captcha-callback&v=1"
|
||||||
assertEquals(expectedIntent.data, intent.data)
|
val expectedUri = Uri.parse(expectedUrl)
|
||||||
|
assertEquals(expectedUri, actualUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.login
|
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.assertCountEquals
|
||||||
import androidx.compose.ui.test.filter
|
import androidx.compose.ui.test.filter
|
||||||
import androidx.compose.ui.test.filterToOne
|
import androidx.compose.ui.test.filterToOne
|
||||||
@ -200,13 +200,13 @@ class LoginScreenTest : BaseComposeTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToCaptcha should call intentHandler startActivity`() {
|
fun `NavigateToCaptcha should call intentHandler startCustomTabsActivity`() {
|
||||||
val intentHandler = mockk<IntentHandler>(relaxed = true) {
|
val intentHandler = mockk<IntentHandler>(relaxed = true) {
|
||||||
every { startActivity(any()) } returns Unit
|
every { startCustomTabsActivity(any()) } returns Unit
|
||||||
}
|
}
|
||||||
val mockIntent = mockk<Intent>()
|
val mockUri = mockk<Uri>()
|
||||||
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||||
every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockIntent))
|
every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockUri))
|
||||||
every { stateFlow } returns MutableStateFlow(
|
every { stateFlow } returns MutableStateFlow(
|
||||||
LoginState(
|
LoginState(
|
||||||
emailAddress = "",
|
emailAddress = "",
|
||||||
@ -225,6 +225,6 @@ class LoginScreenTest : BaseComposeTest() {
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
verify { intentHandler.startActivity(mockIntent) }
|
verify { intentHandler.startCustomTabsActivity(mockUri) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
package com.x8bit.bitwarden.ui.auth.feature.login
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
import android.content.Intent
|
import android.net.Uri
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
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.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.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
@ -174,12 +174,12 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `LoginButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
fun `LoginButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
||||||
runTest {
|
runTest {
|
||||||
val mockkIntent = mockk<Intent>()
|
val mockkUri = mockk<Uri>()
|
||||||
every {
|
every {
|
||||||
LoginResult
|
LoginResult
|
||||||
.CaptchaRequired(captchaId = "mock_captcha_id")
|
.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||||
.generateIntentForCaptcha()
|
.generateUriForCaptcha()
|
||||||
} returns mockkIntent
|
} returns mockkUri
|
||||||
val authRepository = mockk<AuthRepository> {
|
val authRepository = mockk<AuthRepository> {
|
||||||
coEvery { login("test@gmail.com", "", captchaToken = null) } returns
|
coEvery { login("test@gmail.com", "", captchaToken = null) } returns
|
||||||
LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||||
@ -192,7 +192,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
|
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
|
||||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
assertEquals(LoginEvent.NavigateToCaptcha(intent = mockkIntent), awaitItem())
|
assertEquals(LoginEvent.NavigateToCaptcha(uri = mockkUri), awaitItem())
|
||||||
}
|
}
|
||||||
coVerify {
|
coVerify {
|
||||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ minSdk = "28"
|
|||||||
accompanist = "0.30.1"
|
accompanist = "0.30.1"
|
||||||
androidGradlePlugin = "8.1.0"
|
androidGradlePlugin = "8.1.0"
|
||||||
androidxActivity = "1.7.2"
|
androidxActivity = "1.7.2"
|
||||||
|
androidxBrowser = "1.6.0"
|
||||||
androidxComposeBom = "2023.09.02"
|
androidxComposeBom = "2023.09.02"
|
||||||
# TODO: Once the Material3 color scheme changes are no longer in alpha, we should remove this
|
# 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).
|
# individual dependency version and use the Compose BOM version (BIT-702).
|
||||||
@ -46,6 +47,7 @@ zxing = "3.5.2"
|
|||||||
[libraries]
|
[libraries]
|
||||||
# Format: <maintainer>-<artifact-name>
|
# Format: <maintainer>-<artifact-name>
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
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-animation = { module = "androidx.compose.animation:animation" }
|
||||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidxComposeBom" }
|
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidxComposeBom" }
|
||||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidxComposeMaterial3" }
|
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidxComposeMaterial3" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user