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, + ) +}