mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 20:07:59 -06:00
Add WebAuthCallbackActivity to handle hCaptcha callbacks (#705)
This commit is contained in:
parent
49ff8a761d
commit
e3547f4e13
@ -237,6 +237,7 @@ koverReport {
|
|||||||
// OS-level components
|
// OS-level components
|
||||||
"com.x8bit.bitwarden.BitwardenApplication",
|
"com.x8bit.bitwarden.BitwardenApplication",
|
||||||
"com.x8bit.bitwarden.MainActivity*",
|
"com.x8bit.bitwarden.MainActivity*",
|
||||||
|
"com.x8bit.bitwarden.WebAuthCallbackActivity*",
|
||||||
"com.x8bit.bitwarden.data.autofill.BitwardenAutofillService*",
|
"com.x8bit.bitwarden.data.autofill.BitwardenAutofillService*",
|
||||||
"com.x8bit.bitwarden.data.push.BitwardenFirebaseMessagingService*",
|
"com.x8bit.bitwarden.data.push.BitwardenFirebaseMessagingService*",
|
||||||
// Empty Composables
|
// Empty Composables
|
||||||
|
|||||||
@ -34,6 +34,23 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="application/*" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
<data android:mimeType="video/*" />
|
||||||
|
<data android:mimeType="text/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".WebAuthCallbackActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -44,15 +61,6 @@
|
|||||||
android:host="captcha-callback"
|
android:host="captcha-callback"
|
||||||
android:scheme="bitwarden" />
|
android:scheme="bitwarden" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
|
|
||||||
<data android:mimeType="application/*" />
|
|
||||||
<data android:mimeType="image/*" />
|
|
||||||
<data android:mimeType="video/*" />
|
|
||||||
<data android:mimeType="text/*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import android.os.Parcelable
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||||
@ -50,39 +49,34 @@ class MainViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
|
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
|
||||||
val shareData = intentManager.getShareDataFromIntent(action.intent)
|
handleIntent(
|
||||||
when {
|
intent = action.intent,
|
||||||
shareData != null -> {
|
isFirstIntent = true,
|
||||||
authRepository.specialCircumstance =
|
)
|
||||||
UserState.SpecialCircumstance.ShareNewSend(
|
|
||||||
data = shareData,
|
|
||||||
shouldFinishWhenComplete = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNewIntentReceived(action: MainAction.ReceiveNewIntent) {
|
private fun handleNewIntentReceived(action: MainAction.ReceiveNewIntent) {
|
||||||
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
handleIntent(
|
||||||
val shareData = intentManager.getShareDataFromIntent(action.intent)
|
intent = action.intent,
|
||||||
when {
|
isFirstIntent = false,
|
||||||
captchaCallbackTokenResult != null -> {
|
)
|
||||||
authRepository.setCaptchaCallbackTokenResult(
|
}
|
||||||
tokenResult = captchaCallbackTokenResult,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private fun handleIntent(
|
||||||
|
intent: Intent,
|
||||||
|
isFirstIntent: Boolean,
|
||||||
|
) {
|
||||||
|
val shareData = intentManager.getShareDataFromIntent(intent)
|
||||||
|
when {
|
||||||
shareData != null -> {
|
shareData != null -> {
|
||||||
authRepository.specialCircumstance =
|
authRepository.specialCircumstance =
|
||||||
UserState.SpecialCircumstance.ShareNewSend(
|
UserState.SpecialCircumstance.ShareNewSend(
|
||||||
data = shareData,
|
data = shareData,
|
||||||
// Allow users back into the already-running app when completing the
|
// Allow users back into the already-running app when completing the
|
||||||
// Send task.
|
// Send task when this is not the first intent.
|
||||||
shouldFinishWhenComplete = false,
|
shouldFinishWhenComplete = isFirstIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.x8bit.bitwarden
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity to receive callbacks from Custom Chrome tabs or other web-auth related flows such
|
||||||
|
* the current state of the task holding the [MainActivity] can remain undisturbed.
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class WebAuthCallbackActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val webAuthCallbackViewModel: WebAuthCallbackViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
webAuthCallbackViewModel.trySendAction(
|
||||||
|
WebAuthCallbackAction.IntentReceive(intent = intent),
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
.apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.x8bit.bitwarden
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view model that handles logic for the [WebAuthCallbackActivity].
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class WebAuthCallbackViewModel @Inject constructor(
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
|
) : BaseViewModel<Unit, Unit, WebAuthCallbackAction>(Unit) {
|
||||||
|
override fun handleAction(action: WebAuthCallbackAction) {
|
||||||
|
when (action) {
|
||||||
|
is WebAuthCallbackAction.IntentReceive -> handleIntentReceived(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIntentReceived(action: WebAuthCallbackAction.IntentReceive) {
|
||||||
|
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
||||||
|
when {
|
||||||
|
captchaCallbackTokenResult != null -> {
|
||||||
|
authRepository.setCaptchaCallbackTokenResult(
|
||||||
|
tokenResult = captchaCallbackTokenResult,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions for the [WebAuthCallbackViewModel].
|
||||||
|
*/
|
||||||
|
sealed class WebAuthCallbackAction {
|
||||||
|
/**
|
||||||
|
* Receive Intent by the application.
|
||||||
|
*/
|
||||||
|
data class IntentReceive(val intent: Intent) : WebAuthCallbackAction()
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package com.x8bit.bitwarden
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
@ -13,14 +12,10 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
|||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.unmockkStatic
|
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class MainViewModelTest : BaseViewModelTest() {
|
class MainViewModelTest : BaseViewModelTest() {
|
||||||
@ -32,7 +27,6 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||||||
every { activeUserId } returns USER_ID
|
every { activeUserId } returns USER_ID
|
||||||
every { specialCircumstance } returns null
|
every { specialCircumstance } returns null
|
||||||
every { specialCircumstance = any() } just runs
|
every { specialCircumstance = any() } just runs
|
||||||
every { setCaptchaCallbackTokenResult(any()) } just runs
|
|
||||||
}
|
}
|
||||||
private val settingsRepository = mockk<SettingsRepository> {
|
private val settingsRepository = mockk<SettingsRepository> {
|
||||||
every { appTheme } returns AppTheme.DEFAULT
|
every { appTheme } returns AppTheme.DEFAULT
|
||||||
@ -42,16 +36,6 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||||||
every { getShareDataFromIntent(any()) } returns null
|
every { getShareDataFromIntent(any()) } returns null
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setUp() {
|
|
||||||
mockkStatic(Intent::getCaptchaCallbackTokenResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
fun tearDown() {
|
|
||||||
unmockkStatic(Intent::getCaptchaCallbackTokenResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on AppThemeChanged should update state`() {
|
fun `on AppThemeChanged should update state`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
@ -102,29 +86,6 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
val mockIntent = mockk<Intent>()
|
|
||||||
every {
|
|
||||||
mockIntent.getCaptchaCallbackTokenResult()
|
|
||||||
} returns CaptchaCallbackTokenResult.Success(
|
|
||||||
token = "mockk_token",
|
|
||||||
)
|
|
||||||
viewModel.trySendAction(
|
|
||||||
MainAction.ReceiveNewIntent(
|
|
||||||
intent = mockIntent,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
verify {
|
|
||||||
authRepository.setCaptchaCallbackTokenResult(
|
|
||||||
tokenResult = CaptchaCallbackTokenResult.Success(
|
|
||||||
token = "mockk_token",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {
|
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.x8bit.bitwarden
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class WebAuthCallbackViewModelTest : BaseViewModelTest() {
|
||||||
|
private val authRepository = mockk<AuthRepository> {
|
||||||
|
every { setCaptchaCallbackTokenResult(any()) } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic(Intent::getCaptchaCallbackTokenResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(Intent::getCaptchaCallbackTokenResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val mockIntent = mockk<Intent>()
|
||||||
|
every {
|
||||||
|
mockIntent.getCaptchaCallbackTokenResult()
|
||||||
|
} returns CaptchaCallbackTokenResult.Success(
|
||||||
|
token = "mockk_token",
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(
|
||||||
|
WebAuthCallbackAction.IntentReceive(
|
||||||
|
intent = mockIntent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
verify {
|
||||||
|
authRepository.setCaptchaCallbackTokenResult(
|
||||||
|
tokenResult = CaptchaCallbackTokenResult.Success(
|
||||||
|
token = "mockk_token",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel() = WebAuthCallbackViewModel(
|
||||||
|
authRepository = authRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user