BIT-1498: Allow external navigation to Add Send screen (#685)

This commit is contained in:
Brian Yencho 2024-01-19 15:01:43 -06:00 committed by Álison Fernandes
parent bdca79d862
commit eeb22dbfee
24 changed files with 358 additions and 58 deletions

View file

@ -26,7 +26,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleInstancePerTask"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
@ -44,6 +44,15 @@
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

View file

@ -31,6 +31,13 @@ class MainActivity : AppCompatActivity() {
var shouldShowSplashScreen = true var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen } installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mainViewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = intent,
),
)
// Within the app the language will change dynamically and will be managed // Within the app the language will change dynamically and will be managed
// by the OS, but we need to ensure we properly set the language when // by the OS, but we need to ensure we properly set the language when
// upgrading from older versions that handle this differently. // upgrading from older versions that handle this differently.

View file

@ -4,10 +4,12 @@ import android.content.Intent
import android.os.Parcelable 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.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.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
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -21,6 +23,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val intentManager: IntentManager,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
) : BaseViewModel<MainState, Unit, MainAction>( ) : BaseViewModel<MainState, Unit, MainAction>(
MainState( MainState(
@ -37,6 +40,7 @@ class MainViewModel @Inject constructor(
override fun handleAction(action: MainAction) { override fun handleAction(action: MainAction) {
when (action) { when (action) {
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action) is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action) is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
} }
} }
@ -45,8 +49,22 @@ class MainViewModel @Inject constructor(
mutableStateFlow.update { it.copy(theme = action.theme) } mutableStateFlow.update { it.copy(theme = action.theme) }
} }
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
val shareData = intentManager.getShareDataFromIntent(action.intent)
when {
shareData != null -> {
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() val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
val shareData = intentManager.getShareDataFromIntent(action.intent)
when { when {
captchaCallbackTokenResult != null -> { captchaCallbackTokenResult != null -> {
authRepository.setCaptchaCallbackTokenResult( authRepository.setCaptchaCallbackTokenResult(
@ -54,6 +72,16 @@ class MainViewModel @Inject constructor(
) )
} }
shareData != null -> {
authRepository.specialCircumstance =
UserState.SpecialCircumstance.ShareNewSend(
data = shareData,
// Allow users back into the already-running app when completing the
// Send task.
shouldFinishWhenComplete = false,
)
}
else -> Unit else -> Unit
} }
} }
@ -71,6 +99,11 @@ data class MainState(
* Models actions for the [MainActivity]. * Models actions for the [MainActivity].
*/ */
sealed class MainAction { sealed class MainAction {
/**
* Receive first Intent by the application.
*/
data class ReceiveFirstIntent(val intent: Intent) : MainAction()
/** /**
* Receive Intent by the application. * Receive Intent by the application.
*/ */

View file

@ -48,6 +48,15 @@ interface AuthRepository : AuthenticatorProvider {
*/ */
var specialCircumstance: UserState.SpecialCircumstance? var specialCircumstance: UserState.SpecialCircumstance?
/**
* Tracks whether there is an additional account that is pending login/registration in order to
* have multiple accounts available.
*
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
* Note that this call has no effect when there is no [UserState] information available.
*/
var hasPendingAccountAddition: Boolean
/** /**
* Attempt to delete the current account and logout them out upon success. * Attempt to delete the current account and logout them out upon success.
*/ */

View file

@ -73,6 +73,7 @@ class AuthRepositoryImpl(
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() }, private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
) : AuthRepository { ) : AuthRepository {
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow<Boolean>(false)
private val mutableSpecialCircumstanceStateFlow = private val mutableSpecialCircumstanceStateFlow =
MutableStateFlow<UserState.SpecialCircumstance?>(null) MutableStateFlow<UserState.SpecialCircumstance?>(null)
@ -107,12 +108,20 @@ class AuthRepositoryImpl(
authDiskSource.userStateFlow, authDiskSource.userStateFlow,
authDiskSource.userOrganizationsListFlow, authDiskSource.userOrganizationsListFlow,
vaultRepository.vaultStateFlow, vaultRepository.vaultStateFlow,
mutableHasPendingAccountAdditionStateFlow,
mutableSpecialCircumstanceStateFlow, mutableSpecialCircumstanceStateFlow,
) { userStateJson, userOrganizationsList, vaultState, specialCircumstance -> ) {
userStateJson,
userOrganizationsList,
vaultState,
hasPendingAccountAddition,
specialCircumstance,
->
userStateJson userStateJson
?.toUserState( ?.toUserState(
vaultState = vaultState, vaultState = vaultState,
userOrganizationsList = userOrganizationsList, userOrganizationsList = userOrganizationsList,
hasPendingAccountAddition = hasPendingAccountAddition,
specialCircumstance = specialCircumstance, specialCircumstance = specialCircumstance,
vaultUnlockTypeProvider = ::getVaultUnlockType, vaultUnlockTypeProvider = ::getVaultUnlockType,
) )
@ -125,6 +134,7 @@ class AuthRepositoryImpl(
?.toUserState( ?.toUserState(
vaultState = vaultRepository.vaultStateFlow.value, vaultState = vaultRepository.vaultStateFlow.value,
userOrganizationsList = authDiskSource.userOrganizationsList, userOrganizationsList = authDiskSource.userOrganizationsList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
specialCircumstance = mutableSpecialCircumstanceStateFlow.value, specialCircumstance = mutableSpecialCircumstanceStateFlow.value,
vaultUnlockTypeProvider = ::getVaultUnlockType, vaultUnlockTypeProvider = ::getVaultUnlockType,
), ),
@ -140,6 +150,9 @@ class AuthRepositoryImpl(
override var specialCircumstance: UserState.SpecialCircumstance? override var specialCircumstance: UserState.SpecialCircumstance?
by mutableSpecialCircumstanceStateFlow::value by mutableSpecialCircumstanceStateFlow::value
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value
override suspend fun deleteAccount(password: String): DeleteAccountResult { override suspend fun deleteAccount(password: String): DeleteAccountResult {
val profile = authDiskSource.userState?.activeAccount?.profile val profile = authDiskSource.userState?.activeAccount?.profile
?: return DeleteAccountResult.Error ?: return DeleteAccountResult.Error
@ -218,7 +231,7 @@ class AuthRepositoryImpl(
userId = userStateJson.activeUserId, userId = userStateJson.activeUserId,
) )
vaultRepository.sync() vaultRepository.sync()
specialCircumstance = null hasPendingAccountAddition = false
LoginResult.Success LoginResult.Success
} }
@ -268,8 +281,8 @@ class AuthRepositoryImpl(
val previousActiveUserId = currentUserState.activeUserId val previousActiveUserId = currentUserState.activeUserId
if (userId == previousActiveUserId) { if (userId == previousActiveUserId) {
// No switching to do but clear any special circumstances // No switching to do but clear any pending account additions
specialCircumstance = null hasPendingAccountAddition = false
return SwitchAccountResult.NoChange return SwitchAccountResult.NoChange
} }
@ -284,8 +297,8 @@ class AuthRepositoryImpl(
// Clear data for the previous user // Clear data for the previous user
vaultRepository.clearUnlockedData() vaultRepository.clearUnlockedData()
// Clear any special circumstances // Clear any pending account additions
specialCircumstance = null hasPendingAccountAddition = false
return SwitchAccountResult.AccountSwitched return SwitchAccountResult.AccountSwitched
} }

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/** /**
* Represents the overall "user state" of the current active user as well as any users that may be * Represents the overall "user state" of the current active user as well as any users that may be
@ -10,11 +11,14 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment
* @property activeUserId The ID of the current active user. * @property activeUserId The ID of the current active user.
* @property accounts A mapping between user IDs and the [Account] information associated with * @property accounts A mapping between user IDs and the [Account] information associated with
* that user. * that user.
* @property hasPendingAccountAddition Returns `true` if there is an additional account that is
* pending login/registration in order to have multiple accounts available.
* @property specialCircumstance A special circumstance (if any) that may be present. * @property specialCircumstance A special circumstance (if any) that may be present.
*/ */
data class UserState( data class UserState(
val activeUserId: String, val activeUserId: String,
val accounts: List<Account>, val accounts: List<Account>,
val hasPendingAccountAddition: Boolean = false,
val specialCircumstance: SpecialCircumstance? = null, val specialCircumstance: SpecialCircumstance? = null,
) { ) {
init { init {
@ -27,12 +31,6 @@ data class UserState(
val activeAccount: Account val activeAccount: Account
get() = accounts.first { it.userId == activeUserId } get() = accounts.first { it.userId == activeUserId }
/**
* Returns `true` if a new user is in the process of being added, `false` otherwise.
*/
val hasPendingAccountAddition: Boolean
get() = specialCircumstance == SpecialCircumstance.PendingAccountAddition
/** /**
* Basic account information about a given user. * Basic account information about a given user.
* *
@ -65,11 +63,12 @@ data class UserState(
* Represents a special account-related circumstance. * Represents a special account-related circumstance.
*/ */
sealed class SpecialCircumstance { sealed class SpecialCircumstance {
/** /**
* There is an additional account that is pending login/registration in order to have * The app was launched in order to create/share a new Send using the given [data].
* multiple accounts available.
*/ */
data object PendingAccountAddition : SpecialCircumstance() data class ShareNewSend(
val data: IntentManager.ShareData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
} }
} }

View file

@ -47,6 +47,7 @@ fun UserStateJson.toUpdatedUserStateJson(
fun UserStateJson.toUserState( fun UserStateJson.toUserState(
vaultState: VaultState, vaultState: VaultState,
userOrganizationsList: List<UserOrganizations>, userOrganizationsList: List<UserOrganizations>,
hasPendingAccountAddition: Boolean,
specialCircumstance: UserState.SpecialCircumstance?, specialCircumstance: UserState.SpecialCircumstance?,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType, vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
): UserState = ): UserState =
@ -77,5 +78,6 @@ fun UserStateJson.toUserState(
vaultUnlockType = vaultUnlockTypeProvider(userId), vaultUnlockType = vaultUnlockTypeProvider(userId),
) )
}, },
hasPendingAccountAddition = hasPendingAccountAddition,
specialCircumstance = specialCircumstance, specialCircumstance = specialCircumstance,
) )

View file

@ -31,7 +31,7 @@ import java.time.Clock
import javax.inject.Singleton import javax.inject.Singleton
/** /**
* Provides repositories in the auth package. * Provides managers in the platform package.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View file

@ -98,7 +98,7 @@ class VaultUnlockViewModel @Inject constructor(
} }
private fun handleAddAccountClick() { private fun handleAddAccountClick() {
authRepository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition authRepository.hasPendingAccountAddition = true
} }
private fun handleDismissDialog() { private fun handleDismissDialog() {

View file

@ -20,9 +20,12 @@ import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash
import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_FOR_NEW_SEND_GRAPH_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAPH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAPH_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedForNewSendGraph
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraphForNewSend
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -31,6 +34,7 @@ import java.util.concurrent.atomic.AtomicReference
/** /**
* Controls root level [NavHost] for the app. * Controls root level [NavHost] for the app.
*/ */
@Suppress("LongMethod")
@Composable @Composable
fun RootNavScreen( fun RootNavScreen(
viewModel: RootNavViewModel = hiltViewModel(), viewModel: RootNavViewModel = hiltViewModel(),
@ -66,6 +70,7 @@ fun RootNavScreen(
authGraph(navController) authGraph(navController)
vaultUnlockDestination() vaultUnlockDestination()
vaultUnlockedGraph(navController) vaultUnlockedGraph(navController)
vaultUnlockedGraphForNewSend(navController)
} }
val targetRoute = when (state) { val targetRoute = when (state) {
@ -73,6 +78,7 @@ fun RootNavScreen(
RootNavState.Splash -> SPLASH_ROUTE RootNavState.Splash -> SPLASH_ROUTE
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
is RootNavState.VaultUnlocked -> VAULT_UNLOCKED_GRAPH_ROUTE is RootNavState.VaultUnlocked -> VAULT_UNLOCKED_GRAPH_ROUTE
RootNavState.VaultUnlockedForNewSend -> VAULT_UNLOCKED_FOR_NEW_SEND_GRAPH_ROUTE
} }
val currentRoute = navController.currentDestination?.rootLevelRoute() val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -102,6 +108,9 @@ fun RootNavScreen(
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions) RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions) RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions) is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
RootNavState.VaultUnlockedForNewSend -> {
navController.navigateToVaultUnlockedForNewSendGraph(rootNavOptions)
}
} }
} }

View file

@ -51,9 +51,18 @@ class RootNavViewModel @Inject constructor(
userState.hasPendingAccountAddition -> RootNavState.Auth userState.hasPendingAccountAddition -> RootNavState.Auth
userState.activeAccount.isVaultUnlocked -> { userState.activeAccount.isVaultUnlocked -> {
RootNavState.VaultUnlocked( when (userState.specialCircumstance) {
activeUserId = userState.activeAccount.userId, is UserState.SpecialCircumstance.ShareNewSend -> {
) RootNavState.VaultUnlockedForNewSend
}
null,
-> {
RootNavState.VaultUnlocked(
activeUserId = userState.activeAccount.userId,
)
}
}
} }
else -> RootNavState.VaultLocked else -> RootNavState.VaultLocked
@ -91,6 +100,12 @@ sealed class RootNavState : Parcelable {
data class VaultUnlocked( data class VaultUnlocked(
val activeUserId: String, val activeUserId: String,
) : RootNavState() ) : RootNavState()
/**
* App should show the new send screen for an unlocked user.
*/
@Parcelize
data object VaultUnlockedForNewSend : RootNavState()
} }
/** /**

View file

@ -14,6 +14,8 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.generatorModalDestination
import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorModal import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorModal
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.passwordHistoryDestination import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.passwordHistoryDestination
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.ADD_SEND_AS_ROOT_ROUTE
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.addSendAsRootDestination
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.addSendDestination import com.x8bit.bitwarden.ui.tools.feature.send.addsend.addSendDestination
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
@ -30,6 +32,7 @@ import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.vaultQrCodeScanDestinatio
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
const val VAULT_UNLOCKED_GRAPH_ROUTE: String = "vault_unlocked_graph" const val VAULT_UNLOCKED_GRAPH_ROUTE: String = "vault_unlocked_graph"
const val VAULT_UNLOCKED_FOR_NEW_SEND_GRAPH_ROUTE: String = "vault_unlocked_for_new_send_graph"
/** /**
* Navigate to the vault unlocked screen. * Navigate to the vault unlocked screen.
@ -38,6 +41,13 @@ fun NavController.navigateToVaultUnlockedGraph(navOptions: NavOptions? = null) {
navigate(VAULT_UNLOCKED_GRAPH_ROUTE, navOptions) navigate(VAULT_UNLOCKED_GRAPH_ROUTE, navOptions)
} }
/**
* Navigate to the vault unlocked graph for a new send.
*/
fun NavController.navigateToVaultUnlockedForNewSendGraph(navOptions: NavOptions? = null) {
navigate(VAULT_UNLOCKED_FOR_NEW_SEND_GRAPH_ROUTE, navOptions)
}
/** /**
* Add vault unlocked destinations to the root nav graph. * Add vault unlocked destinations to the root nav graph.
*/ */
@ -107,3 +117,17 @@ fun NavGraphBuilder.vaultUnlockedGraph(
generatorModalDestination(onNavigateBack = { navController.popBackStack() }) generatorModalDestination(onNavigateBack = { navController.popBackStack() })
} }
} }
/**
* Add vault unlocked destinations for the new send flow to the root nav graph.
*/
fun NavGraphBuilder.vaultUnlockedGraphForNewSend(
navController: NavController,
) {
navigation(
startDestination = ADD_SEND_AS_ROOT_ROUTE,
route = VAULT_UNLOCKED_FOR_NEW_SEND_GRAPH_ROUTE,
) {
addSendAsRootDestination(onNavigateBack = { navController.popBackStack() })
}
}

View file

@ -0,0 +1,25 @@
package com.x8bit.bitwarden.ui.platform.manager.di
import android.content.Context
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
/**
* Provides UI-based managers in the platform package.
*/
@Module
@InstallIn(SingletonComponent::class)
class PlatformUiManagerModule {
@Provides
fun provideIntentManager(
@ApplicationContext context: Context,
): IntentManager =
IntentManagerImpl(
context = context,
)
}

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
/** /**
* A manager class for simplifying the handling of Android Intents within a given context. * A manager class for simplifying the handling of Android Intents within a given context.
*/ */
@Suppress("TooManyFunctions")
interface IntentManager { interface IntentManager {
/** /**
@ -61,6 +62,11 @@ interface IntentManager {
*/ */
fun getFileDataFromIntent(activityResult: ActivityResult): FileData? fun getFileDataFromIntent(activityResult: ActivityResult): FileData?
/**
* Processes the [intent] and attempts to derive [ShareData] information from it.
*/
fun getShareDataFromIntent(intent: Intent): ShareData?
/** /**
* Creates an intent for choosing a file saved to disk. * Creates an intent for choosing a file saved to disk.
*/ */
@ -74,4 +80,24 @@ interface IntentManager {
val uri: Uri, val uri: Uri,
val sizeBytes: Long, val sizeBytes: Long,
) )
/**
* Represents data for a share request coming from outside the app.
*/
sealed class ShareData {
/**
* The data required to create a new Text Send.
*/
data class TextSend(
val subject: String?,
val text: String,
) : ShareData()
/**
* The data required to create a new File Send.
*/
data class FileSend(
val fileData: IntentManager.FileData,
) : ShareData()
}
} }

View file

@ -122,6 +122,31 @@ class IntentManagerImpl(
return if (uri != null) getLocalFileData(uri) else getCameraFileData() return if (uri != null) getLocalFileData(uri) else getCameraFileData()
} }
@Suppress("ReturnCount")
override fun getShareDataFromIntent(intent: Intent): IntentManager.ShareData? {
if (intent.action != Intent.ACTION_SEND) return null
return if (intent.type?.contains("text/") == true) {
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
val title = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return null
IntentManager.ShareData.TextSend(
subject = subject,
text = title,
)
} else {
getFileDataFromIntent(
ActivityResult(
Activity.RESULT_OK,
intent,
),
)
?.let {
IntentManager.ShareData.FileSend(
fileData = it,
)
}
}
}
override fun createFileChooserIntent(withCameraIntents: Boolean): Intent { override fun createFileChooserIntent(withCameraIntents: Boolean): Intent {
val chooserIntent = Intent.createChooser( val chooserIntent = Intent.createChooser(
Intent(Intent.ACTION_OPEN_DOCUMENT) Intent(Intent.ACTION_OPEN_DOCUMENT)

View file

@ -20,6 +20,8 @@ private const val ADD_SEND_ITEM_TYPE: String = "add_send_item_type"
private const val ADD_SEND_ROUTE: String = private const val ADD_SEND_ROUTE: String =
"$ADD_SEND_ITEM_PREFIX/{$ADD_SEND_ITEM_TYPE}?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" "$ADD_SEND_ITEM_PREFIX/{$ADD_SEND_ITEM_TYPE}?$EDIT_ITEM_ID={$EDIT_ITEM_ID}"
const val ADD_SEND_AS_ROOT_ROUTE: String = ADD_SEND_ITEM_PREFIX
/** /**
* Class to retrieve send add & edit arguments from the [SavedStateHandle]. * Class to retrieve send add & edit arguments from the [SavedStateHandle].
*/ */
@ -28,9 +30,10 @@ data class AddSendArgs(
val sendAddType: AddSendType, val sendAddType: AddSendType,
) { ) {
constructor(savedStateHandle: SavedStateHandle) : this( constructor(savedStateHandle: SavedStateHandle) : this(
sendAddType = when (requireNotNull(savedStateHandle[ADD_SEND_ITEM_TYPE])) { sendAddType = when (savedStateHandle.get<String>(ADD_SEND_ITEM_TYPE)) {
ADD_TYPE -> AddSendType.AddItem ADD_TYPE -> AddSendType.AddItem
EDIT_TYPE -> AddSendType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID])) EDIT_TYPE -> AddSendType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
null -> AddSendType.AddItem
else -> throw IllegalStateException("Unknown VaultAddEditType.") else -> throw IllegalStateException("Unknown VaultAddEditType.")
}, },
) )
@ -52,6 +55,19 @@ fun NavGraphBuilder.addSendDestination(
} }
} }
/**
* Add the new send screen to the nav graph as a root destination for a nested graph.
*/
fun NavGraphBuilder.addSendAsRootDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ADD_SEND_AS_ROOT_ROUTE,
) {
AddSendScreen(onNavigateBack = onNavigateBack)
}
}
/** /**
* Navigate to the new send screen. * Navigate to the new send screen.
*/ */

View file

@ -101,6 +101,9 @@ class AddSendViewModel @Inject constructor(
) { ) {
init { init {
// TODO: Check the special circumstance to place in custom mode when a new send request is
// initiated externally (BIT-1518).
when (val addSendType = state.addSendType) { when (val addSendType = state.addSendType) {
AddSendType.AddItem -> Unit AddSendType.AddItem -> Unit
is AddSendType.EditItem -> { is AddSendType.EditItem -> {

View file

@ -205,7 +205,7 @@ class VaultViewModel @Inject constructor(
} }
private fun handleAddAccountClick() { private fun handleAddAccountClick() {
authRepository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition authRepository.hasPendingAccountAddition = true
} }
private fun handleSyncClick() { private fun handleSyncClick() {

View file

@ -4,17 +4,23 @@ 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.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.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
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() {
@ -24,18 +30,27 @@ class MainViewModelTest : BaseViewModelTest() {
val authRepository = mockk<AuthRepository> { val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { activeUserId } returns USER_ID every { activeUserId } returns USER_ID
every { every { specialCircumstance } returns null
setCaptchaCallbackTokenResult( every { specialCircumstance = any() } just runs
tokenResult = CaptchaCallbackTokenResult.Success( every { setCaptchaCallbackTokenResult(any()) } just runs
token = "mockk_token",
),
)
} just runs
} }
private val settingsRepository = mockk<SettingsRepository> { private val settingsRepository = mockk<SettingsRepository> {
every { appTheme } returns AppTheme.DEFAULT every { appTheme } returns AppTheme.DEFAULT
every { appThemeStateFlow } returns mutableAppThemeFlow every { appThemeStateFlow } returns mutableAppThemeFlow
} }
private val intentManager: IntentManager = mockk {
every { getShareDataFromIntent(any()) } returns null
}
@BeforeEach
fun setUp() {
mockkStatic(CAPTCHA_UTILS_PATH)
}
@AfterEach
fun tearDown() {
unmockkStatic(CAPTCHA_UTILS_PATH)
}
@Test @Test
fun `on AppThemeChanged should update state`() { fun `on AppThemeChanged should update state`() {
@ -65,14 +80,37 @@ class MainViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = mockIntent,
),
)
verify {
authRepository.specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = shareData,
shouldFinishWhenComplete = true,
)
}
}
@Test @Test
fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() { fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent>()
every { data?.host } returns "captcha-callback" every {
every { data?.getQueryParameter("token") } returns "mockk_token" mockIntent.getCaptchaCallbackTokenResult()
every { action } returns Intent.ACTION_VIEW } returns CaptchaCallbackTokenResult.Success(
} token = "mockk_token",
)
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveNewIntent( MainAction.ReceiveNewIntent(
intent = mockIntent, intent = mockIntent,
@ -87,12 +125,37 @@ class MainViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
intent = mockIntent,
),
)
verify {
authRepository.specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
data = shareData,
shouldFinishWhenComplete = false,
)
}
}
private fun createViewModel() = MainViewModel( private fun createViewModel() = MainViewModel(
authRepository = authRepository, authRepository = authRepository,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
intentManager = intentManager,
) )
companion object { companion object {
private const val CAPTCHA_UTILS_PATH =
"com.x8bit.bitwarden.data.auth.repository.util.CaptchaUtilsKt"
private const val USER_ID = "userID" private const val USER_ID = "userID"
private val DEFAULT_USER_STATE = UserState( private val DEFAULT_USER_STATE = UserState(
activeUserId = USER_ID, activeUserId = USER_ID,

View file

@ -68,6 +68,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach 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.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
@ -217,6 +218,7 @@ class AuthRepositoryTest {
SINGLE_USER_STATE_1.toUserState( SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE, vaultState = VAULT_STATE,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
), ),
@ -238,6 +240,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE.toUserState( MULTI_USER_STATE.toUserState(
vaultState = VAULT_STATE, vaultState = VAULT_STATE,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN }, vaultUnlockTypeProvider = { VaultUnlockType.PIN },
), ),
@ -253,6 +256,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE.toUserState( MULTI_USER_STATE.toUserState(
vaultState = emptyVaultState, vaultState = emptyVaultState,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN }, vaultUnlockTypeProvider = { VaultUnlockType.PIN },
), ),
@ -277,6 +281,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE.toUserState( MULTI_USER_STATE.toUserState(
vaultState = emptyVaultState, vaultState = emptyVaultState,
userOrganizationsList = USER_ORGANIZATIONS, userOrganizationsList = USER_ORGANIZATIONS,
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
), ),
@ -306,6 +311,7 @@ class AuthRepositoryTest {
val initialUserState = SINGLE_USER_STATE_1.toUserState( val initialUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE, vaultState = VAULT_STATE,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
) )
@ -316,11 +322,12 @@ class AuthRepositoryTest {
repository.userStateFlow.value, repository.userStateFlow.value,
) )
repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition val mockSpecialCircumstance: UserState.SpecialCircumstance = mockk()
repository.specialCircumstance = mockSpecialCircumstance
assertEquals( assertEquals(
initialUserState.copy( initialUserState.copy(
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition, specialCircumstance = mockSpecialCircumstance,
), ),
repository.userStateFlow.value, repository.userStateFlow.value,
) )
@ -595,7 +602,7 @@ class AuthRepositoryTest {
runTest { runTest {
// Ensure the initial state for User 2 with a account addition // Ensure the initial state for User 2 with a account addition
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2 fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition repository.hasPendingAccountAddition = true
// Set up login for User 1 // Set up login for User 1
val successResponse = GET_TOKEN_RESPONSE_SUCCESS val successResponse = GET_TOKEN_RESPONSE_SUCCESS
@ -665,7 +672,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE, MULTI_USER_STATE,
fakeAuthDiskSource.userState, fakeAuthDiskSource.userState,
) )
assertNull(repository.specialCircumstance) assertFalse(repository.hasPendingAccountAddition)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify { vaultRepository.clearUnlockedData() } verify { vaultRepository.clearUnlockedData() }
} }
@ -1076,11 +1083,12 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `switchAccount when the given userId is the same as the current activeUserId should only clear any special circumstances`() { fun `switchAccount when the given userId is the same as the current activeUserId should reset any pending account additions`() {
val originalUserId = USER_ID_1 val originalUserId = USER_ID_1
val originalUserState = SINGLE_USER_STATE_1.toUserState( val originalUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE, vaultState = VAULT_STATE,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
) )
@ -1089,7 +1097,7 @@ class AuthRepositoryTest {
originalUserState, originalUserState,
repository.userStateFlow.value, repository.userStateFlow.value,
) )
repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition repository.hasPendingAccountAddition = true
assertEquals( assertEquals(
SwitchAccountResult.NoChange, SwitchAccountResult.NoChange,
@ -1100,7 +1108,7 @@ class AuthRepositoryTest {
originalUserState, originalUserState,
repository.userStateFlow.value, repository.userStateFlow.value,
) )
assertNull(repository.specialCircumstance) assertFalse(repository.hasPendingAccountAddition)
verify(exactly = 0) { vaultRepository.clearUnlockedData() } verify(exactly = 0) { vaultRepository.clearUnlockedData() }
} }
@ -1111,6 +1119,7 @@ class AuthRepositoryTest {
val originalUserState = SINGLE_USER_STATE_1.toUserState( val originalUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE, vaultState = VAULT_STATE,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
) )
@ -1134,11 +1143,12 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `switchAccount when the userId is valid should update the current UserState, clear the previously unlocked data, and reset the special circumstance`() { fun `switchAccount when the userId is valid should update the current UserState, clear the previously unlocked data, and reset any pending account additions`() {
val updatedUserId = USER_ID_2 val updatedUserId = USER_ID_2
val originalUserState = MULTI_USER_STATE.toUserState( val originalUserState = MULTI_USER_STATE.toUserState(
vaultState = VAULT_STATE, vaultState = VAULT_STATE,
userOrganizationsList = emptyList(), userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
) )
@ -1147,7 +1157,7 @@ class AuthRepositoryTest {
originalUserState, originalUserState,
repository.userStateFlow.value, repository.userStateFlow.value,
) )
repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition repository.hasPendingAccountAddition = true
assertEquals( assertEquals(
SwitchAccountResult.AccountSwitched, SwitchAccountResult.AccountSwitched,
@ -1158,7 +1168,7 @@ class AuthRepositoryTest {
originalUserState.copy(activeUserId = updatedUserId), originalUserState.copy(activeUserId = updatedUserId),
repository.userStateFlow.value, repository.userStateFlow.value,
) )
assertNull(repository.specialCircumstance) assertFalse(repository.hasPendingAccountAddition)
verify { vaultRepository.clearUnlockedData() } verify { vaultRepository.clearUnlockedData() }
} }

View file

@ -154,6 +154,7 @@ class UserStateJsonExtensionsTest {
), ),
), ),
), ),
hasPendingAccountAddition = false,
specialCircumstance = null, specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN }, vaultUnlockTypeProvider = { VaultUnlockType.PIN },
), ),
@ -185,7 +186,8 @@ class UserStateJsonExtensionsTest {
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
), ),
), ),
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition, hasPendingAccountAddition = true,
specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
), ),
UserStateJson( UserStateJson(
activeUserId = "activeUserId", activeUserId = "activeUserId",
@ -224,9 +226,12 @@ class UserStateJsonExtensionsTest {
), ),
), ),
), ),
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition, hasPendingAccountAddition = true,
specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
), ),
) )
} }
} }
private val MOCK_SPECIAL_CIRCUMSTANCE: UserState.SpecialCircumstance = mockk()

View file

@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJso
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
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.model.UserState.SpecialCircumstance
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -38,8 +37,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
private val authRepository = mockk<AuthRepository>() { private val authRepository = mockk<AuthRepository>() {
every { activeUserId } answers { mutableUserStateFlow.value?.activeUserId } every { activeUserId } answers { mutableUserStateFlow.value?.activeUserId }
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { specialCircumstance } returns null every { hasPendingAccountAddition } returns false
every { specialCircumstance = any() } just runs every { hasPendingAccountAddition = any() } just runs
every { logout() } just runs every { logout() } just runs
every { logout(any()) } just runs every { logout(any()) } just runs
every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched
@ -174,11 +173,11 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on AddAccountClick should update the SpecialCircumstance of the AuthRepository to PendingAccountAddition`() { fun `on AddAccountClick should set hasPendingAccountAddition to true on the AuthRepository`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(VaultUnlockAction.AddAccountClick) viewModel.trySendAction(VaultUnlockAction.AddAccountClick)
verify { verify {
authRepository.specialCircumstance = SpecialCircumstance.PendingAccountAddition authRepository.hasPendingAccountAddition = true
} }
} }

View file

@ -89,5 +89,14 @@ class RootNavScreenTest : BaseComposeTest() {
navOptions = expectedNavOptions, navOptions = expectedNavOptions,
) )
} }
// Make sure navigating to vault unlocked works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForNewSend
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_unlocked_for_new_send_graph",
navOptions = expectedNavOptions,
)
}
} }
} }

View file

@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
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.model.UserState.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -53,8 +52,8 @@ class VaultViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository = private val authRepository: AuthRepository =
mockk { mockk {
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { specialCircumstance } returns null every { hasPendingAccountAddition } returns false
every { specialCircumstance = any() } just runs every { hasPendingAccountAddition = any() } just runs
every { logout(any()) } just runs every { logout(any()) } just runs
every { switchAccount(any()) } answers { switchAccountResult } every { switchAccount(any()) } answers { switchAccountResult }
} }
@ -289,11 +288,11 @@ class VaultViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `on AddAccountClick should update the SpecialCircumstance of the AuthRepository to PendingAccountAddition`() { fun `on AddAccountClick should set hasPendingAccountAddition to true on the AuthRepository`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.AddAccountClick) viewModel.trySendAction(VaultAction.AddAccountClick)
verify { verify {
authRepository.specialCircumstance = SpecialCircumstance.PendingAccountAddition authRepository.hasPendingAccountAddition = true
} }
} }