Handle navigation for auth requests from notification (#934)

This commit is contained in:
David Perez
2024-02-01 00:33:10 -06:00
committed by Álison Fernandes
parent 89dd552908
commit b15dc065be
11 changed files with 148 additions and 17 deletions

View File

@@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
@@ -111,10 +112,21 @@ class MainViewModel @Inject constructor(
intent: Intent,
isFirstIntent: Boolean,
) {
val passwordlessRequestData = intent.getPasswordlessRequestDataIntentOrNull()
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
when {
passwordlessRequestData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = passwordlessRequestData,
// Allow users back into the already-running app when completing the
// autofill task when this is not the first intent.
shouldFinishWhenComplete = isFirstIntent,
)
}
autofillSaveItem != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSave(

View File

@@ -3,14 +3,13 @@ package com.x8bit.bitwarden.data.auth.manager
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.compose.ui.graphics.Color
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -79,9 +78,7 @@ class AuthRequestNotificationManagerImpl(
PendingIntent.getActivity(
context,
NOTIFICATION_REQUEST_CODE,
Intent(context, MainActivity::class.java)
.setAction(NOTIFICATION_ACTION)
.putExtra(NOTIFICATION_DATA, data),
createPasswordlessRequestDataIntent(context, data),
PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
)
@@ -101,9 +98,7 @@ class AuthRequestNotificationManagerImpl(
?: NotificationManagerCompat.IMPORTANCE_DEFAULT
}
const val NOTIFICATION_ACTION: String = "com.x8bit.bitwarden.data.auth.manager.AUTH_REQUEST"
private const val NOTIFICATION_CHANNEL_ID: String = "general_notification_channel"
private const val NOTIFICATION_ID: Int = 2_6072_022
private const val NOTIFICATION_DATA: String = "notificationData"
private const val NOTIFICATION_REQUEST_CODE: Int = 20220801
private const val NOTIFICATION_DEFAULT_TIMEOUT_MILLIS: Long = 15L * 60L * 1_000L

View File

@@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.auth.util
import android.content.Context
import android.content.Intent
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
private const val NOTIFICATION_DATA: String = "notificationData"
/**
* Creates an [Intent] that can be used to navigate the pending auth approval screen.
*/
fun createPasswordlessRequestDataIntent(
context: Context,
data: PasswordlessRequestData,
): Intent =
Intent(context, MainActivity::class.java)
.putExtra(NOTIFICATION_DATA, data)
/**
* Checks if the given [Intent] contains data for passwordless authorization.
* The [PasswordlessRequestData] will be returned when present.
*/
fun Intent.getPasswordlessRequestDataIntentOrNull(): PasswordlessRequestData? =
this.getSafeParcelableExtra(NOTIFICATION_DATA)

View File

@@ -37,4 +37,13 @@ sealed class SpecialCircumstance : Parcelable {
val autofillSelectionData: AutofillSelectionData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
/**
* The app was launched in order to allow the user to authorize a passwordless login.
*/
@Parcelize
data class PasswordlessRequest(
val passwordlessRequestData: PasswordlessRequestData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
}

View File

@@ -11,6 +11,7 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
when (this) {
is SpecialCircumstance.AutofillSave -> this.autofillSaveItem
is SpecialCircumstance.AutofillSelection -> null
is SpecialCircumstance.PasswordlessRequest -> null
is SpecialCircumstance.ShareNewSend -> null
}
@@ -21,5 +22,6 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
when (this) {
is SpecialCircumstance.AutofillSave -> null
is SpecialCircumstance.AutofillSelection -> this.autofillSelectionData
is SpecialCircumstance.PasswordlessRequest -> null
is SpecialCircumstance.ShareNewSend -> null
}

View File

@@ -21,6 +21,7 @@ import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
import com.x8bit.bitwarden.ui.platform.feature.rootnav.util.toVaultItemListingType
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.navigateToLoginApproval
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
@@ -90,6 +91,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForAutofillSave,
is RootNavState.VaultUnlockedForAutofillSelection,
is RootNavState.VaultUnlockedForNewSend,
is RootNavState.VaultUnlockedForAuthRequest,
-> VAULT_UNLOCKED_GRAPH_ROUTE
}
val currentRoute = navController.currentDestination?.rootLevelRoute()
@@ -144,6 +146,14 @@ fun RootNavScreen(
navOptions = rootNavOptions,
)
}
RootNavState.VaultUnlockedForAuthRequest -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToLoginApproval(
fingerprint = null,
navOptions = rootNavOptions,
)
}
}
}

View File

@@ -83,6 +83,10 @@ class RootNavViewModel @Inject constructor(
is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend
is SpecialCircumstance.PasswordlessRequest -> {
RootNavState.VaultUnlockedForAuthRequest
}
null -> {
RootNavState.VaultUnlocked(
activeUserId = userState.activeAccount.userId,
@@ -156,6 +160,12 @@ sealed class RootNavState : Parcelable {
*/
@Parcelize
data object VaultUnlockedForNewSend : RootNavState()
/**
* App should show the auth confirmation screen for an unlocked user.
*/
@Parcelize
data object VaultUnlockedForAuthRequest : RootNavState()
}
/**

View File

@@ -4,20 +4,22 @@ import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val FINGERPRINT: String = "fingerprint"
private const val LOGIN_APPROVAL_PREFIX = "login_approval"
private const val LOGIN_APPROVAL_ROUTE = "$LOGIN_APPROVAL_PREFIX/{$FINGERPRINT}"
private const val LOGIN_APPROVAL_ROUTE = "$LOGIN_APPROVAL_PREFIX?$FINGERPRINT={$FINGERPRINT}"
/**
* Class to retrieve login approval arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class LoginApprovalArgs(val fingerprint: String) {
data class LoginApprovalArgs(val fingerprint: String?) {
constructor(savedStateHandle: SavedStateHandle) : this(
checkNotNull(savedStateHandle[FINGERPRINT]) as String,
fingerprint = savedStateHandle.get<String>(FINGERPRINT),
)
}
@@ -29,6 +31,13 @@ fun NavGraphBuilder.loginApprovalDestination(
) {
composableWithSlideTransitions(
route = LOGIN_APPROVAL_ROUTE,
arguments = listOf(
navArgument(FINGERPRINT) {
type = NavType.StringType
nullable = true
defaultValue = null
},
),
) {
LoginApprovalScreen(
onNavigateBack = onNavigateBack,
@@ -40,8 +49,8 @@ fun NavGraphBuilder.loginApprovalDestination(
* Navigate to the Login Approval screen.
*/
fun NavController.navigateToLoginApproval(
fingerprint: String,
fingerprint: String?,
navOptions: NavOptions? = null,
) {
navigate("$LOGIN_APPROVAL_PREFIX/$fingerprint", navOptions)
navigate("$LOGIN_APPROVAL_PREFIX?$FINGERPRINT=$fingerprint", navOptions)
}

View File

@@ -29,7 +29,7 @@ class LoginApprovalViewModel @Inject constructor(
) : BaseViewModel<LoginApprovalState, LoginApprovalEvent, LoginApprovalAction>(
initialState = savedStateHandle[KEY_STATE]
?: LoginApprovalState(
fingerprint = LoginApprovalArgs(savedStateHandle).fingerprint,
fingerprint = requireNotNull(LoginApprovalArgs(savedStateHandle).fingerprint),
masterPasswordHash = null,
publicKey = "",
requestId = "",