First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.ftue.api"
}
dependencies {
implementation(projects.libraries.architecture)
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface FtueEntryPoint : SimpleFeatureEntryPoint

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.api.state
import kotlinx.coroutines.flow.StateFlow
/**
* Service to manage the First Time User Experience state (aka Onboarding).
*/
interface FtueService {
/** The current state of the FTUE. */
val state: StateFlow<FtueState>
}
/** The state of the FTUE. */
sealed interface FtueState {
/** The FTUE state is unknown, nothing to do for now. */
data object Unknown : FtueState
/** The FTUE state is incomplete. The FTUE flow should be displayed. */
data object Incomplete : FtueState
/** The FTUE state is complete. The FTUE flow should not be displayed anymore. */
data object Complete : FtueState
}

View File

@@ -0,0 +1,60 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.ftue.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
api(projects.features.ftue.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
implementation(projects.features.analytics.api)
implementation(projects.features.logout.api)
implementation(projects.features.securebackup.api)
implementation(projects.features.verifysession.api)
implementation(projects.services.analytics.api)
implementation(projects.features.lockscreen.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
implementation(projects.services.toolbox.api)
implementation(projects.appconfig)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.analytics.noop)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.features.lockscreen.test)
testImplementation(projects.services.toolbox.test)
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultFtueEntryPoint : FtueEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<FtueFlowNode>(buildContext)
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
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.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
import io.element.android.features.ftue.impl.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.state.InternalFtueState
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
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.di.SessionScope
import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class FtueFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val defaultFtueService: DefaultFtueService,
private val analyticsEntryPoint: AnalyticsEntryPoint,
private val lockScreenEntryPoint: LockScreenEntryPoint,
) : BaseFlowNode<FtueFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Placeholder : NavTarget
@Parcelize
data object SessionVerification : NavTarget
@Parcelize
data object NotificationsOptIn : NavTarget
@Parcelize
data object AnalyticsOptIn : NavTarget
@Parcelize
data object LockScreenSetup : NavTarget
}
override fun onBuilt() {
super.onBuilt()
defaultFtueService.ftueStepStateFlow
.filterIsInstance(InternalFtueState.Incomplete::class)
.onEach {
showStep(it.nextStep)
}
.launchIn(lifecycleScope)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Placeholder -> {
emptyNode(buildContext)
}
is NavTarget.SessionVerification -> {
val callback = object : FtueSessionVerificationFlowNode.Callback {
override fun onDone() {
defaultFtueService.onUserCompletedSessionVerification()
}
}
createNode<FtueSessionVerificationFlowNode>(buildContext, listOf(callback))
}
NavTarget.NotificationsOptIn -> {
val callback = object : NotificationsOptInNode.Callback {
override fun onNotificationsOptInFinished() {
defaultFtueService.updateFtueStep()
}
}
createNode<NotificationsOptInNode>(buildContext, listOf(callback))
}
NavTarget.AnalyticsOptIn -> {
analyticsEntryPoint.createNode(this, buildContext)
}
NavTarget.LockScreenSetup -> {
val callback = object : LockScreenEntryPoint.Callback {
override fun onSetupDone() {
defaultFtueService.updateFtueStep()
}
}
lockScreenEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
navTarget = LockScreenEntryPoint.Target.Setup,
callback = callback,
)
}
}
}
private fun showStep(ftueStep: FtueStep) {
when (ftueStep) {
FtueStep.WaitingForInitialState -> {
backstack.newRoot(NavTarget.Placeholder)
}
FtueStep.SessionVerification -> {
backstack.newRoot(NavTarget.SessionVerification)
}
FtueStep.NotificationsOptIn -> {
backstack.newRoot(NavTarget.NotificationsOptIn)
}
FtueStep.AnalyticsOptIn -> {
backstack.replace(NavTarget.AnalyticsOptIn)
}
FtueStep.LockscreenSetup -> {
backstack.newRoot(NavTarget.LockScreenSetup)
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModePresenter
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@BindingContainer
interface FtueModule {
@Binds
fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter<ChooseSelfVerificationModeState>
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
sealed interface NotificationsOptInEvents {
data object ContinueClicked : NotificationsOptInEvents
data object NotNowClicked : NotificationsOptInEvents
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
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 dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
@ContributesNode(AppScope::class)
@AssistedInject
class NotificationsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: NotificationsOptInPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
interface Callback : NodeInputs {
fun onNotificationsOptInFinished()
}
private val callback = inputs<Callback>()
private val presenter: NotificationsOptInPresenter = presenterFactory.create(callback)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
NotificationsOptInView(
state = state,
onBack = { callback.onNotificationsOptInFinished() },
modifier = modifier
)
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
import android.Manifest
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@AssistedInject
class NotificationsOptInPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
@Assisted private val callback: NotificationsOptInNode.Callback,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val permissionStateProvider: PermissionStateProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : Presenter<NotificationsOptInState> {
@AssistedFactory
interface Factory {
fun create(callback: NotificationsOptInNode.Callback): NotificationsOptInPresenter
}
private val postNotificationPermissionsPresenter: PermissionsPresenter =
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
} else {
NoopPermissionsPresenter()
}
@Composable
override fun present(): NotificationsOptInState {
val notificationsPermissionsState = postNotificationPermissionsPresenter.present()
fun handleEvent(event: NotificationsOptInEvents) {
when (event) {
NotificationsOptInEvents.ContinueClicked -> {
if (notificationsPermissionsState.permissionGranted) {
callback.onNotificationsOptInFinished()
} else {
notificationsPermissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
}
NotificationsOptInEvents.NotNowClicked -> {
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
appCoroutineScope.setPermissionDenied()
}
callback.onNotificationsOptInFinished()
}
}
}
LaunchedEffect(notificationsPermissionsState) {
if (notificationsPermissionsState.permissionGranted ||
notificationsPermissionsState.permissionAlreadyDenied) {
callback.onNotificationsOptInFinished()
}
}
return NotificationsOptInState(
notificationsPermissionState = notificationsPermissionsState,
eventSink = ::handleEvent,
)
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun CoroutineScope.setPermissionDenied() = launch {
permissionStateProvider.setPermissionDenied(Manifest.permission.POST_NOTIFICATIONS, true)
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
import io.element.android.libraries.permissions.api.PermissionsState
data class NotificationsOptInState(
val notificationsPermissionState: PermissionsState,
val eventSink: (NotificationsOptInEvents) -> Unit
)

View File

@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.permissions.api.aPermissionsState
open class NotificationsOptInStateProvider : PreviewParameterProvider<NotificationsOptInState> {
override val values: Sequence<NotificationsOptInState>
get() = sequenceOf(
aNotificationsOptInState(),
// Add other states here
)
}
fun aNotificationsOptInState() = NotificationsOptInState(
notificationsPermissionState = aPermissionsState(showDialog = false),
eventSink = {}
)

View File

@@ -0,0 +1,189 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
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.ftue.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
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.Surface
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun NotificationsOptInView(
state: NotificationsOptInState,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = onBack)
HeaderFooterPage(
modifier = modifier
.statusBarsPadding()
.fillMaxSize(),
background = { OnboardingBackground() },
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 28.dp)) },
footer = { NotificationsOptInFooter(state) },
) {
NotificationsOptInContent()
}
}
@Composable
private fun NotificationsOptInHeader(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier,
title = stringResource(R.string.screen_notification_optin_title),
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
iconStyle = BigIcon.Style.Default(CompoundIcons.NotificationsSolid()),
)
}
@Composable
private fun NotificationsOptInFooter(state: NotificationsOptInState) {
ButtonColumnMolecule {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_ok),
onClick = {
state.eventSink(NotificationsOptInEvents.ContinueClicked)
}
)
TextButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_not_now),
onClick = {
state.eventSink(NotificationsOptInEvents.NotNowClicked)
}
)
}
}
@Composable
private fun NotificationsOptInContent() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(
verticalArrangement = Arrangement.spacedBy(
16.dp,
alignment = Alignment.CenterVertically
)
) {
NotificationRow(
avatarLetter = "M",
avatarColorsId = "5",
firstRowPercent = 1f,
secondRowPercent = 0.4f
)
NotificationRow(
avatarLetter = "A",
avatarColorsId = "1",
firstRowPercent = 1f,
secondRowPercent = 1f
)
NotificationRow(
avatarLetter = "T",
avatarColorsId = "4",
firstRowPercent = 0.65f,
secondRowPercent = 0f
)
}
}
}
@Composable
private fun NotificationRow(
avatarLetter: String,
avatarColorsId: String,
firstRowPercent: Float,
secondRowPercent: Float,
) {
Surface(
color = ElementTheme.colors.bgCanvasDisabled,
shape = RoundedCornerShape(14.dp),
shadowElevation = 2.dp,
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Avatar(
avatarData = AvatarData(id = avatarColorsId, name = avatarLetter, size = AvatarSize.NotificationsOptIn),
avatarType = AvatarType.User,
)
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Box(
modifier = Modifier
.clip(CircleShape)
.fillMaxWidth(firstRowPercent)
.height(10.dp)
.background(ElementTheme.colors.borderInteractiveSecondary)
)
if (secondRowPercent > 0f) {
Box(
modifier = Modifier
.clip(CircleShape)
.fillMaxWidth(secondRowPercent)
.height(10.dp)
.background(ElementTheme.colors.borderInteractiveSecondary)
)
}
}
}
}
}
@PreviewsDayNight
@Composable
internal fun NotificationsOptInViewPreview(
@PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState
) {
ElementPreview {
NotificationsOptInView(
onBack = {},
state = state,
)
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
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.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.utils.OpenUrlInTabView
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class FtueSessionVerificationFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
private val secureBackupEntryPoint: SecureBackupEntryPoint,
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data object UseAnotherDevice : NavTarget
@Parcelize
data object EnterRecoveryKey : NavTarget
@Parcelize
data object ResetIdentity : NavTarget
}
interface Callback : Plugin {
fun onDone()
}
private val callback: Callback = callback()
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
override fun onDone() {
lifecycleScope.launch {
// Move to the completed state view in the verification flow
backstack.newRoot(NavTarget.UseAnotherDevice)
}
}
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.Root -> {
val callback = object : ChooseSelfVerificationModeNode.Callback {
override fun navigateToUseAnotherDevice() {
backstack.push(NavTarget.UseAnotherDevice)
}
override fun navigateToUseRecoveryKey() {
backstack.push(NavTarget.EnterRecoveryKey)
}
override fun navigateToResetKey() {
backstack.push(NavTarget.ResetIdentity)
}
override fun navigateToLearnMoreAboutEncryption() {
learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL
}
}
createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback))
}
is NavTarget.UseAnotherDevice -> {
outgoingVerificationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = true,
verificationRequest = VerificationRequest.Outgoing.CurrentSession,
),
callback = object : OutgoingVerificationEntryPoint.Callback {
override fun onDone() {
callback.onDone()
}
override fun onBack() {
backstack.pop()
}
override fun navigateToLearnMoreAboutEncryption() {
// Note that this callback is never called. The "Learn more" link is not displayed
// for the self session interactive verification.
}
}
)
}
is NavTarget.EnterRecoveryKey -> {
secureBackupEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey),
callback = secureBackupEntryPointCallback
)
}
is NavTarget.ResetIdentity -> {
secureBackupEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity),
callback = object : SecureBackupEntryPoint.Callback {
override fun onDone() {
callback.onDone()
}
},
)
}
}
}
private val learnMoreUrl = mutableStateOf<String?>(null)
@Composable
override fun View(modifier: Modifier) {
BackstackView()
OpenUrlInTabView(learnMoreUrl)
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
sealed interface ChooseSelfVerificationModeEvent {
data object SignOut : ChooseSelfVerificationModeEvent
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class ChooseSelfVerificationModeNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: Presenter<ChooseSelfVerificationModeState>,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToUseAnotherDevice()
fun navigateToUseRecoveryKey()
fun navigateToResetKey()
fun navigateToLearnMoreAboutEncryption()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ChooseSelfVerificationModeView(
state = state,
onUseAnotherDevice = callback::navigateToUseAnotherDevice,
onUseRecoveryKey = callback::navigateToUseRecoveryKey,
onResetKey = callback::navigateToResetKey,
onLearnMore = callback::navigateToLearnMoreAboutEncryption,
modifier = modifier,
)
directLogoutView.Render(state = state.directLogoutState)
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
@Inject
class ChooseSelfVerificationModePresenter(
private val encryptionService: EncryptionService,
private val directLogoutPresenter: Presenter<DirectLogoutState>,
) : Presenter<ChooseSelfVerificationModeState> {
@Composable
override fun present(): ChooseSelfVerificationModeState {
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow
.mapState { recoveryState ->
when (recoveryState) {
RecoveryState.WAITING_FOR_SYNC,
RecoveryState.UNKNOWN -> AsyncData.Loading()
RecoveryState.INCOMPLETE -> AsyncData.Success(true)
RecoveryState.ENABLED,
RecoveryState.DISABLED -> AsyncData.Success(false)
}
}
.collectAsState()
val buttonsState by remember {
derivedStateOf {
val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull()
val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull()
if (canUseAnotherDevice == null || canEnterRecoveryKey == null) {
AsyncData.Loading()
} else {
AsyncData.Success(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
)
)
}
}
}
val directLogoutState = directLogoutPresenter.present()
fun handleEvent(event: ChooseSelfVerificationModeEvent) {
when (event) {
ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
}
}
return ChooseSelfVerificationModeState(
buttonsState = buttonsState,
directLogoutState = directLogoutState,
eventSink = ::handleEvent,
)
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
data class ChooseSelfVerificationModeState(
val buttonsState: AsyncData<ButtonsState>,
val directLogoutState: DirectLogoutState,
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
) {
data class ButtonsState(
val canUseAnotherDevice: Boolean,
val canEnterRecoveryKey: Boolean,
)
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncData
class ChooseSelfVerificationModeStateProvider :
PreviewParameterProvider<ChooseSelfVerificationModeState> {
override val values = sequenceOf(
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Loading(),
),
)
}
fun aChooseSelfVerificationModeState(
buttonsState: AsyncData<ChooseSelfVerificationModeState.ButtonsState> = AsyncData.Success(aButtonsState()),
) = ChooseSelfVerificationModeState(
buttonsState = buttonsState,
directLogoutState = aDirectLogoutState(),
eventSink = {},
)
fun aButtonsState(
canUseAnotherDevice: Boolean = true,
canEnterRecoveryKey: Boolean = true,
) = ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
)

View File

@@ -0,0 +1,159 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.ftue.impl.R
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
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
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState,
onUseAnotherDevice: () -> Unit,
onUseRecoveryKey: () -> Unit,
onResetKey: () -> Unit,
onLearnMore: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = LocalActivity.current
BackHandler {
activity?.finish()
}
HeaderFooterPage(
modifier = modifier,
topBar = {
TopAppBar(
title = {},
actions = {
TextButton(
text = stringResource(CommonStrings.action_signout),
onClick = { state.eventSink(ChooseSelfVerificationModeEvent.SignOut) }
)
}
)
},
header = {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(bottom = 16.dp),
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
title = stringResource(id = R.string.screen_identity_confirmation_title),
subTitle = stringResource(id = R.string.screen_identity_confirmation_subtitle)
)
},
footer = {
ChooseSelfVerificationModeButtons(
state = state,
onUseAnotherDevice = onUseAnotherDevice,
onUseRecoveryKey = onUseRecoveryKey,
onResetKey = onResetKey,
)
}
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier
.clickable(onClick = onLearnMore)
.padding(vertical = 4.dp, horizontal = 16.dp),
text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium
)
}
}
}
@Composable
private fun ChooseSelfVerificationModeButtons(
state: ChooseSelfVerificationModeState,
onUseAnotherDevice: () -> Unit,
onUseRecoveryKey: () -> Unit,
onResetKey: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 16.dp)
) {
when (state.buttonsState) {
AsyncData.Uninitialized,
is AsyncData.Failure,
is AsyncData.Loading -> {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = false,
showProgress = true,
text = stringResource(CommonStrings.common_loading),
onClick = {},
)
}
is AsyncData.Success -> {
if (state.buttonsState.data.canUseAnotherDevice) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_use_another_device),
onClick = onUseAnotherDevice,
)
}
if (state.buttonsState.data.canEnterRecoveryKey) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
onClick = onUseRecoveryKey,
)
}
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
onClick = onResetKey,
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun ChooseSelfVerificationModeViewPreview(
@PreviewParameter(ChooseSelfVerificationModeStateProvider::class) state: ChooseSelfVerificationModeState
) = ElementPreview {
ChooseSelfVerificationModeView(
state = state,
onUseAnotherDevice = {},
onUseRecoveryKey = {},
onResetKey = {},
onLearnMore = {},
)
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.state
import android.Manifest
import android.os.Build
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ContributesBinding(SessionScope::class)
@SingleIn(SessionScope::class)
class DefaultFtueService(
private val sdkVersionProvider: BuildVersionSdkIntProvider,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
private val permissionStateProvider: PermissionStateProvider,
private val lockScreenService: LockScreenService,
private val sessionVerificationService: SessionVerificationService,
private val sessionPreferencesStore: SessionPreferencesStore,
) : FtueService {
private val userNeedsToConfirmSessionVerificationSuccess = MutableStateFlow(false)
val ftueStepStateFlow = MutableStateFlow<InternalFtueState>(InternalFtueState.Unknown)
override val state = ftueStepStateFlow
.mapState {
when (it) {
is InternalFtueState.Unknown -> FtueState.Unknown
is InternalFtueState.Incomplete -> FtueState.Incomplete
is InternalFtueState.Complete -> FtueState.Complete
}
}
init {
combine(
sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus ->
if (sessionVerifiedStatus == SessionVerifiedStatus.NotVerified) {
// Ensure we wait for the user to confirm the session verified screen before going further
userNeedsToConfirmSessionVerificationSuccess.value = true
}
},
userNeedsToConfirmSessionVerificationSuccess,
analyticsService.didAskUserConsentFlow.distinctUntilChanged(),
) {
updateFtueStep()
}
.launchIn(sessionCoroutineScope)
}
fun updateFtueStep() = sessionCoroutineScope.launch {
val step = getNextStep(null)
ftueStepStateFlow.value = when (step) {
null -> InternalFtueState.Complete
else -> InternalFtueState.Incomplete(step)
}
}
private suspend fun getNextStep(completedStep: FtueStep? = null): FtueStep? =
when (completedStep) {
null -> if (!isSessionVerificationStateReady()) {
FtueStep.WaitingForInitialState
} else {
getNextStep(FtueStep.WaitingForInitialState)
}
FtueStep.WaitingForInitialState -> if (isSessionNotVerified() || userNeedsToConfirmSessionVerificationSuccess.value) {
FtueStep.SessionVerification
} else {
getNextStep(FtueStep.SessionVerification)
}
FtueStep.SessionVerification -> if (shouldAskNotificationPermissions()) {
FtueStep.NotificationsOptIn
} else {
getNextStep(FtueStep.NotificationsOptIn)
}
FtueStep.NotificationsOptIn -> if (shouldDisplayLockscreenSetup()) {
FtueStep.LockscreenSetup
} else {
getNextStep(FtueStep.LockscreenSetup)
}
FtueStep.LockscreenSetup -> if (needsAnalyticsOptIn()) {
FtueStep.AnalyticsOptIn
} else {
getNextStep(FtueStep.AnalyticsOptIn)
}
FtueStep.AnalyticsOptIn -> null
}
private fun isSessionVerificationStateReady(): Boolean {
return sessionVerificationService.sessionVerifiedStatus.value != SessionVerifiedStatus.Unknown
}
private suspend fun isSessionNotVerified(): Boolean {
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified && !canSkipVerification()
}
private suspend fun canSkipVerification(): Boolean {
return sessionPreferencesStore.isSessionVerificationSkipped().first()
}
private suspend fun needsAnalyticsOptIn(): Boolean {
return analyticsService.didAskUserConsentFlow.first().not()
}
private suspend fun shouldAskNotificationPermissions(): Boolean {
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
val permission = Manifest.permission.POST_NOTIFICATIONS
val isPermissionDenied = permissionStateProvider.isPermissionDenied(permission).first()
val isPermissionGranted = permissionStateProvider.isPermissionGranted(permission)
!isPermissionGranted && !isPermissionDenied
} else {
false
}
}
private suspend fun shouldDisplayLockscreenSetup(): Boolean {
return lockScreenService.isSetupRequired().first()
}
fun onUserCompletedSessionVerification() {
userNeedsToConfirmSessionVerificationSuccess.value = false
}
}
sealed interface FtueStep {
data object WaitingForInitialState : FtueStep
data object SessionVerification : FtueStep
data object NotificationsOptIn : FtueStep
data object AnalyticsOptIn : FtueStep
data object LockscreenSetup : FtueStep
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.state
sealed interface InternalFtueState {
data object Unknown : InternalFtueState
data class Incomplete(
val nextStep: FtueStep,
) : InternalFtueState
data object Complete : InternalFtueState
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можаце пацвердзіць?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Стварыць новы ключ аднаўлення"</string>
<string name="screen_identity_confirmation_subtitle">"Пацвердзіце гэтую прыладу, каб наладзіць бяспечны абмен паведамленнямі."</string>
<string name="screen_identity_confirmation_title">"Пацвердзіце, што гэта вы"</string>
<string name="screen_identity_confirmation_use_another_device">"Выкарыстоўвайце іншую прыладу"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Выкарыстоўваць ключ аднаўлення"</string>
<string name="screen_identity_confirmed_subtitle">"Цяпер вы можаце бяспечна чытаць і адпраўляць паведамленні, і ўсе, з кім вы маеце зносіны ў чаце, таксама могуць давяраць гэтай прыладзе."</string>
<string name="screen_identity_confirmed_title">"Прылада праверана"</string>
<string name="screen_identity_use_another_device">"Выкарыстоўвайце іншую прыладу"</string>
<string name="screen_identity_waiting_on_other_device">"Чаканне на іншай прыладзе…"</string>
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
<string name="screen_session_verification_enter_recovery_key">"Увядзіце ключ аднаўлення"</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можете да потвърдите?"</string>
<string name="screen_identity_confirmation_subtitle">"Потвърдете това устройство, за да настроите защитени съобщения."</string>
<string name="screen_identity_confirmation_title">"Потвърдете самоличността си"</string>
<string name="screen_identity_confirmation_use_another_device">"Използване на друго устройство"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Използване на ключ за възстановяване"</string>
<string name="screen_identity_confirmed_title">"Устройството е потвърдено"</string>
<string name="screen_identity_use_another_device">"Използване на друго устройство"</string>
<string name="screen_notification_optin_subtitle">"Можете да промените настройките си по-късно."</string>
<string name="screen_notification_optin_title">"Разрешете известията и никога не пропускайте съобщение"</string>
<string name="screen_session_verification_enter_recovery_key">"Въвеждане на ключ за възстановяване"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nemůžete potvrdit?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Vytvoření nového klíče pro obnovení"</string>
<string name="screen_identity_confirmation_subtitle">"Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv."</string>
<string name="screen_identity_confirmation_title">"Potvrďte, že jste to vy"</string>
<string name="screen_identity_confirmation_use_another_device">"Použít jiné zařízení"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Použít klíč pro obnovení"</string>
<string name="screen_identity_confirmed_subtitle">"Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat."</string>
<string name="screen_identity_confirmed_title">"Zařízení ověřeno"</string>
<string name="screen_identity_use_another_device">"Použít jiné zařízení"</string>
<string name="screen_identity_waiting_on_other_device">"Čekání na jiném zařízení…"</string>
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
<string name="screen_session_verification_enter_recovery_key">"Zadejte klíč pro obnovení"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Methu cadarnhau?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crëwch allwedd adfer newydd"</string>
<string name="screen_identity_confirmation_subtitle">"Dilyswch y ddyfais hon er mwyn gosod negeseuon diogel."</string>
<string name="screen_identity_confirmation_title">"Cadarnhewch eich hunaniaeth"</string>
<string name="screen_identity_confirmation_use_another_device">"Defnyddiwch ddyfais arall"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Defnyddiwch allwedd adfer"</string>
<string name="screen_identity_confirmed_subtitle">"Nawr gallwch chi ddarllen neu anfon negeseuon yn ddiogel, a gall unrhyw un rydych chi\'n sgwrsio â nhw ymddiried yn y ddyfais hon hefyd."</string>
<string name="screen_identity_confirmed_title">"Dyfais wedi\'i dilysu"</string>
<string name="screen_identity_use_another_device">"Defnyddiwch ddyfais arall"</string>
<string name="screen_identity_waiting_on_other_device">"Yn aros ar ddyfais arall…"</string>
<string name="screen_notification_optin_subtitle">"Gallwch newid eich gosodiadau yn nes ymlaen."</string>
<string name="screen_notification_optin_title">"Caniatáu hysbysiadau a pheidio byth â cholli neges"</string>
<string name="screen_session_verification_enter_recovery_key">"Rhowch eich allwedd adfer"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kan ikke bekræfte?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Opret en ny gendannelsesnøgle"</string>
<string name="screen_identity_confirmation_subtitle">"Verificér denne enhed for at konfigurere sikre meddelelser."</string>
<string name="screen_identity_confirmation_title">"Bekræft din identitet"</string>
<string name="screen_identity_confirmation_use_another_device">"Brug en anden enhed"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Brug gendannelsesnøgle"</string>
<string name="screen_identity_confirmed_subtitle">"Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed."</string>
<string name="screen_identity_confirmed_title">"Enhed verificeret"</string>
<string name="screen_identity_use_another_device">"Brug en anden enhed"</string>
<string name="screen_identity_waiting_on_other_device">"Venter på en anden enhed…"</string>
<string name="screen_notification_optin_subtitle">"Du kan ændre dine indstillinger senere."</string>
<string name="screen_notification_optin_title">"Tillad notifikationer, og gå aldrig glip af en besked"</string>
<string name="screen_session_verification_enter_recovery_key">"Indtast gendannelsesnøgle"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Bestätigung unmöglich?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string>
<string name="screen_identity_confirmation_subtitle">"Verifiziere dieses Gerät, um sichere Chats einzurichten."</string>
<string name="screen_identity_confirmation_title">"Bestätige deine Identität"</string>
<string name="screen_identity_confirmation_use_another_device">"Ein anderes Gerät verwenden"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Wiederherstellungsschlüssel verwenden"</string>
<string name="screen_identity_confirmed_subtitle">"Du kannst jetzt verschlüsselte Nachrichten lesen und versenden. Dein Chatpartner vertraut nun diesem Gerät."</string>
<string name="screen_identity_confirmed_title">"Gerät verifiziert"</string>
<string name="screen_identity_use_another_device">"Ein anderes Gerät verwenden"</string>
<string name="screen_identity_waiting_on_other_device">"Bitte warten bis das andere Gerät bereit ist."</string>
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
<string name="screen_session_verification_enter_recovery_key">"Wiederherstellungsschlüssel eingeben"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Δεν μπορείς να επιβεβαιώσεις;"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Δημιουργία νέου κλειδιού ανάκτησης"</string>
<string name="screen_identity_confirmation_subtitle">"Επαλήθευσε αυτήν τη συσκευή για να ρυθμίσεις την ασφαλή επικοινωνία."</string>
<string name="screen_identity_confirmation_title">"Επιβεβαίωσε ότι είσαι εσύ"</string>
<string name="screen_identity_confirmation_use_another_device">"Χρήση άλλης συσκευής"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Χρήση κλειδιού ανάκτησης"</string>
<string name="screen_identity_confirmed_subtitle">"Τώρα μπορείς να διαβάζεις ή να στέλνεις μηνύματα με ασφάλεια και επίσης μπορεί να εμπιστευτεί αυτήν τη συσκευή οποιοσδήποτε με τον οποίο συνομιλείς."</string>
<string name="screen_identity_confirmed_title">"Επαληθευμένη συσκευή"</string>
<string name="screen_identity_use_another_device">"Χρήση άλλης συσκευής"</string>
<string name="screen_identity_waiting_on_other_device">"Αναμονή σε άλλη συσκευή…"</string>
<string name="screen_notification_optin_subtitle">"Μπορείς να αλλάξεις τις ρυθμίσεις σου αργότερα."</string>
<string name="screen_notification_optin_title">"Επέτρεψε τις ειδοποιήσεις και μην χάσεις ούτε ένα μήνυμα"</string>
<string name="screen_session_verification_enter_recovery_key">"Εισαγωγή κλειδιού ανάκτησης"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"¿No puedes confirmar?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crear una nueva clave de recuperación"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica este dispositivo para configurar la mensajería segura."</string>
<string name="screen_identity_confirmation_title">"Confirma que eres tú"</string>
<string name="screen_identity_confirmation_use_another_device">"Usar otro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usar clave de recuperación"</string>
<string name="screen_identity_confirmed_subtitle">"Ahora puedes leer o enviar mensajes de forma segura y cualquier persona con la que chatees también puede confiar en este dispositivo."</string>
<string name="screen_identity_confirmed_title">"Dispositivo verificado"</string>
<string name="screen_identity_use_another_device">"Usar otro dispositivo"</string>
<string name="screen_identity_waiting_on_other_device">"Esperando en otro dispositivo…"</string>
<string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string>
<string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string>
<string name="screen_session_verification_enter_recovery_key">"Introduce la clave de recuperación"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kas kinnitamine pole võimalik?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Loo uus taastevõti"</string>
<string name="screen_identity_confirmation_subtitle">"Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade."</string>
<string name="screen_identity_confirmation_title">"Kinnita, et see oled sina"</string>
<string name="screen_identity_confirmation_use_another_device">"Kasuta teist seadet"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Kasuta taastevõtit"</string>
<string name="screen_identity_confirmed_subtitle">"Nüüd saad saata või lugeda sõnumeid turvaliselt ning kõik sinu vestluspartnerid võivad usaldada seda seadet."</string>
<string name="screen_identity_confirmed_title">"Seade on verifitseeritud"</string>
<string name="screen_identity_use_another_device">"Kasuta teist seadet"</string>
<string name="screen_identity_waiting_on_other_device">"Ootame teise seadme järgi…"</string>
<string name="screen_notification_optin_subtitle">"Sa võid seadistusi hiljem alati muuta."</string>
<string name="screen_notification_optin_title">"Luba teavitused ja kunagi ei jää sul sõnumid märkamata"</string>
<string name="screen_session_verification_enter_recovery_key">"Sisesta taastevõti"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Ezin duzu baieztatu?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Sortu berreskuratze-gako berria"</string>
<string name="screen_identity_confirmation_subtitle">"Egiaztatu gailua mezularitza segurua konfiguratzeko."</string>
<string name="screen_identity_confirmation_title">"Berretsi zure identitatea"</string>
<string name="screen_identity_confirmation_use_another_device">"Erabili beste gailu bat"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Erabili berreskuratze-gakoa"</string>
<string name="screen_identity_confirmed_subtitle">"Orain mezuak modu seguruan irakurri edo bidal ditzakezu, eta txateatzen duzun edonor ere fida daiteke gailu honetaz."</string>
<string name="screen_identity_confirmed_title">"Gailua egiaztatu da"</string>
<string name="screen_identity_use_another_device">"Erabili beste gailu bat"</string>
<string name="screen_identity_waiting_on_other_device">"Beste gailuaren zain…"</string>
<string name="screen_notification_optin_subtitle">"Geroago alda ditzakezu ezarpenak."</string>
<string name="screen_notification_optin_title">"Baimendu jakinarazpenak eta ez galdu inoiz mezurik"</string>
<string name="screen_session_verification_enter_recovery_key">"Sartu berreskuratze-gakoa"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"نمی‌توانید تأیید کنید؟"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"ایجاد کلید بازیابی جدید"</string>
<string name="screen_identity_confirmation_subtitle">"تأیید این افزاره برای برپایی پیام‌رسانی امن."</string>
<string name="screen_identity_confirmation_title">"تأیید هویتتان"</string>
<string name="screen_identity_confirmation_use_another_device">"استفاده از افزاره‌ای دیگر"</string>
<string name="screen_identity_confirmation_use_recovery_key">"استفاده از کلید بازیابی"</string>
<string name="screen_identity_confirmed_subtitle">"اکنون می‌توانید پیام‌ها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ می‌زنید نیز می‌تواند به این افزاره اعتماد کند."</string>
<string name="screen_identity_confirmed_title">"افزاره تأیید شده"</string>
<string name="screen_identity_use_another_device">"استفاده از افزاره‌ای دیگر"</string>
<string name="screen_identity_waiting_on_other_device">"منتظر افزارهٔ دیگر…"</string>
<string name="screen_notification_optin_subtitle">"می‌توانید بعداً تنظیماتتان را تغییر دهید."</string>
<string name="screen_notification_optin_title">"اجازه به آگاهی‌ها و از دست ندادن پیام‌ها"</string>
<string name="screen_session_verification_enter_recovery_key">"ورود کلید بازیابی"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Etkö voi vahvistaa?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Luo uusi palautusavain"</string>
<string name="screen_identity_confirmation_subtitle">"Vahvista tämä laite suojattua viestintää varten."</string>
<string name="screen_identity_confirmation_title">"Vahvista identiteettisi"</string>
<string name="screen_identity_confirmation_use_another_device">"Käytä toista laitetta"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Käytä palautusavainta"</string>
<string name="screen_identity_confirmed_subtitle">"Nyt voit lukea ja lähettää viestejä turvallisesti, ja kaikki, joiden kanssa keskustelet, voivat myös luottaa tähän laitteeseen."</string>
<string name="screen_identity_confirmed_title">"Laite vahvistettu"</string>
<string name="screen_identity_use_another_device">"Käytä toista laitetta"</string>
<string name="screen_identity_waiting_on_other_device">"Odotetaan toista laitetta…"</string>
<string name="screen_notification_optin_subtitle">"Voit muuttaa asetuksia myöhemmin."</string>
<string name="screen_notification_optin_title">"Salli ilmoitukset ja älä koskaan missaa viestejä"</string>
<string name="screen_session_verification_enter_recovery_key">"Syötä palautusavain"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Confirmation impossible ?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Créer une nouvelle clé de récupération"</string>
<string name="screen_identity_confirmation_subtitle">"Vérifier cette session pour configurer votre messagerie sécurisée."</string>
<string name="screen_identity_confirmation_title">"Confirmez votre identité"</string>
<string name="screen_identity_confirmation_use_another_device">"Utiliser une autre session"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Utiliser la clé de récupération"</string>
<string name="screen_identity_confirmed_subtitle">"Vous pouvez désormais lire ou envoyer des messages en toute sécurité, et toute personne avec qui vous discutez peut également faire confiance à cette session."</string>
<string name="screen_identity_confirmed_title">"Session vérifiée"</string>
<string name="screen_identity_use_another_device">"Utiliser une autre session"</string>
<string name="screen_identity_waiting_on_other_device">"En attente dune autre session…"</string>
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
<string name="screen_session_verification_enter_recovery_key">"Utiliser la clé de récupération"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nem tudja megerősíteni?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Új helyreállítási kulcs létrehozása"</string>
<string name="screen_identity_confirmation_subtitle">"A biztonságos üzenetkezelés beállításához ellenőrizze ezt az eszközt."</string>
<string name="screen_identity_confirmation_title">"Erősítse meg, hogy Ön az"</string>
<string name="screen_identity_confirmation_use_another_device">"Másik eszköz használata"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Helyreállítási kulcs használata"</string>
<string name="screen_identity_confirmed_subtitle">"Mostantól biztonságosan olvashat vagy küldhet üzeneteket, és bármelyik csevegőpartnere megbízhat ebben az eszközben."</string>
<string name="screen_identity_confirmed_title">"Eszköz ellenőrizve"</string>
<string name="screen_identity_use_another_device">"Másik eszköz használata"</string>
<string name="screen_identity_waiting_on_other_device">"Várakozás a másik eszközre…"</string>
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
<string name="screen_session_verification_enter_recovery_key">"Adja meg a helyreállítási kulcsot"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Tidak dapat mengonfirmasi?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Buat kunci pemulihan baru"</string>
<string name="screen_identity_confirmation_subtitle">"Verifikasi perangkat ini untuk menyiapkan perpesanan aman."</string>
<string name="screen_identity_confirmation_title">"Konfirmasi bahwa ini Anda"</string>
<string name="screen_identity_confirmation_use_another_device">"Gunakan perangkat lain"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Gunakan kunci pemulihan"</string>
<string name="screen_identity_confirmed_subtitle">"Sekarang Anda dapat membaca atau mengirim pesan dengan aman, dan siapa pun yang mengobrol dengan Anda juga dapat mempercayai perangkat ini."</string>
<string name="screen_identity_confirmed_title">"Perangkat terverifikasi"</string>
<string name="screen_identity_use_another_device">"Gunakan perangkat lain"</string>
<string name="screen_identity_waiting_on_other_device">"Menunggu di perangkat lain…"</string>
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
<string name="screen_session_verification_enter_recovery_key">"Masukkan kunci pemulihan"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Non puoi confermare?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crea una nuova chiave di recupero"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica questo dispositivo per segnare i tuoi messaggi come sicuri."</string>
<string name="screen_identity_confirmation_title">"Conferma la tua identità"</string>
<string name="screen_identity_confirmation_use_another_device">"Usa un altro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usa la chiave di recupero"</string>
<string name="screen_identity_confirmed_subtitle">"Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo."</string>
<string name="screen_identity_confirmed_title">"Dispositivo verificato"</string>
<string name="screen_identity_use_another_device">"Usa un altro dispositivo"</string>
<string name="screen_identity_waiting_on_other_device">"In attesa sull\'altro dispositivo…"</string>
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
<string name="screen_session_verification_enter_recovery_key">"Inserisci la chiave di recupero"</string>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_create_new_recovery_key">"ახალი აღდგენის გასაღების შექმნა"</string>
<string name="screen_identity_confirmation_subtitle">"დაადასტურეთ ეს მოწყობილობა უსაფრთხო მიმოწერისათვის."</string>
<string name="screen_identity_confirmation_title">"დაამტკიცეთ თქვენი პიროვნება"</string>
<string name="screen_identity_confirmed_subtitle">"ახლა თქვენ შეძლებთ შეტყობინებების წაკითხვას ან გაგზავნას უსაფრთხოდ, სხვა მომხმარებლებსაც შეუძლიათ ამ მოწყობილობას ენდონ."</string>
<string name="screen_identity_confirmed_title">"მოწყობილობა დადასტურებულია"</string>
<string name="screen_identity_waiting_on_other_device">"ველოდებით სხვა მოწყობილობას…"</string>
<string name="screen_notification_optin_subtitle">"თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით."</string>
<string name="screen_notification_optin_title">"ყველა შეტყობინებაზე შეტყობინებების მიღება"</string>
<string name="screen_session_verification_enter_recovery_key">"შეიყვანეთ აღდგენის გასაღები"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"확인할 수 없나요?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"새로운 복구 키 만들기"</string>
<string name="screen_identity_confirmation_subtitle">"보안 메시징을 설정하려면 이 장치를 확인하세요."</string>
<string name="screen_identity_confirmation_title">"본인 확인"</string>
<string name="screen_identity_confirmation_use_another_device">"다른 기기 사용"</string>
<string name="screen_identity_confirmation_use_recovery_key">"복구 키 사용"</string>
<string name="screen_identity_confirmed_subtitle">"이제 메시지를 안전하게 읽거나 보낼 수 있으며, 채팅 상대도 이 기기를 신뢰할 수 있습니다."</string>
<string name="screen_identity_confirmed_title">"기기 검증됨"</string>
<string name="screen_identity_use_another_device">"다른 기기 사용"</string>
<string name="screen_identity_waiting_on_other_device">"다른 기기에서 대기 중…"</string>
<string name="screen_notification_optin_subtitle">"나중에 설정을 변경할 수 있습니다."</string>
<string name="screen_notification_optin_title">"알림을 허용하고 메시지를 놓치지 마세요."</string>
<string name="screen_session_verification_enter_recovery_key">"복구 키를 입력하세요"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kan du ikke bekrefte?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Opprett en ny gjenopprettingsnøkkel"</string>
<string name="screen_identity_confirmation_subtitle">"Verifiser denne enheten for å sette opp sikker meldingsutveksling."</string>
<string name="screen_identity_confirmation_title">"Bekreft identiteten din"</string>
<string name="screen_identity_confirmation_use_another_device">"Bruk en annen enhet"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Bruk gjenopprettingsnøkkel"</string>
<string name="screen_identity_confirmed_subtitle">"Nå kan du lese eller sende meldinger på en sikker måte, og alle du chatter med kan også stole på denne enheten."</string>
<string name="screen_identity_confirmed_title">"Enhet verifisert"</string>
<string name="screen_identity_use_another_device">"Bruk en annen enhet"</string>
<string name="screen_identity_waiting_on_other_device">"Venter på en annen enhet…"</string>
<string name="screen_notification_optin_subtitle">"Du kan endre innstillingene dine senere."</string>
<string name="screen_notification_optin_title">"Tillat varslinger og gå aldri glipp av en melding"</string>
<string name="screen_session_verification_enter_recovery_key">"Skriv inn gjenopprettingsnøkkel"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kan ik dit niet bevestigen?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Maak een nieuwe herstelsleutel"</string>
<string name="screen_identity_confirmation_subtitle">"Verifieer dit apparaat om beveiligde berichten in te stellen."</string>
<string name="screen_identity_confirmation_title">"Bevestig dat jij het bent"</string>
<string name="screen_identity_confirmation_use_another_device">"Gebruik een ander apparaat"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Gebruik de herstelsleutel"</string>
<string name="screen_identity_confirmed_subtitle">"Nu kun je veilig berichten lezen of verzenden, en iedereen met wie je chat kan dit apparaat ook vertrouwen."</string>
<string name="screen_identity_confirmed_title">"Apparaat geverifieerd"</string>
<string name="screen_identity_use_another_device">"Gebruik een ander apparaat"</string>
<string name="screen_identity_waiting_on_other_device">"Wachten op ander apparaat…"</string>
<string name="screen_notification_optin_subtitle">"Je kunt je instellingen later wijzigen."</string>
<string name="screen_notification_optin_title">"Sta meldingen toe en mis nooit meer een bericht"</string>
<string name="screen_session_verification_enter_recovery_key">"Voer herstelsleutel in"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nie możesz potwierdzić?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Utwórz nowy klucz przywracania"</string>
<string name="screen_identity_confirmation_subtitle">"Zweryfikuj to urządzenie, aby skonfigurować bezpieczne przesyłanie wiadomości."</string>
<string name="screen_identity_confirmation_title">"Potwierdź, że to Ty"</string>
<string name="screen_identity_confirmation_use_another_device">"Użyj innego urządzenia"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Użyj klucza przywracania"</string>
<string name="screen_identity_confirmed_subtitle">"Teraz możesz bezpiecznie czytać i wysyłać wiadomości, każdy z kim czatujesz również może ufać temu urządzeniu."</string>
<string name="screen_identity_confirmed_title">"Urządzenie zweryfikowane"</string>
<string name="screen_identity_use_another_device">"Użyj innego urządzenia"</string>
<string name="screen_identity_waiting_on_other_device">"Oczekiwanie na inne urządzenie…"</string>
<string name="screen_notification_optin_subtitle">"Możesz zmienić ustawienia później."</string>
<string name="screen_notification_optin_title">"Zezwól na powiadomienia i nie przegap żadnej wiadomości"</string>
<string name="screen_session_verification_enter_recovery_key">"Wprowadź klucz przywracania"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Não consegue confirmar?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Criar uma nova chave de recuperação"</string>
<string name="screen_identity_confirmation_subtitle">"Verifique este dispositivo para configurar as mensagens seguras."</string>
<string name="screen_identity_confirmation_title">"Confirme sua identidade"</string>
<string name="screen_identity_confirmation_use_another_device">"Usar outro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usar chave de recuperação"</string>
<string name="screen_identity_confirmed_subtitle">"Agora você pode ler ou enviar mensagens com segurança, e qualquer pessoa com quem você conversa também pode confiar neste dispositivo."</string>
<string name="screen_identity_confirmed_title">"Dispositivo verificado"</string>
<string name="screen_identity_use_another_device">"Usar outro dispositivo"</string>
<string name="screen_identity_waiting_on_other_device">"Aguardando o outro dispositivo…"</string>
<string name="screen_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
<string name="screen_notification_optin_title">"Permita as notificações e nunca perca uma mensagem"</string>
<string name="screen_session_verification_enter_recovery_key">"Digitar chave de recuperação"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Não é possível confirmar?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Criar uma nova chave de recuperação"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica este dispositivo para configurar o envio seguro de mensagens."</string>
<string name="screen_identity_confirmation_title">"Confirma que és tu"</string>
<string name="screen_identity_confirmation_use_another_device">"Utilizar outro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Utilizar chave de recuperação"</string>
<string name="screen_identity_confirmed_subtitle">"Agora podes ler ou enviar mensagens de forma segura, e qualquer pessoa com quem converses também pode confiar neste dispositivo."</string>
<string name="screen_identity_confirmed_title">"Dispositivo verificado"</string>
<string name="screen_identity_use_another_device">"Utilizar outro dispositivo"</string>
<string name="screen_identity_waiting_on_other_device">"A aguardar por outros dispositivos…"</string>
<string name="screen_notification_optin_subtitle">"Podes alterar as tuas definições mais tarde."</string>
<string name="screen_notification_optin_title">"Permite as notificações e nunca percas uma mensagem"</string>
<string name="screen_session_verification_enter_recovery_key">"Insere a chave de recuperação"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nu puteți confirma?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Creați o nouă cheie de recuperare"</string>
<string name="screen_identity_confirmation_subtitle">"Verificați acest dispozitiv pentru a configura mesagerie securizată."</string>
<string name="screen_identity_confirmation_title">"Confirmați că sunteți dumneavoastră"</string>
<string name="screen_identity_confirmation_use_another_device">"Utilizați un alt dispozitiv"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Utilizați cheia de recuperare"</string>
<string name="screen_identity_confirmed_subtitle">"Acum puteți citi sau trimite mesaje în siguranță, iar oricine cu care conversați poate avea încredere în acest dispozitiv."</string>
<string name="screen_identity_confirmed_title">"Dispozitiv verificat"</string>
<string name="screen_identity_use_another_device">"Utilizați un alt dispozitiv"</string>
<string name="screen_identity_waiting_on_other_device">"Se așteaptă celălalt dispozitiv…"</string>
<string name="screen_notification_optin_subtitle">"Puteți modifica setările mai târziu."</string>
<string name="screen_notification_optin_title">"Permiteți notificările și nu pierdeți niciodată un mesaj"</string>
<string name="screen_session_verification_enter_recovery_key">"Introduceți cheia de recuperare"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можете подтвердить?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Создайте новый ключ восстановления"</string>
<string name="screen_identity_confirmation_subtitle">"Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями."</string>
<string name="screen_identity_confirmation_title">"Подтвердите, что это вы"</string>
<string name="screen_identity_confirmation_use_another_device">"Использовать другое устройство"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Используйте recovery key"</string>
<string name="screen_identity_confirmed_subtitle">"Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству."</string>
<string name="screen_identity_confirmed_title">"Устройство проверено"</string>
<string name="screen_identity_use_another_device">"Использовать другое устройство"</string>
<string name="screen_identity_waiting_on_other_device">"Ожидание на другом устройстве…"</string>
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
<string name="screen_notification_optin_title">"Разрешите отправку уведомлений и ни одно сообщение не будет пропущено"</string>
<string name="screen_session_verification_enter_recovery_key">"Введите ключ восстановления"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nemôžete potvrdiť?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Vytvoriť nový kľúč na obnovenie"</string>
<string name="screen_identity_confirmation_subtitle">"Ak chcete nastaviť zabezpečené správy, overte toto zariadenie."</string>
<string name="screen_identity_confirmation_title">"Potvrďte, že ste to vy"</string>
<string name="screen_identity_confirmation_use_another_device">"Použite iné zariadenie"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Použiť kľúč na obnovenie"</string>
<string name="screen_identity_confirmed_subtitle">"Teraz môžete bezpečne čítať alebo odosielať správy a tomuto zariadeniu môže dôverovať aj ktokoľvek, s kým konverzujete."</string>
<string name="screen_identity_confirmed_title">"Zariadenie overené"</string>
<string name="screen_identity_use_another_device">"Použite iné zariadenie"</string>
<string name="screen_identity_waiting_on_other_device">"Čaká sa na druhom zariadení…"</string>
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
<string name="screen_session_verification_enter_recovery_key">"Zadajte kľúč na obnovenie"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kan du inte bekräfta?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Skapa en ny återställningsnyckel"</string>
<string name="screen_identity_confirmation_subtitle">"Verifiera den här enheten för att konfigurera säkra meddelanden."</string>
<string name="screen_identity_confirmation_title">"Bekräfta att det är du"</string>
<string name="screen_identity_confirmation_use_another_device">"Använd en annan enhet"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Använd återställningsnyckel"</string>
<string name="screen_identity_confirmed_subtitle">"Nu kan du läsa eller skicka meddelanden säkert, och alla du chattar med kan också lita på den här enheten."</string>
<string name="screen_identity_confirmed_title">"Enhet verifierad"</string>
<string name="screen_identity_use_another_device">"Använd en annan enhet"</string>
<string name="screen_identity_waiting_on_other_device">"Väntar på annan enhet …"</string>
<string name="screen_notification_optin_subtitle">"Du kan ändra dina inställningar senare."</string>
<string name="screen_notification_optin_title">"Tillåt aviseringar och missa aldrig ett meddelande"</string>
<string name="screen_session_verification_enter_recovery_key">"Ange återställningsnyckel"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Onaylayamıyor musunuz?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Yeni bir kurtarma anahtarı oluştur"</string>
<string name="screen_identity_confirmation_subtitle">"Güvenli mesajlaşmayı ayarlamak için bu cihazı doğrulayın."</string>
<string name="screen_identity_confirmation_title">"Kimliğinizi doğrulayın"</string>
<string name="screen_identity_confirmation_use_another_device">"Başka bir cihaz kullan"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Kurtarma anahtarı kullan"</string>
<string name="screen_identity_confirmed_subtitle">"Artık mesajları güvenli bir şekilde okuyabilir veya gönderebilirsiniz ve sohbet ettiğiniz herkes de bu cihaza güvenebilir."</string>
<string name="screen_identity_confirmed_title">"Cihaz doğrulandı"</string>
<string name="screen_identity_use_another_device">"Başka bir cihaz kullan"</string>
<string name="screen_identity_waiting_on_other_device">"Diğer cihazda bekleniyor…"</string>
<string name="screen_notification_optin_subtitle">"Ayarlarınızı daha sonra değiştirebilirsiniz."</string>
<string name="screen_notification_optin_title">"Bildirimlere izin verin ve hiçbir mesajı kaçırmayın"</string>
<string name="screen_session_verification_enter_recovery_key">"Kurtarma anahtarını girin"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можете підтвердити?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Створити новий ключ відновлення"</string>
<string name="screen_identity_confirmation_subtitle">"Верифікуйте цей пристрій, щоб налаштувати безпечний обмін повідомленнями."</string>
<string name="screen_identity_confirmation_title">"Підтвердьте, що це ви"</string>
<string name="screen_identity_confirmation_use_another_device">"Використовуйте інший пристрій"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Використовуйте ключ відновлення"</string>
<string name="screen_identity_confirmed_subtitle">"Тепер ви можете безпечно читати або надсилати повідомлення, і кожен, з ким ви спілкуєтесь, також може довіряти цьому пристрою."</string>
<string name="screen_identity_confirmed_title">"Пристрій перевірено"</string>
<string name="screen_identity_use_another_device">"Використовуйте інший пристрій"</string>
<string name="screen_identity_waiting_on_other_device">"Чекає на інше пристрій…"</string>
<string name="screen_notification_optin_subtitle">"Ви можете змінити свої налаштування пізніше."</string>
<string name="screen_notification_optin_title">"Дозволити сповіщення і ніколи не пропускати повідомлення"</string>
<string name="screen_session_verification_enter_recovery_key">"Введіть ключ відновлення"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"تصدیق نہیں کر سکتے؟"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"ایک نئی بازیابی کلید تخلیق کریں"</string>
<string name="screen_identity_confirmation_subtitle">"محفوظ پیغام رسانی ترتیب دینے کیلئے اس آلے کی توثیق کریں۔"</string>
<string name="screen_identity_confirmation_title">"اپنی شناخت کی تصدیق کریں"</string>
<string name="screen_identity_confirmation_use_another_device">"دوسرا آلہ استعمال کریں"</string>
<string name="screen_identity_confirmation_use_recovery_key">"بازیابی کلید استعمال کریں"</string>
<string name="screen_identity_confirmed_subtitle">"اب آپ محفوظ طریقے سے پیغامات پڑھ یا بھیج سکتے ہیں، اور جسکے ساتھ آپ گفتگو کرتے ہیں وہ بھی اس آلہ پر بھروسہ کر سکتا ہے۔"</string>
<string name="screen_identity_confirmed_title">"آلہ توثیق شدہ"</string>
<string name="screen_identity_use_another_device">"دوسرا آلہ استعمال کریں"</string>
<string name="screen_identity_waiting_on_other_device">"دوسرے آلہ پر منتظر…"</string>
<string name="screen_notification_optin_subtitle">"آپ بعد میں اپنی ترتیبات تبدیل کر سکتے ہیں۔"</string>
<string name="screen_notification_optin_title">"اطلاعات کی اجازت دیں اور کبھی بھی کسی پیغام سے محروم نہ ہوں۔"</string>
<string name="screen_session_verification_enter_recovery_key">"بازیابی کلید درج کریں"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Tasdiqlay olmayapsizmi?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Yangi tiklash kalitini yarating"</string>
<string name="screen_identity_confirmation_subtitle">"Xavfsiz xabarlashuvni sozlash uchun ushbu qurilmani tasdiqlang."</string>
<string name="screen_identity_confirmation_title">"Shaxsingizni tasdiqlang"</string>
<string name="screen_identity_confirmation_use_another_device">"Boshqa qurilmadan foydalanish"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Qayta tiklash kalitidan foydalaning"</string>
<string name="screen_identity_confirmed_subtitle">"Endi xabarlarni xavfsiz tarzda oqish yoki yuborish imkoniyatiga egasiz, shuningdek, siz bilan muloqot qilayotgan har qanday kishi ham bu qurilmaga ishonch bildirishi mumkin."</string>
<string name="screen_identity_confirmed_title">"Qurilma tasdiqlandi"</string>
<string name="screen_identity_use_another_device">"Boshqa qurilmadan foydalanish"</string>
<string name="screen_identity_waiting_on_other_device">"Boshqa qurilmada kutilmoqda…"</string>
<string name="screen_notification_optin_subtitle">"Sozlamalaringizni keyinroq o\'zgartirishingiz mumkin."</string>
<string name="screen_notification_optin_title">"Bildirishnomalarga ruxsat bering va hech qachon xabarni o\'tkazib yubormang"</string>
<string name="screen_session_verification_enter_recovery_key">"Tiklash kalitini kiriting"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"無法確認?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"建立新的復原金鑰"</string>
<string name="screen_identity_confirmation_subtitle">"驗證這部裝置以設定安全通訊。"</string>
<string name="screen_identity_confirmation_title">"確認這是你本人"</string>
<string name="screen_identity_confirmation_use_another_device">"使用另一部裝置"</string>
<string name="screen_identity_confirmation_use_recovery_key">"使用復原金鑰"</string>
<string name="screen_identity_confirmed_subtitle">"您可以安全地讀取和發送訊息了,與您聊天的人也可以信任這部裝置。"</string>
<string name="screen_identity_confirmed_title">"裝置已驗證"</string>
<string name="screen_identity_use_another_device">"使用另一部裝置"</string>
<string name="screen_identity_waiting_on_other_device">"正在等待其他裝置…"</string>
<string name="screen_notification_optin_subtitle">"您稍後仍可變更設定。"</string>
<string name="screen_notification_optin_title">"允許通知,永遠不會錯誤任何訊息"</string>
<string name="screen_session_verification_enter_recovery_key">"輸入復原金鑰"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"无法确认?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"创建新的恢复密钥"</string>
<string name="screen_identity_confirmation_subtitle">"验证此设备以开始安全地收发消息。"</string>
<string name="screen_identity_confirmation_title">"确认这是你"</string>
<string name="screen_identity_confirmation_use_another_device">"使用其他设备"</string>
<string name="screen_identity_confirmation_use_recovery_key">"使用恢复密钥"</string>
<string name="screen_identity_confirmed_subtitle">"现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。"</string>
<string name="screen_identity_confirmed_title">"设备已验证"</string>
<string name="screen_identity_use_another_device">"使用其他设备"</string>
<string name="screen_identity_waiting_on_other_device">"正在等待其他设备……"</string>
<string name="screen_notification_optin_subtitle">"您可以稍后更改设置。"</string>
<string name="screen_notification_optin_title">"允许通知,绝不错过任何消息"</string>
<string name="screen_session_verification_enter_recovery_key">"输入恢复密钥"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Can\'t confirm?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new recovery key"</string>
<string name="screen_identity_confirmation_subtitle">"Verify this device to set up secure messaging."</string>
<string name="screen_identity_confirmation_title">"Confirm your identity"</string>
<string name="screen_identity_confirmation_use_another_device">"Use another device"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Use recovery key"</string>
<string name="screen_identity_confirmed_subtitle">"Now you can read or send messages securely, and anyone you chat with can also trust this device."</string>
<string name="screen_identity_confirmed_title">"Device verified"</string>
<string name="screen_identity_use_another_device">"Use another device"</string>
<string name="screen_identity_waiting_on_other_device">"Waiting on other device…"</string>
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
<string name="screen_session_verification_enter_recovery_key">"Enter recovery key"</string>
</resources>

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.test.FakeLockScreenEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class DefaultFtueEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `test node builder`() = runTest {
val entryPoint = DefaultFtueEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
FtueFlowNode(
buildContext = buildContext,
plugins = plugins,
analyticsEntryPoint = { _, _ -> lambdaError() },
defaultFtueService = createDefaultFtueService(),
lockScreenEntryPoint = FakeLockScreenEntryPoint(),
)
}
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
assertThat(result).isInstanceOf(FtueFlowNode::class.java)
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl
import android.os.Build
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.ftue.impl.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.state.InternalFtueState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultFtueServiceTest {
@Test
fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
emitVerifiedStatus(SessionVerifiedStatus.Unknown)
}
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
)
service.state.test {
// Verification state is unknown, we don't display the flow yet
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
// Verification state is known, we should display the flow if any check is false
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
}
}
@Test
fun `given all checks being true, FtueState is Complete`() = runTest {
val analyticsService = FakeAnalyticsService()
val sessionVerificationService = FakeSessionVerificationService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService,
)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
analyticsService.setDidAskUserConsent()
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
service.updateFtueStep()
service.state.test {
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
assertThat(awaitItem()).isEqualTo(FtueState.Complete)
}
}
@Test
fun `given all checks being true with no analytics, FtueState is Complete`() = runTest {
val analyticsService = NoopAnalyticsService()
val sessionVerificationService = FakeSessionVerificationService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService,
)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
service.updateFtueStep()
service.state.test {
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
assertThat(awaitItem()).isEqualTo(FtueState.Complete)
}
}
@Test
fun `traverse flow`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
}
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService,
)
service.ftueStepStateFlow.test {
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
// Session verification
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.SessionVerification))
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
// User completes verification
service.onUserCompletedSessionVerification()
// Notifications opt in
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.NotificationsOptIn))
permissionStateProvider.setPermissionGranted()
// Simulate event from NotificationsOptInNode.Callback.onNotificationsOptInFinished
service.updateFtueStep()
// Entering PIN code
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.LockscreenSetup))
lockScreenService.setIsPinSetup(true)
// Simulate event from LockScreenEntryPoint.Callback.onSetupDone()
service.updateFtueStep()
// Analytics opt in
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
analyticsService.setDidAskUserConsent()
// Final step
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
}
}
@Test
fun `if a check for a step is true, start from the next one`() = runTest {
val sessionVerificationService = FakeSessionVerificationService()
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService,
)
// Skip first 3 steps
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
service.ftueStepStateFlow.test {
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
// Analytics opt in
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
analyticsService.setDidAskUserConsent()
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
}
}
@Test
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
val sessionVerificationService = FakeSessionVerificationService()
val analyticsService = FakeAnalyticsService()
val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.M,
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
lockScreenService = lockScreenService,
)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
lockScreenService.setIsPinSetup(true)
service.ftueStepStateFlow.test {
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
// Analytics opt in
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
analyticsService.setDidAskUserConsent()
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
}
}
}
internal fun TestScope.createDefaultFtueService(
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
lockScreenService: LockScreenService = FakeLockScreenService(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
// First version where notification permission is required
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
) = DefaultFtueService(
sessionCoroutineScope = backgroundScope,
sessionVerificationService = sessionVerificationService,
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService,
sessionPreferencesStore = sessionPreferencesStore,
)

View File

@@ -0,0 +1,135 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.notifications
import android.os.Build
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.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class NotificationsOptInPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private var isFinished = false
@Test
fun `initial state`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.notificationsPermissionState.showDialog).isFalse()
}
}
@Test
fun `show dialog on continue clicked`() = runTest {
val permissionPresenter = FakePermissionsPresenter()
val presenter = createPresenter(permissionPresenter)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue()
}
}
@Test
fun `finish flow on continue clicked with permission already granted`() = runTest {
val permissionPresenter = FakePermissionsPresenter().apply {
setPermissionGranted()
}
val presenter = createPresenter(permissionPresenter)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
assertThat(isFinished).isTrue()
}
}
@Test
fun `finish flow on not now clicked`() = runTest {
val permissionPresenter = FakePermissionsPresenter()
val presenter = createPresenter(
permissionsPresenter = permissionPresenter,
sdkIntVersion = Build.VERSION_CODES.M
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
assertThat(isFinished).isTrue()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `set permission denied on not now clicked in API 33`() = runTest(StandardTestDispatcher()) {
val permissionPresenter = FakePermissionsPresenter()
val permissionStateProvider = FakePermissionStateProvider()
val presenter = createPresenter(
permissionsPresenter = permissionPresenter,
permissionStateProvider = permissionStateProvider,
sdkIntVersion = Build.VERSION_CODES.TIRAMISU
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
// Allow background coroutines to run
runCurrent()
val isPermissionDenied = runBlocking {
permissionStateProvider.isPermissionDenied("notifications").first()
}
assertThat(isPermissionDenied).isTrue()
}
}
private fun TestScope.createPresenter(
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(),
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
) = NotificationsOptInPresenter(
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
callback = object : NotificationsOptInNode.Callback {
override fun onNotificationsOptInFinished() {
isFinished = true
}
},
appCoroutineScope = this,
permissionStateProvider = permissionStateProvider,
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
)
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ChooseSessionVerificationModePresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
presenter.test {
awaitItem().run {
assertThat(buttonsState.isLoading()).isTrue()
assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue()
}
}
}
@Test
fun `present - state is relayed from EncryptionService, order 1`() = runTest {
val encryptionService = FakeEncryptionService()
val presenter = createPresenter(encryptionService = encryptionService)
presenter.test {
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
// Has device to verify against
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
// Can enter recovery key
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = false,
canEnterRecoveryKey = false,
)
)
}
}
@Test
fun `present - state is relayed from EncryptionService, order 2`() = runTest {
val encryptionService = FakeEncryptionService()
val presenter = createPresenter(encryptionService = encryptionService)
presenter.test {
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
// Can enter recovery key
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
// Has device to verify against
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = false,
canEnterRecoveryKey = false,
)
)
}
}
@Test
fun `present - can use another device`() = runTest {
val encryptionService = FakeEncryptionService()
val presenter = createPresenter(encryptionService = encryptionService)
presenter.test {
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
// Can enter recovery key
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
// Has device to verify against
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(true))
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = true,
canEnterRecoveryKey = false,
)
)
}
}
@Test
fun `present - can enter recovery key`() = runTest {
val encryptionService = FakeEncryptionService()
val presenter = createPresenter(encryptionService = encryptionService)
presenter.test {
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
// Can enter recovery key
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
// Has device to verify against
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = false,
canEnterRecoveryKey = true,
)
)
}
}
@Test
fun `sing out action triggers a direct logout`() = runTest {
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvents, Unit> {}
val logoutPresenter = Presenter<DirectLogoutState> {
aDirectLogoutState(eventSink = logoutEventRecorder)
}
val presenter = createPresenter(directLogoutPresenter = logoutPresenter)
presenter.test {
val initial = awaitItem()
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
logoutEventRecorder.assertions().isCalledOnce()
.with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
}
}
private fun createPresenter(
encryptionService: FakeEncryptionService = FakeEncryptionService(),
directLogoutPresenter: Presenter<DirectLogoutState> = Presenter<DirectLogoutState> { aDirectLogoutState() }
) = ChooseSelfVerificationModePresenter(
encryptionService = encryptionService,
directLogoutPresenter = directLogoutPresenter,
)
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
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.ftue.impl.R
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
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 ChooseSessionVerificationModeViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on learn more invokes the expected callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(),
onLearnMoreClick = callback,
)
rule.clickOn(CommonStrings.action_learn_more)
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on use another device calls the callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))),
onUseAnotherDevice = callback,
)
rule.clickOn(R.string.screen_identity_use_another_device)
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on enter recovery key calls the callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canEnterRecoveryKey = true))),
onEnterRecoveryKey = callback,
)
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on cannot confirm calls the reset keys callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(),
onResetKey = callback,
)
rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onUseAnotherDevice: () -> Unit = EnsureNeverCalled(),
onResetKey: () -> Unit = EnsureNeverCalled(),
onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
) {
setContent {
ChooseSelfVerificationModeView(
state = state,
onLearnMore = onLearnMoreClick,
onUseAnotherDevice = onUseAnotherDevice,
onResetKey = onResetKey,
onUseRecoveryKey = onEnterRecoveryKey,
)
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.ftue.test"
}
dependencies {
implementation(projects.features.ftue.api)
implementation(projects.tests.testutils)
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.test
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import kotlinx.coroutines.flow.MutableStateFlow
class FakeFtueService : FtueService {
override val state: MutableStateFlow<FtueState> = MutableStateFlow(FtueState.Unknown)
suspend fun emitState(newState: FtueState) {
state.emit(newState)
}
}