Browse Source

Merge pull request #1737 from vector-im/feature/bma/improvePages

Create UserStoryFlowPage
pull/1756/head
Benoit Marty 11 months ago committed by GitHub
parent
commit
2de508c6d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt
  2. 108
      features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
  3. 66
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
  4. 59
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
  5. 92
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt
  6. 208
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt
  7. 117
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt
  8. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Day_0_null,NEXUS_5,1.0,en].png
  9. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Night_1_null,NEXUS_5,1.0,en].png

11
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt

@ -17,19 +17,16 @@ @@ -17,19 +17,16 @@
package io.element.android.features.lockscreen.impl.setup.biometric
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Fingerprint
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.features.lockscreen.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.preview.ElementPreview
@ -76,10 +73,8 @@ private fun SetupBiometricFooter( @@ -76,10 +73,8 @@ private fun SetupBiometricFooter(
onSkipClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
ButtonColumnMolecule(
modifier = modifier,
) {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
Button(

108
features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt

@ -18,9 +18,9 @@ package io.element.android.features.logout.impl @@ -18,9 +18,9 @@ package io.element.android.features.logout.impl
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@ -29,11 +29,8 @@ import androidx.compose.ui.res.stringResource @@ -29,11 +29,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.Async
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.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -41,7 +38,6 @@ import io.element.android.libraries.designsystem.theme.components.Button @@ -41,7 +38,6 @@ import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
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.TopAppBar
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
@ -51,7 +47,6 @@ import io.element.android.libraries.testtags.testTag @@ -51,7 +47,6 @@ import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LogoutView(
state: LogoutState,
@ -62,29 +57,23 @@ fun LogoutView( @@ -62,29 +57,23 @@ fun LogoutView(
) {
val eventSink = state.eventSink
HeaderFooterPage(
FlowStepPage(
onBackClicked = onBackClicked,
title = title(state),
subTitle = subtitle(state),
iconResourceId = CommonDrawables.ic_key,
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = {
HeaderContent(state = state)
},
footer = {
BottomMenu(
content = { Content(state) },
buttons = {
Buttons(
state = state,
onChangeRecoveryKeyClicked = onChangeRecoveryKeyClicked,
onLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
}
)
}
) {
Content(state = state)
}
},
)
// Log out confirmation dialog
if (state.showConfirmationDialog) {
@ -92,9 +81,6 @@ fun LogoutView( @@ -92,9 +81,6 @@ fun LogoutView(
title = stringResource(id = CommonStrings.action_signout),
content = stringResource(id = R.string.screen_signout_confirmation_dialog_content),
submitText = stringResource(id = CommonStrings.action_signout),
onCancelClicked = {
eventSink(LogoutEvents.CloseDialogs)
},
onSubmitClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
@ -112,9 +98,6 @@ fun LogoutView( @@ -112,9 +98,6 @@ fun LogoutView(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.error_unknown),
submitText = stringResource(id = CommonStrings.action_signout_anyway),
onCancelClicked = {
eventSink(LogoutEvents.CloseDialogs)
},
onSubmitClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
},
@ -132,30 +115,23 @@ fun LogoutView( @@ -132,30 +115,23 @@ fun LogoutView(
}
@Composable
private fun HeaderContent(
state: LogoutState,
modifier: Modifier = Modifier,
) {
val title = when {
private fun title(state: LogoutState): String {
return when {
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title)
state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_title)
else -> stringResource(CommonStrings.action_signout)
}
val subtitle = when {
}
@Composable
private fun subtitle(state: LogoutState): String? {
return when {
(state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection ->
stringResource(id = R.string.screen_signout_key_backup_offline_subtitle)
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle)
state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle)
else -> null
}
val paddingTop = 0.dp
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = paddingTop),
iconResourceId = CommonDrawables.ic_key,
title = title,
subTitle = subtitle,
)
}
private fun BackupUploadState.isBackingUp(): Boolean {
@ -171,37 +147,33 @@ private fun BackupUploadState.isBackingUp(): Boolean { @@ -171,37 +147,33 @@ private fun BackupUploadState.isBackingUp(): Boolean {
}
@Composable
private fun BottomMenu(
private fun ColumnScope.Buttons(
state: LogoutState,
onLogoutClicked: () -> Unit,
onChangeRecoveryKeyClicked: () -> Unit,
) {
val logoutAction = state.logoutAction
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
if (state.isLastSession) {
OutlinedButton(
text = stringResource(id = CommonStrings.common_settings),
modifier = Modifier.fillMaxWidth(),
onClick = onChangeRecoveryKeyClicked,
)
}
val signOutSubmitRes = when {
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading,
destructive = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.signOut),
onClick = onLogoutClicked,
if (state.isLastSession) {
OutlinedButton(
text = stringResource(id = CommonStrings.common_settings),
modifier = Modifier.fillMaxWidth(),
onClick = onChangeRecoveryKeyClicked,
)
}
val signOutSubmitRes = when {
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading,
destructive = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.signOut),
onClick = onLogoutClicked,
)
}
@Composable

66
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt

@ -18,11 +18,11 @@ package io.element.android.features.securebackup.impl.disable @@ -18,11 +18,11 @@ package io.element.android.features.securebackup.impl.disable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
@ -31,10 +31,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter @@ -31,10 +31,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R
import io.element.android.libraries.architecture.Async
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.button.BackButton
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
@ -42,11 +39,9 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -42,11 +39,9 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.theme.ElementTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SecureBackupDisableView(
state: SecureBackupDisableState,
@ -59,23 +54,16 @@ fun SecureBackupDisableView( @@ -59,23 +54,16 @@ fun SecureBackupDisableView(
onDone()
}
}
HeaderFooterPage(
FlowStepPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = {
HeaderContent()
},
footer = {
BottomMenu(state = state)
}
) {
Content(state = state)
}
onBackClicked = onBackClicked,
title = stringResource(id = R.string.screen_key_backup_disable_title),
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
iconResourceId = CommonDrawables.ic_key_off,
content = { Content(state = state) },
buttons = { Buttons(state = state) },
)
if (state.showConfirmationDialog) {
SecureBackupDisableConfirmationDialog(
onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = true)) },
@ -102,32 +90,16 @@ private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismi @@ -102,32 +90,16 @@ private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismi
}
@Composable
private fun HeaderContent(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key_off,
title = stringResource(id = R.string.screen_key_backup_disable_title),
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
)
}
@Composable
private fun BottomMenu(
private fun ColumnScope.Buttons(
state: SecureBackupDisableState,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
Button(
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
showProgress = state.disableAction.isLoading(),
destructive = true,
modifier = Modifier.fillMaxWidth(),
onClick = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = false)) }
)
}
Button(
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
showProgress = state.disableAction.isLoading(),
destructive = true,
modifier = Modifier.fillMaxWidth(),
onClick = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = false)) }
)
}
@Composable

59
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt

@ -16,29 +16,22 @@ @@ -16,29 +16,22 @@
package io.element.android.features.securebackup.impl.enable
import androidx.compose.foundation.layout.ColumnScope
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.runtime.LaunchedEffect
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.features.securebackup.impl.R
import io.element.android.libraries.architecture.Async
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.button.BackButton
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
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.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SecureBackupEnableView(
state: SecureBackupEnableState,
@ -51,20 +44,12 @@ fun SecureBackupEnableView( @@ -51,20 +44,12 @@ fun SecureBackupEnableView(
onDone()
}
}
HeaderFooterPage(
FlowStepPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = {
HeaderContent()
},
footer = {
BottomMenu(state = state)
}
onBackClicked = onBackClicked,
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
iconResourceId = CommonDrawables.ic_key,
buttons = { Buttons(state = state) }
)
if (state.enableAction is Async.Failure) {
ErrorDialog(
@ -75,31 +60,15 @@ fun SecureBackupEnableView( @@ -75,31 +60,15 @@ fun SecureBackupEnableView(
}
@Composable
private fun HeaderContent(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key,
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
subTitle = null,
)
}
@Composable
private fun BottomMenu(
private fun ColumnScope.Buttons(
state: SecureBackupEnableState,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
Button(
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
showProgress = state.enableAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
)
}
Button(
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
showProgress = state.enableAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
)
}
@PreviewsDayNight

92
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt

@ -16,9 +16,9 @@ @@ -16,9 +16,9 @@
package io.element.android.features.securebackup.impl.enter
import androidx.compose.foundation.layout.ColumnScope
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
@ -26,19 +26,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter @@ -26,19 +26,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
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.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.async.AsyncView
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SecureBackupEnterRecoveryKeyView(
state: SecureBackupEnterRecoveryKeyState,
@ -53,79 +48,46 @@ fun SecureBackupEnterRecoveryKeyView( @@ -53,79 +48,46 @@ fun SecureBackupEnterRecoveryKeyView(
onErrorDismiss = { state.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) },
)
HeaderFooterPage(
FlowStepPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = {
HeaderContent()
},
footer = {
BottomMenu(
state = state,
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
},
)
}
) {
Content(
state = state,
onChange = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it))
},
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
})
}
}
@Composable
private fun HeaderContent(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
onBackClicked = onBackClicked,
iconResourceId = CommonDrawables.ic_key,
title = stringResource(id = R.string.screen_recovery_key_confirm_title),
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
content = { Content(state = state) },
buttons = { Buttons(state = state) }
)
}
@Composable
private fun BottomMenu(
state: SecureBackupEnterRecoveryKeyState,
onSubmit: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_confirm),
enabled = state.isSubmitEnabled,
showProgress = state.submitAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = onSubmit
)
}
}
@Composable
private fun Content(
state: SecureBackupEnterRecoveryKeyState,
onChange: (String) -> Unit,
onSubmit: () -> Unit,
) {
RecoveryKeyView(
modifier = Modifier.padding(top = 52.dp),
state = state.recoveryKeyViewState,
onClick = null,
onChange = onChange,
onSubmit = onSubmit,
onChange = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it))
},
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
},
)
}
@Composable
private fun ColumnScope.Buttons(
state: SecureBackupEnterRecoveryKeyState,
) {
Button(
text = stringResource(id = CommonStrings.action_confirm),
enabled = state.isSubmitEnabled,
showProgress = state.submitAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
}
)
}

208
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt

@ -16,10 +16,9 @@ @@ -16,10 +16,9 @@
package io.element.android.features.securebackup.impl.setup
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.ColumnScope
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.platform.LocalContext
@ -28,24 +27,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameter @@ -28,24 +27,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
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.button.BackButton
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SecureBackupSetupView(
state: SecureBackupSetupState,
@ -53,68 +46,15 @@ fun SecureBackupSetupView( @@ -53,68 +46,15 @@ fun SecureBackupSetupView(
onBackClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val canGoBack = state.canGoBack()
BackHandler(enabled = canGoBack) {
onBackClicked()
}
HeaderFooterPage(
FlowStepPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
if (canGoBack) {
BackButton(onClick = onBackClicked)
}
},
title = {},
)
},
header = {
HeaderContent(state = state)
},
footer = {
val chooserTitle = stringResource(id = R.string.screen_recovery_key_save_action)
BottomMenu(
state = state,
onSaveClicked = { key ->
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = chooserTitle,
text = key,
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
},
onDone = {
if (state.setupState is SetupState.CreatedAndSaved) {
onDone()
} else {
state.eventSink.invoke(SecureBackupSetupEvents.Done)
}
},
)
}
) {
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
val clickLambda = if (formattedRecoveryKey != null) {
{
context.copyToClipboard(
formattedRecoveryKey,
context.getString(R.string.screen_recovery_key_copied_to_clipboard)
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
}
} else {
if (!state.recoveryKeyViewState.inProgress) {
{
state.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey)
}
} else {
null
}
}
Content(state = state.recoveryKeyViewState, onClick = clickLambda)
}
onBackClicked = onBackClicked.takeIf { state.canGoBack() },
title = title(state),
subTitle = subtitle(state),
iconResourceId = CommonDrawables.ic_key,
content = { Content(state) },
buttons = { Buttons(state, onDone = onDone) },
)
if (state.showSaveConfirmationDialog) {
ConfirmationDialog(
@ -134,12 +74,8 @@ private fun SecureBackupSetupState.canGoBack(): Boolean { @@ -134,12 +74,8 @@ private fun SecureBackupSetupState.canGoBack(): Boolean {
}
@Composable
private fun HeaderContent(
state: SecureBackupSetupState,
modifier: Modifier = Modifier,
) {
val setupState = state.setupState
val title = when (setupState) {
private fun title(state: SecureBackupSetupState): String {
return when (state.setupState) {
SetupState.Init,
SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory)
stringResource(id = R.string.screen_recovery_key_change_title)
@ -149,7 +85,11 @@ private fun HeaderContent( @@ -149,7 +85,11 @@ private fun HeaderContent(
is SetupState.CreatedAndSaved ->
stringResource(id = R.string.screen_recovery_key_save_title)
}
val subTitle = when (setupState) {
}
@Composable
private fun subtitle(state: SecureBackupSetupState): String {
return when (state.setupState) {
SetupState.Init,
SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory)
stringResource(id = R.string.screen_recovery_key_change_description)
@ -159,67 +99,87 @@ private fun HeaderContent( @@ -159,67 +99,87 @@ private fun HeaderContent(
is SetupState.CreatedAndSaved ->
stringResource(id = R.string.screen_recovery_key_save_description)
}
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key,
title = title,
subTitle = subTitle,
)
}
@Composable
private fun BottomMenu(
private fun Content(
state: SecureBackupSetupState,
onSaveClicked: (String) -> Unit,
onDone: () -> Unit,
) {
val setupState = state.setupState
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
when (setupState) {
SetupState.Init,
SetupState.Creating -> {
Button(
text = stringResource(id = CommonStrings.action_done),
enabled = false,
modifier = Modifier.fillMaxWidth(),
onClick = onDone
)
}
is SetupState.Created,
is SetupState.CreatedAndSaved -> {
OutlinedButton(
text = stringResource(id = R.string.screen_recovery_key_save_action),
leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_download),
modifier = Modifier.fillMaxWidth(),
onClick = { onSaveClicked(setupState.recoveryKey()!!) },
)
Button(
text = stringResource(id = CommonStrings.action_done),
modifier = Modifier.fillMaxWidth(),
onClick = onDone,
)
val context = LocalContext.current
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
val clickLambda = if (formattedRecoveryKey != null) {
{
context.copyToClipboard(
formattedRecoveryKey,
context.getString(R.string.screen_recovery_key_copied_to_clipboard)
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
}
} else {
if (!state.recoveryKeyViewState.inProgress) {
{
state.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey)
}
} else {
null
}
}
}
@Composable
private fun Content(
state: RecoveryKeyViewState,
onClick: (() -> Unit)?,
) {
val modifier = Modifier.padding(top = 52.dp)
RecoveryKeyView(
modifier = modifier,
state = state,
onClick = onClick,
modifier = Modifier.padding(top = 52.dp),
state = state.recoveryKeyViewState,
onClick = clickLambda,
onChange = null,
onSubmit = null,
)
}
@Composable
private fun ColumnScope.Buttons(
state: SecureBackupSetupState,
onDone: () -> Unit,
) {
val context = LocalContext.current
val chooserTitle = stringResource(id = R.string.screen_recovery_key_save_action)
when (state.setupState) {
SetupState.Init,
SetupState.Creating -> {
Button(
text = stringResource(id = CommonStrings.action_done),
enabled = false,
modifier = Modifier.fillMaxWidth(),
onClick = onDone
)
}
is SetupState.Created,
is SetupState.CreatedAndSaved -> {
OutlinedButton(
text = stringResource(id = R.string.screen_recovery_key_save_action),
leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_download),
modifier = Modifier.fillMaxWidth(),
onClick = {
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = chooserTitle,
text = state.setupState.recoveryKey()!!,
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
},
)
Button(
text = stringResource(id = CommonStrings.action_done),
modifier = Modifier.fillMaxWidth(),
onClick = {
if (state.setupState is SetupState.CreatedAndSaved) {
onDone()
} else {
state.eventSink.invoke(SecureBackupSetupEvents.Done)
}
},
)
}
}
}
@PreviewsDayNight
@Composable
internal fun SecureBackupSetupViewPreview(

117
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.atomic.pages
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.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.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
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.theme.ElementTheme
/**
* A Page with:
* - a top bar as TobAppBar with optional back button (displayed if [onBackClicked] is not null)
* - a header, as IconTitleSubtitleMolecule
* - a content.
* - a footer, as ButtonColumnMolecule
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FlowStepPage(
iconResourceId: Int?,
title: String,
modifier: Modifier = Modifier,
onBackClicked: (() -> Unit)? = null,
subTitle: String? = null,
content: @Composable () -> Unit = {},
buttons: @Composable ColumnScope.() -> Unit = {},
) {
BackHandler(enabled = onBackClicked != null) {
onBackClicked?.invoke()
}
HeaderFooterPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
if (onBackClicked != null) {
BackButton(onClick = onBackClicked)
}
},
title = {},
)
},
header = {
IconTitleSubtitleMolecule(
iconResourceId = iconResourceId,
title = title,
subTitle = subTitle,
)
},
content = content,
footer = {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
buttons()
}
}
)
}
@PreviewsDayNight
@Composable
internal fun FlowStepPagePreview() = ElementPreview {
FlowStepPage(
onBackClicked = {},
title = "Title",
subTitle = "Subtitle",
iconResourceId = R.drawable.ic_compound_computer,
content = {
Box(
Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Content",
style = ElementTheme.typography.fontHeadingXlBold
)
}
},
buttons = {
TextButton(text = "A button", onClick = { })
Button(text = "Continue", onClick = { })
}
)
}

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Day_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Night_1_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save