diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 53680fd44ea..887f48e203b 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.deeplink) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.oidc.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) @@ -66,6 +67,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.features.networkmonitor.test) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index d200acb84e8..87e08b6bb90 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -42,8 +42,6 @@ import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint @@ -58,6 +56,8 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt index d41fbbfebb3..73ad6f60085 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt @@ -17,12 +17,12 @@ package io.element.android.appnav.intent import android.content.Intent -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcIntentResolver import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcIntentResolver import timber.log.Timber import javax.inject.Inject diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt index aa0fe9cbf12..8134c091f61 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt @@ -21,9 +21,6 @@ import android.content.Intent import android.net.Uri import androidx.core.net.toUri import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.impl.oidc.DefaultOidcIntentResolver -import io.element.android.features.login.impl.oidc.OidcUrlParser import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkParser @@ -33,6 +30,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver +import io.element.android.libraries.oidc.impl.OidcUrlParser import io.element.android.tests.testutils.lambda.lambdaError import org.junit.Assert.assertThrows import org.junit.Test diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt index a5dc8402670..3e4ed218a5a 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt @@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( @Parcelize data object EnterRecoveryKey : NavTarget + + @Parcelize + data object ResetIdentity : NavTarget } interface Callback : Plugin { @@ -85,6 +88,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( override fun onDone() { plugins().forEach { it.onDone() } } + + override fun onResetKey() { + backstack.push(NavTarget.ResetIdentity) + } }) .build() } @@ -94,6 +101,16 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( .callback(secureBackupEntryPointCallback) .build() } + is NavTarget.ResetIdentity -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity)) + .callback(object : SecureBackupEntryPoint.Callback { + override fun onDone() { + plugins().forEach { it.onDone() } + } + }) + .build() + } } } diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 6fcdc2c1de3..3fbe55f1f9e 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.permissions.api) implementation(projects.libraries.qrcode) + implementation(projects.libraries.oidc.api) implementation(libs.androidx.browser) implementation(platform(libs.network.retrofit.bom)) implementation(libs.network.retrofit) @@ -65,6 +66,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.permissions.test) testImplementation(projects.tests.testutils) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 0af102a4038..13aac03de4a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -36,12 +36,7 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.api.LoginFlowType -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker -import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler -import io.element.android.features.login.impl.oidc.webview.OidcNode import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode @@ -56,6 +51,9 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -64,11 +62,10 @@ import kotlinx.parcelize.Parcelize class LoginFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, - private val customTabHandler: CustomTabHandler, private val accountProviderDataSource: AccountProviderDataSource, private val defaultLoginUserStory: DefaultLoginUserStory, private val oidcActionFlow: OidcActionFlow, + private val oidcEntryPoint: OidcEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -146,11 +143,11 @@ class LoginFlowNode @AssistedInject constructor( ) val callback = object : ConfirmAccountProviderNode.Callback { override fun onOidcDetails(oidcDetails: OidcDetails) { - if (customTabAvailabilityChecker.supportCustomTab()) { + if (oidcEntryPoint.canUseCustomTab()) { // In this case open a Chrome Custom tab activity?.let { customChromeTabStarted = true - customTabHandler.open(it, darkTheme, oidcDetails.url) + oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url) } } else { // Fallback to WebView mode @@ -201,8 +198,7 @@ class LoginFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(callback)) } is NavTarget.OidcView -> { - val input = OidcNode.Inputs(navTarget.oidcDetails) - createNode(buildContext, plugins = listOf(input)) + oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url) } is NavTarget.WaitList -> { val inputs = WaitListNode.Inputs( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 27aec75739f..fdf7994d7eb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -27,15 +27,15 @@ import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, private val authenticationService: MatrixAuthenticationService, - private val defaultOidcActionFlow: DefaultOidcActionFlow, + private val oidcActionFlow: OidcActionFlow, private val defaultLoginUserStory: DefaultLoginUserStory, ) : Presenter { data class Params( @@ -65,7 +65,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( } LaunchedEffect(Unit) { - defaultOidcActionFlow.collect { oidcAction -> + oidcActionFlow.collect { oidcAction -> if (oidcAction != null) { onOidcAction(oidcAction, loginFlowAction) } @@ -133,6 +133,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( } } } - defaultOidcActionFlow.reset() + oidcActionFlow.reset() } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 5a02797bdb3..3875a4b245b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -20,10 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -31,6 +29,8 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate import kotlinx.coroutines.test.runTest @@ -274,7 +274,7 @@ class ConfirmAccountProviderPresenterTest { params = params, accountProviderDataSource = accountProviderDataSource, authenticationService = matrixAuthenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + oidcActionFlow = defaultOidcActionFlow, defaultLoginUserStory = defaultLoginUserStory, ) } diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt index 45e3a75738f..416ecff1bc7 100644 --- a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt @@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint { @Parcelize data object CreateNewRecoveryKey : InitialTarget + + @Parcelize + data object ResetIdentity : InitialTarget } data class Params(val initialElement: InitialTarget) : NodeInputs diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index 41f3ba8942c..a2a2e04a5d8 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.oidc.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) api(libs.statemachine) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index f54bfaee967..d741eb11a7e 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -34,6 +34,7 @@ import io.element.android.features.securebackup.impl.createkey.CreateNewRecovery import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode +import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode import io.element.android.features.securebackup.impl.root.SecureBackupRootNode import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode import io.element.android.libraries.architecture.BackstackView @@ -48,10 +49,11 @@ class SecureBackupFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) { + initialElement = when (plugins.filterIsInstance().first().initialElement) { SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey + is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity }, savedStateMap = buildContext.savedStateMap, ), @@ -79,6 +81,9 @@ class SecureBackupFlowNode @AssistedInject constructor( @Parcelize data object CreateNewRecoveryKey : NavTarget + + @Parcelize + data object ResetIdentity : NavTarget } private val callbacks = plugins() @@ -146,6 +151,14 @@ class SecureBackupFlowNode @AssistedInject constructor( NavTarget.CreateNewRecoveryKey -> { createNode(buildContext) } + is NavTarget.ResetIdentity -> { + val callback = object : ResetIdentityFlowNode.Callback { + override fun onDone() { + callbacks.forEach { it.onDone() } + } + } + createNode(buildContext, listOf(callback)) + } } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt new file mode 100644 index 00000000000..16850890a7f --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset + +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ResetIdentityFlowManager @Inject constructor( + private val matrixClient: MatrixClient, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val sessionVerificationService: SessionVerificationService, +) { + private val resetHandleFlow: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) + val currentHandleFlow: StateFlow> = resetHandleFlow + private var whenResetIsDoneWaitingJob: Job? = null + + fun whenResetIsDone(block: () -> Unit) { + whenResetIsDoneWaitingJob = sessionCoroutineScope.launch { + sessionVerificationService.sessionVerifiedStatus.filterIsInstance().first() + block() + } + } + + fun getResetHandle(): StateFlow> { + return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) { + resetHandleFlow + } else { + resetHandleFlow.value = AsyncData.Loading() + + sessionCoroutineScope.launch { + matrixClient.encryptionService().startIdentityReset() + .onSuccess { handle -> + resetHandleFlow.value = if (handle != null) { + AsyncData.Success(handle) + } else { + AsyncData.Failure(IllegalStateException("Could not get a reset identity handle")) + } + } + .onFailure { resetHandleFlow.value = AsyncData.Failure(it) } + } + + resetHandleFlow + } + } + + suspend fun cancel() { + currentHandleFlow.value.dataOrNull()?.cancel() + resetHandleFlow.value = AsyncData.Uninitialized + + whenResetIsDoneWaitingJob?.cancel() + whenResetIsDoneWaitingJob = null + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt new file mode 100644 index 00000000000..50dcc2dd12a --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset + +import android.app.Activity +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode +import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle +import io.element.android.libraries.oidc.api.OidcEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize +import timber.log.Timber + +@ContributesNode(SessionScope::class) +class ResetIdentityFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val resetIdentityFlowManager: ResetIdentityFlowManager, + private val coroutineScope: CoroutineScope, + private val oidcEntryPoint: OidcEntryPoint, +) : BaseFlowNode( + backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), + buildContext = buildContext, + plugins = plugins, +) { + interface Callback : Plugin { + fun onDone() + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data object ResetPassword : NavTarget + + @Parcelize + data class ResetOidc(val url: String) : NavTarget + } + + private lateinit var activity: Activity + private var resetJob: Job? = null + + override fun onBuilt() { + super.onBuilt() + + resetIdentityFlowManager.whenResetIsDone { + plugins().forEach { it.onDone() } + } + + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + // If the custom tab was opened, we need to cancel the reset job + // when we come back to the node if the reset wasn't successful + cancelResetJob() + } + + override fun onDestroy(owner: LifecycleOwner) { + // Make sure we cancel the reset job when the node is destroyed, just in case + cancelResetJob() + } + }) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.Root -> { + val callback = object : ResetIdentityRootNode.Callback { + override fun onContinue() { + coroutineScope.startReset() + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.ResetPassword -> { + val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found") + createNode( + buildContext, + listOf(ResetIdentityPasswordNode.Inputs(handle)) + ) + } + is NavTarget.ResetOidc -> { + oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.url) + } + } + } + + private fun CoroutineScope.startReset() = launch { + resetIdentityFlowManager.getResetHandle() + .collectLatest { state -> + when (state) { + is AsyncData.Failure -> { + cancelResetJob() + Timber.e(state.error, "Could not load the reset identity handle.") + } + is AsyncData.Success -> { + when (val handle = state.data) { + is IdentityOidcResetHandle -> { + if (oidcEntryPoint.canUseCustomTab()) { + activity.openUrlInChromeCustomTab(null, false, handle.url) + } else { + backstack.push(NavTarget.ResetOidc(handle.url)) + } + resetJob = launch { handle.resetOidc() } + } + is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) + } + } + else -> Unit + } + } + } + + private fun cancelResetJob() { + resetJob?.cancel() + resetJob = null + coroutineScope.launch { resetIdentityFlowManager.cancel() } + } + + @Composable + override fun View(modifier: Modifier) { + // Workaround to get the current activity + if (!this::activity.isInitialized) { + activity = LocalContext.current as Activity + } + + val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState() + if (startResetState.isLoading()) { + ProgressDialog( + properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true), + onDismissRequest = { cancelResetJob() } + ) + } + + BackstackView(modifier) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt new file mode 100644 index 00000000000..b76dff920f1 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +sealed interface ResetIdentityPasswordEvent { + data class Reset(val password: String) : ResetIdentityPasswordEvent + data object DismissError : ResetIdentityPasswordEvent +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt new file mode 100644 index 00000000000..75d487d00e0 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle + +@ContributesNode(SessionScope::class) +class ResetIdentityPasswordNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + coroutineDispatchers: CoroutineDispatchers, +) : Node(buildContext, plugins = plugins) { + data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs + + private val presenter = ResetIdentityPasswordPresenter( + identityPasswordResetHandle = inputs().handle, + dispatchers = coroutineDispatchers + ) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ResetIdentityPasswordView( + state = state, + onBack = ::navigateUp + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt new file mode 100644 index 00000000000..baa8ab28449 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class ResetIdentityPasswordPresenter( + private val identityPasswordResetHandle: IdentityPasswordResetHandle, + private val dispatchers: CoroutineDispatchers, +) : Presenter { + @Composable + override fun present(): ResetIdentityPasswordState { + val coroutineScope = rememberCoroutineScope() + + val resetAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + + fun handleEvent(event: ResetIdentityPasswordEvent) { + when (event) { + is ResetIdentityPasswordEvent.Reset -> coroutineScope.reset(event.password, resetAction) + ResetIdentityPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized + } + } + + return ResetIdentityPasswordState( + resetAction = resetAction.value, + eventSink = ::handleEvent + ) + } + + private fun CoroutineScope.reset(password: String, action: MutableState>) = launch(dispatchers.io) { + suspend { + identityPasswordResetHandle.resetPassword(password).getOrThrow() + }.runCatchingUpdatingState(action) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt new file mode 100644 index 00000000000..47662b623e8 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import io.element.android.libraries.architecture.AsyncAction + +data class ResetIdentityPasswordState( + val resetAction: AsyncAction, + val eventSink: (ResetIdentityPasswordEvent) -> Unit, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt new file mode 100644 index 00000000000..1a1e7684261 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +class ResetIdentityPasswordStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aResetIdentityPasswordState(), + aResetIdentityPasswordState(resetAction = AsyncAction.Loading), + aResetIdentityPasswordState(resetAction = AsyncAction.Success(Unit)), + aResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("Failed"))), + ) +} + +private fun aResetIdentityPasswordState( + resetAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (ResetIdentityPasswordEvent) -> Unit = {}, +) = ResetIdentityPasswordState( + resetAction = resetAction, + eventSink = eventSink, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt new file mode 100644 index 00000000000..074752ddead --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.securebackup.impl.R +import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun ResetIdentityPasswordView( + state: ResetIdentityPasswordState, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val passwordState = textFieldState(stateValue = "") + FlowStepPage( + modifier = modifier, + iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), + title = stringResource(R.string.screen_reset_encryption_password_title), + subTitle = stringResource(R.string.screen_reset_encryption_password_subtitle), + onBackClick = onBack, + content = { + Content( + text = passwordState.value, + onTextChange = { newText -> + if (state.resetAction.isFailure()) { + state.eventSink(ResetIdentityPasswordEvent.DismissError) + } + passwordState.value = newText + }, + hasError = state.resetAction.isFailure(), + ) + }, + buttons = { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_reset_identity), + onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) }, + destructive = true, + enabled = passwordState.value.isNotEmpty(), + ) + } + ) + + // On success we need to wait until the screen is automatically dismissed, so we keep the progress dialog + if (state.resetAction.isLoading() || state.resetAction.isSuccess()) { + ProgressDialog() + } +} + +@Composable +private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Boolean) { + var showPassword by remember { mutableStateOf(false) } + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .onTabOrEnterKeyFocusNext(LocalFocusManager.current), + value = text, + onValueChange = onTextChange, + label = { Text(stringResource(CommonStrings.common_password)) }, + placeholder = { Text(stringResource(R.string.screen_reset_encryption_password_placeholder)) }, + singleLine = true, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = + if (showPassword) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() + val description = + if (showPassword) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) + + IconButton(onClick = { showPassword = !showPassword }) { + Icon(imageVector = image, description) + } + }, + isError = hasError, + supportingText = if (hasError) { + { Text(stringResource(R.string.screen_reset_encryption_password_error)) } + } else { + null + } + ) +} + +@PreviewsDayNight +@Composable +internal fun ResetIdentityPasswordViewPreview(@PreviewParameter(ResetIdentityPasswordStateProvider::class) state: ResetIdentityPasswordState) { + ElementPreview { + ResetIdentityPasswordView( + state = state, + onBack = {} + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt new file mode 100644 index 00000000000..a1ec4cbe826 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +sealed interface ResetIdentityRootEvent { + data object Continue : ResetIdentityRootEvent + data object DismissDialog : ResetIdentityRootEvent +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt new file mode 100644 index 00000000000..3dd9876fc0d --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class ResetIdentityRootNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onContinue() + } + + private val presenter = ResetIdentityRootPresenter() + private val callback: Callback = plugins.filterIsInstance().first() + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ResetIdentityRootView( + modifier = modifier, + state = state, + onContinue = callback::onContinue, + onBack = ::navigateUp, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt new file mode 100644 index 00000000000..11c96e9ad8f --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter + +class ResetIdentityRootPresenter : Presenter { + @Composable + override fun present(): ResetIdentityRootState { + var displayConfirmDialog by remember { mutableStateOf(false) } + + fun handleEvent(event: ResetIdentityRootEvent) { + displayConfirmDialog = when (event) { + ResetIdentityRootEvent.Continue -> true + ResetIdentityRootEvent.DismissDialog -> false + } + } + + return ResetIdentityRootState( + displayConfirmationDialog = displayConfirmDialog, + eventSink = ::handleEvent + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt new file mode 100644 index 00000000000..de1054c97f4 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +data class ResetIdentityRootState( + val displayConfirmationDialog: Boolean, + val eventSink: (ResetIdentityRootEvent) -> Unit, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt new file mode 100644 index 00000000000..8d780343fe3 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +class ResetIdentityRootStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ResetIdentityRootState( + displayConfirmationDialog = false, + eventSink = {} + ), + ResetIdentityRootState( + displayConfirmationDialog = true, + eventSink = {} + ) + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt new file mode 100644 index 00000000000..9983f06b44e --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.securebackup.impl.R +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun ResetIdentityRootView( + state: ResetIdentityRootState, + onContinue: () -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + FlowStepPage( + modifier = modifier, + iconStyle = BigIcon.Style.AlertSolid, + title = stringResource(R.string.screen_encryption_reset_title), + subTitle = stringResource(R.string.screen_encryption_reset_subtitle), + isScrollable = true, + content = { Content() }, + buttons = { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = CommonStrings.action_continue), + onClick = { state.eventSink(ResetIdentityRootEvent.Continue) }, + destructive = true, + ) + }, + onBackClick = onBack, + ) + + if (state.displayConfirmationDialog) { + ConfirmationDialog( + title = stringResource(R.string.screen_reset_encryption_confirmation_alert_title), + content = stringResource(R.string.screen_reset_encryption_confirmation_alert_subtitle), + submitText = stringResource(R.string.screen_reset_encryption_confirmation_alert_action), + onSubmitClick = { + state.eventSink(ResetIdentityRootEvent.DismissDialog) + onContinue() + }, + destructiveSubmit = true, + onDismiss = { state.eventSink(ResetIdentityRootEvent.DismissDialog) } + ) + } +} + +@Composable +private fun Content() { + Column( + modifier = Modifier.padding(top = 8.dp, bottom = 40.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + InfoListOrganism( + modifier = Modifier.fillMaxWidth(), + items = persistentListOf( + InfoListItem( + message = stringResource(R.string.screen_encryption_reset_bullet_1), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Check(), + contentDescription = null, + tint = ElementTheme.colors.iconSuccessPrimary, + ) + }, + ), + InfoListItem( + message = stringResource(R.string.screen_encryption_reset_bullet_2), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Close(), + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + ) + }, + ), + InfoListItem( + message = stringResource(R.string.screen_encryption_reset_bullet_3), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Close(), + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + ) + }, + ), + ), + backgroundColor = ElementTheme.colors.bgActionSecondaryHovered, + ) + + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_encryption_reset_footer), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textActionPrimary, + textAlign = TextAlign.Center, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) { + ElementPreview { + ResetIdentityRootView( + state = state, + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index e1159031a2d..85e801fce17 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -16,6 +16,12 @@ "Follow the instructions to create a new recovery key" "Save your new recovery key in a password manager or encrypted note" "Reset the encryption for your account using another device" + "Your account details, contacts, preferences, and chat list will be kept" + "You will lose your existing message history" + "You will need to verify all your existing devices and contacts again" + "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key." + "If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. " + "Reset your identity in case you can’t confirm another way" "Turn off" "You will lose your encrypted messages if you are signed out of all devices." "Are you sure you want to turn off backup?" @@ -51,4 +57,11 @@ "Make sure you can store your recovery key somewhere safe" "Recovery setup successful" "Set up recovery" + "Yes, reset now" + "This process is irreversible." + "Are you sure you want to reset your encryption?" + "An unknown error happened. Please check your account password is correct and try again." + "Enter…" + "Confirm that you want to reset your encryption." + "Enter your account password to continue" diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt new file mode 100644 index 00000000000..eb33fe5c366 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ResetIdentityFlowManagerTest { + @Test + fun `getResetHandle - emits a reset handle`() = runTest { + val startResetLambda = lambdaRecorder> { Result.success(FakeIdentityPasswordResetHandle()) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isSuccess()).isTrue() + startResetLambda.assertions().isCalledOnce() + } + } + + @Test + fun `getResetHandle - om successful handle retrieval returns that same handle`() = runTest { + val startResetLambda = lambdaRecorder> { Result.success(FakeIdentityPasswordResetHandle()) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + var result: AsyncData.Success? = null + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + result = awaitItem() as? AsyncData.Success + assertThat(result).isNotNull() + } + + flowManager.getResetHandle().test { + assertThat(awaitItem()).isSameInstanceAs(result) + } + } + + @Test + fun `getResetHandle - will fail if it receives a null reset handle`() = runTest { + val startResetLambda = lambdaRecorder> { Result.success(null) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isFailure()).isTrue() + startResetLambda.assertions().isCalledOnce() + } + } + + @Test + fun `getResetHandle - fails gracefully when receiving an exception from the encryption service`() = runTest { + val startResetLambda = lambdaRecorder> { Result.failure(IllegalStateException("Failure")) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isFailure()).isTrue() + startResetLambda.assertions().isCalledOnce() + } + } + + @Test + fun `cancel - resets the state and calls cancel on the reset handle`() = runTest { + val cancelLambda = lambdaRecorder { } + val resetHandle = FakeIdentityPasswordResetHandle(cancelLambda = cancelLambda) + val startResetLambda = lambdaRecorder> { Result.success(resetHandle) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isSuccess()).isTrue() + + flowManager.cancel() + cancelLambda.assertions().isCalledOnce() + assertThat(awaitItem().isUninitialized()).isTrue() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `whenResetIsDone - will trigger the lambda when verification status is verified`() = runTest { + val verificationService = FakeSessionVerificationService() + val flowManager = createFlowManager(sessionVerificationService = verificationService) + var isDone = false + + flowManager.whenResetIsDone { + isDone = true + } + + assertThat(isDone).isFalse() + + verificationService.emitVerifiedStatus(SessionVerifiedStatus.Unknown) + advanceUntilIdle() + assertThat(isDone).isFalse() + + verificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified) + advanceUntilIdle() + assertThat(isDone).isFalse() + + verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) + advanceUntilIdle() + assertThat(isDone).isTrue() + } + + private fun TestScope.createFlowManager( + encryptionService: FakeEncryptionService = FakeEncryptionService(), + client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService), + sessionCoroutineScope: CoroutineScope = this, + sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), + ) = ResetIdentityFlowManager( + matrixClient = client, + sessionCoroutineScope = sessionCoroutineScope, + sessionVerificationService = sessionVerificationService, + ) +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt new file mode 100644 index 00000000000..059983df3d2 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ResetIdentityPasswordPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.resetAction.isUninitialized()).isTrue() + } + } + + @Test + fun `present - Reset event succeeds`() = runTest { + val resetLambda = lambdaRecorder> { _ -> Result.success(Unit) } + val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda) + val presenter = createPresenter(identityResetHandle = resetHandle) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityPasswordEvent.Reset("password")) + assertThat(awaitItem().resetAction.isLoading()).isTrue() + assertThat(awaitItem().resetAction.isSuccess()).isTrue() + } + } + + @Test + fun `present - Reset event can fail gracefully`() = runTest { + val resetLambda = lambdaRecorder> { _ -> Result.failure(IllegalStateException("Failed")) } + val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda) + val presenter = createPresenter(identityResetHandle = resetHandle) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityPasswordEvent.Reset("password")) + assertThat(awaitItem().resetAction.isLoading()).isTrue() + assertThat(awaitItem().resetAction.isFailure()).isTrue() + } + } + + @Test + fun `present - DismissError event resets the state`() = runTest { + val resetLambda = lambdaRecorder> { _ -> Result.failure(IllegalStateException("Failed")) } + val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda) + val presenter = createPresenter(identityResetHandle = resetHandle) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityPasswordEvent.Reset("password")) + assertThat(awaitItem().resetAction.isLoading()).isTrue() + assertThat(awaitItem().resetAction.isFailure()).isTrue() + + initialState.eventSink(ResetIdentityPasswordEvent.DismissError) + assertThat(awaitItem().resetAction.isUninitialized()).isTrue() + } + } + + private fun TestScope.createPresenter( + identityResetHandle: FakeIdentityPasswordResetHandle = FakeIdentityPasswordResetHandle(), + ) = ResetIdentityPasswordPresenter( + identityPasswordResetHandle = identityResetHandle, + dispatchers = testCoroutineDispatchers(), + ) +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt new file mode 100644 index 00000000000..2449146399c --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ResetIdentityPasswordViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `pressing the back HW button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), + onBack = it, + ) + rule.pressBackKey() + } + } + + @Test + fun `clicking on the back navigation button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), + onBack = it, + ) + rule.pressBack() + } + } + + @Test + fun `clicking 'Reset identity' confirms the reset`() { + val eventsRecorder = EventsRecorder() + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder), + ) + rule.onNodeWithText("Password").performTextInput("A password") + + rule.clickOn(CommonStrings.action_reset_identity) + + eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password")) + } + + @Test + fun `modifying the password dismisses the error state`() { + val eventsRecorder = EventsRecorder() + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), + ) + rule.onNodeWithText("Password").performTextInput("A password") + + eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) + } +} + +private fun AndroidComposeTestRule.setResetPasswordView( + state: ResetIdentityPasswordState, + onBack: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ResetIdentityPasswordView(state = state, onBack = onBack) + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt new file mode 100644 index 00000000000..feb00bf4de5 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ResetIdentityRootPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = ResetIdentityRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.displayConfirmationDialog).isFalse() + } + } + + @Test + fun `present - Continue event displays the confirmation dialog`() = runTest { + val presenter = ResetIdentityRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityRootEvent.Continue) + + assertThat(awaitItem().displayConfirmationDialog).isTrue() + } + } + + @Test + fun `present - DismissDialog event hides the confirmation dialog`() = runTest { + val presenter = ResetIdentityRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityRootEvent.Continue) + assertThat(awaitItem().displayConfirmationDialog).isTrue() + + initialState.eventSink(ResetIdentityRootEvent.DismissDialog) + assertThat(awaitItem().displayConfirmationDialog).isFalse() + } + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt new file mode 100644 index 00000000000..da7f11ba42e --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.securebackup.impl.R +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class ResetIdentityRootViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `pressing the back HW button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), + onBack = it, + ) + rule.pressBackKey() + } + } + + @Test + fun `clicking on the back navigation button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), + onBack = it, + ) + rule.pressBack() + } + } + + @Test + @Config(qualifiers = "h720dp") + fun `clicking Continue displays the confirmation dialog`() { + val eventsRecorder = EventsRecorder() + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), + ) + + rule.clickOn(CommonStrings.action_continue) + + eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) + } + + @Test + fun `clicking 'Yes, reset now' confirms the reset`() { + ensureCalledOnce { + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), + onContinue = it, + ) + rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action) + } + } + + @Test + fun `clicking Cancel dismisses the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder), + ) + + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog) + } +} + +private fun AndroidComposeTestRule.setResetRootView( + state: ResetIdentityRootState, + onBack: () -> Unit = EnsureNeverCalled(), + onContinue: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ResetIdentityRootView(state = state, onContinue = onContinue, onBack = onBack) + } +} diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt index 8d19ca56989..deb5cdf2672 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt @@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onEnterRecoveryKey() + fun onResetKey() fun onDone() } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index 9ce13586831..0ed9524626f 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -43,6 +43,7 @@ class VerifySelfSessionNode @AssistedInject constructor( state = state, modifier = modifier, onEnterRecoveryKey = callback::onEnterRecoveryKey, + onResetKey = callback::onResetKey, onFinish = callback::onDone, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index 6b908e3ebd6..446114752e8 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -20,6 +20,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -53,6 +54,7 @@ import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -66,6 +68,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver fun VerifySelfSessionView( state: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit, + onResetKey: () -> Unit, onFinish: () -> Unit, modifier: Modifier = Modifier, ) { @@ -115,6 +118,7 @@ fun VerifySelfSessionView( goBack = ::resetFlow, onEnterRecoveryKey = onEnterRecoveryKey, onFinish = onFinish, + onResetKey = onResetKey, ) } ) { @@ -226,6 +230,7 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie private fun BottomMenu( screenState: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit, + onResetKey: () -> Unit, goBack: () -> Unit, onFinish: () -> Unit, ) { @@ -236,42 +241,72 @@ private fun BottomMenu( when (verificationViewState) { is FlowStep.Initial -> { - if (verificationViewState.isLastDevice) { - BottomMenu( - positiveButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key), - onPositiveButtonClick = onEnterRecoveryKey, - ) - } else { - BottomMenu( - positiveButtonTitle = stringResource(R.string.screen_identity_use_another_device), - onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, - negativeButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key), - onNegativeButtonClick = onEnterRecoveryKey, + BottomMenu { + if (verificationViewState.isLastDevice) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + onClick = onEnterRecoveryKey, + ) + } else { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_use_another_device), + onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, + ) + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + onClick = onEnterRecoveryKey, + ) + } + // This option should always be displayed + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), + onClick = onResetKey, ) } } is FlowStep.Canceled -> { - BottomMenu( - positiveButtonTitle = stringResource(R.string.screen_session_verification_positive_button_canceled), - onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, - negativeButtonTitle = stringResource(CommonStrings.action_cancel), - onNegativeButtonClick = goBack, - ) + BottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_positive_button_canceled), + onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_cancel), + onClick = goBack, + ) + } } is FlowStep.Ready -> { - BottomMenu( - positiveButtonTitle = stringResource(CommonStrings.action_start), - onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) }, - negativeButtonTitle = stringResource(CommonStrings.action_cancel), - onNegativeButtonClick = goBack, - ) + BottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_start), + onClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) }, + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_cancel), + onClick = goBack, + ) + } } is FlowStep.AwaitingOtherDeviceResponse -> { - BottomMenu( - positiveButtonTitle = stringResource(R.string.screen_identity_waiting_on_other_device), - onPositiveButtonClick = {}, - isLoading = true, - ) + BottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_waiting_on_other_device), + onClick = {}, + showProgress = true, + ) + // Placeholder so the 1st button keeps its vertical position + Spacer(modifier = Modifier.height(40.dp)) + } } is FlowStep.Verifying -> { val positiveButtonTitle = if (isVerifying) { @@ -279,23 +314,34 @@ private fun BottomMenu( } else { stringResource(R.string.screen_session_verification_they_match) } - BottomMenu( - positiveButtonTitle = positiveButtonTitle, - onPositiveButtonClick = { - if (!isVerifying) { - eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - } - }, - negativeButtonTitle = stringResource(R.string.screen_session_verification_they_dont_match), - onNegativeButtonClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) }, - isLoading = isVerifying, - ) + BottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = positiveButtonTitle, + showProgress = isVerifying, + onClick = { + if (!isVerifying) { + eventSink(VerifySelfSessionViewEvents.ConfirmVerification) + } + }, + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_they_dont_match), + onClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) }, + ) + } } is FlowStep.Completed -> { - BottomMenu( - positiveButtonTitle = stringResource(CommonStrings.action_continue), - onPositiveButtonClick = onFinish, - ) + BottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_continue), + onClick = onFinish, + ) + // Placeholder so the 1st button keeps its vertical position + Spacer(modifier = Modifier.height(48.dp)) + } } is FlowStep.Skipped -> return } @@ -303,35 +349,13 @@ private fun BottomMenu( @Composable private fun BottomMenu( - positiveButtonTitle: String?, - onPositiveButtonClick: () -> Unit, modifier: Modifier = Modifier, - negativeButtonTitle: String? = null, - negativeButtonEnabled: Boolean = negativeButtonTitle != null, - onNegativeButtonClick: () -> Unit = {}, - isLoading: Boolean = false, + buttons: @Composable ColumnScope.() -> Unit, ) { ButtonColumnMolecule( modifier = modifier.padding(bottom = 16.dp) ) { - if (positiveButtonTitle != null) { - Button( - text = positiveButtonTitle, - showProgress = isLoading, - modifier = Modifier.fillMaxWidth(), - onClick = onPositiveButtonClick, - ) - } - if (negativeButtonTitle != null) { - TextButton( - text = negativeButtonTitle, - modifier = Modifier.fillMaxWidth(), - onClick = onNegativeButtonClick, - enabled = negativeButtonEnabled, - ) - } else { - Spacer(modifier = Modifier.height(48.dp)) - } + buttons() } } @@ -341,6 +365,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta VerifySelfSessionView( state = state, onEnterRecoveryKey = {}, + onResetKey = {}, onFinish = {}, ) } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt index f19f3e1f298..7e5bf928e27 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -217,12 +217,14 @@ class VerifySelfSessionViewTest { state: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), + onResetKey: () -> Unit = EnsureNeverCalled(), ) { setContent { VerifySelfSessionView( state = state, onEnterRecoveryKey = onEnterRecoveryKey, onFinish = onFinished, + onResetKey = onResetKey, ) } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index ec0d9662c7f..425133b797b 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -47,12 +47,19 @@ fun Activity.openUrlInChromeCustomTab( true -> CustomTabsIntent.COLOR_SCHEME_DARK } ) + .setShareIdentityEnabled(false) // Note: setting close button icon does not work // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) .apply { session?.let { setSession(it) } } .build() + .apply { + // Disable download button + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true) + // Disable bookmark button + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true) + } .launchUrl(this, Uri.parse(url)) } catch (activityNotFoundException: ActivityNotFoundException) { // TODO context.toast(R.string.error_no_external_application_found) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index f47487c6346..5afb81648ba 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -62,4 +62,52 @@ interface EncryptionService { * called the fingerprint of the device. */ suspend fun deviceEd25519(): String? + + /** + * Starts the identity reset process. This will return a handle that can be used to reset the identity. + */ + suspend fun startIdentityReset(): Result +} + +/** + * A handle to reset the user's identity. + */ +interface IdentityResetHandle { + /** + * Cancel the reset process and drops the existing handle in the SDK. + */ + suspend fun cancel() +} + +/** + * A handle to reset the user's identity with a password login type. + */ +interface IdentityPasswordResetHandle : IdentityResetHandle { + /** + * Reset the password of the user. + * + * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is + * called, or the identity is reset. + * + * @param password the current password, which will be validated before the process takes place. + */ + suspend fun resetPassword(password: String): Result +} + +/** + * A handle to reset the user's identity with an OIDC login type. + */ +interface IdentityOidcResetHandle : IdentityResetHandle { + /** + * The URL to open in a webview/custom tab to reset the identity. + */ + val url: String + + /** + * Reset the identity using the OIDC flow. + * + * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is + * called, or the identity is reset. + */ + suspend fun resetOidc(): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index 68ab4a611e3..ae681b27719 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -18,10 +18,12 @@ package io.element.android.libraries.matrix.impl.encryption import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.impl.sync.RustSyncService @@ -54,6 +56,7 @@ internal class RustEncryptionService( private val dispatchers: CoroutineDispatchers, ) : EncryptionService { private val service: Encryption = client.encryption() + private val sessionId = SessionId(client.session().userId) private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() private val backupUploadStateMapper = BackupUploadStateMapper() @@ -198,4 +201,12 @@ internal class RustEncryptionService( override suspend fun deviceEd25519(): String? { return service.ed25519Key() } + + override suspend fun startIdentityReset(): Result { + return runCatching { + service.resetIdentity()?.let { handle -> + RustIdentityResetHandleFactory.create(sessionId, handle) + }?.getOrNull() + } + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt new file mode 100644 index 00000000000..c4c20eb7d62 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle +import org.matrix.rustcomponents.sdk.AuthData +import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails +import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType + +object RustIdentityResetHandleFactory { + fun create( + userId: UserId, + identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle + ): Result { + return runCatching { + when (val authType = identityResetHandle.authType()) { + is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) + // User interactive authentication (user + password) + CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle) + } + } + } +} + +class RustPasswordIdentityResetHandle( + private val userId: UserId, + private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, +) : IdentityPasswordResetHandle { + override suspend fun resetPassword(password: String): Result { + return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } + } + + override suspend fun cancel() { + identityResetHandle.cancelAndDestroy() + } +} + +class RustOidcIdentityResetHandle( + private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, + override val url: String, +) : IdentityOidcResetHandle { + override suspend fun resetOidc(): Result { + return runCatching { identityResetHandle.reset(null) } + } + + override suspend fun cancel() { + identityResetHandle.cancelAndDestroy() + } +} + +private suspend fun org.matrix.rustcomponents.sdk.IdentityResetHandle.cancelAndDestroy() { + cancel() + destroy() +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index b864c69b0b2..82ab5e251f9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -20,13 +20,17 @@ import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -class FakeEncryptionService : EncryptionService { +class FakeEncryptionService( + var startIdentityResetLambda: () -> Result = { lambdaError() }, +) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) @@ -118,6 +122,10 @@ class FakeEncryptionService : EncryptionService { enableRecoveryProgressStateFlow.emit(state) } + override suspend fun startIdentityReset(): Result { + return startIdentityResetLambda() + } + companion object { const val FAKE_RECOVERY_KEY = "fake" } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt new file mode 100644 index 00000000000..69087163d28 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.encryption + +import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle + +class FakeIdentityOidcResetHandle( + override val url: String = "", + var resetOidcLambda: () -> Result = { error("Not implemented") }, + var cancelLambda: () -> Unit = { error("Not implemented") }, +) : IdentityOidcResetHandle { + override suspend fun resetOidc(): Result { + return resetOidcLambda() + } + + override suspend fun cancel() { + cancelLambda() + } +} + +class FakeIdentityPasswordResetHandle( + var resetPasswordLambda: (String) -> Result = { _ -> error("Not implemented") }, + var cancelLambda: () -> Unit = { error("Not implemented") }, +) : IdentityPasswordResetHandle { + override suspend fun resetPassword(password: String): Result { + return resetPasswordLambda(password) + } + + override suspend fun cancel() { + cancelLambda() + } +} diff --git a/libraries/oidc/api/build.gradle.kts b/libraries/oidc/api/build.gradle.kts new file mode 100644 index 00000000000..4e01547d2ca --- /dev/null +++ b/libraries/oidc/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.oidc.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt similarity index 93% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt index 6d878728796..cc171599601 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.api.oidc +package io.element.android.libraries.oidc.api sealed interface OidcAction { data object GoBack : OidcAction diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt similarity index 79% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt index 004e7c8a51e..1fd67cfaf5b 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt @@ -14,8 +14,12 @@ * limitations under the License. */ -package io.element.android.features.login.api.oidc +package io.element.android.libraries.oidc.api + +import kotlinx.coroutines.flow.FlowCollector interface OidcActionFlow { fun post(oidcAction: OidcAction) + suspend fun collect(collector: FlowCollector) + fun reset() } diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt new file mode 100644 index 00000000000..c00bf263aff --- /dev/null +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.oidc.api + +import android.app.Activity +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node + +interface OidcEntryPoint { + fun canUseCustomTab(): Boolean + fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) + fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt similarity index 93% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt index a6ecf26fca9..77597fd4322 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.api.oidc +package io.element.android.libraries.oidc.api import android.content.Intent diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts new file mode 100644 index 00000000000..db1cdf8d0a0 --- /dev/null +++ b/libraries/oidc/impl/build.gradle.kts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + id("kotlin-parcelize") + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "io.element.android.libraries.oidc.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + implementation(projects.appconfig) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(libs.androidx.browser) + implementation(platform(libs.network.retrofit.bom)) + implementation(libs.network.retrofit) + implementation(libs.serialization.json) + api(projects.libraries.oidc.api) + + testImplementation(libs.test.junit) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.permissions.test) + testImplementation(projects.tests.testutils) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt index 424e9f13bc2..6258e67b0ed 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl import android.content.Context import androidx.browser.customtabs.CustomTabsClient diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt new file mode 100644 index 00000000000..185c27bf19a --- /dev/null +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.oidc.impl + +import android.app.Activity +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.oidc.api.OidcEntryPoint +import io.element.android.libraries.oidc.impl.webview.OidcNode +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultOidcEntryPoint @Inject constructor( + private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, +) : OidcEntryPoint { + override fun canUseCustomTab(): Boolean { + return customTabAvailabilityChecker.supportCustomTab() + } + + override fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) { + assert(canUseCustomTab()) { "Custom tab is not supported in this device." } + activity.openUrlInChromeCustomTab(null, darkTheme, url) + } + + override fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node { + assert(!canUseCustomTab()) { "Custom tab should be used instead of the fallback node." } + val inputs = OidcNode.Inputs(OidcDetails(url)) + return parentNode.createNode(buildContext, listOf(inputs)) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt similarity index 85% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt index 8b6844e0f3d..9777cee22fe 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl import android.content.Intent import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcIntentResolver import io.element.android.libraries.di.AppScope +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcIntentResolver import javax.inject.Inject @ContributesBinding(AppScope::class) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt index 8c8c895acda..ae502d0a8cd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.matrix.api.auth.OidcConfig +import io.element.android.libraries.oidc.api.OidcAction import javax.inject.Inject /** diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt similarity index 97% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt index b83c0eb1acc..21c673145ed 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.customtab +package io.element.android.libraries.oidc.impl.customtab import android.app.Activity import android.content.ComponentName diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt similarity index 82% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt index 17dfa8418f7..0c0737b43e7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.customtab +package io.element.android.libraries.oidc.impl.customtab import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @@ -34,11 +34,11 @@ class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow { mutableStateFlow.value = oidcAction } - suspend fun collect(collector: FlowCollector) { + override suspend fun collect(collector: FlowCollector) { mutableStateFlow.collect(collector) } - fun reset() { + override fun reset() { mutableStateFlow.value = null } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt similarity index 86% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt index 9d2f945e25b..602d50628a8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview -import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.libraries.oidc.api.OidcAction sealed interface OidcEvents { data object Cancel : OidcEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt index 5cd7cf0c3b6..d31bdcd0320 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt index 3f10ba256a7..5e886644f61 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -25,11 +25,11 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.oidc.api.OidcAction import kotlinx.coroutines.launch class OidcPresenter @AssistedInject constructor( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt index dc4f26d51e6..8b1b01b03dd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.auth.OidcDetails diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt index 5d446574628..7127c9160e9 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt similarity index 63% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt index c07078cdff7..0ba9d4ea528 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt @@ -14,32 +14,39 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview +import android.annotation.SuppressLint import android.webkit.WebView import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView -import io.element.android.features.login.impl.oidc.OidcUrlParser import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.oidc.impl.OidcUrlParser +@OptIn(ExperimentalMaterial3Api::class) @Composable fun OidcView( state: OidcState, onNavigateBack: () -> Unit, modifier: Modifier = Modifier, ) { + val isPreview = LocalInspectionMode.current val oidcUrlParser = remember { OidcUrlParser() } var webView by remember { mutableStateOf(null) } fun shouldOverrideUrl(url: String): Boolean { @@ -55,7 +62,7 @@ fun OidcView( OidcWebViewClient(::shouldOverrideUrl) } - BackHandler { + fun onBack() { if (webView?.canGoBack().orFalse()) { webView?.goBack() } else { @@ -64,12 +71,35 @@ fun OidcView( } } - Box(modifier = modifier.statusBarsPadding()) { + BackHandler { onBack() } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + BackButton(onClick = ::onBack) + }, + ) + } + ) { contentPadding -> AndroidView( + modifier = Modifier.padding(contentPadding), factory = { context -> WebView(context).apply { - webViewClient = oidcWebViewClient - loadUrl(state.oidcDetails.url) + if (!isPreview) { + webViewClient = oidcWebViewClient + settings.apply { + @SuppressLint("SetJavaScriptEnabled") + javaScriptEnabled = true + allowContentAccess = true + allowFileAccess = true + databaseEnabled = true + domStorageEnabled = true + } + loadUrl(state.oidcDetails.url) + } }.also { webView = it } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt index a9cd576e6de..d20eabf00e4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import android.webkit.WebResourceRequest import android.webkit.WebView diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt index 446754aced7..8f587966bf3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview fun interface WebViewEventListener { /** diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt similarity index 94% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt index 6f63673b234..895e416faa0 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.matrix.api.auth.OidcConfig +import io.element.android.libraries.oidc.api.OidcAction import org.junit.Assert import org.junit.Test diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt similarity index 97% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt index 38d0506dd8d..994334d2e9a 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt @@ -16,17 +16,17 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.oidc.api.OidcAction import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index ef997755dd8..730afa291e9 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -268,12 +268,6 @@ Reason: %1$s." "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" - "Your account details, contacts, preferences, and chat list will be kept" - "You will lose your existing message history" - "You will need to verify all your existing devices and contacts again" - "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key." - "If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. " - "Reset your identity in case you can’t confirm another way" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -284,12 +278,6 @@ Reason: %1$s." "%1$d Pinned messages" "Pinned messages" - "Yes, reset now" - "This process is irreversible." - "Are you sure you want to reset your encryption?" - "Enter…" - "Confirm that you want to reset your encryption." - "Enter your account password to continue" "Pinned messages" "Failed processing media to upload, please try again." "Could not retrieve user details" diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 9fd82af4ae4..bd83d527ca6 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -116,6 +116,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:mediaviewer:impl")) implementation(project(":libraries:troubleshoot:impl")) implementation(project(":libraries:fullscreenintent:impl")) + implementation(project(":libraries:oidc:impl")) } fun DependencyHandlerScope.allServicesImpl() { diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png deleted file mode 100644 index 1022157a43f..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38b4c852a4bb957a10e24a72b70d7edd89a921983a9426592dc1fc53c1d54d99 -size 5450 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png deleted file mode 100644 index 39071712f4e..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9cdc07cc74bf427e7ae0244b97543f65aecb6b2f8fee7958d18906d333b868b8 -size 8848 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png deleted file mode 100644 index 15ff876cf4a..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0c39aabac135aa3954ac881a43cc2bd79a2e357ec76f9aa46351b2985c17d73 -size 5437 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png deleted file mode 100644 index f99c8e006d8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c773553943c9728f1e80b6ca6491c9c8c50f9394f3bd1cb430abb24029a3f926 -size 7791 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png new file mode 100644 index 00000000000..a49af920d90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e09769c04ba21aba09eb0de5865f659ad092ba2e46a8b3933f95b1170b09d303 +size 28540 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png new file mode 100644 index 00000000000..4c6ae5f1496 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3702ebe296d7a14e20b3d683fb79f6b2455064a8986ce48c870b5cfd68dc5933 +size 27407 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png new file mode 100644 index 00000000000..4c6ae5f1496 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3702ebe296d7a14e20b3d683fb79f6b2455064a8986ce48c870b5cfd68dc5933 +size 27407 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png new file mode 100644 index 00000000000..1d5e048087b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b683ed9986e3fd139ee735e51a369322ba3652b2b8a578c05a61f26ee27898e0 +size 39996 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png new file mode 100644 index 00000000000..96ec36ca5ef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14d4ed02bb2f949c4cbba84933bbbfb0c550ad261695a371ef12e74a2dbd8812 +size 27600 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png new file mode 100644 index 00000000000..c4e2dae54bb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f3dc9352250ba7495b6af312f3f370463291d29b76c40d4b3c340b77aa5712 +size 25455 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png new file mode 100644 index 00000000000..c4e2dae54bb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f3dc9352250ba7495b6af312f3f370463291d29b76c40d4b3c340b77aa5712 +size 25455 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png new file mode 100644 index 00000000000..0fa0e6a5196 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b380335d847fa37e5319541f46f341c5fe8016f3af48765cfe7537c59dd1f58a +size 38379 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png new file mode 100644 index 00000000000..499713a15dd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9b7a1e2d20171a78da4ce4f590372e1d84f9624c4d56a011e74f91105c09f36 +size 79989 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png new file mode 100644 index 00000000000..6bc0eec9c01 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43f2eb2cb1f1b986b58d2614c892f2f7f15f1b696a656165ba1f8644f7759476 +size 62679 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png new file mode 100644 index 00000000000..8685aa5e425 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3c38d62929774fe6e6e003f656efbeec1f675b5dbb9994a917c4978f64d61a9 +size 78280 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png new file mode 100644 index 00000000000..e79557dae61 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08e5a9d4e1bc45e9411b74c7277f34fa0ba8d46fdd1556b65b9c50cc78f1468e +size 60595 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png index b5f11147ba5..cf48a305b74 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45548cd4afaac73a72ab4738c099506eb9dead138e8e0cf5668a75c4948b81f6 -size 26027 +oid sha256:ee9c90a91ef8703c4878ea9314121ad8343dd3392c051e7e3e3079198a32ef99 +size 30555 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png index a989b3b4748..bfef3aada5e 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88f3de0736a207057ce0e71fea632e5197874d93045e87443a957ef0da64b8bd -size 22760 +oid sha256:a4b44b23029dd1ff76a082a29c0b39584a1e20c92795eb4815d632608abd2858 +size 22702 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png index 4a5ee01fb81..a2d07ed6a45 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:146a2a2439de11035332340ce27e622ff20b661d600fc2977d1f82deb81d0f21 -size 24828 +oid sha256:ce48c81c355317abd6b9de4600e86492c3acde2807ca3572e064b61abc06239c +size 29426 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png index a4cde41390f..69eb60569f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281e08f29d84da5c1e7c01d701bf5d8f415b6d80fb8a1298742d6e629314a7d8 -size 21186 +oid sha256:4d9810f3b326a05492c870ef22dbceec19112dba8beaa000066504cc96d1387a +size 23913 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png index 6ec093bbc87..31ba47d4a41 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf610f5f758e65ae67836a2b40924e966a8add94fd00605c89ef75ddbd3bf22c -size 25324 +oid sha256:453cab91aa056f3536c899354a2fb5de21519e7dce3d5675b1cbca702000e2c1 +size 29602 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png index fddd555a334..89dcb3654d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2ed85817f1d95d1259e07aa566ede6d40360a688e29c0c463378770dd091540 -size 22003 +oid sha256:f468a4a9140c4dc23ab3d394049f59b86f0ca2f332bee50abf8eaa0acce166b5 +size 22030 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png index 130cf67cf45..39ce0ef07f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:354c1daceebd169b35a6db0173c954a3425c534156fdc662edfba4d710f51a49 -size 24155 +oid sha256:c21120a08c871c7f64952c20be2c90b28590901f8e52893702775b6b2f176892 +size 28524 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png index 4c87611adad..7d1bfb8eab2 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d6ce5c3b44127d5d661ac450ea2cf48617e81e4ae40dfb6d9a5cf89f3591800 -size 20598 +oid sha256:1e46ffc3c3679eb5490d01c34d02314bae5611a7b10fb3c758331a28ae78f065 +size 23267 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png new file mode 100644 index 00000000000..785f56af7f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:154a56d848cc735efc25274052ce92b1a20bc8f80f75e91d0d4cfc7d488cd246 +size 5874 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png new file mode 100644 index 00000000000..ae8924e66eb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0915d4781155524bece1db737050ceb78ef76128da39b339fd67dd4a2a4dd79d +size 9229 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png new file mode 100644 index 00000000000..c41c1c79218 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dd3462935091cfe022f0e6fc71b082eed7dab4769def13398a81a90f871b61a +size 5807 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png new file mode 100644 index 00000000000..98b81898ef9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7cd23334c15b9039cbf0924a82703035b4498e67489418e5244cf2df576af9c +size 8111 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 47ef2c1c51f..82fb4f22777 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -210,7 +210,10 @@ "screen_chat_backup_.*", "screen_key_backup_disable_.*", "screen_recovery_key_.*", - "screen_create_new_recovery_key_.*" + "screen_create_new_recovery_key_.*", + "screen_encryption_reset.*", + "screen_reset_encryption.*", + "screen\\.reset_encryption.*" ] }, {