diff --git a/app/src/main/kotlin/cz/muni/fi/rpg/ui/WfrpMasterApp.kt b/app/src/main/kotlin/cz/muni/fi/rpg/ui/WfrpMasterApp.kt index 447cdba07..5bbb9fbb9 100644 --- a/app/src/main/kotlin/cz/muni/fi/rpg/ui/WfrpMasterApp.kt +++ b/app/src/main/kotlin/cz/muni/fi/rpg/ui/WfrpMasterApp.kt @@ -19,6 +19,7 @@ import cz.frantisekmasa.wfrp_master.common.invitation.InvitationLinkScreen import cz.frantisekmasa.wfrp_master.common.partyList.PartyListScreen import cz.frantisekmasa.wfrp_master.common.shell.DrawerShell import cz.frantisekmasa.wfrp_master.common.shell.NetworkStatusBanner +import cz.frantisekmasa.wfrp_master.common.shell.SnackbarScaffold import cz.muni.fi.rpg.ui.shell.ProvideDIContainer import cz.muni.fi.rpg.ui.shell.Startup import kotlinx.coroutines.launch @@ -47,10 +48,12 @@ fun WfrpMasterApp() { true } ) { navigator -> - DrawerShell(drawerState) { - SlideTransition(navigator) { - ProvideNavigationTransaction(it) { - it.Content() + SnackbarScaffold { + DrawerShell(drawerState) { + SlideTransition(navigator) { + ProvideNavigationTransaction(it) { + it.Content() + } } } } diff --git a/common/src/androidMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt b/common/src/androidMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt index f52698566..33ac2ea22 100644 --- a/common/src/androidMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt +++ b/common/src/androidMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt @@ -15,8 +15,16 @@ private val Context.settingsDataStore by preferencesDataStore("settings") actual class SettingsStorage(context: Context,) { private val storage = context.settingsDataStore - actual suspend fun edit(key: SettingsKey, update: (T?) -> T) { - storage.edit { it[key] = update(it[key]) } + actual suspend fun edit(key: SettingsKey, update: (T?) -> T?) { + storage.edit { + val newValue = update(it[key]) + + if (newValue == null) { + it.remove(key) + } else { + it[key] = newValue + } + } } actual fun watch(key: SettingsKey): Flow { diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt index c13d7b9f8..d82113271 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt @@ -6,12 +6,12 @@ import kotlinx.coroutines.flow.Flow import kotlin.jvm.JvmName expect class SettingsStorage { - suspend fun edit(key: SettingsKey, update: (T?) -> T) + suspend fun edit(key: SettingsKey, update: (T?) -> T?) fun watch(key: SettingsKey): Flow } -suspend fun SettingsStorage.edit(key: SettingsKey, value: T) { +suspend fun SettingsStorage.edit(key: SettingsKey, value: T?) { edit(key) { value } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/DrawerShell.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/DrawerShell.kt index b3aefbd51..1ece377ba 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/DrawerShell.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/DrawerShell.kt @@ -1,13 +1,9 @@ package cz.frantisekmasa.wfrp_master.common.shell import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material.DrawerState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalDrawer -import androidx.compose.material.Scaffold -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect @@ -18,8 +14,6 @@ import cz.frantisekmasa.wfrp_master.common.core.shared.SettingsStorage import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.LocalHamburgerButtonHandler import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.FullScreenProgress -import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder -import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.PersistentSnackbarHolder import cz.frantisekmasa.wfrp_master.common.settings.AppSettings import cz.frantisekmasa.wfrp_master.common.settings.Language import dev.icerock.moko.resources.desc.StringDesc @@ -30,7 +24,7 @@ import org.kodein.di.instance @Composable @ExperimentalMaterialApi -fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable (PaddingValues) -> Unit) { +fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable () -> Unit) { val settings: SettingsStorage by localDI().instance() val language = remember { settings.watch(AppSettings.LANGUAGE) @@ -56,21 +50,10 @@ fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable (PaddingValue }, content = { val coroutineScope = rememberCoroutineScope() - val snackbarHostState = remember { SnackbarHostState() } - val persistentSnackbarHolder = - remember(coroutineScope, snackbarHostState) { - PersistentSnackbarHolder(coroutineScope, snackbarHostState) - } CompositionLocalProvider( LocalHamburgerButtonHandler provides { coroutineScope.launch { drawerState.open() } }, - LocalPersistentSnackbarHolder provides persistentSnackbarHolder, - content = { - Scaffold( - scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState), - content = bodyContent, - ) - }, + content = bodyContent, ) }, ) diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/SnackbarScaffold.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/SnackbarScaffold.kt new file mode 100644 index 000000000..8eaa1efe3 --- /dev/null +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/SnackbarScaffold.kt @@ -0,0 +1,31 @@ +package cz.frantisekmasa.wfrp_master.common.shell + +import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder +import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.PersistentSnackbarHolder + +@Composable +fun SnackbarScaffold(content: @Composable () -> Unit) { + val coroutineScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val persistentSnackbarHolder = + remember(coroutineScope, snackbarHostState) { + PersistentSnackbarHolder(coroutineScope, snackbarHostState) + } + + Scaffold( + scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState), + content = { + CompositionLocalProvider( + LocalPersistentSnackbarHolder provides persistentSnackbarHolder, + content = content, + ) + }, + ) +} diff --git a/common/src/commonMain/resources/MR/base/strings.xml b/common/src/commonMain/resources/MR/base/strings.xml index a47cc81cd..a9bbcce8f 100644 --- a/common/src/commonMain/resources/MR/base/strings.xml +++ b/common/src/commonMain/resources/MR/base/strings.xml @@ -29,6 +29,9 @@ Plate Soft Leather Sign in + Log out + Reset password + Send Email Password Existing account found @@ -38,6 +41,7 @@ Invalid password You will lose access to these parties: You are not signed-in.\nSigning-in lets you keep access to parties between devices. + Email with instructions for password reset was sent You are signed-in as Unknown error occurred We could not sign you in via Google.\nYour data will be tied to this device, but you can always sign in later in Settings. diff --git a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/FirebaseTokenHolder.kt b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/FirebaseTokenHolder.kt index 55b92f70f..47725812e 100644 --- a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/FirebaseTokenHolder.kt +++ b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/FirebaseTokenHolder.kt @@ -3,7 +3,7 @@ package cz.frantisekmasa.wfrp_master.common class FirebaseTokenHolder { private var token: String? = null - fun setToken(token: String) { + fun setToken(token: String?) { this.token = token } diff --git a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/auth/AuthenticationManager.kt b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/auth/AuthenticationManager.kt index c3bdcae22..aff356fdc 100644 --- a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/auth/AuthenticationManager.kt +++ b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/auth/AuthenticationManager.kt @@ -127,6 +127,12 @@ class AuthenticationManager( return body } + suspend fun logout() { + tokenHolder.setToken(null) + settings.edit(REFRESH_TOKEN, null) + statusFlow.emit(AuthenticationStatus.NotAuthenticated) + } + @Serializable sealed class SignInResponse { @Serializable @@ -155,6 +161,50 @@ class AuthenticationManager( val returnSecureToken: Boolean = true, ) + @Serializable + data class Failure(val error: Error) + + /** + * https://firebase.google.com/docs/reference/rest/auth#section-send-password-reset-email + */ + suspend fun resetPassword(email: String): PasswordResetResult { + val response = http.post( + "https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=$API_KEY" + ) { + contentType(ContentType.Application.Json) + setBody( + PasswordResetRequest( + requestType = "PASSWORD_RESET", + email = email, + ) + ) + } + + if (response.status != HttpStatusCode.OK) { + val body = response.body() + + if (body.error.message == "EMAIL_NOT_FOUND") { + return PasswordResetResult.EmailNotFound + } + + return PasswordResetResult.UnknownError + } + + return PasswordResetResult.Success + } + + @Serializable + private data class PasswordResetRequest( + val requestType: String, + val email: String, + ) + + sealed interface PasswordResetResult { + object Success : PasswordResetResult + object EmailNotFound : PasswordResetResult + object UnknownError : PasswordResetResult + } + companion object { private val REFRESH_TOKEN = stringKey("firebase_refresh_token") private const val API_KEY = "AIzaSyDO4Y4wWcY4HdYcsp8zcLMpMjwUJ_9q3Fw" diff --git a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt index e26be4fb3..c83ea3b0a 100644 --- a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt +++ b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt @@ -24,8 +24,15 @@ actual class SettingsStorage() { awaitClose { preferences.removePreferenceChangeListener(listener) } } - actual suspend fun edit(key: SettingsKey, update: (T?) -> T) { - key.set(preferences, update(key.get(preferences))) + actual suspend fun edit(key: SettingsKey, update: (T?) -> T?) { + val newValue = update(key.get(preferences)) + + if (newValue == null) { + preferences.remove(key.name) + } else { + key.set(preferences, newValue) + } + preferences.sync() } @@ -41,16 +48,19 @@ actual class SettingsStorage() { } actual class SettingsKey( + val name: String, val set: (Preferences, T) -> Unit, val get: (Preferences) -> T? ) actual fun booleanSettingsKey(name: String): SettingsKey = SettingsKey( + name = name, get = { if (name in it.keys()) it.getBoolean(name, false) else null }, set = { preferences, value -> preferences.putBoolean(name, value) }, ) actual fun stringSetKey(name: String): SettingsKey> = SettingsKey( + name = name, get = { preferences -> if (name in preferences.keys()) preferences.get(name, "") @@ -68,6 +78,7 @@ actual fun stringSetKey(name: String): SettingsKey> = SettingsKey( ) actual fun stringKey(name: String): SettingsKey = SettingsKey( + name = name, get = { if (name in it.keys()) it.get(name, "") else null }, set = { preferences, value -> preferences.put(name, value) }, ) diff --git a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/settings/SignInCard.kt b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/settings/SignInCard.kt index 7e930fa95..5e71fd66a 100644 --- a/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/settings/SignInCard.kt +++ b/common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/settings/SignInCard.kt @@ -1,8 +1,58 @@ package cz.frantisekmasa.wfrp_master.common.settings +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cz.frantisekmasa.wfrp_master.common.Str +import cz.frantisekmasa.wfrp_master.common.auth.AuthenticationManager +import cz.frantisekmasa.wfrp_master.common.core.auth.LocalUser +import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.CardButton +import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardContainer +import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardTitle +import cz.frantisekmasa.wfrp_master.common.core.ui.text.SingleLineTextValue +import cz.frantisekmasa.wfrp_master.common.core.utils.launchLogged +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.Dispatchers +import org.kodein.di.compose.localDI +import org.kodein.di.instance @Composable actual fun SignInCard(settingsScreenModel: SettingsScreenModel) { - // Add support for Google auth + CardContainer( + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CardTitle(stringResource(Str.settings_title_account)) + + val email = LocalUser.current.email + + if (email != null) { + SingleLineTextValue(stringResource(Str.authentication_label_email), email) + } + + val auth: AuthenticationManager by localDI().instance() + val coroutineScope = rememberCoroutineScope() + + CardButton( + text = stringResource(Str.authentication_button_log_out), + onClick = { + coroutineScope.launchLogged(Dispatchers.IO) { + auth.logout() + } + } + ) + } + } } diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 962adb352..f3e54ed2e 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -21,6 +21,9 @@ kotlin { sourceSets { named("jvmMain") { + languageSettings.apply { + optIn("androidx.compose.material.ExperimentalMaterialApi") + } dependencies { implementation(compose.desktop.currentOs) implementation(project(":common:firebase")) diff --git a/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/WfrpMasterApplication.kt b/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/WfrpMasterApplication.kt index 8c582b926..3a2d89b8e 100644 --- a/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/WfrpMasterApplication.kt +++ b/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/WfrpMasterApplication.kt @@ -21,6 +21,7 @@ import cz.frantisekmasa.wfrp_master.common.core.ui.responsive.ScreenWithBreakpoi import cz.frantisekmasa.wfrp_master.common.core.ui.theme.Theme import cz.frantisekmasa.wfrp_master.common.partyList.PartyListScreen import cz.frantisekmasa.wfrp_master.common.shell.DrawerShell +import cz.frantisekmasa.wfrp_master.common.shell.SnackbarScaffold import cz.frantisekmasa.wfrp_master.desktop.interop.DesktopEmailInitiator import cz.frantisekmasa.wfrp_master.desktop.interop.DesktopUrlOpener import cz.frantisekmasa.wfrp_master.desktop.interop.NativeFileChooser @@ -49,27 +50,29 @@ object WfrpMasterApplication { ) { Window(onCloseRequest = ::exitApplication) { Theme { - Startup { - ScreenWithBreakpoints { - val drawerState = rememberDrawerState(DrawerValue.Closed) + SnackbarScaffold { + Startup { + ScreenWithBreakpoints { + val drawerState = rememberDrawerState(DrawerValue.Closed) - Navigator( - screens = listOf(PartyListScreen), - onBackPressed = { - if (drawerState.isOpen) { - coroutineScope.launch { drawerState.close() } - return@Navigator false - } + Navigator( + screens = listOf(PartyListScreen), + onBackPressed = { + if (drawerState.isOpen) { + coroutineScope.launch { drawerState.close() } + return@Navigator false + } - true - } - ) { navigator -> - DrawerShell(drawerState) { - val screen = navigator.lastItem + true + } + ) { navigator -> + DrawerShell(drawerState) { + val screen = navigator.lastItem - navigator.saveableState("currentScreen") { - ProvideNavigationTransaction(screen) { - screen.Content() + navigator.saveableState("currentScreen") { + ProvideNavigationTransaction(screen) { + screen.Content() + } } } } diff --git a/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/AuthenticationScreen.kt b/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/AuthenticationScreen.kt index df43e1997..9ef1f0469 100644 --- a/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/AuthenticationScreen.kt +++ b/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/AuthenticationScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -87,6 +88,7 @@ fun AuthenticationScreen() { val errorUnknown = stringResource(Str.authentication_messages_unknown_error) Button( + modifier = Modifier.fillMaxWidth(), onClick = { validate = true @@ -114,6 +116,22 @@ fun AuthenticationScreen() { ) { Text(stringResource(Str.authentication_button_sign_in)) } + + var resetPasswordDialogVisible by remember { mutableStateOf(false) } + + if (resetPasswordDialogVisible) { + ResetPasswordDialog( + auth = auth, + onDismissRequest = { resetPasswordDialogVisible = false }, + ) + } + + TextButton( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { resetPasswordDialogVisible = true }, + ) { + Text(stringResource(Str.authentication_button_reset_password)) + } } } } diff --git a/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/ResetPasswordDialog.kt b/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/ResetPasswordDialog.kt new file mode 100644 index 000000000..b8c20ae4b --- /dev/null +++ b/desktop/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/desktop/auth/ResetPasswordDialog.kt @@ -0,0 +1,110 @@ +package cz.frantisekmasa.wfrp_master.desktop.auth + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.AlertDialog +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cz.frantisekmasa.wfrp_master.common.Str +import cz.frantisekmasa.wfrp_master.common.auth.AuthenticationManager +import cz.frantisekmasa.wfrp_master.common.auth.AuthenticationManager.PasswordResetResult +import cz.frantisekmasa.wfrp_master.common.core.ui.forms.CallbackRule +import cz.frantisekmasa.wfrp_master.common.core.ui.forms.TextInput +import cz.frantisekmasa.wfrp_master.common.core.ui.forms.inputValue +import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder +import cz.frantisekmasa.wfrp_master.common.core.utils.launchLogged +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.Dispatchers + +@Composable +fun ResetPasswordDialog( + auth: AuthenticationManager, + onDismissRequest: () -> Unit, +) { + val email = inputValue( + "", + CallbackRule( + stringResource(Str.authentication_messages_invalid_email) + ) { EMAIL_REGEX.matches(it) } + ) + + var validate by remember { mutableStateOf(false) } + var processing by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(stringResource(Str.authentication_button_reset_password)) }, + text = { + Column(Modifier.width(400.dp)) { + if (processing) { + LinearProgressIndicator(Modifier.fillMaxWidth()) + } else { + TextInput( + label = stringResource(Str.authentication_label_email), + value = email, + validate = validate, + modifier = Modifier.fillMaxWidth(), + ) + } + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(Str.common_ui_button_dismiss).uppercase()) + } + }, + confirmButton = { + val coroutineScope = rememberCoroutineScope() + val snackbarHolder = LocalPersistentSnackbarHolder.current + val errorEmailNotFound = stringResource(Str.authentication_messages_email_not_found) + val errorUnknown = stringResource(Str.messages_error_unknown) + val messageEmailSent = stringResource(Str.authentication_messages_reset_password_email_sent) + + TextButton( + enabled = !processing, + onClick = { + validate = true + if (email.isValid()) { + processing = true + coroutineScope.launchLogged(Dispatchers.IO) { + try { + when (auth.resetPassword(email.normalizedValue)) { + PasswordResetResult.Success -> { + snackbarHolder.showSnackbar( + messageEmailSent, + SnackbarDuration.Long, + ) + onDismissRequest() + } + PasswordResetResult.EmailNotFound -> { + snackbarHolder.showSnackbar(errorEmailNotFound) + } + PasswordResetResult.UnknownError -> { + snackbarHolder.showSnackbar(errorUnknown) + } + } + } finally { + processing = false + } + } + } + } + ) { + Text(stringResource(Str.authentication_button_send).uppercase()) + } + } + ) +} + +private val EMAIL_REGEX = Regex("^[a-zA-Z0-9_!#\$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+\$")