diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f2ee6a904d..3ccec0217f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,7 +26,7 @@
@@ -44,6 +44,15 @@
android:host="captcha-callback"
android:scheme="bitwarden" />
+
+
+
+
+
+
+
+
+
(
MainState(
@@ -37,6 +40,7 @@ class MainViewModel @Inject constructor(
override fun handleAction(action: MainAction) {
when (action) {
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
+ is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
}
}
@@ -45,8 +49,22 @@ class MainViewModel @Inject constructor(
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) {
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
+ val shareData = intentManager.getShareDataFromIntent(action.intent)
when {
captchaCallbackTokenResult != null -> {
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
}
}
@@ -71,6 +99,11 @@ data class MainState(
* Models actions for the [MainActivity].
*/
sealed class MainAction {
+ /**
+ * Receive first Intent by the application.
+ */
+ data class ReceiveFirstIntent(val intent: Intent) : MainAction()
+
/**
* Receive Intent by the application.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt
index c075e6a8d6..24ec6d4857 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt
@@ -48,6 +48,15 @@ interface AuthRepository : AuthenticatorProvider {
*/
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.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
index 75e2f88d9c..56f1fb9c51 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
@@ -73,6 +73,7 @@ class AuthRepositoryImpl(
dispatcherManager: DispatcherManager,
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
) : AuthRepository {
+ private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
private val mutableSpecialCircumstanceStateFlow =
MutableStateFlow(null)
@@ -107,12 +108,20 @@ class AuthRepositoryImpl(
authDiskSource.userStateFlow,
authDiskSource.userOrganizationsListFlow,
vaultRepository.vaultStateFlow,
+ mutableHasPendingAccountAdditionStateFlow,
mutableSpecialCircumstanceStateFlow,
- ) { userStateJson, userOrganizationsList, vaultState, specialCircumstance ->
+ ) {
+ userStateJson,
+ userOrganizationsList,
+ vaultState,
+ hasPendingAccountAddition,
+ specialCircumstance,
+ ->
userStateJson
?.toUserState(
vaultState = vaultState,
userOrganizationsList = userOrganizationsList,
+ hasPendingAccountAddition = hasPendingAccountAddition,
specialCircumstance = specialCircumstance,
vaultUnlockTypeProvider = ::getVaultUnlockType,
)
@@ -125,6 +134,7 @@ class AuthRepositoryImpl(
?.toUserState(
vaultState = vaultRepository.vaultStateFlow.value,
userOrganizationsList = authDiskSource.userOrganizationsList,
+ hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
specialCircumstance = mutableSpecialCircumstanceStateFlow.value,
vaultUnlockTypeProvider = ::getVaultUnlockType,
),
@@ -140,6 +150,9 @@ class AuthRepositoryImpl(
override var specialCircumstance: UserState.SpecialCircumstance?
by mutableSpecialCircumstanceStateFlow::value
+ override var hasPendingAccountAddition: Boolean
+ by mutableHasPendingAccountAdditionStateFlow::value
+
override suspend fun deleteAccount(password: String): DeleteAccountResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return DeleteAccountResult.Error
@@ -218,7 +231,7 @@ class AuthRepositoryImpl(
userId = userStateJson.activeUserId,
)
vaultRepository.sync()
- specialCircumstance = null
+ hasPendingAccountAddition = false
LoginResult.Success
}
@@ -268,8 +281,8 @@ class AuthRepositoryImpl(
val previousActiveUserId = currentUserState.activeUserId
if (userId == previousActiveUserId) {
- // No switching to do but clear any special circumstances
- specialCircumstance = null
+ // No switching to do but clear any pending account additions
+ hasPendingAccountAddition = false
return SwitchAccountResult.NoChange
}
@@ -284,8 +297,8 @@ class AuthRepositoryImpl(
// Clear data for the previous user
vaultRepository.clearUnlockedData()
- // Clear any special circumstances
- specialCircumstance = null
+ // Clear any pending account additions
+ hasPendingAccountAddition = false
return SwitchAccountResult.AccountSwitched
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt
index 906006f65c..9206032d06 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt
@@ -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.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
@@ -10,11 +11,14 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment
* @property activeUserId The ID of the current active user.
* @property accounts A mapping between user IDs and the [Account] information associated with
* 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.
*/
data class UserState(
val activeUserId: String,
val accounts: List,
+ val hasPendingAccountAddition: Boolean = false,
val specialCircumstance: SpecialCircumstance? = null,
) {
init {
@@ -27,12 +31,6 @@ data class UserState(
val activeAccount: Account
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.
*
@@ -65,11 +63,12 @@ data class UserState(
* Represents a special account-related circumstance.
*/
sealed class SpecialCircumstance {
-
/**
- * There is an additional account that is pending login/registration in order to have
- * multiple accounts available.
+ * The app was launched in order to create/share a new Send using the given [data].
*/
- data object PendingAccountAddition : SpecialCircumstance()
+ data class ShareNewSend(
+ val data: IntentManager.ShareData,
+ val shouldFinishWhenComplete: Boolean,
+ ) : SpecialCircumstance()
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt
index 7fe49efe3a..4d789aae2a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt
@@ -47,6 +47,7 @@ fun UserStateJson.toUpdatedUserStateJson(
fun UserStateJson.toUserState(
vaultState: VaultState,
userOrganizationsList: List,
+ hasPendingAccountAddition: Boolean,
specialCircumstance: UserState.SpecialCircumstance?,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
): UserState =
@@ -77,5 +78,6 @@ fun UserStateJson.toUserState(
vaultUnlockType = vaultUnlockTypeProvider(userId),
)
},
+ hasPendingAccountAddition = hasPendingAccountAddition,
specialCircumstance = specialCircumstance,
)
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt
index 8a05fe2815..69cf18a391 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt
@@ -31,7 +31,7 @@ import java.time.Clock
import javax.inject.Singleton
/**
- * Provides repositories in the auth package.
+ * Provides managers in the platform package.
*/
@Module
@InstallIn(SingletonComponent::class)
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt
index cf7cd7ba4d..a54fadcd1c 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt
@@ -98,7 +98,7 @@ class VaultUnlockViewModel @Inject constructor(
}
private fun handleAddAccountClick() {
- authRepository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
+ authRepository.hasPendingAccountAddition = true
}
private fun handleDismissDialog() {
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt
index 5e8ccea84b..512c901c37 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt
@@ -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.navigateToSplash
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.navigateToVaultUnlockedForNewSendGraph
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.vaultUnlockedGraphForNewSend
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -31,6 +34,7 @@ import java.util.concurrent.atomic.AtomicReference
/**
* Controls root level [NavHost] for the app.
*/
+@Suppress("LongMethod")
@Composable
fun RootNavScreen(
viewModel: RootNavViewModel = hiltViewModel(),
@@ -66,6 +70,7 @@ fun RootNavScreen(
authGraph(navController)
vaultUnlockDestination()
vaultUnlockedGraph(navController)
+ vaultUnlockedGraphForNewSend(navController)
}
val targetRoute = when (state) {
@@ -73,6 +78,7 @@ fun RootNavScreen(
RootNavState.Splash -> SPLASH_ROUTE
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
is RootNavState.VaultUnlocked -> VAULT_UNLOCKED_GRAPH_ROUTE
+ RootNavState.VaultUnlockedForNewSend -> VAULT_UNLOCKED_FOR_NEW_SEND_GRAPH_ROUTE
}
val currentRoute = navController.currentDestination?.rootLevelRoute()
@@ -102,6 +108,9 @@ fun RootNavScreen(
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
+ RootNavState.VaultUnlockedForNewSend -> {
+ navController.navigateToVaultUnlockedForNewSendGraph(rootNavOptions)
+ }
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt
index 12d3412486..79c65a0b9a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt
@@ -51,9 +51,18 @@ class RootNavViewModel @Inject constructor(
userState.hasPendingAccountAddition -> RootNavState.Auth
userState.activeAccount.isVaultUnlocked -> {
- RootNavState.VaultUnlocked(
- activeUserId = userState.activeAccount.userId,
- )
+ when (userState.specialCircumstance) {
+ is UserState.SpecialCircumstance.ShareNewSend -> {
+ RootNavState.VaultUnlockedForNewSend
+ }
+
+ null,
+ -> {
+ RootNavState.VaultUnlocked(
+ activeUserId = userState.activeAccount.userId,
+ )
+ }
+ }
}
else -> RootNavState.VaultLocked
@@ -91,6 +100,12 @@ sealed class RootNavState : Parcelable {
data class VaultUnlocked(
val activeUserId: String,
) : RootNavState()
+
+ /**
+ * App should show the new send screen for an unlocked user.
+ */
+ @Parcelize
+ data object VaultUnlockedForNewSend : RootNavState()
}
/**
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt
index 1c79f3d469..15157f512e 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt
@@ -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.passwordhistory.navigateToPasswordHistory
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.model.AddSendType
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
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.
@@ -38,6 +41,13 @@ fun NavController.navigateToVaultUnlockedGraph(navOptions: NavOptions? = null) {
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.
*/
@@ -107,3 +117,17 @@ fun NavGraphBuilder.vaultUnlockedGraph(
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() })
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt
new file mode 100644
index 0000000000..33dd5dd0ca
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt
@@ -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,
+ )
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt
index ce4df7f54d..ef0c7b1f6d 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
/**
* A manager class for simplifying the handling of Android Intents within a given context.
*/
+@Suppress("TooManyFunctions")
interface IntentManager {
/**
@@ -61,6 +62,11 @@ interface IntentManager {
*/
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.
*/
@@ -74,4 +80,24 @@ interface IntentManager {
val uri: Uri,
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()
+ }
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt
index 4dd5979cc4..9919a10bec 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt
@@ -122,6 +122,31 @@ class IntentManagerImpl(
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 {
val chooserIntent = Intent.createChooser(
Intent(Intent.ACTION_OPEN_DOCUMENT)
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt
index 9bf6ae03bf..89261b9fcf 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt
@@ -20,6 +20,8 @@ private const val ADD_SEND_ITEM_TYPE: String = "add_send_item_type"
private const val ADD_SEND_ROUTE: String =
"$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].
*/
@@ -28,9 +30,10 @@ data class AddSendArgs(
val sendAddType: AddSendType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
- sendAddType = when (requireNotNull(savedStateHandle[ADD_SEND_ITEM_TYPE])) {
+ sendAddType = when (savedStateHandle.get(ADD_SEND_ITEM_TYPE)) {
ADD_TYPE -> AddSendType.AddItem
EDIT_TYPE -> AddSendType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
+ null -> AddSendType.AddItem
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.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt
index 262540bcda..9f8cc8a90a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt
@@ -101,6 +101,9 @@ class AddSendViewModel @Inject constructor(
) {
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) {
AddSendType.AddItem -> Unit
is AddSendType.EditItem -> {
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
index b87eff20ee..619bb4c09f 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
@@ -205,7 +205,7 @@ class VaultViewModel @Inject constructor(
}
private fun handleAddAccountClick() {
- authRepository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
+ authRepository.hasPendingAccountAddition = true
}
private fun handleSyncClick() {
diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
index 2e02117bd8..c048a0b730 100644
--- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
@@ -4,17 +4,23 @@ 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
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.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() {
@@ -24,18 +30,27 @@ class MainViewModelTest : BaseViewModelTest() {
val authRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { activeUserId } returns USER_ID
- every {
- setCaptchaCallbackTokenResult(
- tokenResult = CaptchaCallbackTokenResult.Success(
- token = "mockk_token",
- ),
- )
- } just runs
+ every { specialCircumstance } returns null
+ every { specialCircumstance = any() } just runs
+ every { setCaptchaCallbackTokenResult(any()) } just runs
}
private val settingsRepository = mockk {
every { appTheme } returns AppTheme.DEFAULT
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
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()
+ val shareData = mockk()
+ 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
fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
val viewModel = createViewModel()
- val mockIntent = mockk {
- every { data?.host } returns "captcha-callback"
- every { data?.getQueryParameter("token") } returns "mockk_token"
- every { action } returns Intent.ACTION_VIEW
- }
+ val mockIntent = mockk()
+ every {
+ mockIntent.getCaptchaCallbackTokenResult()
+ } returns CaptchaCallbackTokenResult.Success(
+ token = "mockk_token",
+ )
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
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()
+ val shareData = mockk()
+ 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(
authRepository = authRepository,
settingsRepository = settingsRepository,
+ intentManager = intentManager,
)
companion object {
+ private const val CAPTCHA_UTILS_PATH =
+ "com.x8bit.bitwarden.data.auth.repository.util.CaptchaUtilsKt"
private const val USER_ID = "userID"
private val DEFAULT_USER_STATE = UserState(
activeUserId = USER_ID,
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
index 8b477a5ee4..149b969b1d 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
@@ -68,6 +68,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
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.assertTrue
import org.junit.jupiter.api.BeforeEach
@@ -217,6 +218,7 @@ class AuthRepositoryTest {
SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
),
@@ -238,6 +240,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
),
@@ -253,6 +256,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE.toUserState(
vaultState = emptyVaultState,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
),
@@ -277,6 +281,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE.toUserState(
vaultState = emptyVaultState,
userOrganizationsList = USER_ORGANIZATIONS,
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
),
@@ -306,6 +311,7 @@ class AuthRepositoryTest {
val initialUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
@@ -316,11 +322,12 @@ class AuthRepositoryTest {
repository.userStateFlow.value,
)
- repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
+ val mockSpecialCircumstance: UserState.SpecialCircumstance = mockk()
+ repository.specialCircumstance = mockSpecialCircumstance
assertEquals(
initialUserState.copy(
- specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
+ specialCircumstance = mockSpecialCircumstance,
),
repository.userStateFlow.value,
)
@@ -595,7 +602,7 @@ class AuthRepositoryTest {
runTest {
// Ensure the initial state for User 2 with a account addition
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
- repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
+ repository.hasPendingAccountAddition = true
// Set up login for User 1
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
@@ -665,7 +672,7 @@ class AuthRepositoryTest {
MULTI_USER_STATE,
fakeAuthDiskSource.userState,
)
- assertNull(repository.specialCircumstance)
+ assertFalse(repository.hasPendingAccountAddition)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify { vaultRepository.clearUnlockedData() }
}
@@ -1076,11 +1083,12 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
@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 originalUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
@@ -1089,7 +1097,7 @@ class AuthRepositoryTest {
originalUserState,
repository.userStateFlow.value,
)
- repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
+ repository.hasPendingAccountAddition = true
assertEquals(
SwitchAccountResult.NoChange,
@@ -1100,7 +1108,7 @@ class AuthRepositoryTest {
originalUserState,
repository.userStateFlow.value,
)
- assertNull(repository.specialCircumstance)
+ assertFalse(repository.hasPendingAccountAddition)
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
}
@@ -1111,6 +1119,7 @@ class AuthRepositoryTest {
val originalUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
@@ -1134,11 +1143,12 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
@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 originalUserState = MULTI_USER_STATE.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
@@ -1147,7 +1157,7 @@ class AuthRepositoryTest {
originalUserState,
repository.userStateFlow.value,
)
- repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
+ repository.hasPendingAccountAddition = true
assertEquals(
SwitchAccountResult.AccountSwitched,
@@ -1158,7 +1168,7 @@ class AuthRepositoryTest {
originalUserState.copy(activeUserId = updatedUserId),
repository.userStateFlow.value,
)
- assertNull(repository.specialCircumstance)
+ assertFalse(repository.hasPendingAccountAddition)
verify { vaultRepository.clearUnlockedData() }
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt
index 46424c2530..8434eed890 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt
@@ -154,6 +154,7 @@ class UserStateJsonExtensionsTest {
),
),
),
+ hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
),
@@ -185,7 +186,8 @@ class UserStateJsonExtensionsTest {
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
),
),
- specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
+ hasPendingAccountAddition = true,
+ specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
),
UserStateJson(
activeUserId = "activeUserId",
@@ -224,9 +226,12 @@ class UserStateJsonExtensionsTest {
),
),
),
- specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
+ hasPendingAccountAddition = true,
+ specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
),
)
}
}
+
+private val MOCK_SPECIAL_CIRCUMSTANCE: UserState.SpecialCircumstance = mockk()
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt
index f9e73b060c..159f0c50b8 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt
@@ -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.model.SwitchAccountResult
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.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
@@ -38,8 +37,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
private val authRepository = mockk() {
every { activeUserId } answers { mutableUserStateFlow.value?.activeUserId }
every { userStateFlow } returns mutableUserStateFlow
- every { specialCircumstance } returns null
- every { specialCircumstance = any() } just runs
+ every { hasPendingAccountAddition } returns false
+ every { hasPendingAccountAddition = any() } just runs
every { logout() } just runs
every { logout(any()) } just runs
every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched
@@ -174,11 +173,11 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@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()
viewModel.trySendAction(VaultUnlockAction.AddAccountClick)
verify {
- authRepository.specialCircumstance = SpecialCircumstance.PendingAccountAddition
+ authRepository.hasPendingAccountAddition = true
}
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt
index e3bff6ec1a..567f9ddfc2 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt
@@ -89,5 +89,14 @@ class RootNavScreenTest : BaseComposeTest() {
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,
+ )
+ }
}
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
index 17b6e04431..dafc6cfb50 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
@@ -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.SwitchAccountResult
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.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
@@ -53,8 +52,8 @@ class VaultViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository =
mockk {
every { userStateFlow } returns mutableUserStateFlow
- every { specialCircumstance } returns null
- every { specialCircumstance = any() } just runs
+ every { hasPendingAccountAddition } returns false
+ every { hasPendingAccountAddition = any() } just runs
every { logout(any()) } just runs
every { switchAccount(any()) } answers { switchAccountResult }
}
@@ -289,11 +288,11 @@ class VaultViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@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()
viewModel.trySendAction(VaultAction.AddAccountClick)
verify {
- authRepository.specialCircumstance = SpecialCircumstance.PendingAccountAddition
+ authRepository.hasPendingAccountAddition = true
}
}