diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 541fb7c78c..0b972cc9cf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -237,6 +237,7 @@ koverReport {
// OS-level components
"com.x8bit.bitwarden.BitwardenApplication",
"com.x8bit.bitwarden.MainActivity*",
+ "com.x8bit.bitwarden.WebAuthCallbackActivity*",
"com.x8bit.bitwarden.data.autofill.BitwardenAutofillService*",
"com.x8bit.bitwarden.data.push.BitwardenFirebaseMessagingService*",
// Empty Composables
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3ccec0217f..8cf63df222 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,6 +34,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -44,15 +61,6 @@
android:host="captcha-callback"
android:scheme="bitwarden" />
-
-
-
-
-
-
-
-
-
{
- authRepository.specialCircumstance =
- UserState.SpecialCircumstance.ShareNewSend(
- data = shareData,
- shouldFinishWhenComplete = true,
- )
- }
- }
+ handleIntent(
+ intent = action.intent,
+ isFirstIntent = true,
+ )
}
private fun handleNewIntentReceived(action: MainAction.ReceiveNewIntent) {
- val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
- val shareData = intentManager.getShareDataFromIntent(action.intent)
- when {
- captchaCallbackTokenResult != null -> {
- authRepository.setCaptchaCallbackTokenResult(
- tokenResult = captchaCallbackTokenResult,
- )
- }
+ handleIntent(
+ intent = action.intent,
+ isFirstIntent = false,
+ )
+ }
+ private fun handleIntent(
+ intent: Intent,
+ isFirstIntent: Boolean,
+ ) {
+ val shareData = intentManager.getShareDataFromIntent(intent)
+ when {
shareData != null -> {
authRepository.specialCircumstance =
UserState.SpecialCircumstance.ShareNewSend(
data = shareData,
// Allow users back into the already-running app when completing the
- // Send task.
- shouldFinishWhenComplete = false,
+ // Send task when this is not the first intent.
+ shouldFinishWhenComplete = isFirstIntent,
)
}
-
- else -> Unit
}
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/WebAuthCallbackActivity.kt b/app/src/main/java/com/x8bit/bitwarden/WebAuthCallbackActivity.kt
new file mode 100644
index 0000000000..81d1005aaf
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/WebAuthCallbackActivity.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/WebAuthCallbackViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/WebAuthCallbackViewModel.kt
new file mode 100644
index 0000000000..cb0b6698a2
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/WebAuthCallbackViewModel.kt
@@ -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) {
+ 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()
+}
diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
index 832fbd2f75..4af05fda77 100644
--- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden
import android.content.Intent
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
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.platform.repository.SettingsRepository
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.just
import io.mockk.mockk
-import io.mockk.mockkStatic
import io.mockk.runs
-import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
-import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class MainViewModelTest : BaseViewModelTest() {
@@ -32,7 +27,6 @@ class MainViewModelTest : BaseViewModelTest() {
every { activeUserId } returns USER_ID
every { specialCircumstance } returns null
every { specialCircumstance = any() } just runs
- every { setCaptchaCallbackTokenResult(any()) } just runs
}
private val settingsRepository = mockk {
every { appTheme } returns AppTheme.DEFAULT
@@ -42,16 +36,6 @@ class MainViewModelTest : BaseViewModelTest() {
every { getShareDataFromIntent(any()) } returns null
}
- @BeforeEach
- fun setUp() {
- mockkStatic(Intent::getCaptchaCallbackTokenResult)
- }
-
- @AfterEach
- fun tearDown() {
- unmockkStatic(Intent::getCaptchaCallbackTokenResult)
- }
-
@Test
fun `on AppThemeChanged should update state`() {
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()
- 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")
@Test
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {
diff --git a/app/src/test/java/com/x8bit/bitwarden/WebAuthCallbackViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/WebAuthCallbackViewModelTest.kt
new file mode 100644
index 0000000000..6c0b5076ab
--- /dev/null
+++ b/app/src/test/java/com/x8bit/bitwarden/WebAuthCallbackViewModelTest.kt
@@ -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 {
+ 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()
+ 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,
+ )
+}