diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f76f5432ac..f3f51dcd5f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -238,6 +238,15 @@
android:authorities="${applicationId}.firebaseinitprovider"
tools:node="remove" />
+
+
+
) -> Unit
- operator fun invoke(context: Context, enrollmentResultHandler: (Either) -> Unit) {
+ operator fun invoke(
+ context: Context,
+ isNewClient: Boolean,
+ enrollmentResultHandler: (Either) -> Unit
+ ) {
this.enrollmentResultHandler = enrollmentResultHandler
scope.launch {
- enrollE2EI.initialEnrollment().fold({
+ enrollE2EI.initialEnrollment(isNewClientRegistration = isNewClient).fold({
enrollmentResultHandler(Either.Left(it))
}, {
if (it is E2EIEnrollmentResult.Initialized) {
initialEnrollmentResult = it
- OAuthUseCase(context, it.target, it.oAuthState).launch(
+ OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch(
context.getActivity()!!.activityResultRegistry,
::oAuthResultHandler
)
diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt
index 6e0476116e..12c2885903 100644
--- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt
+++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt
@@ -28,6 +28,8 @@ import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContracts
import com.wire.android.appLogger
import com.wire.android.util.deeplink.DeepLinkProcessor
+import com.wire.android.util.removeQueryParams
+import kotlinx.serialization.json.JsonObject
import net.openid.appauth.AppAuthConfiguration
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
@@ -41,7 +43,9 @@ import net.openid.appauth.ResponseTypeValues
import net.openid.appauth.browser.BrowserAllowList
import net.openid.appauth.browser.VersionedBrowserMatcher
import net.openid.appauth.connectivity.ConnectionBuilder
+import org.json.JSONObject
import java.net.HttpURLConnection
+import java.net.URI
import java.net.URL
import java.security.MessageDigest
import java.security.SecureRandom
@@ -52,7 +56,7 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
-class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: String?) {
+class OAuthUseCase(context: Context, private val authUrl: String, private val claims: JsonObject, oAuthState: String?) {
private var authState: AuthState = oAuthState?.let {
AuthState.jsonDeserialize(it)
} ?: AuthState()
@@ -117,7 +121,7 @@ class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: St
handleActivityResult(result, resultHandler)
}
AuthorizationServiceConfiguration.fetchFromUrl(
- Uri.parse(authUrl.plus(IDP_CONFIGURATION_PATH)),
+ Uri.parse(URI(authUrl).removeQueryParams().toString().plus(IDP_CONFIGURATION_PATH)),
{ configuration, ex ->
if (ex == null) {
authServiceConfig = configuration!!
@@ -177,7 +181,8 @@ class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: St
AuthorizationRequest.Scope.EMAIL,
AuthorizationRequest.Scope.PROFILE,
AuthorizationRequest.Scope.OFFLINE_ACCESS
- ).build()
+ ).setClaims(JSONObject(claims.toString()))
+ .build()
private fun AuthorizationRequest.Builder.setCodeVerifier(): AuthorizationRequest.Builder {
val codeVerifier = getCodeVerifier()
diff --git a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt
index b4f2378bf8..eb2a9cd3cb 100644
--- a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt
+++ b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt
@@ -47,7 +47,7 @@ class MigrateClientsDataUseCase @Inject constructor(
private val scalaUserDBProvider: ScalaUserDatabaseProvider,
private val userDataStoreProvider: UserDataStoreProvider
) {
- @Suppress("ReturnCount")
+ @Suppress("ReturnCount", "ComplexMethod")
suspend operator fun invoke(userId: UserId, isFederated: Boolean): Either =
scalaUserDBProvider.clientDAO(userId.value).flatMap { clientDAO ->
val clientId = clientDAO.clientInfo()?.clientId?.let { ClientId(it) }
@@ -103,6 +103,19 @@ class MigrateClientsDataUseCase @Inject constructor(
userDataStoreProvider.getOrCreate(userId).setInitialSyncCompleted()
}
}
+
+ is RegisterClientResult.E2EICertificateRequired ->
+ withTimeoutOrNull(SYNC_START_TIMEOUT) {
+ syncManager.waitUntilStartedOrFailure()
+ }.let {
+ it ?: Either.Left(NetworkFailure.NoNetworkConnection(null))
+ }.flatMap {
+ syncManager.waitUntilLiveOrFailure()
+ .onSuccess {
+ userDataStoreProvider.getOrCreate(userId).setInitialSyncCompleted()
+ TODO() // TODO: ask question about this!
+ }
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
index e1b4e05ae2..b3bf8103ea 100644
--- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
@@ -66,6 +66,7 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topappbar.CommonTopAppBar
import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel
import com.wire.android.ui.destinations.ConversationScreenDestination
+import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.ImportMediaScreenDestination
@@ -158,9 +159,9 @@ class WireActivity : AppCompatActivity() {
val startDestination = when (viewModel.initialAppState) {
InitialAppState.NOT_MIGRATED -> MigrationScreenDestination
InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination
- InitialAppState.LOGGED_IN -> HomeScreenDestination
- }
-
+ InitialAppState.ENROLL_E2EI -> E2EIEnrollmentScreenDestination
+ InitialAppState.LOGGED_IN -> HomeScreenDestination
+ }
appLogger.i("$TAG composable content")
setComposableContent(startDestination) {
appLogger.i("$TAG splash hide")
diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
index 5d934eb6b9..84574392d9 100644
--- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
@@ -29,6 +29,7 @@ import com.wire.android.appLogger
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.AuthServerConfigProvider
import com.wire.android.di.KaliumCoreLogic
+import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider
import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider
import com.wire.android.di.ObserveSyncStateUseCaseProvider
import com.wire.android.feature.AccountSwitchUseCase
@@ -110,6 +111,7 @@ class WireActivityViewModel @Inject constructor(
private val currentScreenManager: CurrentScreenManager,
private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory,
private val globalDataStore: GlobalDataStore,
+ private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory
) : ViewModel() {
var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState())
@@ -142,12 +144,16 @@ class WireActivityViewModel @Inject constructor(
private val _observeSyncFlowState: MutableStateFlow = MutableStateFlow(null)
val observeSyncFlowState: StateFlow = _observeSyncFlowState
+ private val _observeE2EIState: MutableStateFlow = MutableStateFlow(null)
+ private val observeE2EIState: StateFlow = _observeE2EIState
+
init {
observeSyncState()
observeUpdateAppState()
observeNewClientState()
observeScreenshotCensoringConfigState()
observeAppThemeState()
+ observerE2EIState()
}
private fun observeAppThemeState() {
@@ -160,6 +166,18 @@ class WireActivityViewModel @Inject constructor(
}
}
+ fun observerE2EIState() {
+ viewModelScope.launch(dispatchers.io()) {
+ observeUserId
+ .flatMapLatest {
+ it?.let { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(it).observeIfE2EIIsRequiredDuringLogin() }
+ ?: flowOf(null)
+ }
+ .distinctUntilChanged()
+ .collect { _observeE2EIState.emit(it) }
+ }
+ }
+
private fun observeSyncState() {
viewModelScope.launch(dispatchers.io()) {
observeUserId
@@ -233,6 +251,7 @@ class WireActivityViewModel @Inject constructor(
get() = when {
shouldMigrate() -> InitialAppState.NOT_MIGRATED
shouldLogIn() -> InitialAppState.NOT_LOGGED_IN
+ blockedByE2EI() -> InitialAppState.ENROLL_E2EI
else -> InitialAppState.LOGGED_IN
}
@@ -263,8 +282,10 @@ class WireActivityViewModel @Inject constructor(
// to handle the deepLinks above user needs to be Logged in
// do nothing, already handled by initialAppState
}
+
result is DeepLinkResult.JoinConversation ->
onConversationInviteDeepLink(result.code, result.key, result.domain, onOpenConversation)
+
result != null -> onResult(result)
result is DeepLinkResult.Unknown -> appLogger.e("unknown deeplink result $result")
}
@@ -391,6 +412,10 @@ class WireActivityViewModel @Inject constructor(
fun shouldLogIn(): Boolean = !hasValidCurrentSession()
+ fun blockedByE2EI(): Boolean {
+ return observeE2EIState.value == true
+ }
+
private fun hasValidCurrentSession(): Boolean = runBlocking {
// TODO: the usage of currentSessionFlow is a temporary solution, it should be replaced with a proper solution
currentSessionFlow().first().let {
@@ -510,5 +535,5 @@ data class GlobalAppState(
)
enum class InitialAppState {
- NOT_MIGRATED, NOT_LOGGED_IN, LOGGED_IN
+ NOT_MIGRATED, NOT_LOGGED_IN, LOGGED_IN, ENROLL_E2EI
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt
index d8b8fda010..c7a075c102 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt
@@ -187,6 +187,11 @@ class CreateAccountCodeViewModel @Inject constructor(
is RegisterClientResult.Success -> {
onSuccess()
}
+
+ is RegisterClientResult.E2EICertificateRequired -> {
+ // TODO
+ onSuccess()
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt
index 1e8e7c3122..4f5e3911eb 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt
@@ -51,6 +51,20 @@ data class Device(
mlsPublicKeys = client.mlsPublicKeys,
e2eiCertificateStatus = e2eiCertificateStatus
)
+
+ fun updateFromClient(client: Client): Device = copy(
+ name = client.displayName(),
+ clientId = client.id,
+ registrationTime = client.registrationTime?.toIsoDateTimeString(),
+ lastActiveInWholeWeeks = client.lastActiveInWholeWeeks(),
+ isValid = client.isValid,
+ isVerifiedProteus = client.isVerified,
+ mlsPublicKeys = client.mlsPublicKeys,
+ )
+
+ fun updateE2EICertificateStatus(e2eiCertificateStatus: CertificateStatus): Device = copy(
+ e2eiCertificateStatus = e2eiCertificateStatus
+ )
}
/**
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt
index 72c89dbeaf..ba16c4bb82 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt
@@ -61,6 +61,7 @@ import com.wire.android.ui.common.textfield.clearAutofillTree
import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.common.visbility.rememberVisibilityState
+import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.InitialSyncScreenDestination
import com.wire.android.ui.destinations.RemoveDeviceScreenDestination
@@ -81,11 +82,14 @@ fun RegisterDeviceScreen(navigator: Navigator) {
is RegisterDeviceFlowState.Success -> {
navigator.navigate(
NavigationCommand(
- destination = if (flowState.initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination,
+ destination = if (flowState.isE2EIRequired) E2EIEnrollmentScreenDestination
+ else if (flowState.initialSyncCompleted) HomeScreenDestination
+ else InitialSyncScreenDestination,
backStackMode = BackStackMode.CLEAR_WHOLE
)
)
}
+
is RegisterDeviceFlowState.TooManyDevices -> navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination))
else ->
RegisterDeviceContent(
@@ -189,6 +193,7 @@ private fun PasswordTextField(state: RegisterDeviceState, onPasswordChange: (Tex
state = when (state.flowState) {
is RegisterDeviceFlowState.Error.InvalidCredentialsError ->
WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password))
+
else -> WireTextFieldState.Default
},
imeAction = ImeAction.Done,
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt
index c0169ccdc8..766959596d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt
@@ -20,17 +20,26 @@ package com.wire.android.ui.authentication.devices.register
import androidx.compose.ui.text.input.TextFieldValue
import com.wire.kalium.logic.CoreFailure
+import com.wire.kalium.logic.data.conversation.ClientId
+import com.wire.kalium.logic.data.user.UserId
data class RegisterDeviceState(
val password: TextFieldValue = TextFieldValue(""),
val continueEnabled: Boolean = false,
val flowState: RegisterDeviceFlowState = RegisterDeviceFlowState.Default
)
+
sealed class RegisterDeviceFlowState {
object Default : RegisterDeviceFlowState()
object Loading : RegisterDeviceFlowState()
object TooManyDevices : RegisterDeviceFlowState()
- data class Success(val initialSyncCompleted: Boolean) : RegisterDeviceFlowState()
+ data class Success(
+ val initialSyncCompleted: Boolean,
+ val isE2EIRequired: Boolean,
+ val clientId: ClientId,
+ val userId: UserId? = null
+ ) : RegisterDeviceFlowState()
+
sealed class Error : RegisterDeviceFlowState() {
object InvalidCredentialsError : Error()
data class GenericError(val coreFailure: CoreFailure) : Error()
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt
index bc9ac87c01..b7d0354d7f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt
@@ -83,8 +83,25 @@ class RegisterDeviceViewModel @Inject constructor(
)) {
is RegisterClientResult.Failure.TooManyClients ->
updateFlowState(RegisterDeviceFlowState.TooManyDevices)
+
is RegisterClientResult.Success ->
- updateFlowState(RegisterDeviceFlowState.Success(userDataStore.initialSyncCompleted.first()))
+ updateFlowState(
+ RegisterDeviceFlowState.Success(
+ userDataStore.initialSyncCompleted.first(),
+ false,
+ registerDeviceResult.client.id
+ )
+ )
+
+ is RegisterClientResult.E2EICertificateRequired ->
+ updateFlowState(
+ RegisterDeviceFlowState.Success(
+ userDataStore.initialSyncCompleted.first(),
+ true,
+ registerDeviceResult.client.id,
+ registerDeviceResult.userId
+ )
+ )
is RegisterClientResult.Failure.Generic -> state = state.copy(
continueEnabled = true,
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt
index 1e8f40bdbf..a0c36cdc40 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt
@@ -58,6 +58,7 @@ import com.wire.android.ui.common.divider.WireDivider
import com.wire.android.ui.common.rememberTopBarElevationState
import com.wire.android.ui.common.textfield.clearAutofillTree
import com.wire.android.ui.common.visbility.rememberVisibilityState
+import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.InitialSyncScreenDestination
import com.wire.android.util.dialogErrorStrings
@@ -73,9 +74,11 @@ fun RemoveDeviceScreen(navigator: Navigator) {
val state: RemoveDeviceState = viewModel.state
val clearSessionState: ClearSessionState = clearSessionViewModel.state
- fun navigateAfterSuccess(initialSyncCompleted: Boolean) = navigator.navigate(
+ fun navigateAfterSuccess(initialSyncCompleted: Boolean, isE2EIRequired: Boolean) = navigator.navigate(
NavigationCommand(
- destination = if (initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination,
+ destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination
+ else if (initialSyncCompleted) HomeScreenDestination
+ else InitialSyncScreenDestination,
backStackMode = BackStackMode.CLEAR_WHOLE
)
)
@@ -84,9 +87,9 @@ fun RemoveDeviceScreen(navigator: Navigator) {
RemoveDeviceContent(
state = state,
clearSessionState = clearSessionState,
- onItemClicked = { viewModel.onItemClicked(it) { navigateAfterSuccess(it) } },
+ onItemClicked = { viewModel.onItemClicked(it, ::navigateAfterSuccess) },
onPasswordChange = viewModel::onPasswordChange,
- onRemoveConfirm = { viewModel.onRemoveConfirmed { navigateAfterSuccess(it) } },
+ onRemoveConfirm = { viewModel.onRemoveConfirmed(::navigateAfterSuccess) },
onDialogDismiss = viewModel::onDialogDismissed,
onErrorDialogDismiss = viewModel::clearDeleteClientError,
onBackButtonClicked = clearSessionViewModel::onBackButtonClicked,
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt
index 44422ce461..ceb0f3b653 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt
@@ -92,7 +92,7 @@ class RemoveDeviceViewModel @Inject constructor(
updateStateIfDialogVisible { state.copy(error = RemoveDeviceError.None) }
}
- fun onItemClicked(device: Device, onCompleted: (initialSyncCompleted: Boolean) -> Unit) {
+ fun onItemClicked(device: Device, onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) {
viewModelScope.launch {
val isPasswordRequired: Boolean = when (val passwordRequiredResult = isPasswordRequired()) {
is IsPasswordRequiredUseCase.Result.Failure -> {
@@ -113,7 +113,7 @@ class RemoveDeviceViewModel @Inject constructor(
}
}
- private suspend fun registerClient(password: String?, onCompleted: (initialSyncCompleted: Boolean) -> Unit) {
+ private suspend fun registerClient(password: String?, onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) {
registerClientUseCase(
RegisterClientUseCase.RegisterClientParam(password, null)
).also { result ->
@@ -125,12 +125,17 @@ class RemoveDeviceViewModel @Inject constructor(
is RegisterClientResult.Failure.Generic -> state = state.copy(error = RemoveDeviceError.GenericError(result.genericFailure))
is RegisterClientResult.Failure.InvalidCredentials -> state = state.copy(error = RemoveDeviceError.InvalidCredentialsError)
is RegisterClientResult.Failure.TooManyClients -> loadClientsList()
- is RegisterClientResult.Success -> onCompleted(userDataStore.initialSyncCompleted.first())
+ is RegisterClientResult.Success -> onCompleted(userDataStore.initialSyncCompleted.first(), false)
+ is RegisterClientResult.E2EICertificateRequired -> onCompleted(userDataStore.initialSyncCompleted.first(), true)
}
}
}
- private suspend fun deleteClient(password: String?, device: Device, onCompleted: (initialSyncCompleted: Boolean) -> Unit) {
+ private suspend fun deleteClient(
+ password: String?,
+ device: Device,
+ onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit
+ ) {
when (val deleteResult = deleteClientUseCase(DeleteClientParam(password, device.clientId))) {
is DeleteClientResult.Failure.Generic -> {
state = state.copy(error = RemoveDeviceError.GenericError(deleteResult.genericFailure))
@@ -147,7 +152,7 @@ class RemoveDeviceViewModel @Inject constructor(
}
}
- fun onRemoveConfirmed(onCompleted: (initialSyncCompleted: Boolean) -> Unit) {
+ fun onRemoveConfirmed(onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) {
(state.removeDeviceDialogState as? RemoveDeviceDialogState.Visible)?.let { dialogStateVisible ->
updateStateIfDialogVisible { state.copy(removeDeviceDialogState = it.copy(loading = true, removeEnabled = false)) }
viewModelScope.launch {
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt
index 55a656be33..acad24526f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt
@@ -74,6 +74,7 @@ import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogState
import com.wire.android.ui.common.rememberTopBarElevationState
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.common.visbility.rememberVisibilityState
+import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.InitialSyncScreenDestination
import com.wire.android.ui.destinations.RemoveDeviceScreenDestination
@@ -98,13 +99,12 @@ fun LoginScreen(
LoginContent(
navigator::navigateBack,
- { initialSyncCompleted ->
- navigator.navigate(
- NavigationCommand(
- if (initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination,
- BackStackMode.CLEAR_WHOLE
- )
- )
+ { initialSyncCompleted, isE2EIRequired ->
+ val destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination
+ else if (initialSyncCompleted) HomeScreenDestination
+ else InitialSyncScreenDestination
+
+ navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE))
},
{ navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) },
loginViewModel,
@@ -117,7 +117,7 @@ fun LoginScreen(
@Composable
private fun LoginContent(
onBackPressed: () -> Unit,
- onSuccess: (initialSyncCompleted: Boolean) -> Unit,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit,
onRemoveDeviceNeeded: () -> Unit,
viewModel: LoginViewModel,
loginEmailViewModel: LoginEmailViewModel,
@@ -146,7 +146,7 @@ private fun LoginContent(
@Composable
private fun MainLoginContent(
onBackPressed: () -> Unit,
- onSuccess: (initialSyncCompleted: Boolean) -> Unit,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit,
onRemoveDeviceNeeded: () -> Unit,
viewModel: LoginViewModel,
loginEmailViewModel: LoginEmailViewModel,
@@ -353,6 +353,6 @@ enum class LoginTabItem(@StringRes override val titleResId: Int) : TabItem {
@Composable
private fun PreviewLoginScreen() {
WireTheme {
- MainLoginContent({}, {}, {}, hiltViewModel(), hiltViewModel(), ssoLoginResult = null)
+ MainLoginContent({}, { _, _ -> }, {}, hiltViewModel(), hiltViewModel(), ssoLoginResult = null)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt
index a3a326cd82..f03624ae09 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt
@@ -77,7 +77,7 @@ import kotlinx.coroutines.launch
@Composable
fun LoginEmailScreen(
- onSuccess: (initialSyncCompleted: Boolean) -> Unit,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit,
onRemoveDeviceNeeded: () -> Unit,
loginEmailViewModel: LoginEmailViewModel,
scrollState: ScrollState = rememberScrollState()
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt
index f2dcd0f71d..43fb16021c 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt
@@ -46,7 +46,7 @@ import com.wire.android.util.ui.UIText
@Composable
fun LoginEmailVerificationCodeScreen(
- onSuccess: (initialSyncCompleted: Boolean) -> Unit,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit,
viewModel: LoginEmailViewModel = hiltViewModel()
) = LoginEmailVerificationCodeContent(
viewModel.secondFactorVerificationCodeState,
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt
index 52f2e6a9e3..85361e510b 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt
@@ -72,7 +72,7 @@ class LoginEmailViewModel @Inject constructor(
)
@Suppress("LongMethod")
- fun login(onSuccess: (initialSyncCompleted: Boolean) -> Unit) {
+ fun login(onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) {
loginState = loginState.copy(emailLoginLoading = true, loginError = LoginError.None).updateEmailLoginEnabled()
viewModelScope.launch {
val authScope = withContext(dispatchers.io()) { resolveCurrentAuthScope() } ?: return@launch
@@ -123,7 +123,12 @@ class LoginEmailViewModel @Inject constructor(
}
is RegisterClientResult.Success -> {
- onSuccess(isInitialSyncCompleted(storedUserId))
+ onSuccess(isInitialSyncCompleted(storedUserId), false)
+ }
+
+ is RegisterClientResult.E2EICertificateRequired -> {
+ onSuccess(isInitialSyncCompleted(storedUserId), true)
+ return@launch
}
}
}
@@ -215,7 +220,7 @@ class LoginEmailViewModel @Inject constructor(
loginState = loginState.copy(proxyPassword = newText).updateEmailLoginEnabled()
}
- fun onCodeChange(newValue: CodeFieldValue, onSuccess: (initialSyncCompleted: Boolean) -> Unit) {
+ fun onCodeChange(newValue: CodeFieldValue, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) {
secondFactorVerificationCodeState = secondFactorVerificationCodeState.copy(codeInput = newValue, isCurrentCodeInvalid = false)
if (newValue.isFullyFilled) {
login(onSuccess)
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt
index 211e26273c..a7ceb4f44f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt
@@ -61,7 +61,7 @@ import kotlinx.coroutines.flow.onEach
@Composable
fun LoginSSOScreen(
- onSuccess: (initialSyncCompleted: Boolean) -> Unit,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit,
onRemoveDeviceNeeded: () -> Unit,
ssoLoginResult: DeepLinkResult.SSOLogin?,
scrollState: ScrollState = rememberScrollState()
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt
index 263f1f25b9..af0e111012 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt
@@ -197,9 +197,13 @@ class LoginSSOViewModel @Inject constructor(
}
}
- @Suppress("ComplexMethod")
+ @Suppress("ComplexMethod", "LongMethod")
@VisibleForTesting
- fun establishSSOSession(cookie: String, serverConfigId: String, onSuccess: (initialSyncCompleted: Boolean) -> Unit) {
+ fun establishSSOSession(
+ cookie: String,
+ serverConfigId: String,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit
+ ) {
loginState = loginState.copy(ssoLoginLoading = true, loginError = LoginError.None).updateSSOLoginEnabled()
viewModelScope.launch {
val authScope =
@@ -251,13 +255,17 @@ class LoginSSOViewModel @Inject constructor(
registerClient(storedUserId, null).let {
when (it) {
is RegisterClientResult.Success -> {
- onSuccess(isInitialSyncCompleted(storedUserId))
+ onSuccess(isInitialSyncCompleted(storedUserId), false)
}
is RegisterClientResult.Failure -> {
updateSSOLoginError(it.toLoginError())
return@launch
}
+
+ is RegisterClientResult.E2EICertificateRequired -> {
+ onSuccess(isInitialSyncCompleted(storedUserId), true)
+ }
}
}
}
@@ -272,15 +280,18 @@ class LoginSSOViewModel @Inject constructor(
savedStateHandle.set(SSO_CODE_SAVED_STATE_KEY, newText.text)
}
- fun handleSSOResult(ssoLoginResult: DeepLinkResult.SSOLogin?, onSuccess: (initialSyncCompleted: Boolean) -> Unit) =
+ fun handleSSOResult(
+ ssoLoginResult: DeepLinkResult.SSOLogin?,
+ onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit
+ ) =
when (ssoLoginResult) {
- is DeepLinkResult.SSOLogin.Success -> {
- establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, onSuccess)
- }
+ is DeepLinkResult.SSOLogin.Success -> {
+ establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, onSuccess)
+ }
- is DeepLinkResult.SSOLogin.Failure -> updateSSOLoginError(LoginError.DialogError.SSOResultError(ssoLoginResult.ssoError))
- null -> {}
- }
+ is DeepLinkResult.SSOLogin.Failure -> updateSSOLoginError(LoginError.DialogError.SSOResultError(ssoLoginResult.ssoError))
+ null -> {}
+ }
private fun openWebUrl(url: String) {
viewModelScope.launch {
diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
index e492e64a7d..8978e3ed04 100644
--- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
@@ -128,7 +128,7 @@ class DebugDataOptionsViewModel
}
fun enrollE2EICertificate(context: Context) {
- e2eiCertificateUseCase(context) { result ->
+ e2eiCertificateUseCase(context, false) { result ->
result.fold({
state = state.copy(
certificate = (it as E2EIFailure.FailedOAuth).reason, showCertificate = true
diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt
new file mode 100644
index 0000000000..e341e844c9
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt
@@ -0,0 +1,231 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.e2eiEnrollment
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.wire.android.R
+import com.wire.android.feature.NavigationSwitchAccountActions
+import com.wire.android.navigation.BackStackMode
+import com.wire.android.navigation.NavigationCommand
+import com.wire.android.navigation.Navigator
+import com.wire.android.navigation.style.PopUpNavigationAnimation
+import com.wire.android.ui.common.ClickableText
+import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.button.WirePrimaryButton
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dialogs.CancelLoginDialogContent
+import com.wire.android.ui.common.dialogs.CancelLoginDialogState
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.common.scaffold.WireScaffold
+import com.wire.android.ui.common.topappbar.NavigationIconType
+import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
+import com.wire.android.ui.common.visbility.rememberVisibilityState
+import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination
+import com.wire.android.ui.destinations.InitialSyncScreenDestination
+import com.wire.android.ui.home.E2EIErrorWithDismissDialog
+import com.wire.android.ui.home.E2EISuccessDialog
+import com.wire.android.ui.markdown.MarkdownConstants
+import com.wire.android.ui.theme.WireTheme
+import com.wire.android.ui.theme.wireDimensions
+import com.wire.android.ui.theme.wireTypography
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@RootNavGraph
+@Destination(
+ style = PopUpNavigationAnimation::class
+)
+@Composable
+fun E2EIEnrollmentScreen(
+ navigator: Navigator,
+ viewModel: E2EIEnrollmentViewModel = hiltViewModel(),
+) {
+ val state = viewModel.state
+ val context = LocalContext.current
+
+ E2EIEnrollmentScreenContent(
+ state = state,
+ dismissSuccess = {
+ navigator.navigate(NavigationCommand(InitialSyncScreenDestination, BackStackMode.CLEAR_WHOLE))
+ viewModel.finalizeMLSClient()
+ },
+ dismissErrorDialog = viewModel::dismissErrorDialog,
+ enrollE2EICertificate = { viewModel.enrollE2EICertificate(context) },
+ openCertificateDetails = {
+ navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate)))
+ },
+ onBackButtonClicked = viewModel::onBackButtonClicked,
+ onCancelEnrollmentClicked = { viewModel.onCancelEnrollmentClicked(NavigationSwitchAccountActions(navigator::navigate)) },
+ onProceedEnrollmentClicked = viewModel::onProceedEnrollmentClicked
+ )
+}
+
+@Composable
+private fun E2EIEnrollmentScreenContent(
+ state: E2EIEnrollmentState,
+ dismissSuccess: () -> Unit,
+ dismissErrorDialog: () -> Unit,
+ enrollE2EICertificate: () -> Unit,
+ openCertificateDetails: () -> Unit,
+ onBackButtonClicked: () -> Unit,
+ onCancelEnrollmentClicked: () -> Unit,
+ onProceedEnrollmentClicked: () -> Unit
+) {
+ val uriHandler = LocalUriHandler.current
+ BackHandler {
+ onBackButtonClicked()
+ }
+ val cancelLoginDialogState = rememberVisibilityState()
+ CancelLoginDialogContent(
+ dialogState = cancelLoginDialogState,
+ onActionButtonClicked = {
+ onCancelEnrollmentClicked()
+ },
+ onProceedButtonClicked = {
+ onProceedEnrollmentClicked()
+ }
+ )
+ if (state.showCancelLoginDialog) {
+ cancelLoginDialogState.show(
+ cancelLoginDialogState.savedState ?: CancelLoginDialogState
+ )
+ } else {
+ cancelLoginDialogState.dismiss()
+ }
+ WireScaffold(
+ topBar = {
+ WireCenterAlignedTopAppBar(
+ elevation = 0.dp,
+ title = stringResource(id = R.string.end_to_end_identity_required_dialog_title),
+ navigationIconType = NavigationIconType.Close,
+ onNavigationPressed = onBackButtonClicked
+ )
+ },
+ bottomBar = {
+ Column(
+ Modifier
+ .wrapContentWidth(Alignment.CenterHorizontally)
+ ) {
+ WirePrimaryButton(
+ onClick = enrollE2EICertificate,
+ text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button),
+ state = WireButtonState.Default,
+ loading = state.isLoading,
+ modifier = Modifier.padding(
+ top = dimensions().spacing16x,
+ start = dimensions().spacing16x,
+ end = dimensions().spacing16x,
+ bottom = dimensions().spacing16x
+ )
+ )
+ }
+ }
+ ) { internalPadding ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(PaddingValues(MaterialTheme.wireDimensions.dialogContentPadding))
+ ) {
+ Spacer(modifier = Modifier.height(internalPadding.calculateTopPadding()))
+ val text = buildAnnotatedString {
+ val style = SpanStyle(
+ color = colorsScheme().onBackground,
+ fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
+ fontSize = MaterialTheme.wireTypography.body01.fontSize,
+ fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
+ fontStyle = MaterialTheme.wireTypography.body01.fontStyle
+ )
+ withStyle(style) { append(stringResource(id = R.string.end_to_end_identity_required_dialog_text_no_snooze)) }
+ }
+ ClickableText(
+ text = text,
+ style = MaterialTheme.wireTypography.body01,
+ modifier = Modifier.padding(
+ top = MaterialTheme.wireDimensions.dialogTextsSpacing,
+ bottom = MaterialTheme.wireDimensions.dialogTextsSpacing,
+ ),
+ onClick = { offset ->
+ text.getStringAnnotations(
+ tag = MarkdownConstants.TAG_URL,
+ start = offset,
+ end = offset,
+ ).firstOrNull()?.let { result -> uriHandler.openUri(result.item) }
+ }
+ )
+ }
+
+ if (state.isCertificateEnrollError) {
+ E2EIErrorWithDismissDialog(
+ isE2EILoading = state.isLoading,
+ updateCertificate = enrollE2EICertificate,
+ onDismiss = dismissErrorDialog
+ )
+ }
+
+ if (state.isCertificateEnrollSuccess) {
+ E2EISuccessDialog(
+ openCertificateDetails = openCertificateDetails,
+ dismissDialog = dismissSuccess
+ )
+ }
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewE2EIEnrollmentScreenContent() {
+ WireTheme {
+ E2EIEnrollmentScreenContent(E2EIEnrollmentState(), {}, {}, {}, {}, {}, {}) { }
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewE2EIEnrollmentScreenContentWithSuccess() {
+ WireTheme {
+ E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollSuccess = true), {}, {}, {}, {}, {}, {}) { }
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewE2EIEnrollmentScreenContentWithError() {
+ WireTheme {
+ E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollError = true), {}, {}, {}, {}, {}, {}) { }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt
new file mode 100644
index 0000000000..514ca785d3
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt
@@ -0,0 +1,123 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.e2eiEnrollment
+
+import android.content.Context
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.wire.android.appLogger
+import com.wire.android.feature.AccountSwitchUseCase
+import com.wire.android.feature.SwitchAccountActions
+import com.wire.android.feature.SwitchAccountParam
+import com.wire.android.feature.e2ei.GetE2EICertificateUseCase
+import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment
+import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult
+import com.wire.kalium.logic.feature.session.CurrentSessionResult
+import com.wire.kalium.logic.feature.session.CurrentSessionUseCase
+import com.wire.kalium.logic.feature.session.DeleteSessionUseCase
+import com.wire.kalium.logic.functional.fold
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+data class E2EIEnrollmentState(
+ val certificate: String = "null",
+ val showCertificate: Boolean = false,
+ val isLoading: Boolean = false,
+ val isCertificateEnrollError: Boolean = false,
+ val isCertificateEnrollSuccess: Boolean = false,
+ val showCancelLoginDialog: Boolean = false
+)
+
+@HiltViewModel
+class E2EIEnrollmentViewModel @Inject constructor(
+ private val e2eiCertificateUseCase: GetE2EICertificateUseCase,
+ private val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment,
+ private val currentSession: CurrentSessionUseCase,
+ private val deleteSession: DeleteSessionUseCase,
+ private val switchAccount: AccountSwitchUseCase
+) : ViewModel() {
+ var state by mutableStateOf(E2EIEnrollmentState())
+
+ fun finalizeMLSClient() {
+ viewModelScope.launch {
+ finalizeMLSClientAfterE2EIEnrollment.invoke()
+ }
+ }
+
+ fun onBackButtonClicked() {
+ state = state.copy(showCancelLoginDialog = true)
+ }
+
+ fun onProceedEnrollmentClicked() {
+ state = state.copy(showCancelLoginDialog = false)
+ }
+
+ fun onCancelEnrollmentClicked(switchAccountActions: SwitchAccountActions) {
+ state = state.copy(showCancelLoginDialog = false)
+ viewModelScope.launch {
+ currentSession().let {
+ when (it) {
+ is CurrentSessionResult.Success -> {
+ deleteSession(it.accountInfo.userId)
+ }
+ is CurrentSessionResult.Failure.Generic -> {
+ appLogger.e("failed to delete session")
+ }
+ CurrentSessionResult.Failure.SessionNotFound -> {
+ appLogger.e("session not found")
+ }
+ }
+ }
+ }.invokeOnCompletion {
+ viewModelScope.launch {
+ switchAccount(SwitchAccountParam.TryToSwitchToNextAccount)
+ .callAction(switchAccountActions)
+ }
+ }
+ }
+ fun enrollE2EICertificate(context: Context) {
+ state = state.copy(isLoading = true)
+ e2eiCertificateUseCase(context, true) { result ->
+ result.fold({
+ state = state.copy(
+ isLoading = false,
+ isCertificateEnrollError = true
+ )
+ }, {
+ if (it is E2EIEnrollmentResult.Finalized) {
+ state = state.copy(
+ certificate = it.certificate,
+ isCertificateEnrollSuccess = true,
+ isCertificateEnrollError = false,
+ isLoading = false
+ )
+ }
+ })
+ }
+ }
+
+ fun dismissErrorDialog() {
+ state = state.copy(
+ isCertificateEnrollError = false,
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt
index 39f0e77dc0..70007e52c2 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt
@@ -275,7 +275,10 @@ class FeatureFlagNotificationViewModel @Inject constructor(
fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired, context: Context) {
featureFlagState = featureFlagState.copy(isE2EILoading = true)
currentUserId?.let { userId ->
- GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke(context) { result ->
+ GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke(
+ context,
+ isNewClient = false
+ ) { result ->
result.fold({
featureFlagState = featureFlagState.copy(
isE2EILoading = false,
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt
index c9366037c4..ae7a9ec28d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt
@@ -112,7 +112,8 @@ class DeviceDetailsViewModel @Inject constructor(
state.copy(
isE2eiCertificateActivated = true,
e2eiCertificate = certificate.certificate,
- isLoadingCertificate = false
+ isLoadingCertificate = false,
+ device = state.device.updateE2EICertificateStatus(certificate.certificate.status)
)
} else {
state.copy(isE2eiCertificateActivated = false, isLoadingCertificate = false)
@@ -122,7 +123,7 @@ class DeviceDetailsViewModel @Inject constructor(
fun enrollE2eiCertificate(context: Context) {
state = state.copy(isLoadingCertificate = true)
- enrolE2EICertificateUseCase(context) { result ->
+ enrolE2EICertificateUseCase(context, false) { result ->
result.fold({
state = state.copy(
isLoadingCertificate = false,
@@ -162,7 +163,7 @@ class DeviceDetailsViewModel @Inject constructor(
is GetClientDetailsResult.Success -> {
state.copy(
- device = Device(result.client),
+ device = state.device.updateFromClient(result.client),
isCurrentDevice = result.isCurrentClient,
removeDeviceDialogState = RemoveDeviceDialogState.Hidden,
canBeRemoved = !result.isCurrentClient && isSelfClient && result.client.type == ClientType.Permanent,
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt
index 18a0acf72c..dd9af69f75 100644
--- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt
@@ -67,7 +67,8 @@ class SelfDevicesViewModel @Inject constructor(
state.copy(
isLoadingClientsList = false,
currentDevice = result.clients
- .firstOrNull { it.id == currentClientId }?.let { Device(it, e2eiCertificates[it.id.value]?.status) },
+ .firstOrNull { it.id == currentClientId }
+ ?.let { Device(it, e2eiCertificates[it.id.value]?.status) },
deviceList = result.clients
.filter { it.id != currentClientId }
.map { Device(it, e2eiCertificates[it.id.value]?.status) }
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt
index 70f3093c9f..b883aff5c7 100644
--- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt
@@ -154,4 +154,4 @@ fun E2eiCertificateDetailsContent(
)
}
-const val CERTIFICATE_FILE_NAME = "certificate.pem"
+const val CERTIFICATE_FILE_NAME = "certificate.txt"
diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
index 542e38e0fc..ecc3afd296 100644
--- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
+++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
@@ -34,6 +34,7 @@ import com.wire.android.ui.destinations.CreateAccountSummaryScreenDestination
import com.wire.android.ui.destinations.CreatePersonalAccountOverviewScreenDestination
import com.wire.android.ui.destinations.CreateTeamAccountOverviewScreenDestination
import com.wire.android.ui.destinations.Destination
+import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.ImportMediaScreenDestination
import com.wire.android.ui.destinations.IncomingCallScreenDestination
@@ -43,6 +44,7 @@ import com.wire.android.ui.destinations.LoginScreenDestination
import com.wire.android.ui.destinations.MigrationScreenDestination
import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
+import com.wire.android.ui.destinations.RegisterDeviceScreenDestination
import com.wire.android.ui.destinations.RemoveDeviceScreenDestination
import com.wire.android.ui.destinations.SelfDevicesScreenDestination
import com.wire.android.ui.destinations.WelcomeScreenDestination
@@ -213,6 +215,8 @@ sealed class CurrentScreen {
is CreateAccountSummaryScreenDestination,
is MigrationScreenDestination,
is InitialSyncScreenDestination,
+ is E2EIEnrollmentScreenDestination,
+ is RegisterDeviceScreenDestination,
is RemoveDeviceScreenDestination -> AuthRelated
else -> SomeOther
diff --git a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt
index ca24a25971..92795d61b3 100644
--- a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt
+++ b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt
@@ -61,3 +61,8 @@ fun sanitizeUrl(url: String): String {
return url // Return the original URL if any errors occur
}
}
+
+fun URI.removeQueryParams(): URI {
+ val regex = Regex("[?&][^=]+=[^&]*")
+ return URI(this.toString().replace(regex, ""))
+}
diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt
index 7c16353127..7b61cf6769 100644
--- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt
@@ -26,6 +26,7 @@ import com.wire.android.config.TestDispatcherProvider
import com.wire.android.config.mockUri
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.AuthServerConfigProvider
+import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider
import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider
import com.wire.android.di.ObserveSyncStateUseCaseProvider
import com.wire.android.feature.AccountSwitchUseCase
@@ -588,6 +589,10 @@ class WireActivityViewModelTest {
}
private class Arrangement {
+
+ // TODO add tests for cases when observeIfE2EIIsRequiredDuringLogin emits semothing
+ private val observeIfE2EIIsRequiredDuringLogin = MutableSharedFlow()
+
init {
// Tests setup
MockKAnnotations.init(this, relaxUnitFun = true)
@@ -608,6 +613,8 @@ class WireActivityViewModelTest {
coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled)
coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther)
coEvery { globalDataStore.selectedThemeOptionFlow() } returns flowOf(ThemeOption.LIGHT)
+ coEvery { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(any()).observeIfE2EIIsRequiredDuringLogin() } returns
+ observeIfE2EIIsRequiredDuringLogin
}
@MockK
@@ -663,6 +670,9 @@ class WireActivityViewModelTest {
@MockK
private lateinit var observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory
+ @MockK
+ private lateinit var observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory
+
@MockK
lateinit var globalDataStore: GlobalDataStore
@@ -691,7 +701,8 @@ class WireActivityViewModelTest {
clearNewClientsForUser = clearNewClientsForUser,
currentScreenManager = currentScreenManager,
observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory,
- globalDataStore = globalDataStore
+ globalDataStore = globalDataStore,
+ observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory
)
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt
index 71d5c8d9db..c4c079e269 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt
@@ -116,7 +116,7 @@ class LoginEmailViewModelTest {
private lateinit var authenticationScope: AuthenticationScope
@MockK(relaxed = true)
- private lateinit var onSuccess: (Boolean) -> Unit
+ private lateinit var onSuccess: (Boolean, Boolean) -> Unit
private lateinit var loginViewModel: LoginEmailViewModel
@@ -210,7 +210,7 @@ class LoginEmailViewModelTest {
advanceUntilIdle()
coVerify(exactly = 1) { loginUseCase(any(), any(), any(), any(), any()) }
coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) }
- coVerify(exactly = 1) { onSuccess(true) }
+ coVerify(exactly = 1) { onSuccess(true, false) }
}
@Test
@@ -233,7 +233,7 @@ class LoginEmailViewModelTest {
advanceUntilIdle()
coVerify(exactly = 1) { loginUseCase(any(), any(), any(), any(), any()) }
coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) }
- coVerify(exactly = 1) { onSuccess(false) }
+ coVerify(exactly = 1) { onSuccess(false, false) }
}
@Test
@@ -440,7 +440,7 @@ class LoginEmailViewModelTest {
advanceUntilIdle()
coVerify(exactly = 1) { loginUseCase(email, any(), any(), any(), code) }
coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) }
- coVerify(exactly = 1) { onSuccess(any()) }
+ coVerify(exactly = 1) { onSuccess(any(), any()) }
}
@Test
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt
index f99282e6a3..e6e33468d5 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt
@@ -121,7 +121,7 @@ class LoginSSOViewModelTest {
private lateinit var fetchSSOSettings: FetchSSOSettingsUseCase
@MockK(relaxed = true)
- private lateinit var onSuccess: (Boolean) -> Unit
+ private lateinit var onSuccess: (Boolean, Boolean) -> Unit
private lateinit var loginViewModel: LoginSSOViewModel
@@ -262,7 +262,7 @@ class LoginSSOViewModelTest {
coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) }
coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) }
coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) }
- coVerify(exactly = 1) { onSuccess(false) }
+ coVerify(exactly = 1) { onSuccess(false, false) }
}
@Test
@@ -288,7 +288,7 @@ class LoginSSOViewModelTest {
coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) }
coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) }
coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) }
- coVerify(exactly = 1) { onSuccess(true) }
+ coVerify(exactly = 1) { onSuccess(true, false) }
}
@Test
@@ -312,7 +312,7 @@ class LoginSSOViewModelTest {
coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) }
coVerify(exactly = 0) { loginViewModel.registerClient(any(), null) }
coVerify(exactly = 0) { addAuthenticatedUserUseCase(any(), any(), any(), any()) }
- verify(exactly = 0) { onSuccess(any()) }
+ verify(exactly = 0) { onSuccess(any(), any()) }
}
@Test
@@ -353,7 +353,7 @@ class LoginSSOViewModelTest {
loginViewModel.handleSSOResult(DeepLinkResult.SSOLogin.Success("", ""), onSuccess)
advanceUntilIdle()
- verify(exactly = 1) { onSuccess(any()) }
+ verify(exactly = 1) { onSuccess(any(), any()) }
}
@Test
@@ -376,7 +376,7 @@ class LoginSSOViewModelTest {
coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) }
coVerify(exactly = 0) { loginViewModel.registerClient(any(), null) }
coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) }
- verify(exactly = 0) { onSuccess(any()) }
+ verify(exactly = 0) { onSuccess(any(), any()) }
}
@Test
@@ -405,7 +405,7 @@ class LoginSSOViewModelTest {
coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) }
coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) }
coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) }
- verify(exactly = 0) { onSuccess(any()) }
+ verify(exactly = 0) { onSuccess(any(), any()) }
}
@Test
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt
index 9029b72f58..845c3a2f72 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt
@@ -38,7 +38,6 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
-import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.junit.jupiter.api.Test
@@ -77,7 +76,7 @@ class SearchUserViewModelTest {
)
}
- verify(exactly = 1) {
+ coVerify(exactly = 1) {
arrangement.federatedSearchParser(any())
}
}
@@ -114,7 +113,7 @@ class SearchUserViewModelTest {
)
}
- verify(exactly = 1) {
+ coVerify(exactly = 1) {
arrangement.federatedSearchParser(any())
}
}
@@ -170,7 +169,7 @@ class SearchUserViewModelTest {
)
}
- verify(exactly = 1) {
+ coVerify(exactly = 1) {
arrangement.federatedSearchParser(any())
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt
index 428f868d68..12af6ab551 100644
--- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt
@@ -279,7 +279,7 @@ class DeviceDetailsViewModelTest {
viewModel.enrollE2eiCertificate(arrangement.context)
coVerify {
- arrangement.enrolE2EICertificateUseCase(any(), any())
+ arrangement.enrolE2EICertificateUseCase(any(), any(), any())
}
assertTrue(viewModel.state.isLoadingCertificate)
}
diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt
index 8786a24986..0088a7ea5a 100644
--- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt
+++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt
@@ -25,6 +25,6 @@ object AndroidSdk {
object AndroidApp {
const val id = "com.wire.android"
- const val versionName = "4.6.0"
+ const val versionName = "4.7.0"
val versionCode = Versionizer().versionCode
}
diff --git a/kalium b/kalium
index aa8d9077dc..0773557532 160000
--- a/kalium
+++ b/kalium
@@ -1 +1 @@
-Subproject commit aa8d9077dcf4be11b174de6f78f3ed5869765edf
+Subproject commit 07735575323400ca9c43e6259ec2f264e24669ad