diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1b6bac1db2..c71197a832 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -7,6 +7,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc4bc2dc55..aab3bc8f20 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -267,6 +267,8 @@ dependencies { implementation(project(":design-system")) implementation(project(":presentation-compose")) + implementation(libs.proton.core.pass.validator) + debugImplementation(libs.bundles.app.debug) // Environment configuration diff --git a/app/src/main/kotlin/ch/protonmail/android/navigation/Launcher.kt b/app/src/main/kotlin/ch/protonmail/android/navigation/Launcher.kt index d1b1601a01..5a7db7e226 100644 --- a/app/src/main/kotlin/ch/protonmail/android/navigation/Launcher.kt +++ b/app/src/main/kotlin/ch/protonmail/android/navigation/Launcher.kt @@ -45,6 +45,7 @@ fun Launcher(activityActions: MainActivity.Actions, viewModel: LauncherViewModel onRecoveryEmail = { viewModel.submit(LauncherViewModel.Action.OpenRecoveryEmail) }, onReportBug = { viewModel.submit(LauncherViewModel.Action.OpenReport) }, onSignIn = { viewModel.submit(LauncherViewModel.Action.SignIn(it)) }, + onSignUp = { viewModel.submit(LauncherViewModel.Action.SignUp) }, onSubscription = { viewModel.submit(LauncherViewModel.Action.OpenSubscription) }, onSwitchToAccount = { viewModel.submit(LauncherViewModel.Action.SwitchToAccount(it)) }, onRequestNotificationPermission = { @@ -69,6 +70,7 @@ object Launcher { data class Actions( val onSignIn: (UserId?) -> Unit, + val onSignUp: () -> Unit, val onSubscription: () -> Unit, val onReportBug: () -> Unit, val onPasswordManagement: () -> Unit, diff --git a/app/src/main/kotlin/ch/protonmail/android/navigation/LauncherViewModel.kt b/app/src/main/kotlin/ch/protonmail/android/navigation/LauncherViewModel.kt index 58625ee54d..397b56a01b 100644 --- a/app/src/main/kotlin/ch/protonmail/android/navigation/LauncherViewModel.kt +++ b/app/src/main/kotlin/ch/protonmail/android/navigation/LauncherViewModel.kt @@ -49,6 +49,7 @@ import me.proton.android.core.auth.presentation.AuthOrchestrator import me.proton.android.core.auth.presentation.login.LoginInput import me.proton.android.core.auth.presentation.onAddAccountResult import me.proton.android.core.auth.presentation.onLoginResult +import me.proton.android.core.auth.presentation.onSignUpResult import me.proton.android.core.payment.presentation.PaymentOrchestrator import me.proton.android.core.payment.presentation.onUpgradeResult import me.proton.core.domain.entity.UserId @@ -153,7 +154,8 @@ class LauncherViewModel @Inject constructor( with(authOrchestrator) { register(context) onAddAccountResult { result -> if (!result) context.finish() } - onLoginResult { result -> if (result != null) onSwitchToAccount(result.userId.toUserId()) } + onLoginResult { result -> if (result != null) { onSwitchToAccount(result.userId.toUserId()) } } + onSignUpResult { result -> if (result != null) { onSwitchToAccount(result.userId.toUserId()) } } userSessionRepository.observe(context.lifecycle, minActiveState = Lifecycle.State.RESUMED) .onAccountTwoFactorNeeded { startSecondFactorWorkflow(it.userId.toLocalUserId()) } .onAccountTwoPasswordNeeded { startTwoPassModeWorkflow(it.userId.toLocalUserId()) } @@ -177,6 +179,7 @@ class LauncherViewModel @Inject constructor( is Action.OpenSubscription -> onOpenSubscription() is Action.RequestNotificationPermission -> onRequestNotificationPermission() is Action.SignIn -> onSignIn(action.userId) + is Action.SignUp -> onSignUp() is Action.SwitchToAccount -> onSwitchToAccount(action.userId) } } @@ -213,6 +216,10 @@ class LauncherViewModel @Inject constructor( authOrchestrator.startLoginWorkflow(LoginInput(username = address)) } + private fun onSignUp() = viewModelScope.launch { + authOrchestrator.startSignUpWorkflow() + } + private fun onSwitchToAccount(userId: UserId) = viewModelScope.launch { setPrimaryAccount(userId) } @@ -231,6 +238,7 @@ class LauncherViewModel @Inject constructor( data object OpenSubscription : Action data object RequestNotificationPermission : Action data class SignIn(val userId: UserId?) : Action + data object SignUp : Action data class SwitchToAccount(val userId: UserId) : Action } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitest/e2e/mailbox/MessageLoadingTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitest/e2e/mailbox/MessageLoadTests.kt similarity index 99% rename from app/src/uiTest/kotlin/ch/protonmail/android/uitest/e2e/mailbox/MessageLoadingTests.kt rename to app/src/uiTest/kotlin/ch/protonmail/android/uitest/e2e/mailbox/MessageLoadTests.kt index 3488dfa271..05b5062e11 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitest/e2e/mailbox/MessageLoadingTests.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitest/e2e/mailbox/MessageLoadTests.kt @@ -48,7 +48,7 @@ import org.junit.Test @SmokeTest @HiltAndroidTest @UninstallModules(ServerProofModule::class) -internal class MessageLoadingTests : MockedNetworkTest() { +internal class MessageLoadTests : MockedNetworkTest() { @JvmField @BindValue diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1bf4a0d54e..a1e65bd9d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ lottie = "6.1.0" material = "1.12.0" mockk = "1.13.17" paparazzi = "1.3.4" -proton-core = "32.0.1" +proton-core = "32.1.0" proton-rust-core = "0.81.0" kotlinpoet-ksp = "2.1.0" leakcanary = "2.14" @@ -227,6 +227,7 @@ proton-core-network-data = { module = "me.proton.core:network-data", version.ref proton-core-network-domain = { module = "me.proton.core:network-domain", version.ref = "proton-core" } proton-core-notification-dagger = { module = "me.proton.core:notification-dagger", version.ref = "proton-core" } proton-core-observability-dagger = { module = "me.proton.core:observability-dagger", version.ref = "proton-core" } +proton-core-pass-validator = { module = "me.proton.core:pass-validator", version.ref = "proton-core" } proton-core-payment-dagger = { module = "me.proton.core:payment-dagger", version.ref = "proton-core" } proton-core-paymentIap-dagger = { module = "me.proton.core:payment-iap-dagger", version.ref = "proton-core" } proton-core-plan-dagger = { module = "me.proton.core:plan-dagger", version.ref = "proton-core" } diff --git a/shared/core/auth/presentation/build.gradle.kts b/shared/core/auth/presentation/build.gradle.kts index 68b089dc19..ca7cf84b5f 100644 --- a/shared/core/auth/presentation/build.gradle.kts +++ b/shared/core/auth/presentation/build.gradle.kts @@ -62,6 +62,8 @@ dependencies { implementation(libs.proton.core.challenge.data) implementation(libs.proton.core.challenge.domain) implementation(libs.proton.core.challenge.presentation) + implementation(libs.proton.core.account.domain) +// implementation(libs.proton.core.challenge) implementation(libs.proton.core.domain) implementation(libs.proton.core.presentation) implementation(libs.proton.core.presentationCompose) diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ActivityResultContracts.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ActivityResultContracts.kt index e287538049..fb1c89f9d3 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ActivityResultContracts.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ActivityResultContracts.kt @@ -30,6 +30,7 @@ import me.proton.android.core.auth.presentation.login.LoginOutput import me.proton.android.core.auth.presentation.secondfactor.SecondFactorActivity import me.proton.android.core.auth.presentation.secondfactor.SecondFactorArg import me.proton.android.core.auth.presentation.signup.SignUpActivity +import me.proton.android.core.auth.presentation.signup.SignupOutput import me.proton.android.core.auth.presentation.twopass.TwoPassActivity import me.proton.android.core.auth.presentation.twopass.TwoPassArg @@ -100,14 +101,17 @@ object StartTwoPassMode : ActivityResultContract() { } } -object StartSignUp : ActivityResultContract() { +object StartSignUp : ActivityResultContract() { override fun createIntent(context: Context, input: Unit) = Intent(context, SignUpActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } - override fun parseResult(resultCode: Int, intent: Intent?): Boolean = when (resultCode) { - Activity.RESULT_OK -> true - else -> false + override fun parseResult(resultCode: Int, intent: Intent?): SignupOutput? = when (resultCode) { + Activity.RESULT_OK -> intent?.let { + IntentCompat.getParcelableExtra(it, SignUpActivity.ARG_OUTPUT, SignupOutput::class.java) + } + + else -> null } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/AuthOrchestrator.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/AuthOrchestrator.kt index c1f35bf588..1752d388f2 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/AuthOrchestrator.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/AuthOrchestrator.kt @@ -22,6 +22,7 @@ import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher import me.proton.android.core.auth.presentation.login.LoginInput import me.proton.android.core.auth.presentation.login.LoginOutput +import me.proton.android.core.auth.presentation.signup.SignupOutput import javax.inject.Inject class AuthOrchestrator @Inject constructor() { @@ -38,7 +39,7 @@ class AuthOrchestrator @Inject constructor() { private var onLoginHelpResultListener: ((result: Boolean) -> Unit)? = {} private var onSecondFactorResultListener: ((result: Boolean) -> Unit)? = {} private var onTwoPassModeResultListener: ((result: Boolean) -> Unit)? = {} - private var onSignUpResultListener: ((result: Boolean) -> Unit)? = {} + private var onSignUpResultListener: ((result: SignupOutput?) -> Unit)? = {} private fun registerAddAccountResult(caller: ActivityResultCaller): ActivityResultLauncher = caller.registerForActivityResult(StartAddAccount) { @@ -93,7 +94,7 @@ class AuthOrchestrator @Inject constructor() { onTwoPassModeResultListener = block } - fun setOnSignUpResult(block: (result: Boolean) -> Unit) { + fun setOnSignUpResult(block: (result: SignupOutput?) -> Unit) { onSignUpResultListener = block } @@ -205,7 +206,7 @@ fun AuthOrchestrator.onTwoPassModeResult(block: (result: Boolean) -> Unit): Auth return this } -fun AuthOrchestrator.onSignUpResult(block: (result: Boolean) -> Unit): AuthOrchestrator { +fun AuthOrchestrator.onSignUpResult(block: (result: SignupOutput?) -> Unit): AuthOrchestrator { setOnSignUpResult { block(it) } return this } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/LogTag.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/LogTag.kt index 1ed9931a09..bffeeee6a0 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/LogTag.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/LogTag.kt @@ -20,5 +20,6 @@ package me.proton.android.core.auth.presentation object LogTag { const val DEFAULT = "core.auth.presentation.default" const val LOGIN = "core.auth.presentation.login" + const val SIGNUP = "core.auth.presentation.signup" const val SESSION = "core.auth.presentation.session" } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordState.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ProtonSecureActivity.kt similarity index 60% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordState.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ProtonSecureActivity.kt index 5cb9d1b699..9836a72ddb 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordState.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/ProtonSecureActivity.kt @@ -16,18 +16,15 @@ * along with Proton Mail. If not, see . */ -package me.proton.android.core.auth.presentation.signup +package me.proton.android.core.auth.presentation -sealed interface CreatePasswordState { - data object Idle : CreatePasswordState - data object Loading : CreatePasswordState - data object Validating : CreatePasswordState - data class FormError(val message: String?) : CreatePasswordState - data class Success(val password: String) : CreatePasswordState +import me.proton.core.presentation.ui.ProtonActivity +import me.proton.core.presentation.utils.ProtectScreenConfiguration +import me.proton.core.presentation.utils.protectScreen + +abstract class ProtonSecureActivity : ProtonActivity() { + + private val configuration = ProtectScreenConfiguration(true) + private val screenProtector by protectScreen(configuration) - val isLoading: Boolean - get() = when (this) { - is Validating -> true - else -> false - } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/addaccount/AddAccountActivity.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/addaccount/AddAccountActivity.kt index b2c49d6a97..16d3d52dbd 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/addaccount/AddAccountActivity.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/addaccount/AddAccountActivity.kt @@ -23,10 +23,10 @@ import androidx.activity.compose.setContent import dagger.hilt.android.AndroidEntryPoint import me.proton.android.core.auth.presentation.AuthOrchestrator import me.proton.android.core.auth.presentation.onLoginResult +import me.proton.android.core.auth.presentation.onSignUpResult import me.proton.core.compose.theme.ProtonTheme import me.proton.core.presentation.ui.ProtonActivity import me.proton.core.presentation.utils.addOnBackPressedCallback -import me.proton.core.presentation.utils.showToast import javax.inject.Inject @AndroidEntryPoint @@ -46,19 +46,14 @@ class AddAccountActivity : ProtonActivity() { addOnBackPressedCallback { onClose() } authOrchestrator.register(this) - authOrchestrator.onLoginResult { result -> - if (result != null) onSuccess() - } + authOrchestrator.onLoginResult { result -> if (result != null) onSuccess() } + authOrchestrator.onSignUpResult { result -> if (result != null) onSuccess() } setContent { ProtonTheme { AddAccountScreenMail( onSignInClicked = { authOrchestrator.startLoginWorkflow() }, - onSignUpClicked = { - showToast("Coming soon") - // Enable when ready: - // authOrchestrator.startSignUpWorkflow() - } + onSignUpClicked = { authOrchestrator.startSignUpWorkflow() } ) } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/login/LoginActivity.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/login/LoginActivity.kt index 02cf013c26..180112e7c9 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/login/LoginActivity.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/login/LoginActivity.kt @@ -24,24 +24,19 @@ import androidx.activity.compose.setContent import androidx.core.content.IntentCompat import dagger.hilt.android.AndroidEntryPoint import me.proton.android.core.auth.presentation.AuthOrchestrator +import me.proton.android.core.auth.presentation.ProtonSecureActivity import me.proton.android.core.auth.presentation.R import me.proton.core.compose.theme.ProtonTheme -import me.proton.core.presentation.ui.ProtonActivity -import me.proton.core.presentation.utils.ProtectScreenConfiguration import me.proton.core.presentation.utils.addOnBackPressedCallback import me.proton.core.presentation.utils.errorToast -import me.proton.core.presentation.utils.protectScreen import javax.inject.Inject @AndroidEntryPoint -class LoginActivity : ProtonActivity() { +class LoginActivity : ProtonSecureActivity() { @Inject lateinit var authOrchestrator: AuthOrchestrator - private val configuration = ProtectScreenConfiguration(true) - private val screenProtector by protectScreen(configuration) - private val input: LoginInput get() = requireNotNull(IntentCompat.getParcelableExtra(intent, ARG_INPUT, LoginInput::class.java)) diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CountryCodeDropDown.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CountryCodeDropDown.kt deleted file mode 100644 index 55616fa51f..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CountryCodeDropDown.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ -@file:OptIn(ExperimentalMaterialApi::class) - -package me.proton.android.core.auth.presentation.signup - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material.Card -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ExposedDropdownMenuBox -import androidx.compose.material.ListItem -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT -import me.proton.core.compose.theme.ProtonTheme -import me.proton.core.compose.theme.ProtonTypography -import me.proton.core.compose.theme.defaultNorm - -data class Country( - val countryCode: String, - val callingCode: Int? = null, - val name: String, - val flagId: Int = 0 -) - -@Composable -fun CountryCodeDropDown( - modifier: Modifier = Modifier, - isLoading: Boolean = false, - data: List = emptyList(), - onInputChanged: (Country) -> Unit = {} -) { - var expanded by remember { mutableStateOf(false) } - var selected by remember { mutableStateOf(data.firstOrNull()) } - - if (selected == null) { - return - } - - ExposedDropdownMenuBox( - expanded = expanded && !isLoading, - onExpandedChange = {} - ) { - Card( - modifier = modifier.clickable { expanded = !expanded }, - contentColor = ProtonTheme.colors.textNorm, - elevation = 0.dp - ) { - CountryListItem( - country = selected, - trailing = { - if (data.size > 1) { - TrailingIcon(expanded = expanded) - } - } - ) - } - - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - data.forEach { country -> - DropdownMenuItem( - onClick = { - expanded = false - selected = country - onInputChanged(country) - }, - contentPadding = PaddingValues(0.dp, 0.dp) - ) { - CountryListItem(country = country) - } - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun CountryListItem( - country: Country?, - modifier: Modifier = Modifier, - trailing: @Composable (() -> Unit)? = null -) { - ListItem( - modifier = modifier, - text = { - Text( - text = "+${country?.callingCode}", - style = ProtonTypography.Default.defaultNorm - ) - }, - trailing = { trailing?.invoke() } - ) -} - -@Preview(name = "Light mode", showBackground = true) -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) -@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) -@Preview(name = "Tablet", device = Devices.PIXEL_C) -@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) -@Composable -internal fun CountryDropDownPreview() { - ProtonTheme { - CountryCodeDropDown( - data = listOf( - Country( - countryCode = "CH", - callingCode = 1, - name = "Switzerland" - ) - ) - ) - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordViewModel.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordViewModel.kt deleted file mode 100644 index ce70b23360..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordViewModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import me.proton.core.compose.viewmodel.stopTimeoutMillis -import javax.inject.Inject - -@HiltViewModel -class CreatePasswordViewModel @Inject constructor() : ViewModel() { - - private val mutableAction = MutableStateFlow(null) - - val state: StateFlow = mutableAction.flatMapLatest { action -> - when (action) { - null -> flowOf(CreatePasswordState.Idle) - is CreatePasswordAction.SetNavigationDone -> onSetNavigationDone() - is CreatePasswordAction.Submit -> onCreatePassword(action.confirmPassword) - } - }.stateIn(viewModelScope, WhileSubscribed(stopTimeoutMillis), CreatePasswordState.Idle) - - fun submit(action: CreatePasswordAction) = viewModelScope.launch { - mutableAction.emit(action) - } - - private fun onCreatePassword(password: String): Flow = flow { - emit(CreatePasswordState.Loading) - emit(CreatePasswordState.Success(password)) - } - - private fun onSetNavigationDone(): Flow = flow { - emit(CreatePasswordState.Idle) - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryOperation.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryOperation.kt deleted file mode 100644 index eb714b7b0e..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryOperation.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -import me.proton.core.challenge.domain.entity.ChallengeFrameDetails - -sealed interface RecoveryMethodOperation - -sealed interface CreateRecoveryAction : RecoveryMethodOperation { - data class SelectCreateRecovery(val recoveryMethod: RecoveryMethod) : CreateRecoveryAction - data class SubmitEmail(val email: String, val recoveryFrameDetails: ChallengeFrameDetails) : CreateRecoveryAction - data class SubmitPhone( - val callingCode: String, - val phoneNumber: String, - val recoveryFrameDetails: ChallengeFrameDetails - ) : CreateRecoveryAction - - data object SetNavigationDone : CreateRecoveryAction -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryState.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryState.kt deleted file mode 100644 index d48cfc8ce8..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryState.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -sealed class CreateRecoveryState( - open val recoveryMethod: RecoveryMethod, - open val countries: List? = null -) { - - data class Idle( - override val recoveryMethod: RecoveryMethod, - override val countries: List? = null - ) : CreateRecoveryState(recoveryMethod, countries) - - data class Loading( - override val recoveryMethod: RecoveryMethod, - override val countries: List? = null - ) : CreateRecoveryState(recoveryMethod, countries) - - data class Validating( - override val recoveryMethod: RecoveryMethod, - override val countries: List - ) : CreateRecoveryState(recoveryMethod) - - sealed class FormError( - override val recoveryMethod: RecoveryMethod, - open val message: String? - ) : CreateRecoveryState(recoveryMethod) { - - data class Email( - override val recoveryMethod: RecoveryMethod, - override val message: String? - ) : FormError(recoveryMethod, message) - - data class Phone( - override val recoveryMethod: RecoveryMethod, - override val message: String? - ) : FormError(recoveryMethod, message) - } - - data class Success( - override val recoveryMethod: RecoveryMethod, - val value: String - ) : CreateRecoveryState(recoveryMethod) - - val isLoading: Boolean - get() = when (this) { - is Validating -> true - else -> false - } -} - -enum class RecoveryMethod(val value: Int) { - Email(0), Phone(1); - - companion object { - val map = entries.associateBy { it.value } - fun enumOf(value: Int) = map[value] ?: Email - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryViewModel.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryViewModel.kt deleted file mode 100644 index 7f07c0d3c6..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryViewModel.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import me.proton.core.challenge.domain.ChallengeManager -import me.proton.core.challenge.domain.entity.ChallengeFrameDetails -import me.proton.core.compose.viewmodel.stopTimeoutMillis -import javax.inject.Inject - -@HiltViewModel -class CreateRecoveryViewModel @Inject constructor( - private val challengeManager: ChallengeManager -) : ViewModel() { - - private val mutableAction = MutableStateFlow(null) - - val state: StateFlow = mutableAction.flatMapLatest { action -> - when (action) { - null -> flowOf(CreateRecoveryState.Idle(RecoveryMethod.Email)) - is CreateRecoveryAction.SelectCreateRecovery -> onSelectRecoveryMethod(action.recoveryMethod) - is CreateRecoveryAction.SubmitEmail -> onSubmitEmail(action.email, action.recoveryFrameDetails) - is CreateRecoveryAction.SubmitPhone -> onSubmitPhone( - action.callingCode, - action.phoneNumber, - action.recoveryFrameDetails - ) - - is CreateRecoveryAction.SetNavigationDone -> onSetNavigationDone() - } - }.stateIn(viewModelScope, WhileSubscribed(stopTimeoutMillis), CreateRecoveryState.Idle(RecoveryMethod.Email)) - - private fun onSelectRecoveryMethod(recoveryMethod: RecoveryMethod): Flow = flow { - val countries = when (recoveryMethod) { - RecoveryMethod.Phone -> listOf( - Country("CH", 41, "Switzerland"), - Country("US", 1, "US") - ) - - else -> null - } - emit(CreateRecoveryState.Idle(recoveryMethod, countries)) - } - - private fun onSubmitEmail(email: String, recoveryFrameDetails: ChallengeFrameDetails): Flow = - flow { - challengeManager.addOrUpdateFrameToFlow(recoveryFrameDetails) - emit(CreateRecoveryState.Success(RecoveryMethod.Email, email)) - } - - private fun onSubmitPhone( - callingCode: String, - phoneNumber: String, - recoveryFrameDetails: ChallengeFrameDetails - ): Flow = flow { - challengeManager.addOrUpdateFrameToFlow(recoveryFrameDetails) - emit(CreateRecoveryState.Success(RecoveryMethod.Phone, "$callingCode$phoneNumber")) - } - - private fun onSetNavigationDone(): Flow = flow { - emit(state.value) - } - - fun submit(action: CreateRecoveryAction) = viewModelScope.launch { - mutableAction.emit(action) - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameScreen.kt deleted file mode 100644 index de2fb4c6a9..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameScreen.kt +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -@file:Suppress("UseComposableActions") - -package me.proton.android.core.auth.presentation.signup - -import android.content.res.Configuration -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Divider -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import me.proton.android.core.auth.presentation.R -import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT -import me.proton.android.core.auth.presentation.challenge.SIGNUP_CHALLENGE_FLOW_NAME -import me.proton.android.core.auth.presentation.challenge.SIGNUP_CHALLENGE_USERNAME_FRAME -import me.proton.android.core.auth.presentation.challenge.TextChange -import me.proton.core.challenge.presentation.compose.LocalClipManager -import me.proton.core.challenge.presentation.compose.LocalClipManager.OnClipChangedDisposableEffect -import me.proton.core.challenge.presentation.compose.PayloadController -import me.proton.core.challenge.presentation.compose.payload -import me.proton.core.compose.component.ProtonCloseButton -import me.proton.core.compose.component.ProtonOutlinedTextFieldWithError -import me.proton.core.compose.component.ProtonSolidButton -import me.proton.core.compose.component.ProtonTextButton -import me.proton.core.compose.component.appbar.ProtonTopAppBar -import me.proton.core.compose.theme.LocalColors -import me.proton.core.compose.theme.ProtonDimens -import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing -import me.proton.core.compose.theme.ProtonTheme -import me.proton.core.compose.theme.ProtonTypography -import me.proton.core.compose.theme.defaultSmallWeak - -internal const val USERNAME_FIELD_TAG = "USERNAME_FIELD_TAG" -internal const val EMAIL_FIELD_TAG = "EMAIL_FIELD_TAG" -internal const val PHONE_FIELD_TAG = "PHONE_FIELD_TAG" - -@Composable -fun CreateUsernameScreen( - modifier: Modifier = Modifier, - onCloseClicked: () -> Unit = {}, - onErrorMessage: (String?) -> Unit = {}, - onSuccess: (String) -> Unit = {}, - viewModel: CreateUsernameViewModel = hiltViewModel() -) { - val state by viewModel.state.collectAsStateWithLifecycle() - CreateUsernameScreen( - modifier = modifier, - onCloseClicked = onCloseClicked, - onUsernameSubmitted = { viewModel.submit(it) }, - onCreateExternalClicked = { viewModel.submit(it) }, - onCreateInternalClicked = { viewModel.submit(it) }, - onErrorMessage = onErrorMessage, - onSuccess = { - onSuccess(it) - viewModel.submit(CreateUsernameAction.SetNavigationDone) - }, - state = state - ) -} - -@Composable -fun CreateUsernameScreen( - modifier: Modifier = Modifier, - onCloseClicked: () -> Unit = {}, - onUsernameSubmitted: (CreateUsernameAction.Submit) -> Unit = {}, - onCreateExternalClicked: (CreateUsernameAction.CreateExternalAccount) -> Unit = {}, - onCreateInternalClicked: (CreateUsernameAction.CreateInternalAccount) -> Unit = {}, - onErrorMessage: (String?) -> Unit = {}, - onSuccess: (String) -> Unit = {}, - state: CreateUsernameState -) { - LaunchedEffect(state) { - when (state) { - is CreateUsernameState.Error -> onErrorMessage(state.message) - is CreateUsernameState.FormError -> onErrorMessage(state.message) - is CreateUsernameState.Success -> onSuccess(state.username) - else -> Unit - } - } - ChooseUsernameScaffold( - modifier = modifier, - onCloseClicked = onCloseClicked, - onUsernameSubmitted = onUsernameSubmitted, - onCreateExternalClicked = onCreateExternalClicked, - onCreateInternalClicked = onCreateInternalClicked, - state = state - ) -} - -@Composable -fun ChooseUsernameScaffold( - modifier: Modifier = Modifier, - onCloseClicked: () -> Unit = {}, - onUsernameSubmitted: (CreateUsernameAction.Submit) -> Unit = {}, - onCreateExternalClicked: (CreateUsernameAction.CreateExternalAccount) -> Unit, - onCreateInternalClicked: (CreateUsernameAction.CreateInternalAccount) -> Unit, - @DrawableRes protonLogo: Int = R.drawable.ic_logo_proton, - @StringRes titleText: Int = R.string.auth_signup_title, - state: CreateUsernameState -) { - val inputError = state is CreateUsernameState.FormError - val isLoading = state.isLoading - val accountType = state.accountType - val domains = if (state is CreateUsernameState.Idle) state.domains else null - - Scaffold( - modifier = modifier, - topBar = { - ProtonTopAppBar( - title = {}, - navigationIcon = { - ProtonCloseButton(onCloseClicked = onCloseClicked) - }, - backgroundColor = LocalColors.current.backgroundNorm - ) - } - ) { paddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { - Column( - modifier = Modifier - .padding(top = ProtonDimens.SmallSpacing) - .verticalScroll(rememberScrollState()) - ) { - Image( - modifier = Modifier - .height(64.dp) - .align(Alignment.CenterHorizontally), - painter = painterResource(protonLogo), - contentDescription = null, - alignment = Alignment.Center - ) - - Text( - text = stringResource(titleText), - style = ProtonTypography.Default.headline, - textAlign = TextAlign.Center, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = ProtonDimens.MediumSpacing) - ) - - when (accountType) { - AccountType.Internal -> - CreateInternalForm( - modifier = modifier, - enabled = !isLoading, - onUsernameSubmitted = onUsernameSubmitted, - onCreateExternalClicked = onCreateExternalClicked, - usernameError = when { - inputError -> stringResource(R.string.auth_signup_username_assistive_text) - else -> null - }, - isLoading = isLoading, - domains = domains - ) - - AccountType.External -> - CreateExternalForm( - modifier = modifier, - enabled = !isLoading, - onExternalEmailSubmitted = onUsernameSubmitted, - onCreateInternalClicked = onCreateInternalClicked, - emailError = when { - inputError -> stringResource(R.string.auth_signup_email_assistive_text) - else -> null - } - ) - } - } - } - } -} - -@Composable -private fun CreateInternalForm( - modifier: Modifier = Modifier, - onUsernameSubmitted: (CreateUsernameAction.Submit) -> Unit, - onCreateExternalClicked: (CreateUsernameAction.CreateExternalAccount) -> Unit, - @StringRes subtitleText: Int = R.string.auth_signup_subtitle, - usernameError: String? = null, - isLoading: Boolean = false, - enabled: Boolean, - domains: List? -) { - val scope = rememberCoroutineScope() - var username by rememberSaveable { mutableStateOf("") } - var domain by rememberSaveable { mutableStateOf("") } - val usernameChanges = remember { MutableStateFlow(TextChange()) } - val usernameHasFocus = remember { mutableStateOf(false) } - val usernamePayloadController = remember { PayloadController() } - val usernameTextCopies = remember { MutableStateFlow("") } - - LocalClipManager.current?.OnClipChangedDisposableEffect { - if (usernameHasFocus.value) usernameTextCopies.value = it - } - - fun onSubmit() = scope.launch { - val usernameFrameDetails = usernamePayloadController.flush() - onUsernameSubmitted(CreateUsernameAction.Submit(username, AccountType.Internal, usernameFrameDetails)) - } - - Column { - Text( - text = stringResource(subtitleText), - style = ProtonTypography.Default.defaultSmallWeak, - textAlign = TextAlign.Center, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = ProtonDimens.SmallSpacing) - ) - - Column( - modifier = modifier.padding(DefaultSpacing) - ) { - ProtonOutlinedTextFieldWithError( - text = username, - onValueChanged = { - usernameChanges.value = usernameChanges.value.roll(it) - username = it - }, - enabled = enabled, - errorText = usernameError, - label = { Text(text = stringResource(id = R.string.auth_signup_email_username)) }, - singleLine = true, - modifier = Modifier - .onFocusChanged { usernameHasFocus.value = it.hasFocus } - .fillMaxWidth() - .padding(top = DefaultSpacing) - .payload( - flow = SIGNUP_CHALLENGE_FLOW_NAME, - frame = SIGNUP_CHALLENGE_USERNAME_FRAME, - onTextChanged = usernameChanges.map { it.toPair() }, - onTextCopied = usernameTextCopies, - onFrameUpdated = {}, - payloadController = usernamePayloadController - ) - .testTag(USERNAME_FIELD_TAG) - ) - - DomainDropDown( - isLoading = isLoading, - data = domains ?: emptyList(), - onInputChanged = { domain = it } - ) - - ProtonSolidButton( - contained = false, - enabled = enabled, - loading = isLoading, - onClick = ::onSubmit, - modifier = Modifier - .padding(top = ProtonDimens.MediumSpacing) - .height(ProtonDimens.DefaultButtonMinHeight) - ) { - Text(text = stringResource(R.string.auth_signup_next)) - } - - Divider( - modifier = Modifier.padding(top = ProtonDimens.MediumSpacing), - color = LocalColors.current.separatorNorm - ) - - ProtonTextButton( - contained = false, - onClick = { onCreateExternalClicked(CreateUsernameAction.CreateExternalAccount) }, - modifier = Modifier - .padding(vertical = ProtonDimens.MediumSpacing) - .height(ProtonDimens.DefaultButtonMinHeight) - ) { - Text(text = stringResource(R.string.auth_signup_use_current_email)) - } - - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center, - text = stringResource(id = R.string.auth_signup_internal_footnote), - style = ProtonTypography.Default.defaultSmallWeak - ) - } - } -} - - -@Composable -private fun CreateExternalForm( - modifier: Modifier = Modifier, - onExternalEmailSubmitted: (CreateUsernameAction.Submit) -> Unit, - onCreateInternalClicked: (CreateUsernameAction.CreateInternalAccount) -> Unit, - emailError: String? = null, - enabled: Boolean -) { - val scope = rememberCoroutineScope() - var email by rememberSaveable { mutableStateOf("") } - val emailChanges = remember { MutableStateFlow(TextChange()) } - val emailHasFocus = remember { mutableStateOf(false) } - val emailPayloadController = remember { PayloadController() } - val emailTextCopies = remember { MutableStateFlow("") } - - LocalClipManager.current?.OnClipChangedDisposableEffect { - if (emailHasFocus.value) emailTextCopies.value = it - } - - fun onSubmit() = scope.launch { - val emailFrameDetails = emailPayloadController.flush() - onExternalEmailSubmitted(CreateUsernameAction.Submit(email, AccountType.External, emailFrameDetails)) - } - - Column( - modifier = modifier.padding(DefaultSpacing) - ) { - ProtonOutlinedTextFieldWithError( - text = email, - onValueChanged = { - emailChanges.value = emailChanges.value.roll(it) - email = it - }, - enabled = enabled, - errorText = emailError, - label = { Text(text = stringResource(id = R.string.auth_email)) }, - singleLine = true, - modifier = Modifier - .onFocusChanged { emailHasFocus.value = it.hasFocus } - .fillMaxWidth() - .padding(top = DefaultSpacing) - .payload( - flow = SIGNUP_CHALLENGE_FLOW_NAME, - frame = SIGNUP_CHALLENGE_USERNAME_FRAME, - onTextChanged = emailChanges.map { it.toPair() }, - onTextCopied = emailTextCopies, - onFrameUpdated = {}, - payloadController = emailPayloadController - ) - .testTag(EMAIL_FIELD_TAG) - ) - - ProtonSolidButton( - contained = false, - enabled = enabled, - loading = !enabled, - onClick = ::onSubmit, - modifier = Modifier - .padding(top = ProtonDimens.MediumSpacing) - .height(ProtonDimens.DefaultButtonMinHeight) - ) { - Text(text = stringResource(R.string.auth_signup_next)) - } - - Divider( - modifier = Modifier.padding(top = ProtonDimens.MediumSpacing), - color = LocalColors.current.separatorNorm - ) - - ProtonTextButton( - contained = false, - onClick = { onCreateInternalClicked(CreateUsernameAction.CreateInternalAccount) }, - modifier = Modifier - .padding(vertical = ProtonDimens.MediumSpacing) - .height(ProtonDimens.DefaultButtonMinHeight) - ) { - Text(text = stringResource(R.string.auth_signup_get_encrypted_email)) - } - - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center, - text = stringResource(id = R.string.auth_signup_external_footnote), - style = ProtonTypography.Default.defaultSmallWeak - ) - } -} - -@Preview(name = "Light mode", showBackground = true) -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) -@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) -@Preview(name = "Tablet", device = Devices.PIXEL_C) -@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) -@Composable -internal fun CreateUsernameScreenPreview() { - ProtonTheme { - CreateUsernameScreen() - } -} - -@Preview(name = "Light mode", showBackground = true) -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) -@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) -@Preview(name = "Tablet", device = Devices.PIXEL_C) -@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) -@Composable -internal fun CreateInternalPreview() { - ProtonTheme { - CreateInternalForm( - enabled = true, - onUsernameSubmitted = {}, - onCreateExternalClicked = {}, - usernameError = stringResource(id = R.string.auth_login_assistive_text), - domains = listOf("protonmail.com", "protonmail.ch") - ) - } -} - -@Preview(name = "Light mode", showBackground = true) -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) -@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) -@Preview(name = "Tablet", device = Devices.PIXEL_C) -@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) -@Composable -internal fun CreateExternalPreview() { - ProtonTheme { - CreateExternalForm( - enabled = true, - onExternalEmailSubmitted = {}, - onCreateInternalClicked = {}, - emailError = stringResource(id = R.string.auth_login_assistive_text) - ) - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameState.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameState.kt deleted file mode 100644 index ffa849bad1..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameState.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -sealed class CreateUsernameState( - open val accountType: AccountType -) { - - data class Idle( - override val accountType: AccountType, - val domains: List? = null - ) : CreateUsernameState(accountType) - - data class Loading( - override val accountType: AccountType - ) : CreateUsernameState(accountType) - - data class Validating( - override val accountType: AccountType - ) : CreateUsernameState(accountType) - - data class Error( - override val accountType: AccountType, - val message: String? - ) : CreateUsernameState(accountType) - - data class FormError( - override val accountType: AccountType, - val message: String? - ) : CreateUsernameState(accountType) - - data class Success( - override val accountType: AccountType, - val username: String - ) : CreateUsernameState(accountType) - - val isLoading: Boolean - get() = when (this) { - is Validating, - is Loading -> true - else -> false - } -} - -enum class AccountType { - Internal, External -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameViewModel.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameViewModel.kt deleted file mode 100644 index e3cd704fae..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameViewModel.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import me.proton.core.challenge.domain.ChallengeManager -import me.proton.core.challenge.domain.entity.ChallengeFrameDetails -import me.proton.core.compose.viewmodel.stopTimeoutMillis -import me.proton.core.presentation.savedstate.state -import javax.inject.Inject - -@HiltViewModel -class CreateUsernameViewModel @Inject constructor( - private val challengeManager: ChallengeManager, - savedStateHandle: SavedStateHandle -) : ViewModel() { - - private var currentAccountType: AccountType by savedStateHandle.state(AccountType.External) - private var domains: List = emptyList() - private val mutableAction = MutableStateFlow(null) - - val state: StateFlow = mutableAction.flatMapLatest { action -> - when (action) { - null -> flowOf(CreateUsernameState.Idle(currentAccountType)) - is CreateUsernameAction.SetNavigationDone -> onSetNavigationDone() - is CreateUsernameAction.CreateExternalAccount -> onCreateExternalAccount() - is CreateUsernameAction.CreateInternalAccount -> onCreateInternalAccount() - is CreateUsernameAction.Load -> onInit(currentAccountType) - is CreateUsernameAction.Submit -> onSubmit(action.type, action.value, action.usernameFrameDetails) - } - }.stateIn(viewModelScope, WhileSubscribed(stopTimeoutMillis), CreateUsernameState.Loading(currentAccountType)) - - private fun onInit(accountType: AccountType): Flow = flow { - emitAll( - when (accountType) { - AccountType.Internal -> onCreateInternalAccount() - AccountType.External -> onCreateExternalAccount() - } - ) - } - - private fun onCreateExternalAccount(): Flow = flow { - currentAccountType = AccountType.External - emit(CreateUsernameState.Idle(AccountType.External)) - } - - @Suppress("ForbiddenComment") - private fun onCreateInternalAccount(): Flow = flow { - currentAccountType = AccountType.Internal - emit(CreateUsernameState.Loading(AccountType.Internal)) - // fixme: refactor to fetch from API - domains = listOf("protonmail.com", "protonmail.ch") - emit(CreateUsernameState.Idle(AccountType.Internal, domains)) - } - - private fun onSubmit( - accountType: AccountType, - username: String, - usernameFrameDetails: ChallengeFrameDetails - ): Flow = flow { - challengeManager.addOrUpdateFrameToFlow(usernameFrameDetails) - emit(CreateUsernameState.Success(accountType, username)) - } - - private fun onSetNavigationDone(): Flow = flow { - emit(CreateUsernameState.Idle(currentAccountType, domains)) - } - - fun submit(action: CreateUsernameAction) = viewModelScope.launch { - mutableAction.emit(action) - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpActivity.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpActivity.kt index 057119be97..b115beb0a8 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpActivity.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpActivity.kt @@ -18,33 +18,36 @@ package me.proton.android.core.auth.presentation.signup import android.app.Activity +import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import me.proton.android.core.auth.presentation.AuthOrchestrator +import me.proton.android.core.auth.presentation.ProtonSecureActivity import me.proton.android.core.auth.presentation.R +import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addCountryPickerDialog import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addCreatePasswordScreen import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addCreateRecoveryScreen import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addCreateUsernameScreen -import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addMainScreen +import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addSignUpCongratsScreen +import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addSignUpCreateUserScreen +import me.proton.android.core.auth.presentation.signup.SignUpRoutes.addSkipRecoveryDialog +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel import me.proton.core.compose.theme.ProtonTheme -import me.proton.core.presentation.ui.ProtonActivity -import me.proton.core.presentation.utils.ProtectScreenConfiguration import me.proton.core.presentation.utils.addOnBackPressedCallback import me.proton.core.presentation.utils.errorToast -import me.proton.core.presentation.utils.protectScreen import javax.inject.Inject @AndroidEntryPoint -class SignUpActivity : ProtonActivity() { +class SignUpActivity : ProtonSecureActivity() { @Inject lateinit var authOrchestrator: AuthOrchestrator - private val configuration = ProtectScreenConfiguration(true) - private val screenProtector by protectScreen(configuration) + private val signUpViewModel: SignUpViewModel by viewModels() override fun onDestroy() { authOrchestrator.unregister() @@ -61,28 +64,48 @@ class SignUpActivity : ProtonActivity() { setContent { ProtonTheme { val navController = rememberNavController() + NavHost( + route = "sign_up", navController = navController, startDestination = SignUpRoutes.Route.CreateUsername() ) { addCreateUsernameScreen( navController = navController, - onClose = { navController.popBackStack() }, - onErrorMessage = { onErrorMessage(it) } + onClose = { onClose() }, + onErrorMessage = { onErrorMessage(it) }, + navGraphViewModel = signUpViewModel ) addCreatePasswordScreen( navController = navController, - onClose = { navController.popBackStack() }, - onErrorMessage = { onErrorMessage(it) } + onErrorMessage = { onErrorMessage(it) }, + navGraphViewModel = signUpViewModel ) addCreateRecoveryScreen( navController = navController, onErrorMessage = { onErrorMessage(it) }, - onClose = { navController.popBackStack() } + navGraphViewModel = signUpViewModel ) - addMainScreen( + addSkipRecoveryDialog( + navController = navController, + navGraphViewModel = signUpViewModel + ) + addCountryPickerDialog( + navController = navController, + navGraphViewModel = signUpViewModel + ) + addSignUpCreateUserScreen( onErrorMessage = { onErrorMessage(it) }, - onSuccess = { onSuccess() } + navController = navController, + navGraphViewModel = signUpViewModel + ) + addSignUpCongratsScreen( + onSuccess = { userId -> onSuccess(userId) }, + onError = { + onErrorMessage(it) + onClose() + }, + navGraphViewModel = signUpViewModel ) } } @@ -90,7 +113,10 @@ class SignUpActivity : ProtonActivity() { } private fun onErrorMessage(message: String?) { - errorToast(message ?: getString(R.string.presentation_error_general)) + val errorMessage = if (message.isNullOrEmpty()) { + getString(R.string.presentation_error_general) + } else message + errorToast(errorMessage) } private fun onClose() { @@ -98,8 +124,16 @@ class SignUpActivity : ProtonActivity() { finish() } - private fun onSuccess() { - setResult(Activity.RESULT_OK) + private fun onSuccess(userId: String) { + setResult( + Activity.RESULT_OK, + Intent().apply { putExtra(ARG_OUTPUT, SignupOutput(userId = userId)) } + ) finish() } + + companion object { + + const val ARG_OUTPUT = "arg.signupOutput" + } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpOperation.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpOperation.kt index 2ef51d63ce..26dfa2b1ae 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpOperation.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpOperation.kt @@ -18,8 +18,94 @@ package me.proton.android.core.auth.presentation.signup -sealed interface SignUpOperation : RecoveryMethodOperation +import me.proton.android.core.auth.presentation.signup.ui.Country +import me.proton.core.account.domain.entity.AccountType +import me.proton.core.challenge.domain.entity.ChallengeFrameDetails + +sealed interface SignUpOperation sealed interface SignUpAction : SignUpOperation { - data object SignUp : SignUpAction + data object CreatePlan : SignUpAction // add in the future + data object CreateUser : SignUpAction + data object FinalizeSignup : SignUpAction +} + +sealed interface CreateUsernameAction : SignUpAction { + data class LoadData( + val accountType: AccountType + ) : CreateUsernameAction + + data object CreateExternalAccount : CreateUsernameAction + data object CreateInternalAccount : CreateUsernameAction + data class Perform( + val unused: Long = System.currentTimeMillis(), + val value: String, + val domain: String?, + val accountType: AccountType, + val usernameFrameDetails: ChallengeFrameDetails + ) : CreateUsernameAction + + data class CreateUsernameClosed( + val back: Boolean = false + ) : CreateUsernameAction +} + +sealed interface CreatePasswordAction : SignUpAction { + data object LoadData : CreatePasswordAction + data class Perform( + val unused: Long = System.currentTimeMillis(), + val password: String, + val confirmPassword: String + ) : CreatePasswordAction + + data class CreatePasswordClosed( + val back: Boolean = false + ) : CreatePasswordAction +} + +sealed interface CreateRecoveryAction : SignUpAction { + + data class SelectRecoveryMethod( + val unused: Long = System.currentTimeMillis(), + val recoveryMethod: RecoveryMethod, + val locale: String + ) : CreateRecoveryAction + + data class SubmitRecoveryEmail( + val unused: Long = System.currentTimeMillis(), + val recoveryMethod: RecoveryMethod = RecoveryMethod.Email, + val email: String, + val recoveryFrameDetails: ChallengeFrameDetails + ) : CreateRecoveryAction + + data class SubmitRecoveryPhone( + val unused: Long = System.currentTimeMillis(), + val recoveryMethod: RecoveryMethod = RecoveryMethod.Phone, + val callingCode: String, + val phoneNumber: String, + val recoveryFrameDetails: ChallengeFrameDetails + ) : CreateRecoveryAction + + sealed interface DialogAction : CreateRecoveryAction { + data object WantSkipRecovery : DialogAction + + data object RecoverySkipped : DialogAction + + data object WantSkipDialogClosed : DialogAction + + data class PickCountry( + val recoveryMethod: RecoveryMethod = RecoveryMethod.Phone + ) : DialogAction + + data class CountryPicked( + val recoveryMethod: RecoveryMethod = RecoveryMethod.Phone, + val country: Country + ) : DialogAction + + data object CountryPickerClosed : DialogAction + } + + data class CreateRecoveryClosed( + val back: Boolean = false + ) : CreateRecoveryAction } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpRoutes.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpRoutes.kt index 802b9c0a26..414ce4aa7d 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpRoutes.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpRoutes.kt @@ -18,17 +18,22 @@ package me.proton.android.core.auth.presentation.signup +import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import androidx.navigation.compose.dialog +import me.proton.android.core.auth.presentation.signup.ui.CountryPickerScreen +import me.proton.android.core.auth.presentation.signup.ui.CreatePasswordScreen +import me.proton.android.core.auth.presentation.signup.ui.CreateRecoveryScreen +import me.proton.android.core.auth.presentation.signup.ui.CreateRecoverySkipDialog +import me.proton.android.core.auth.presentation.signup.ui.CreateUsernameScreen +import me.proton.android.core.auth.presentation.signup.ui.SignUpCongratsScreen +import me.proton.android.core.auth.presentation.signup.ui.SignUpLoadingScreen +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel object SignUpRoutes { sealed class Route(val route: String) { - data object SignUp : Route("auth/signup") { - - operator fun invoke() = route - } - data object CreateUsername : Route("auth/signup/create/username") { operator fun invoke() = route @@ -43,77 +48,171 @@ object SignUpRoutes { operator fun invoke() = route } - } - @Suppress("ForbiddenComment") - fun NavGraphBuilder.addMainScreen( - onErrorMessage: (String?) -> Unit, - onSuccess: () -> Unit // todo: add user id - ) { - composable( - route = Route.SignUp.route - ) { - SignUpScreen( - onSuccess = { onSuccess() }, - onErrorMessage = { onErrorMessage(it) } - ) + data object CountryPicker : Route("auth/signup/create/recovery/countrypicker") { + + operator fun invoke() = route + } + + data object SkipRecovery : Route("auth/signup/create/recovery/skip") { + + operator fun invoke() = route + } + + data object SignUpCreateUser : Route("auth/signup/user/create") { + + operator fun invoke() = route + } + + data object SignUpCongrats : Route("auth/signup/congrats") { + + operator fun invoke() = route } } fun NavGraphBuilder.addCreateUsernameScreen( navController: NavHostController, onClose: () -> Unit, - onErrorMessage: (String?) -> Unit + onErrorMessage: (String?) -> Unit, + navGraphViewModel: SignUpViewModel ) { composable( route = Route.CreateUsername.route ) { CreateUsernameScreen( onCloseClicked = { onClose() }, + onBackClicked = { onClose() }, onErrorMessage = { onErrorMessage(it) }, - onSuccess = { - val password = it - navController.navigate(Route.CreatePassword()) - } + onSuccess = { route -> + navController.navigate(route) { + launchSingleTop = true + } + }, + viewModel = navGraphViewModel ) } } fun NavGraphBuilder.addCreatePasswordScreen( navController: NavHostController, - onClose: () -> Unit, - onErrorMessage: (String?) -> Unit + onErrorMessage: (String?) -> Unit, + navGraphViewModel: SignUpViewModel ) { composable( route = Route.CreatePassword.route ) { CreatePasswordScreen( - onBackClicked = { navController.popBackStack() }, + onBackClicked = { + navController.popBackStack() + }, onErrorMessage = { onErrorMessage(it) }, - onSuccess = { - val password = it - navController.navigate(Route.CreateRecovery()) - } + onSuccess = { route -> + navController.navigate(route) { + launchSingleTop = true + } + }, + viewModel = navGraphViewModel ) } } fun NavGraphBuilder.addCreateRecoveryScreen( navController: NavHostController, - onClose: () -> Unit, - onErrorMessage: (String?) -> Unit + onErrorMessage: (String?) -> Unit, + navGraphViewModel: SignUpViewModel ) { composable( route = Route.CreateRecovery.route ) { CreateRecoveryScreen( - onBackClicked = { onClose() }, - onSkipClicked = { onClose() }, + onBackClicked = { navController.popBackStack() }, + onWantSkip = { + navController.navigate(Route.SkipRecovery.route) { + launchSingleTop = true + } + }, + onCountryPickerClicked = { + navController.navigate(Route.CountryPicker.route) { + launchSingleTop = true + } + }, onErrorMessage = { onErrorMessage(it) }, - onSuccess = { method, value -> - val recovery = it // add to viewmodel - navController.navigate(Route.SignUp()) - } + onSuccess = { route -> + navController.navigate(route) { + launchSingleTop = true + } + }, + viewModel = navGraphViewModel + ) + } + } + + fun NavGraphBuilder.addCountryPickerDialog(navController: NavHostController, navGraphViewModel: SignUpViewModel) { + dialog( + route = Route.CountryPicker.route, + dialogProperties = DialogProperties( + usePlatformDefaultWidth = false + ) + ) { + CountryPickerScreen( + viewModel = navGraphViewModel, + onCloseClick = { navController.popBackStack() } + ) + } + } + + fun NavGraphBuilder.addSkipRecoveryDialog(navController: NavHostController, navGraphViewModel: SignUpViewModel) { + dialog(route = Route.SkipRecovery.route) { + CreateRecoverySkipDialog( + onCloseClicked = { navController.popBackStack() }, + onSkip = { route -> + navController.navigate(route) { + launchSingleTop = true + restoreState = true + } + }, + viewModel = navGraphViewModel + ) + } + } + + fun NavGraphBuilder.addSignUpCreateUserScreen( + navController: NavHostController, + onErrorMessage: (String?) -> Unit, + navGraphViewModel: SignUpViewModel + ) { + composable( + route = Route.SignUpCreateUser.route + ) { + SignUpLoadingScreen( + onErrorMessage = { + onErrorMessage(it) + navController.popBackStack() + }, + onSuccess = { + navController.navigate(Route.SignUpCongrats.route) { + launchSingleTop = true + } + }, + viewModel = navGraphViewModel + ) + } + } + + fun NavGraphBuilder.addSignUpCongratsScreen( + onSuccess: (String) -> Unit, + onError: (String?) -> Unit, + navGraphViewModel: SignUpViewModel + ) { + composable( + route = Route.SignUpCongrats.route + ) { + SignUpCongratsScreen( + onStartUsingApp = { onSuccess(it) }, + onErrorMessage = { + onError(it) + }, + viewModel = navGraphViewModel ) } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpScreen.kt deleted file mode 100644 index f3ec6c81ad..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpScreen.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle - -@Composable -fun SignUpScreen( - modifier: Modifier = Modifier, - onSuccess: () -> Unit = {}, - onErrorMessage: (String?) -> Unit = {}, - viewModel: SignUpViewModel = hiltViewModel() -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - SignUpScreen( - modifier = modifier, - onSuccess = onSuccess, - onErrorMessage = onErrorMessage, - state = state - ) -} - -@Composable -fun SignUpScreen( - modifier: Modifier = Modifier, - onSuccess: () -> Unit = {}, - onErrorMessage: (String?) -> Unit = {}, - state: SignUpState -) { - LaunchedEffect(state) { - when (state) { - is SignUpState.Error -> onErrorMessage(state.message) - is SignUpState.Success -> onSuccess() - else -> Unit - } - } - SignUpLoading(modifier = modifier) -} - - diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameOperation.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpScreenMapper.kt similarity index 50% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameOperation.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpScreenMapper.kt index 5f4a2e082c..b9f7b9b36a 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameOperation.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpScreenMapper.kt @@ -18,19 +18,20 @@ package me.proton.android.core.auth.presentation.signup -import me.proton.core.challenge.domain.entity.ChallengeFrameDetails +import me.proton.android.core.auth.presentation.LogTag +import me.proton.core.util.kotlin.CoreLogger +import uniffi.proton_account_uniffi.SimpleSignupState -sealed interface CreateUsernameOperation - -sealed interface CreateUsernameAction : CreateUsernameOperation { - data object Load : CreateUsernameAction - data object CreateExternalAccount : CreateUsernameAction - data object CreateInternalAccount : CreateUsernameAction - data class Submit( - val value: String, - val type: AccountType, - val usernameFrameDetails: ChallengeFrameDetails - ) : CreateUsernameAction - - data object SetNavigationDone : CreateUsernameAction +fun SimpleSignupState.mapToNavigationRoute(): String { + return when (this) { + SimpleSignupState.WANT_USERNAME -> SignUpRoutes.Route.CreateUsername + SimpleSignupState.WANT_PASSWORD -> SignUpRoutes.Route.CreatePassword + SimpleSignupState.WANT_RECOVERY -> SignUpRoutes.Route.CreateRecovery + SimpleSignupState.WANT_CREATE -> SignUpRoutes.Route.SignUpCreateUser + SimpleSignupState.COMPLETE -> SignUpRoutes.Route.SignUpCongrats + SimpleSignupState.INVALID -> { + CoreLogger.e(LogTag.SIGNUP, "Received invalid state") + throw IllegalStateException("Received invalid state from Rust.") + } + }.route } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpState.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpState.kt index 92fe98c81b..6d8bc04b6a 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpState.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpState.kt @@ -18,11 +18,190 @@ package me.proton.android.core.auth.presentation.signup +import me.proton.android.core.auth.presentation.signup.SignUpState.DataInput +import me.proton.android.core.auth.presentation.signup.ui.Country +import me.proton.android.core.auth.presentation.signup.ui.Domain +import me.proton.core.account.domain.entity.AccountType + sealed interface SignUpState { + sealed interface DataInput : SignUpState data object SigningUp : SignUpState - - data class Error(val message: String?) : SignUpState - - @Suppress("ForbiddenComment") - data object Success : SignUpState // todo: add user data + data class SignUpError(val message: String?) : SignUpState + data object SignUpSuccess : SignUpState + data class LoginSuccess(val userId: String) : SignUpState } + +// region username +sealed interface CreateUsernameState : DataInput { + + val accountType: AccountType + val isLoading: Boolean + + data class Idle( + override val accountType: AccountType, + override val isLoading: Boolean = false + ) : CreateUsernameState + + data class Load( + override val accountType: AccountType, + override val isLoading: Boolean = true + ) : CreateUsernameState + + data class LoadingComplete( + override val accountType: AccountType, + val domains: List? = null, + override val isLoading: Boolean = false + ) : CreateUsernameState + + data class Creating( + override val accountType: AccountType, + override val isLoading: Boolean = false + ) : CreateUsernameState + + sealed class ValidationError( + override val accountType: AccountType, + override val isLoading: Boolean = false + ) : CreateUsernameState { + + data object UsernameEmpty : ValidationError(AccountType.Username, false) + + data object InternalUsernameEmpty : ValidationError(AccountType.Internal, false) + + data object EmailEmpty : ValidationError(AccountType.External, false) + + data class Other( + override val accountType: AccountType, + val field: ValidationField, + val message: String? + ) : ValidationError(accountType, false) + } + + data class Error( + val unused: Long = System.currentTimeMillis(), + override val accountType: AccountType, + override val isLoading: Boolean = false, + val message: String? + ) : CreateUsernameState + + data class Success( + override val accountType: AccountType, + override val isLoading: Boolean = false, + val username: String, + val domain: String? = null, + val route: String + ) : CreateUsernameState + + data class Closed( + override val accountType: AccountType, + override val isLoading: Boolean = false + ) : CreateUsernameState +} + +enum class ValidationField { + USERNAME, + EMAIL +} +// endregion + +// region password +sealed interface CreatePasswordState : DataInput { + + data object Idle : CreatePasswordState + data object Creating : CreatePasswordState + sealed interface ValidationError : CreatePasswordState { + data object PasswordEmpty : ValidationError + data object ConfirmPasswordMissMatch : ValidationError + data class Other( + val message: String? + ) : ValidationError + } + + data class Error( + val unused: Long = System.currentTimeMillis(), + val message: String? + ) : CreatePasswordState + + data class Success(val route: String) : CreatePasswordState + data object Closed : CreatePasswordState +} +// endregion + +// region recovery +sealed interface CreateRecoveryState : DataInput { + + data class Idle( + val recoveryMethod: RecoveryMethod = RecoveryMethod.Email, + val countries: List? = null, + val defaultCountry: Country? + ) : CreateRecoveryState + + data class Creating( + val recoveryMethod: RecoveryMethod + ) : CreateRecoveryState + + sealed class ValidationError( + open val message: String? + ) : CreateRecoveryState { + + data class Email( + override val message: String? + ) : ValidationError(message) + + data class Phone( + override val message: String? + ) : ValidationError(message) + } + + data class Error( + val unused: Long = System.currentTimeMillis(), + val recoveryMethod: RecoveryMethod, + val message: String? + ) : CreateRecoveryState + + data class Success( + val recoveryMethod: RecoveryMethod, + val value: String, + val route: String + ) : CreateRecoveryState + + data class WantCountryPicker( + val recoveryMethod: RecoveryMethod, + val countries: List + ) : CreateRecoveryState + + data class OnCountryPicked( + val recoveryMethod: RecoveryMethod, + val country: Country + ) : CreateRecoveryState + + data class CountryPickerFailed( + val recoveryMethod: RecoveryMethod, + val country: Country? + ) : CreateRecoveryState + + data class WantSkip( + val recoveryMethod: RecoveryMethod + ) : CreateRecoveryState + + data class SkipSuccess( + val recoveryMethod: RecoveryMethod, + val route: String + ) : CreateRecoveryState + + data class SkipFailed( + val recoveryMethod: RecoveryMethod + ) : CreateRecoveryState + + data object Closed : CreateRecoveryState +} + +enum class RecoveryMethod(val value: Int) { + Email(0), Phone(1); + + companion object { + + val map = entries.associateBy { it.value } + fun enumOf(value: Int) = map[value] ?: Email + } +} +// endregion diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpViewModel.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpViewModel.kt deleted file mode 100644 index 7974a7f21c..0000000000 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2022 Proton Technologies AG - * This file is part of Proton Technologies AG and Proton Mail. - * - * Proton Mail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Proton Mail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Proton Mail. If not, see . - */ - -package me.proton.android.core.auth.presentation.signup - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import me.proton.core.compose.viewmodel.stopTimeoutMillis -import javax.inject.Inject - -@HiltViewModel -class SignUpViewModel @Inject constructor() : ViewModel() { - - private val mutableAction: MutableStateFlow = MutableStateFlow(null) - - val state: StateFlow = mutableAction.flatMapLatest { _ -> - onSignUp() - }.stateIn(viewModelScope, WhileSubscribed(stopTimeoutMillis), SignUpState.SigningUp) - - private fun onSignUp() = flow { - emit(SignUpState.SigningUp) - } - - fun submit(action: SignUpAction) = viewModelScope.launch { - mutableAction.emit(action) - } -} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordOperation.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignupOutput.kt similarity index 75% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordOperation.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignupOutput.kt index 68874476c4..967eba11f0 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordOperation.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignupOutput.kt @@ -18,9 +18,8 @@ package me.proton.android.core.auth.presentation.signup -sealed interface CreatePasswordOperation +import android.os.Parcelable +import kotlinx.parcelize.Parcelize -sealed interface CreatePasswordAction : CreatePasswordOperation { - data class Submit(val password: String, val confirmPassword: String) : CreatePasswordAction - data object SetNavigationDone : CreatePasswordAction -} +@Parcelize +data class SignupOutput(val userId: String) : Parcelable diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CountryCodeDropDown.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CountryCodeDropDown.kt new file mode 100644 index 0000000000..f24a0ce6a7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CountryCodeDropDown.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ +@file:OptIn(ExperimentalMaterialApi::class) + +package me.proton.android.core.auth.presentation.signup.ui + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import me.proton.android.core.auth.presentation.R +import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.compose.theme.ProtonTypography +import me.proton.core.compose.theme.defaultNorm + +data class Country( + val countryCode: String, + val callingCode: Int? = null, + val name: String, + val flagId: Int = 0 +) + +@Composable +fun CountryCodeDropDown( + modifier: Modifier = Modifier, + country: Country?, + onNavigateToCountryPicker: () -> Unit +) { + Card( + modifier = modifier + .clickable(onClick = onNavigateToCountryPicker), + contentColor = ProtonTheme.colors.textNorm, + elevation = 0.dp, + backgroundColor = ProtonTheme.colors.backgroundSecondary + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "+${country?.callingCode ?: ""}", + style = ProtonTypography.Default.defaultNorm, + color = ProtonTheme.colors.textNorm, + modifier = Modifier.weight(1f) + ) + + Icon( + painter = painterResource(R.drawable.ic_proton_chevron_down), + contentDescription = stringResource(R.string.auth_signup_recovery_select_country) + ) + } + } +} + +@Preview(name = "Light mode", showBackground = true) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) +@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) +@Preview(name = "Tablet", device = Devices.PIXEL_C) +@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) +@Composable +internal fun CountryDropDownPreview() { + ProtonTheme { + CountryCodeDropDown( + country = Country( + countryCode = "CH", + callingCode = 1, + name = "Switzerland" + ), + onNavigateToCountryPicker = {} + ) + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CountryPickerScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CountryPickerScreen.kt new file mode 100644 index 0000000000..a12c5f194e --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CountryPickerScreen.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.ui + +import java.util.Locale +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import me.proton.android.core.auth.presentation.R +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.WantCountryPicker +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.CountryPickerFailed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.OnCountryPicked +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel +import me.proton.core.compose.component.ProtonCloseButton +import me.proton.core.compose.theme.ProtonDimens +import me.proton.core.compose.theme.ProtonTypography +import me.proton.core.compose.theme.defaultNorm +import me.proton.core.compose.theme.defaultSmallWeak + +@Composable +fun CountryPickerScreen( + modifier: Modifier = Modifier, + onCloseClick: () -> Unit = {}, + viewModel: SignUpViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val currentState = state + LaunchedEffect(currentState) { + when (currentState) { + is OnCountryPicked, + is CountryPickerFailed -> onCloseClick() + else -> Unit + } + } + + when (currentState) { + is WantCountryPicker -> CountryPickerContentScreen( + modifier = modifier, + onCloseClick = { + viewModel.perform(CreateRecoveryAction.DialogAction.CountryPickerClosed) + }, + onCountrySelected = { + viewModel.perform(CreateRecoveryAction.DialogAction.CountryPicked(country = it)) + }, + state = currentState + ) + + else -> Unit + } +} + +@Composable +fun CountryPickerContentScreen( + modifier: Modifier = Modifier, + onCloseClick: () -> Unit = {}, + onCountrySelected: (Country) -> Unit = {}, + state: WantCountryPicker +) { + var searchQuery by rememberSaveable { mutableStateOf("") } + + val filteredCountries by remember(searchQuery, state.countries) { + derivedStateOf { + if (searchQuery.isEmpty()) { + state.countries + } else { + state.countries.filter { country -> + country.name.contains(searchQuery, ignoreCase = true) || country.countryCode.contains( + searchQuery, + ignoreCase = true + ) + } + } + } + } + + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colors.background) + .padding(ProtonDimens.DefaultSpacing) + ) { + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = ProtonDimens.DefaultSpacing), + verticalAlignment = Alignment.CenterVertically + ) { + ProtonCloseButton(onCloseClicked = onCloseClick) + + ProtonSearchBar( + modifier = Modifier + .weight(1f) + .padding(start = ProtonDimens.DefaultSpacing), + query = searchQuery, + onQueryChange = { searchQuery = it }, + placeholder = stringResource(id = R.string.auth_signup_countries_search) + ) + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + items(filteredCountries) { country -> + CountryItem( + country = country, + onClick = { onCountrySelected(country) } + ) + } + } + } + } +} + +@Composable +fun ProtonSearchBar( + modifier: Modifier = Modifier, + query: String, + onQueryChange: (String) -> Unit, + placeholder: String +) { + TextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(8.dp) + ), + placeholder = { + Text( + text = placeholder, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_proton_magnifier), + contentDescription = null, + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + }, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + singleLine = true + ) +} + +@Composable +fun CountryItem(country: Country, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + contentDescription = null, + alignment = Alignment.Center, + painter = painterResource(id = country.getFlagDrawable()) + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) { + Text( + text = country.name, + style = ProtonTypography.Default.defaultNorm + ) + Text( + text = country.countryCode, + style = ProtonTypography.Default.defaultSmallWeak + ) + } + Text( + text = "+${country.callingCode}", + style = ProtonTypography.Default.defaultNorm + ) + } +} + +@Composable +fun Country.getFlagDrawable(): Int { + val context = LocalContext.current + return context.resources.getIdentifier( + "flag_${countryCode.lowercase(Locale.ROOT)}", + "drawable", + context.packageName + ) +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreatePasswordScreen.kt similarity index 54% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordScreen.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreatePasswordScreen.kt index 352a3125a8..ab4cba4313 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordScreen.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreatePasswordScreen.kt @@ -18,18 +18,18 @@ @file:Suppress("UseComposableActions") -package me.proton.android.core.auth.presentation.signup +package me.proton.android.core.auth.presentation.signup.ui import android.content.res.Configuration +import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -39,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -47,6 +46,17 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import me.proton.android.core.auth.presentation.R import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT +import me.proton.android.core.auth.presentation.signup.CreatePasswordAction +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Closed +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Creating +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Error +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Idle +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Success +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.ValidationError.ConfirmPasswordMissMatch +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.ValidationError.Other +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.ValidationError.PasswordEmpty +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel import me.proton.core.compose.component.HyperlinkText import me.proton.core.compose.component.ProtonPasswordOutlinedTextFieldWithError import me.proton.core.compose.component.ProtonSolidButton @@ -61,45 +71,65 @@ fun CreatePasswordScreen( modifier: Modifier = Modifier, onBackClicked: () -> Unit, onErrorMessage: (String?) -> Unit = {}, - onSuccess: (String) -> Unit = {}, - viewModel: CreatePasswordViewModel = hiltViewModel() + onSuccess: (String) -> Unit = { }, + viewModel: SignUpViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() - CreatePasswordScreen( + + BackHandler(enabled = true) { + viewModel.perform(CreatePasswordAction.CreatePasswordClosed(back = true)) + } + + when (state) { + is Closed -> onBackClicked() + else -> Unit + } + + CreatePasswordContent( modifier = modifier, - onBackClicked = onBackClicked, - onPasswordSubmitted = { viewModel.submit(it) }, - onErrorMessage = onErrorMessage, - onSuccess = { - onSuccess(it) - viewModel.submit(CreatePasswordAction.SetNavigationDone) + onBackClicked = { + viewModel.perform(CreatePasswordAction.CreatePasswordClosed(back = true)) }, + onPasswordSubmitted = { viewModel.perform(it) }, + onErrorMessage = onErrorMessage, + onSuccess = { route -> onSuccess(route) }, state = state ) } @Composable -fun CreatePasswordScreen( +fun CreatePasswordContent( modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}, - onPasswordSubmitted: (CreatePasswordAction.Submit) -> Unit = {}, + onPasswordSubmitted: (CreatePasswordAction.Perform) -> Unit = {}, onErrorMessage: (String?) -> Unit = {}, onSuccess: (String) -> Unit = {}, - state: CreatePasswordState = CreatePasswordState.Idle + state: SignUpState = Idle ) { + val isLoading = state is Creating + LaunchedEffect(state) { when (state) { - is CreatePasswordState.FormError -> onErrorMessage(state.message) - is CreatePasswordState.Success -> onSuccess(state.password) + is Error -> onErrorMessage(state.message) + is Success -> onSuccess(state.route) else -> Unit } } - val isLoading = state is CreatePasswordState.Loading var password by remember { mutableStateOf("") } var confirmPassword by remember { mutableStateOf("") } - val passwordError = (state as? CreatePasswordState.FormError)?.message - val confirmPasswordError = (state as? CreatePasswordState.FormError)?.message + + val passwordError = when (state) { + is PasswordEmpty -> stringResource(R.string.auth_signup_validation_password) + is Other -> state.message ?: stringResource(R.string.auth_signup_validation_password_input_invalid) + else -> null + } + + val confirmPasswordError = when (state) { + is ConfirmPasswordMissMatch -> stringResource(R.string.auth_signup_validation_passwords_do_not_match) + is Other -> state.message ?: stringResource(R.string.auth_signup_validation_password_input_invalid) + else -> null + } Scaffold( modifier = modifier, @@ -107,12 +137,7 @@ fun CreatePasswordScreen( ProtonTopAppBar( title = {}, navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - painterResource(id = me.proton.core.presentation.R.drawable.ic_proton_close), - contentDescription = stringResource(id = R.string.auth_login_close) - ) - } + NavigationBackButton(onBackClicked = onBackClicked) }, backgroundColor = LocalColors.current.backgroundNorm ) @@ -131,35 +156,34 @@ fun CreatePasswordScreen( text = stringResource(id = R.string.auth_signup_create_password) ) - ProtonPasswordOutlinedTextFieldWithError( - text = password, - onValueChanged = { password = it }, + PasswordField( + password = password, + onPasswordChanged = { password = it }, enabled = !isLoading, - singleLine = true, - label = { Text(text = stringResource(id = R.string.auth_signup_password)) }, errorText = passwordError, modifier = Modifier.padding(top = ProtonDimens.MediumSpacing) ) - ProtonPasswordOutlinedTextFieldWithError( - text = confirmPassword, - onValueChanged = { confirmPassword = it }, + PasswordConfirmationField( + confirmPassword = confirmPassword, + onConfirmPasswordChanged = { confirmPassword = it }, enabled = !isLoading, - singleLine = true, - label = { Text(text = stringResource(id = R.string.auth_signup_repeat_password)) }, errorText = confirmPasswordError, modifier = Modifier.padding(top = ProtonDimens.MediumSpacing) ) - ProtonSolidButton( - contained = false, - loading = isLoading, - modifier = Modifier - .padding(top = ProtonDimens.MediumSpacing) - .height(ProtonDimens.DefaultButtonMinHeight), - onClick = { onPasswordSubmitted(CreatePasswordAction.Submit(password, confirmPassword)) } - ) { - Text(text = stringResource(id = R.string.auth_signup_next)) - } + + NextButton( + isLoading = isLoading, + onClick = { + onPasswordSubmitted( + CreatePasswordAction.Perform( + password = password, + confirmPassword = confirmPassword + ) + ) + }, + modifier = Modifier.padding(top = ProtonDimens.MediumSpacing) + ) TermsPolicyFooter() } @@ -167,6 +191,62 @@ fun CreatePasswordScreen( } } +@Composable +private fun PasswordField( + password: String, + onPasswordChanged: (String) -> Unit, + enabled: Boolean, + errorText: String?, + modifier: Modifier = Modifier +) { + ProtonPasswordOutlinedTextFieldWithError( + text = password, + onValueChanged = onPasswordChanged, + enabled = enabled, + singleLine = true, + label = { Text(text = stringResource(id = R.string.auth_signup_password)) }, + errorText = errorText, + modifier = modifier + ) +} + +@Composable +private fun PasswordConfirmationField( + confirmPassword: String, + onConfirmPasswordChanged: (String) -> Unit, + enabled: Boolean, + errorText: String?, + modifier: Modifier = Modifier +) { + ProtonPasswordOutlinedTextFieldWithError( + text = confirmPassword, + onValueChanged = onConfirmPasswordChanged, + enabled = enabled, + singleLine = true, + label = { Text(text = stringResource(id = R.string.auth_signup_repeat_password)) }, + errorText = errorText, + modifier = modifier + ) +} + +@Composable +private fun NextButton( + isLoading: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + ProtonSolidButton( + contained = false, + loading = isLoading, + modifier = modifier + .fillMaxWidth() + .height(ProtonDimens.DefaultButtonMinHeight), + onClick = onClick + ) { + Text(text = stringResource(id = R.string.auth_signup_next)) + } +} + @Composable fun TermsPolicyFooter() { LearnMoreText(text = R.string.auth_signup_terms_privacy_conditions_footer) @@ -195,6 +275,7 @@ fun LearnMoreText(@StringRes text: Int) { ) } + @Preview(name = "Light mode", showBackground = true) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) @@ -204,6 +285,6 @@ fun LearnMoreText(@StringRes text: Int) { @Composable internal fun CreatePasswordScreenPreview() { ProtonTheme { - CreatePasswordScreen() + CreatePasswordContent() } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateRecoveryScreen.kt similarity index 64% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryScreen.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateRecoveryScreen.kt index cf66d129e2..362293e9c8 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryScreen.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateRecoveryScreen.kt @@ -18,9 +18,11 @@ @file:Suppress("UseComposableActions") -package me.proton.android.core.auth.presentation.signup +package me.proton.android.core.auth.presentation.signup.ui +import java.util.Locale import android.content.res.Configuration +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -34,8 +36,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.Scaffold import androidx.compose.material.Tab import androidx.compose.material.TabRow @@ -58,7 +58,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices @@ -73,6 +72,31 @@ import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT import me.proton.android.core.auth.presentation.challenge.SIGNUP_CHALLENGE_FLOW_NAME import me.proton.android.core.auth.presentation.challenge.SIGNUP_CHALLENGE_RECOVERY_FRAME import me.proton.android.core.auth.presentation.challenge.TextChange +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.CreateRecoveryClosed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.PickCountry +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.WantSkipRecovery +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.SelectRecoveryMethod +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.SubmitRecoveryEmail +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.SubmitRecoveryPhone +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Closed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.CountryPickerFailed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Creating +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Error +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Idle +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.OnCountryPicked +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.SkipFailed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.SkipSuccess +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Success +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.ValidationError +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.ValidationError.Email +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.ValidationError.Phone +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.WantCountryPicker +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.WantSkip +import me.proton.android.core.auth.presentation.signup.RecoveryMethod +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel import me.proton.core.challenge.presentation.compose.PayloadController import me.proton.core.challenge.presentation.compose.payload import me.proton.core.compose.component.ProtonOutlinedTextFieldWithError @@ -95,42 +119,65 @@ import me.proton.core.compose.theme.defaultStrongNorm fun CreateRecoveryScreen( modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}, - onSkipClicked: () -> Unit = {}, + onWantSkip: () -> Unit = {}, + onCountryPickerClicked: (List) -> Unit = {}, onErrorMessage: (String?) -> Unit = {}, - onSuccess: (RecoveryMethod, String) -> Unit = { method, value -> }, - viewModel: CreateRecoveryViewModel = hiltViewModel() + onSuccess: (String) -> Unit = { }, + viewModel: SignUpViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() + + BackHandler(enabled = true) { + viewModel.perform(CreateRecoveryClosed(back = true)) + } + + LaunchedEffect(state) { + when (state) { + is Closed -> onBackClicked() + is WantSkip -> onWantSkip() + else -> Unit + } + } + CreateRecoveryScreen( modifier = modifier, - onBackClicked = onBackClicked, - onSkipClicked = onSkipClicked, - onTabSelected = { viewModel.submit(CreateRecoveryAction.SelectCreateRecovery(it)) }, - onRecoverySubmitted = { viewModel.submit(it) }, - onErrorMessage = onErrorMessage, - onSuccess = { recoveryMethod, value -> - onSuccess(recoveryMethod, value) - viewModel.submit(CreateRecoveryAction.SetNavigationDone) + onBackClicked = { viewModel.perform(CreateRecoveryClosed(back = true)) }, + onWantSkip = { viewModel.perform(WantSkipRecovery) }, + onCountryPickerClicked = onCountryPickerClicked, + onTabSelected = { + viewModel.perform( + SelectRecoveryMethod( + recoveryMethod = it, + locale = Locale.getDefault().country + ) + ) }, + onRecoverySubmitted = { viewModel.perform(it) }, + onCountryPicker = { viewModel.perform(PickCountry()) }, + onErrorMessage = onErrorMessage, + onSuccess = { route -> onSuccess(route) }, state = state ) } @Composable fun CreateRecoveryScreen( - state: CreateRecoveryState, modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}, - onSkipClicked: () -> Unit = {}, + onWantSkip: () -> Unit = {}, + onCountryPickerClicked: (List) -> Unit = {}, onErrorMessage: (String?) -> Unit = {}, onTabSelected: (RecoveryMethod) -> Unit = {}, onRecoverySubmitted: (CreateRecoveryAction) -> Unit = {}, - onSuccess: (RecoveryMethod, String) -> Unit = { recovery, method -> } + onCountryPicker: () -> Unit = {}, + onSuccess: (String) -> Unit = { }, + state: SignUpState ) { LaunchedEffect(state) { when (state) { - is CreateRecoveryState.FormError -> onErrorMessage(state.message) - is CreateRecoveryState.Success -> onSuccess(state.recoveryMethod, state.value) + is ValidationError -> onErrorMessage(state.message) + is Success -> onSuccess(state.route) + is WantCountryPicker -> onCountryPickerClicked(state.countries) else -> Unit } } @@ -138,9 +185,10 @@ fun CreateRecoveryScreen( RecoveryMethodScaffold( modifier = modifier, onBackClicked = onBackClicked, - onSkipClicked = onSkipClicked, + onWantSkipClicked = onWantSkip, onRecoverySubmitted = onRecoverySubmitted, onTabSelected = onTabSelected, + onCountryPicker = onCountryPicker, state = state ) } @@ -149,14 +197,18 @@ fun CreateRecoveryScreen( fun RecoveryMethodScaffold( modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}, - onSkipClicked: () -> Unit = {}, + onWantSkipClicked: () -> Unit = {}, onTabSelected: (RecoveryMethod) -> Unit = {}, onRecoverySubmitted: (CreateRecoveryAction) -> Unit = {}, - state: CreateRecoveryState + onCountryPicker: () -> Unit = {}, + state: SignUpState ) { - val isLoading = state is CreateRecoveryState.Loading - val emailError = (state as? CreateRecoveryState.FormError.Email)?.message - val phoneError = (state as? CreateRecoveryState.FormError.Phone)?.message + val isLoading = state is Creating + val emailError = (state as? Email)?.message + val phoneError = (state as? Phone)?.message + val selectedCountry = (state as? CreateRecoveryState)?.country() + + val currentMethod = (state as? CreateRecoveryState)?.currentMethod() ?: RecoveryMethod.Email Scaffold( modifier = modifier, @@ -164,16 +216,11 @@ fun RecoveryMethodScaffold( ProtonTopAppBar( title = {}, navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - painterResource(id = me.proton.core.presentation.R.drawable.ic_proton_close), - contentDescription = stringResource(id = R.string.auth_login_close) - ) - } + NavigationBackButton(onBackClicked = onBackClicked) }, actions = { ProtonTextButton( - onClick = onSkipClicked + onClick = onWantSkipClicked ) { Text( text = stringResource(id = R.string.auth_signup_recovery_skip), @@ -202,7 +249,7 @@ fun RecoveryMethodScaffold( Text( text = stringResource(id = R.string.auth_signup_recovery_subtitle), style = ProtonTypography.Default.defaultSmallWeak, - textAlign = TextAlign.Center, + textAlign = TextAlign.Start, modifier = Modifier .align(Alignment.CenterHorizontally) .padding(top = SmallSpacing) @@ -211,35 +258,58 @@ fun RecoveryMethodScaffold( RecoveryTabs( modifier = modifier, tabs = listOf("Email", "Phone"), + initialSelectedMethod = currentMethod, onTabSelected = { onTabSelected(RecoveryMethod.enumOf(it)) } ) - when (state.recoveryMethod) { - RecoveryMethod.Email -> RecoveryMethodFormEmail( - loading = isLoading, - emailError = emailError, - onEmailSubmitted = onRecoverySubmitted - ) - - RecoveryMethod.Phone -> RecoveryMethodFormPhone( - loading = isLoading, - data = state.countries ?: emptyList(), - emailError = phoneError, - onPhoneSubmitted = onRecoverySubmitted - ) - } + RecoveryMethodsForms( + currentMethod = currentMethod, + isLoading = isLoading, + emailError = emailError, + phoneError = phoneError, + selectedCountry = selectedCountry, + onRecoverySubmitted = onRecoverySubmitted, + onCountryPicker = onCountryPicker + ) } } } } +@Composable +fun RecoveryMethodsForms( + currentMethod: RecoveryMethod, + isLoading: Boolean, + emailError: String?, + phoneError: String?, + selectedCountry: Country?, + onRecoverySubmitted: (CreateRecoveryAction) -> Unit, + onCountryPicker: () -> Unit +) { + when (currentMethod) { + RecoveryMethod.Email -> RecoveryMethodFormEmail( + loading = isLoading, + emailError = emailError, + onEmailSubmitted = onRecoverySubmitted + ) + + RecoveryMethod.Phone -> RecoveryMethodFormPhone( + loading = isLoading, + selectedCountry = selectedCountry, + emailError = phoneError, + onPhoneSubmitted = onRecoverySubmitted, + onCountryPicker = onCountryPicker + ) + } +} + @Composable fun RecoveryMethodFormEmail( loading: Boolean = false, emailError: String? = null, - onEmailSubmitted: (CreateRecoveryAction.SubmitEmail) -> Unit = {} + onEmailSubmitted: (SubmitRecoveryEmail) -> Unit = {} ) { var email by rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope() @@ -250,7 +320,7 @@ fun RecoveryMethodFormEmail( fun onSubmit() = scope.launch { val frameDetails = emailPayloadController.flush() - onEmailSubmitted(CreateRecoveryAction.SubmitEmail(email, frameDetails)) + onEmailSubmitted(SubmitRecoveryEmail(email = email, recoveryFrameDetails = frameDetails)) } ProtonOutlinedTextFieldWithError( @@ -295,10 +365,11 @@ fun RecoveryMethodFormEmail( fun RecoveryMethodFormPhone( emailError: String? = null, loading: Boolean = false, - data: List = emptyList(), - onPhoneSubmitted: (CreateRecoveryAction.SubmitPhone) -> Unit + selectedCountry: Country?, + onPhoneSubmitted: (SubmitRecoveryPhone) -> Unit, + onCountryPicker: () -> Unit ) { - var callingCode by rememberSaveable { mutableStateOf("") } + var callingCode by rememberSaveable { mutableStateOf(selectedCountry?.callingCode?.toString() ?: "") } var phoneNumber by rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope() @@ -307,9 +378,19 @@ fun RecoveryMethodFormPhone( val phonePayloadController = remember { PayloadController() } val phoneTextCopies = remember { MutableStateFlow("") } + LaunchedEffect(selectedCountry) { + callingCode = selectedCountry?.callingCode?.toString() ?: "" + } + fun onSubmit() = scope.launch { val frameDetails = phonePayloadController.flush() - onPhoneSubmitted(CreateRecoveryAction.SubmitPhone(callingCode, phoneNumber, frameDetails)) + onPhoneSubmitted( + SubmitRecoveryPhone( + callingCode = callingCode, + phoneNumber = phoneNumber, + recoveryFrameDetails = frameDetails + ) + ) } Row( @@ -324,9 +405,8 @@ fun RecoveryMethodFormPhone( modifier = Modifier .padding(top = LargeSpacing) .height(TextFieldDefaults.MinHeight), - isLoading = loading, - data = data, - onInputChanged = { callingCode = it.callingCode.toString() } // int? + country = selectedCountry, + onNavigateToCountryPicker = onCountryPicker ) } @@ -378,9 +458,10 @@ fun RecoveryMethodFormPhone( fun RecoveryTabs( modifier: Modifier = Modifier, tabs: List, + initialSelectedMethod: RecoveryMethod = RecoveryMethod.Email, onTabSelected: (Int) -> Unit = {} ) { - var selectedIndex by remember { mutableIntStateOf(0) } + var selectedIndex by remember { mutableIntStateOf(initialSelectedMethod.value) } TabRow( modifier = modifier @@ -401,7 +482,7 @@ fun RecoveryTabs( selected = selectedIndex == index, onClick = { selectedIndex = index - onTabSelected(selectedIndex) + onTabSelected(index) } ) { Text( @@ -414,6 +495,29 @@ fun RecoveryTabs( } } +private fun CreateRecoveryState.country(): Country? = when (this) { + is OnCountryPicked -> country + is CountryPickerFailed -> country + is Idle -> defaultCountry + else -> null +} + +private fun CreateRecoveryState.currentMethod() = when (this) { + is Idle -> recoveryMethod + is WantCountryPicker -> recoveryMethod + is OnCountryPicked -> recoveryMethod + is Creating -> recoveryMethod + is Error -> recoveryMethod + is Success -> recoveryMethod + is Email -> RecoveryMethod.Email + is Phone -> RecoveryMethod.Phone + is WantSkip -> recoveryMethod + is SkipSuccess -> recoveryMethod + is SkipFailed -> recoveryMethod + is CountryPickerFailed -> recoveryMethod + Closed -> RecoveryMethod.Email // default +} + @Preview(name = "Light mode", showBackground = true) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) @@ -438,12 +542,13 @@ internal fun RecoveryMethodScreenPreview() { internal fun CreateRecoveryPhoneScreenPreview() { ProtonTheme { CreateRecoveryScreen( - state = CreateRecoveryState.Idle( - RecoveryMethod.Phone, + state = Idle( + recoveryMethod = RecoveryMethod.Phone, listOf( Country("CH", 41, "Switzerland"), Country("US", 1, "US") - ) + ), + defaultCountry = Country("CH", 41, "Switzerland") ) ) } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateRecoverySkipDialog.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateRecoverySkipDialog.kt new file mode 100644 index 0000000000..8bc3433ca7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateRecoverySkipDialog.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import me.proton.android.core.auth.presentation.R +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.RecoverySkipped +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.WantSkipDialogClosed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.SkipFailed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.SkipSuccess +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.WantSkip +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel +import me.proton.core.compose.component.ProtonAlertDialog +import me.proton.core.compose.component.ProtonAlertDialogButton +import me.proton.core.compose.component.ProtonAlertDialogText +import me.proton.core.compose.theme.ProtonTheme + +@Composable +fun CreateRecoverySkipDialog( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = { }, + onSkip: (String) -> Unit = { }, + viewModel: SignUpViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val currentState = state + LaunchedEffect(currentState) { + when (currentState) { + is SkipSuccess -> onSkip(currentState.route) + is SkipFailed -> onCloseClicked() + else -> Unit + } + } + + when (state) { + is WantSkip -> { + CreateRecoverySkipDialog( + modifier = modifier, + onCloseClicked = { viewModel.perform(WantSkipDialogClosed) }, + onSkipClicked = { viewModel.perform(RecoverySkipped) } + ) + } + + else -> Unit + } + +} + +@Composable +fun CreateRecoverySkipDialog( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = { }, + onSkipClicked: () -> Unit = { } +) { + ProtonAlertDialog( + modifier = modifier, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + securePolicy = SecureFlagPolicy.Inherit + ), + title = stringResource(R.string.auth_signup_skip_recovery_title), + text = { ProtonAlertDialogText(stringResource(R.string.auth_signup_skip_recovery_description)) }, + onDismissRequest = { }, + confirmButton = { + Column { + ProtonAlertDialogButton( + modifier = Modifier.align(Alignment.End), + title = stringResource(R.string.auth_signup_skip_recovery), + onClick = onSkipClicked, + loading = false + ) + } + }, + dismissButton = { + Column { + ProtonAlertDialogButton( + modifier = Modifier.align(Alignment.End), + title = stringResource(R.string.auth_signup_set_recovery), + onClick = onCloseClicked, + loading = false + ) + } + } + ) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = false) +@Composable +internal fun ChangePasswordDialogPreview() { + ProtonTheme { + CreateRecoverySkipDialog() + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateUsernameScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateUsernameScreen.kt new file mode 100644 index 0000000000..a116b6890e --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/CreateUsernameScreen.kt @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +@file:Suppress("UseComposableActions") + +package me.proton.android.core.auth.presentation.signup.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusState +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import me.proton.android.core.auth.presentation.R +import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT +import me.proton.android.core.auth.presentation.challenge.SIGNUP_CHALLENGE_FLOW_NAME +import me.proton.android.core.auth.presentation.challenge.SIGNUP_CHALLENGE_USERNAME_FRAME +import me.proton.android.core.auth.presentation.challenge.TextChange +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.CreateExternalAccount +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.CreateInternalAccount +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.CreateUsernameClosed +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.LoadData +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.Perform +import me.proton.android.core.auth.presentation.signup.CreateUsernameState +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel +import me.proton.core.account.domain.entity.AccountType +import me.proton.core.challenge.presentation.compose.LocalClipManager +import me.proton.core.challenge.presentation.compose.LocalClipManager.OnClipChangedDisposableEffect +import me.proton.core.challenge.presentation.compose.PayloadController +import me.proton.core.challenge.presentation.compose.payload +import me.proton.core.compose.component.ProtonCloseButton +import me.proton.core.compose.component.ProtonOutlinedTextFieldWithError +import me.proton.core.compose.component.ProtonSolidButton +import me.proton.core.compose.component.ProtonTextButton +import me.proton.core.compose.component.appbar.ProtonTopAppBar +import me.proton.core.compose.theme.LocalColors +import me.proton.core.compose.theme.ProtonDimens +import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.compose.theme.ProtonTypography +import me.proton.core.compose.theme.defaultSmallWeak + +internal const val USERNAME_FIELD_TAG = "USERNAME_FIELD_TAG" +internal const val EMAIL_FIELD_TAG = "EMAIL_FIELD_TAG" +internal const val PHONE_FIELD_TAG = "PHONE_FIELD_TAG" + +@Composable +fun CreateUsernameScreen( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = {}, + onBackClicked: () -> Unit = {}, + onErrorMessage: (String?) -> Unit = {}, + onSuccess: (String) -> Unit = {}, + viewModel: SignUpViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + BackHandler(enabled = true) { + viewModel.perform(CreateUsernameClosed(back = true)) + } + + LaunchedEffect(state) { + when (state) { + is CreateUsernameState.Closed -> onBackClicked() + else -> Unit + } + } + + CreateUsernameScreen( + modifier = modifier, + onCloseClicked = onCloseClicked, + onUsernameSubmitted = { viewModel.perform(it) }, + onCreateExternalClicked = { viewModel.perform(it) }, + onCreateInternalClicked = { viewModel.perform(it) }, + onErrorMessage = onErrorMessage, + onSuccess = { signupState -> onSuccess(signupState) }, + onLoad = { viewModel.perform(LoadData(it)) }, + state = state + ) +} + +@Composable +fun CreateUsernameScreen( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = {}, + onUsernameSubmitted: (Perform) -> Unit = {}, + onCreateExternalClicked: (CreateExternalAccount) -> Unit = {}, + onCreateInternalClicked: (CreateInternalAccount) -> Unit = {}, + onErrorMessage: (String?) -> Unit = {}, + onSuccess: (String) -> Unit = {}, + onLoad: (AccountType) -> Unit = { }, + state: SignUpState +) { + LaunchedEffect(state) { + when (state) { + is CreateUsernameState.Idle -> {} + is CreateUsernameState.Error -> onErrorMessage(state.message) + is CreateUsernameState.Success -> onSuccess(state.route) + is CreateUsernameState.Load -> onLoad(state.accountType) + else -> Unit + } + } + + UsernameScreenScaffold( + modifier = modifier, + onCloseClicked = onCloseClicked, + onUsernameSubmitted = onUsernameSubmitted, + onCreateExternalClicked = onCreateExternalClicked, + onCreateInternalClicked = onCreateInternalClicked, + state = state + ) +} + +@Composable +fun UsernameScreenScaffold( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = {}, + onUsernameSubmitted: (Perform) -> Unit = {}, + onCreateExternalClicked: (CreateExternalAccount) -> Unit, + onCreateInternalClicked: (CreateInternalAccount) -> Unit, + @DrawableRes protonLogo: Int = R.drawable.ic_logo_proton, + @StringRes titleText: Int = R.string.auth_signup_title, + state: SignUpState +) { + val isLoading = (state as? CreateUsernameState)?.isLoading ?: false + val accountType = (state as? CreateUsernameState)?.accountType ?: AccountType.Internal // default + var domains by rememberSaveable { mutableStateOf>(emptyList()) } + + LaunchedEffect(state) { + if (state is CreateUsernameState.LoadingComplete) { + domains = state.domains ?: emptyList() + } + } + + Scaffold( + modifier = modifier, + topBar = { + ProtonTopAppBar( + title = {}, + navigationIcon = { + ProtonCloseButton(onCloseClicked = onCloseClicked) + }, + backgroundColor = LocalColors.current.backgroundNorm + ) + } + ) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + Column( + modifier = Modifier + .padding(top = ProtonDimens.SmallSpacing) + .verticalScroll(rememberScrollState()) + ) { + ScreenHeader( + logoResource = protonLogo, + titleTextResource = titleText + ) + + when (accountType) { + AccountType.Internal -> { + val validationError = when (state) { + is CreateUsernameState.ValidationError.InternalUsernameEmpty -> + stringResource(R.string.auth_signup_validation_username) + + is CreateUsernameState.ValidationError.Other -> + state.message ?: stringResource(R.string.auth_signup_validation_username_input_invalid) + + else -> null + } + InternalAccountForm( + enabled = !isLoading, + isLoading = isLoading, + onUsernameSubmitted = onUsernameSubmitted, + onCreateExternalClicked = onCreateExternalClicked, + validationError = validationError, + domains = domains + ) + } + + AccountType.External -> { + val validationError = when (state) { + is CreateUsernameState.ValidationError.EmailEmpty -> + stringResource(R.string.auth_signup_validation_email) + + is CreateUsernameState.ValidationError.Other -> + state.message ?: stringResource(R.string.auth_signup_validation_email_input_invalid) + + else -> null + } + ExternalAccountForm( + enabled = !isLoading, + onExternalEmailSubmitted = onUsernameSubmitted, + onCreateInternalClicked = onCreateInternalClicked, + validationError = validationError + ) + } + + AccountType.Username -> { + val validationError = when (state) { + is CreateUsernameState.ValidationError.UsernameEmpty -> + stringResource(R.string.auth_signup_validation_username) + + is CreateUsernameState.ValidationError.Other -> + state.message ?: stringResource(R.string.auth_signup_validation_username_input_invalid) + + else -> null + } + UsernameForm( + enabled = !isLoading, + isLoading = isLoading, + onUsernameSubmitted = onUsernameSubmitted, + validationError = validationError + ) + } + } + } + } + } +} + +@Composable +private fun ScreenHeader(@DrawableRes logoResource: Int, @StringRes titleTextResource: Int) { + Image( + modifier = Modifier + .height(64.dp) + .fillMaxWidth(), + painter = painterResource(logoResource), + contentDescription = null, + alignment = Alignment.Center + ) + + Text( + text = stringResource(titleTextResource), + style = ProtonTypography.Default.headline, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = ProtonDimens.MediumSpacing) + ) +} + +@Composable +private fun UsernameForm( + enabled: Boolean, + isLoading: Boolean, + onUsernameSubmitted: (Perform) -> Unit, + validationError: String?, + @StringRes subtitleText: Int = R.string.auth_signup_subtitle +) { + val scope = rememberCoroutineScope() + var username by rememberSaveable { mutableStateOf("") } + val usernameChanges = remember { MutableStateFlow(TextChange()) } + val usernameHasFocus = remember { mutableStateOf(false) } + val usernamePayloadController = remember { PayloadController() } + val usernameTextCopies = remember { MutableStateFlow("") } + + LocalClipManager.current?.OnClipChangedDisposableEffect { + if (usernameHasFocus.value) usernameTextCopies.value = it + } + + fun onSubmit() = scope.launch { + val usernameFrameDetails = usernamePayloadController.flush() + onUsernameSubmitted( + Perform( + value = username, + domain = null, + accountType = AccountType.Username, + usernameFrameDetails = usernameFrameDetails + ) + ) + } + + Column { + FormSubtitle(subtitleText) + + Column( + modifier = Modifier.padding(DefaultSpacing) + ) { + UsernameTextField( + username = username, + onUsernameChanged = { + usernameChanges.value = usernameChanges.value.roll(it) + username = it + }, + enabled = enabled, + errorText = validationError, + onFocusChanged = { usernameHasFocus.value = it.hasFocus }, + usernameChanges = usernameChanges, + usernameTextCopies = usernameTextCopies, + usernamePayloadController = usernamePayloadController + ) + + NextButton( + enabled = enabled, + isLoading = isLoading, + onClick = ::onSubmit + ) + } + } +} + +@Composable +private fun InternalAccountForm( + enabled: Boolean, + isLoading: Boolean, + onUsernameSubmitted: (Perform) -> Unit, + onCreateExternalClicked: (CreateExternalAccount) -> Unit, + validationError: String?, + domains: List, + @StringRes subtitleText: Int = R.string.auth_signup_subtitle +) { + val scope = rememberCoroutineScope() + var username by rememberSaveable { mutableStateOf("") } + var domain by rememberSaveable { mutableStateOf("") } + val usernameChanges = remember { MutableStateFlow(TextChange()) } + val usernameHasFocus = remember { mutableStateOf(false) } + val usernamePayloadController = remember { PayloadController() } + val usernameTextCopies = remember { MutableStateFlow("") } + + LocalClipManager.current?.OnClipChangedDisposableEffect { + if (usernameHasFocus.value) usernameTextCopies.value = it + } + + fun onSubmit() = scope.launch { + val usernameFrameDetails = usernamePayloadController.flush() + onUsernameSubmitted( + Perform( + value = username, + domain = domain, + accountType = AccountType.Internal, + usernameFrameDetails = usernameFrameDetails + ) + ) + } + + Column { + FormSubtitle(subtitleText) + + Column( + modifier = Modifier.padding(DefaultSpacing) + ) { + UsernameTextField( + username = username, + onUsernameChanged = { + usernameChanges.value = usernameChanges.value.roll(it) + username = it + }, + enabled = enabled, + errorText = validationError, + onFocusChanged = { usernameHasFocus.value = it.hasFocus }, + usernameChanges = usernameChanges, + usernameTextCopies = usernameTextCopies, + usernamePayloadController = usernamePayloadController + ) + + DomainDropDown( + isLoading = isLoading, + data = domains, + onInputChanged = { domain = it ?: "" } + ) + + NextButton( + enabled = enabled, + isLoading = isLoading, + onClick = ::onSubmit + ) + + FormDivider() + + AlternateAccountOption( + onClick = { onCreateExternalClicked(CreateExternalAccount) }, + textRes = R.string.auth_signup_use_current_email + ) + + FormFootnote(R.string.auth_signup_internal_footnote) + } + } +} + +@Composable +private fun ExternalAccountForm( + enabled: Boolean, + onExternalEmailSubmitted: (Perform) -> Unit, + onCreateInternalClicked: (CreateInternalAccount) -> Unit, + validationError: String? +) { + val scope = rememberCoroutineScope() + var email by rememberSaveable { mutableStateOf("") } + val emailChanges = remember { MutableStateFlow(TextChange()) } + val emailHasFocus = remember { mutableStateOf(false) } + val emailPayloadController = remember { PayloadController() } + val emailTextCopies = remember { MutableStateFlow("") } + + LocalClipManager.current?.OnClipChangedDisposableEffect { + if (emailHasFocus.value) emailTextCopies.value = it + } + + fun onSubmit() = scope.launch { + val emailFrameDetails = emailPayloadController.flush() + onExternalEmailSubmitted( + Perform( + value = email, + domain = null, + accountType = AccountType.External, + usernameFrameDetails = emailFrameDetails + ) + ) + } + + Column( + modifier = Modifier.padding(DefaultSpacing) + ) { + EmailTextField( + email = email, + onEmailChanged = { + emailChanges.value = emailChanges.value.roll(it) + email = it + }, + enabled = enabled, + errorText = validationError, + onFocusChanged = { emailHasFocus.value = it.hasFocus }, + emailChanges = emailChanges, + emailTextCopies = emailTextCopies, + emailPayloadController = emailPayloadController + ) + + NextButton( + enabled = enabled, + isLoading = !enabled, + onClick = ::onSubmit + ) + + FormDivider() + + AlternateAccountOption( + onClick = { onCreateInternalClicked(CreateInternalAccount) }, + textRes = R.string.auth_signup_get_encrypted_email + ) + + FormFootnote(R.string.auth_signup_external_footnote) + } +} + +@Composable +private fun UsernameTextField( + username: String, + onUsernameChanged: (String) -> Unit, + enabled: Boolean, + errorText: String?, + onFocusChanged: (FocusState) -> Unit, + usernameChanges: MutableStateFlow, + usernameTextCopies: MutableStateFlow, + usernamePayloadController: PayloadController +) { + ProtonOutlinedTextFieldWithError( + text = username, + onValueChanged = onUsernameChanged, + enabled = enabled, + errorText = errorText, + label = { Text(text = stringResource(id = R.string.auth_signup_email_username)) }, + singleLine = true, + modifier = Modifier + .onFocusChanged(onFocusChanged) + .fillMaxWidth() + .padding(top = DefaultSpacing) + .payload( + flow = SIGNUP_CHALLENGE_FLOW_NAME, + frame = SIGNUP_CHALLENGE_USERNAME_FRAME, + onTextChanged = usernameChanges.map { it.toPair() }, + onTextCopied = usernameTextCopies, + onFrameUpdated = {}, + payloadController = usernamePayloadController + ) + .testTag(USERNAME_FIELD_TAG) + ) +} + +@Composable +private fun EmailTextField( + email: String, + onEmailChanged: (String) -> Unit, + enabled: Boolean, + errorText: String?, + onFocusChanged: (FocusState) -> Unit, + emailChanges: MutableStateFlow, + emailTextCopies: MutableStateFlow, + emailPayloadController: PayloadController +) { + ProtonOutlinedTextFieldWithError( + text = email, + onValueChanged = onEmailChanged, + enabled = enabled, + errorText = errorText, + label = { Text(text = stringResource(id = R.string.auth_email)) }, + singleLine = true, + modifier = Modifier + .onFocusChanged(onFocusChanged) + .fillMaxWidth() + .padding(top = DefaultSpacing) + .payload( + flow = SIGNUP_CHALLENGE_FLOW_NAME, + frame = SIGNUP_CHALLENGE_USERNAME_FRAME, + onTextChanged = emailChanges.map { it.toPair() }, + onTextCopied = emailTextCopies, + onFrameUpdated = {}, + payloadController = emailPayloadController + ) + .testTag(EMAIL_FIELD_TAG) + ) +} + +@Composable +private fun FormSubtitle(@StringRes textRes: Int) { + Text( + text = stringResource(textRes), + style = ProtonTypography.Default.defaultSmallWeak, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = ProtonDimens.SmallSpacing) + ) +} + +@Composable +private fun NextButton( + enabled: Boolean, + isLoading: Boolean, + onClick: () -> Unit +) { + ProtonSolidButton( + contained = false, + enabled = enabled, + loading = isLoading, + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(top = ProtonDimens.MediumSpacing) + .height(ProtonDimens.DefaultButtonMinHeight) + ) { + Text(text = stringResource(R.string.auth_signup_next)) + } +} + +@Composable +private fun FormDivider() { + Divider( + modifier = Modifier.padding(top = ProtonDimens.MediumSpacing), + color = LocalColors.current.separatorNorm + ) +} + +@Composable +private fun AlternateAccountOption(onClick: () -> Unit, @StringRes textRes: Int) { + ProtonTextButton( + contained = false, + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ProtonDimens.MediumSpacing) + .height(ProtonDimens.DefaultButtonMinHeight) + ) { + Text(text = stringResource(textRes)) + } +} + +@Composable +private fun FormFootnote(@StringRes textRes: Int) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = textRes), + style = ProtonTypography.Default.defaultSmallWeak + ) +} + +@Preview(name = "Light mode", showBackground = true) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) +@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) +@Preview(name = "Tablet", device = Devices.PIXEL_C) +@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) +@Composable +internal fun CreateUsernameScreenPreview() { + ProtonTheme { + CreateUsernameScreen( + state = CreateUsernameState.Load(AccountType.Internal) + ) + } +} + +@Preview(name = "Light mode", showBackground = true) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) +@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) +@Preview(name = "Tablet", device = Devices.PIXEL_C) +@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) +@Composable +internal fun CreateInternalPreview() { + ProtonTheme { + InternalAccountForm( + enabled = true, + isLoading = false, + onUsernameSubmitted = {}, + onCreateExternalClicked = {}, + validationError = null, + domains = listOf("protonmail.com", "protonmail.ch") + ) + } +} + +@Preview(name = "Light mode", showBackground = true) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) +@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) +@Preview(name = "Tablet", device = Devices.PIXEL_C) +@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) +@Composable +internal fun CreateExternalPreview() { + ProtonTheme { + ExternalAccountForm( + enabled = true, + onExternalEmailSubmitted = {}, + onCreateInternalClicked = {}, + validationError = "Email is empty" + ) + } +} + +@Preview(name = "Light mode", showBackground = true) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) +@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) +@Preview(name = "Tablet", device = Devices.PIXEL_C) +@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) +@Composable +internal fun CreateUsernamePreview() { + ProtonTheme { + UsernameForm( + enabled = true, + isLoading = false, + validationError = null, + onUsernameSubmitted = {} + ) + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/DomainDropDown.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/DomainDropDown.kt similarity index 97% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/DomainDropDown.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/DomainDropDown.kt index e84b108e8e..ac1b79f370 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/DomainDropDown.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/DomainDropDown.kt @@ -17,7 +17,7 @@ */ @file:OptIn(ExperimentalMaterialApi::class) -package me.proton.android.core.auth.presentation.signup +package me.proton.android.core.auth.presentation.signup.ui import android.content.res.Configuration import androidx.compose.foundation.clickable @@ -51,13 +51,14 @@ typealias Domain = String fun DomainDropDown( isLoading: Boolean = false, data: List = emptyList(), - onInputChanged: (Domain) -> Unit = {} + onInputChanged: (Domain?) -> Unit = {} ) { var expanded by remember { mutableStateOf(false) } var selected by remember { mutableStateOf(null) } if (selected == null) { selected = data.firstOrNull() + onInputChanged(selected) } ExposedDropdownMenuBox( diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/NavigationBackButton.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/NavigationBackButton.kt new file mode 100644 index 0000000000..69be116748 --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/NavigationBackButton.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.ui + +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import ch.protonmail.android.design.compose.theme.ProtonTheme +import me.proton.core.presentation.R + +@Composable +fun NavigationBackButton(modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}) { + IconButton( + modifier = modifier, + onClick = onBackClicked + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back), + tint = ProtonTheme.colors.iconNorm, + contentDescription = stringResource(id = R.string.presentation_back) + ) + } +} + +@Preview +@Composable +fun ProtonCloseButtonPreview() { + me.proton.core.compose.theme.ProtonTheme { + NavigationBackButton() + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/SignUpCongratsScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/SignUpCongratsScreen.kt new file mode 100644 index 0000000000..07523c47bf --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/SignUpCongratsScreen.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.ui + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import me.proton.android.core.auth.presentation.R +import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT +import me.proton.android.core.auth.presentation.signup.SignUpAction.FinalizeSignup +import me.proton.android.core.auth.presentation.signup.SignUpState.LoginSuccess +import me.proton.android.core.auth.presentation.signup.SignUpState.SignUpError +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel +import me.proton.core.compose.component.ProtonSolidButton +import me.proton.core.compose.theme.ProtonDimens +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.compose.theme.ProtonTypography +import me.proton.core.compose.theme.defaultSmallWeak + +@Composable +fun SignUpCongratsScreen( + modifier: Modifier = Modifier, + onStartUsingApp: (String) -> Unit = {}, + onErrorMessage: (String?) -> Unit = {}, + @DrawableRes congratsLogo: Int = R.drawable.ic_congratulations, + @StringRes titleText: Int = R.string.auth_signup_congratulations_title, + @StringRes subtitleText: Int = R.string.auth_signup_congratulations_subtitle, + @StringRes buttonText: Int = R.string.auth_signup_start_using_proton, + viewModel: SignUpViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val currentState = state + LaunchedEffect(currentState) { + when (currentState) { + is LoginSuccess -> onStartUsingApp(currentState.userId) + is SignUpError -> onErrorMessage(currentState.message) + else -> Unit + } + } + + SignUpCongratsScreen( + modifier = modifier, + congratsLogo = congratsLogo, + titleText = titleText, + subtitleText = subtitleText, + buttonText = buttonText, + onStartClicked = { viewModel.perform(FinalizeSignup) } + ) +} + +@Composable +fun SignUpCongratsScreen( + modifier: Modifier = Modifier, + onStartClicked: () -> Unit = {}, + @DrawableRes congratsLogo: Int = R.drawable.ic_congratulations, + @StringRes titleText: Int = R.string.auth_signup_congratulations_title, + @StringRes subtitleText: Int = R.string.auth_signup_congratulations_subtitle, + @StringRes buttonText: Int = R.string.auth_signup_start_using_proton +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = ProtonDimens.DefaultSpacing), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier + .width(160.dp) + .height(160.dp) + .align(Alignment.CenterHorizontally), + painter = painterResource(congratsLogo), + contentDescription = null, + alignment = Alignment.Center + ) + + Text( + text = stringResource(titleText), + style = ProtonTypography.Default.headline, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = ProtonDimens.MediumSpacing) + ) + + Text( + text = stringResource(subtitleText), + style = ProtonTypography.Default.defaultSmallWeak, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = ProtonDimens.SmallSpacing) + ) + } + + ProtonSolidButton( + contained = false, + onClick = onStartClicked, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ProtonDimens.LargeSpacing) + .height(ProtonDimens.DefaultButtonMinHeight) + ) { + Text(text = stringResource(buttonText)) + } + } +} + +@Preview(name = "Light mode", showBackground = true) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT) +@Preview(name = "Foldable", device = Devices.PIXEL_FOLD) +@Preview(name = "Tablet", device = Devices.PIXEL_C) +@Preview(name = "Horizontal", widthDp = 800, heightDp = 360) +@Composable +internal fun SignUpCongratsPreview() { + ProtonTheme { + SignUpCongratsScreen() + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadingScreen.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/SignUpLoadingScreen.kt similarity index 69% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadingScreen.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/SignUpLoadingScreen.kt index 9c45716749..a3d7b8762d 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadingScreen.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/SignUpLoadingScreen.kt @@ -16,7 +16,7 @@ * along with Proton Mail. If not, see . */ -package me.proton.android.core.auth.presentation.signup +package me.proton.android.core.auth.presentation.signup.ui import android.content.res.Configuration import androidx.annotation.StringRes @@ -31,6 +31,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,23 +40,71 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import me.proton.android.core.auth.presentation.R import me.proton.android.core.auth.presentation.addaccount.SMALL_SCREEN_HEIGHT +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState +import me.proton.android.core.auth.presentation.signup.SignUpAction +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.SignUpState.SignUpError +import me.proton.android.core.auth.presentation.signup.SignUpState.SignUpSuccess +import me.proton.android.core.auth.presentation.signup.SignUpState.SigningUp +import me.proton.android.core.auth.presentation.signup.viewmodel.SignUpViewModel import me.proton.core.compose.theme.ProtonDimens import me.proton.core.compose.theme.ProtonTheme import me.proton.core.compose.theme.ProtonTypography import me.proton.core.compose.theme.defaultSmallWeak @Composable -fun SignUpLoading( +fun SignUpLoadingScreen( + modifier: Modifier = Modifier, + onErrorMessage: (String?) -> Unit = {}, + onSuccess: () -> Unit = {}, + viewModel: SignUpViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(state) { + when (state) { + is CreateRecoveryState.SkipSuccess, + is CreateRecoveryState.Success -> { + viewModel.perform(SignUpAction.CreateUser) + } + + else -> Unit + } + } + + SignUpLoadingScreen( + modifier = modifier, + onErrorMessage = onErrorMessage, + onSuccess = onSuccess, + state = state + ) +} + +@Composable +fun SignUpLoadingScreen( modifier: Modifier = Modifier, @StringRes titleText: Int = R.string.auth_signup_your_account_is_being_setup, - @StringRes subtitleText: Int = R.string.auth_signup_please_wait + @StringRes subtitleText: Int = R.string.auth_signup_please_wait, + onErrorMessage: (String?) -> Unit = {}, + onSuccess: () -> Unit = {}, + state: SignUpState ) { + LaunchedEffect(state) { + when (state) { + is SignUpSuccess -> onSuccess() + is SignUpError -> onErrorMessage(state.message) + else -> Unit + } + } + Column( modifier = modifier .fillMaxHeight() @@ -104,6 +153,8 @@ fun SignUpLoading( @Composable internal fun SignUpLoadingPreview() { ProtonTheme { - SignUpLoading() + SignUpLoadingScreen( + state = SigningUp + ) } } diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/TrailingIcon.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/TrailingIcon.kt similarity index 95% rename from shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/TrailingIcon.kt rename to shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/TrailingIcon.kt index 8a27010479..80b3d1b067 100644 --- a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/TrailingIcon.kt +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/ui/TrailingIcon.kt @@ -16,7 +16,7 @@ * along with Proton Mail. If not, see . */ -package me.proton.android.core.auth.presentation.signup +package me.proton.android.core.auth.presentation.signup.ui import androidx.compose.material.Icon import androidx.compose.runtime.Composable diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/PasswordHandler.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/PasswordHandler.kt new file mode 100644 index 0000000000..45313fc25b --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/PasswordHandler.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.viewmodel + +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import me.proton.android.core.auth.presentation.signup.CreatePasswordAction +import me.proton.android.core.auth.presentation.signup.CreatePasswordAction.CreatePasswordClosed +import me.proton.android.core.auth.presentation.signup.CreatePasswordAction.LoadData +import me.proton.android.core.auth.presentation.signup.CreatePasswordAction.Perform +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Closed +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Creating +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Error +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Idle +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.Success +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.ValidationError.PasswordEmpty +import me.proton.android.core.auth.presentation.signup.CreatePasswordState.ValidationError.ConfirmPasswordMissMatch +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.mapToNavigationRoute +import uniffi.proton_account_uniffi.SignupException +import uniffi.proton_account_uniffi.SignupFlow +import uniffi.proton_account_uniffi.SignupFlowSubmitPasswordResult + +/** + * Handler responsible for password-related actions during signup process. + */ +class PasswordHandler private constructor( + private val getFlow: suspend () -> SignupFlow +) : ErrorHandler { + + fun handleAction(action: CreatePasswordAction) = when (action) { + is LoadData -> flowOf(Idle) + is Perform -> handlePasswordSubmit( + password = action.password, + confirmPassword = action.confirmPassword + ) + + is CreatePasswordClosed -> handleClose(action.back) + } + + private fun handlePasswordSubmit(password: String, confirmPassword: String) = flow { + emit(Creating) + + when (val result = getFlow().submitPassword(password, confirmPassword)) { + is SignupFlowSubmitPasswordResult.Error -> { + val state = when (result.v1) { + is SignupException.PasswordEmpty -> PasswordEmpty + + is SignupException.PasswordsNotMatching -> ConfirmPasswordMissMatch + + else -> Error(message = result.v1.getErrorMessage()) + } + emit(state) + } + + is SignupFlowSubmitPasswordResult.Ok -> { + val route = result.v1.mapToNavigationRoute() + emit(Success(route)) + } + } + } + + private fun handleClose(back: Boolean) = flow { + if (back) { + getFlow().stepBack() + } + emit(Closed) + } + + override fun handleError(throwable: Throwable): SignUpState = Error(message = throwable.message) + + companion object { + + fun create(getFlow: suspend () -> SignupFlow) = PasswordHandler(getFlow) + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/RecoveryHandler.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/RecoveryHandler.kt new file mode 100644 index 0000000000..995653e7dd --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/RecoveryHandler.kt @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.viewmodel + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.flow +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.CreateRecoveryClosed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.CountryPicked +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.CountryPickerClosed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.PickCountry +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.RecoverySkipped +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.WantSkipDialogClosed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.DialogAction.WantSkipRecovery +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.SelectRecoveryMethod +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.SubmitRecoveryEmail +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction.SubmitRecoveryPhone +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Closed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.CountryPickerFailed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Creating +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Error +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Idle +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.OnCountryPicked +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.SkipFailed +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.SkipSuccess +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.Success +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.ValidationError.Email +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.ValidationError.Phone +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.WantCountryPicker +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState.WantSkip +import me.proton.android.core.auth.presentation.signup.RecoveryMethod +import me.proton.android.core.auth.presentation.signup.mapToNavigationRoute +import me.proton.android.core.auth.presentation.signup.ui.Country +import me.proton.core.challenge.domain.entity.ChallengeFrameDetails +import uniffi.proton_account_uniffi.SignupFlow +import uniffi.proton_account_uniffi.SignupFlowAvailableCountriesResult +import uniffi.proton_account_uniffi.SignupFlowSkipRecoveryResult +import uniffi.proton_account_uniffi.SignupFlowSubmitRecoveryEmailResult +import uniffi.proton_account_uniffi.SignupFlowSubmitRecoveryPhoneResult +import uniffi.proton_account_uniffi.Country as RustCountry + +/** + * Handler responsible for account recovery actions during signup process. + */ +class RecoveryHandler private constructor( + private val getFlow: suspend () -> SignupFlow, + private val coroutineDispatcher: CoroutineDispatcher +) : ErrorHandler { + + @Volatile + private var cachedCountries: List? = null + + @Volatile + private var defaultCountry: Country? = null + + @Volatile + private var selectedRecoveryMethod: RecoveryMethod = RecoveryMethod.Email + + @Volatile + private var selectedCountry: Country? = null + + fun handleAction(action: CreateRecoveryAction) = when (action) { + is SelectRecoveryMethod -> + handleSelectRecoveryMethod(action.recoveryMethod, action.locale) + + is SubmitRecoveryEmail -> handleSubmitRecoveryEmail( + email = action.email, + recoveryFrameDetails = action.recoveryFrameDetails + ) + + is SubmitRecoveryPhone -> handleSubmitRecoveryPhone( + callingCode = action.callingCode, + phoneNumber = action.phoneNumber, + recoveryFrameDetails = action.recoveryFrameDetails + ) + + is PickCountry -> handleCountryPicker() + is CountryPicked -> handleCountryPicked(action.country) + is CountryPickerClosed -> handleCountryDialogClose() + + is WantSkipRecovery -> handleWantSkipRecovery() + is RecoverySkipped -> handleRecoverySkipped() + is WantSkipDialogClosed -> handleSkipDialogClose() + + is CreateRecoveryClosed -> handleClose(action.back) + } + + private fun handleSelectRecoveryMethod(recoveryMethod: RecoveryMethod, localeFilter: String) = flow { + if (recoveryMethod == RecoveryMethod.Phone) { + if (cachedCountries.isNullOrEmpty()) { + when (val result = getFlow().availableCountries(localeFilter)) { + is SignupFlowAvailableCountriesResult.Error -> {} + is SignupFlowAvailableCountriesResult.Ok -> { + cachedCountries = result.v1.countries.map { it.toCountry() } + defaultCountry = result.v1.defaultCountry?.toCountry() + } + } + + selectedCountry = defaultCountry + } + } + selectedRecoveryMethod = recoveryMethod + emit(Idle(selectedRecoveryMethod, cachedCountries, selectedCountry)) + } + + @Suppress("ForbiddenComment") + private fun handleSubmitRecoveryEmail(email: String, recoveryFrameDetails: ChallengeFrameDetails) = flow { + if (email.isBlank()) { + emit(WantSkip(selectedRecoveryMethod)) + return@flow + } + + emit(Creating(selectedRecoveryMethod)) + // TODO: add challenge-fingerprinting + + when (val result = getFlow().submitRecoveryEmail(email)) { + is SignupFlowSubmitRecoveryEmailResult.Error -> { + emit(Email(message = result.v1.getErrorMessage())) + } + + is SignupFlowSubmitRecoveryEmailResult.Ok -> { + val route = result.v1.mapToNavigationRoute() + emit(Success(selectedRecoveryMethod, email, route)) + } + } + } + + @Suppress("ForbiddenComment") + private fun handleSubmitRecoveryPhone( + callingCode: String, + phoneNumber: String, + recoveryFrameDetails: ChallengeFrameDetails + ) = flow { + if (phoneNumber.isBlank()) { + emit(WantSkip(selectedRecoveryMethod)) + return@flow + } + + emit(Creating(selectedRecoveryMethod)) + // TODO: add challenge-fingerpriting + + val fullPhoneNumber = "$callingCode$phoneNumber" + when (val result = getFlow().submitRecoveryPhone(fullPhoneNumber)) { + is SignupFlowSubmitRecoveryPhoneResult.Error -> { + emit(Phone(message = result.v1.getErrorMessage())) + } + + is SignupFlowSubmitRecoveryPhoneResult.Ok -> { + val route = result.v1.mapToNavigationRoute() + emit(Success(selectedRecoveryMethod, fullPhoneNumber, route)) + } + } + } + + private fun handleCountryPicker() = flow { + emit(WantCountryPicker(selectedRecoveryMethod, cachedCountries ?: emptyList())) + } + + private fun handleCountryPicked(country: Country) = flow { + selectedCountry = country + emit(OnCountryPicked(selectedRecoveryMethod, country)) + } + + private fun handleWantSkipRecovery() = flow { + emit(WantSkip(selectedRecoveryMethod)) + } + + private fun handleRecoverySkipped() = flow { + when (val result = getFlow().skipRecovery()) { + is SignupFlowSkipRecoveryResult.Error -> { + emit( + Error( + recoveryMethod = selectedRecoveryMethod, + message = result.v1.getErrorMessage() + ) + ) + } + + is SignupFlowSkipRecoveryResult.Ok -> { + val route = result.v1.mapToNavigationRoute() + emit(SkipSuccess(recoveryMethod = selectedRecoveryMethod, route = route)) + } + } + } + + private fun handleSkipDialogClose() = flow { + emit(SkipFailed(recoveryMethod = selectedRecoveryMethod)) + } + + private fun handleCountryDialogClose() = flow { + emit( + CountryPickerFailed( + recoveryMethod = selectedRecoveryMethod, + country = selectedCountry + ) + ) + } + + private fun handleClose(back: Boolean) = flow { + if (back) { + getFlow().stepBack() + } + emit(Closed) + } + + override fun handleError(throwable: Throwable) = + Error(recoveryMethod = selectedRecoveryMethod, message = throwable.message) + + companion object { + + fun create(getFlow: suspend () -> SignupFlow, coroutineDispatcher: CoroutineDispatcher): RecoveryHandler = + RecoveryHandler(getFlow, coroutineDispatcher) + } +} + +fun RustCountry.toCountry() = Country( + countryCode = countryCode, + callingCode = phoneCode.toInt(), + name = countryEn +) diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/SignUpViewModel.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/SignUpViewModel.kt new file mode 100644 index 0000000000..b673036ada --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/SignUpViewModel.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import me.proton.android.core.auth.presentation.IODispatcher +import me.proton.android.core.auth.presentation.LogTag +import me.proton.android.core.auth.presentation.signup.CreatePasswordAction +import me.proton.android.core.auth.presentation.signup.CreatePasswordState +import me.proton.android.core.auth.presentation.signup.CreateRecoveryAction +import me.proton.android.core.auth.presentation.signup.CreateRecoveryState +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction +import me.proton.android.core.auth.presentation.signup.CreateUsernameState +import me.proton.android.core.auth.presentation.signup.SignUpAction +import me.proton.android.core.auth.presentation.signup.SignUpAction.CreateUser +import me.proton.android.core.auth.presentation.signup.SignUpAction.FinalizeSignup +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.SignUpState.LoginSuccess +import me.proton.android.core.auth.presentation.signup.SignUpState.SignUpError +import me.proton.android.core.auth.presentation.signup.SignUpState.SignUpSuccess +import me.proton.android.core.auth.presentation.signup.SignUpState.SigningUp +import me.proton.core.account.domain.entity.AccountType +import me.proton.core.compose.viewmodel.BaseViewModel +import me.proton.core.presentation.savedstate.state +import me.proton.core.util.kotlin.CoreLogger +import uniffi.proton_account_uniffi.SignupException +import uniffi.proton_account_uniffi.SignupFlowCompleteResult +import uniffi.proton_account_uniffi.SignupFlowCreateResult +import uniffi.proton_mail_uniffi.MailSession +import uniffi.proton_mail_uniffi.MailSessionGetAccountResult +import uniffi.proton_mail_uniffi.MailSessionGetAccountSessionsResult +import uniffi.proton_mail_uniffi.MailSessionNewSignupFlowResult +import uniffi.proton_mail_uniffi.StoredAccount +import uniffi.proton_mail_uniffi.StoredSession +import javax.inject.Inject + +@Suppress("TooGenericExceptionCaught") +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + requiredAccountType: AccountType, + private val sessionInterface: MailSession, + @IODispatcher private val coroutineDispatcher: CoroutineDispatcher +) : BaseViewModel( + initialAction = CreateUsernameAction.LoadData( + savedStateHandle.get("accountType") ?: requiredAccountType + ), + initialState = CreateUsernameState.Load(savedStateHandle.get("accountType") ?: requiredAccountType), + sharingStarted = SharingStarted.Lazily +) { + + private var currentAccountType: AccountType by savedStateHandle.state(requiredAccountType) + private val signUpFlowDeferred = viewModelScope.async { + sessionInterface.newSignupFlow() + } + private val usernameHandler = UsernameHandler.create( + getFlow = { getSignUpFlow() }, + getCurrentAccountType = { currentAccountType }, + updateAccountType = { type -> currentAccountType = type }, + coroutineDispatcher = coroutineDispatcher + ) + + private val passwordHandler = PasswordHandler.create( + getFlow = { getSignUpFlow() } + ) + + private val recoveryHandler = RecoveryHandler.create( + getFlow = { getSignUpFlow() }, + coroutineDispatcher = coroutineDispatcher + ) + + private suspend fun getSignUpFlow() = (signUpFlowDeferred.await() as MailSessionNewSignupFlowResult.Ok).v1 + + override fun onAction(action: SignUpAction) = when (action) { + is CreateUsernameAction -> usernameHandler.handleAction(action) + is CreatePasswordAction -> passwordHandler.handleAction(action) + is CreateRecoveryAction -> recoveryHandler.handleAction(action) + is CreateUser -> handleCreateUser() + is FinalizeSignup -> finalizeSignUp() + else -> emptyFlow() + } + + override suspend fun FlowCollector.onError(throwable: Throwable) { + val currentState = state.value + val errorState = when (currentState) { + is CreateUsernameState -> usernameHandler.handleError(throwable) + is CreatePasswordState -> passwordHandler.handleError(throwable) + is CreateRecoveryState -> recoveryHandler.handleError(throwable) + else -> SignUpError(throwable.message) + } + emit(errorState) + } + + private fun handleCreateUser() = flow { + emit(SigningUp) + when (val result = getSignUpFlow().create()) { + is SignupFlowCreateResult.Error -> emitAll(result.v1.onSignUpError()) + is SignupFlowCreateResult.Ok -> emit(SignUpSuccess) + } + } + + private fun finalizeSignUp() = flow { + when (val result = getSignUpFlow().complete()) { + is SignupFlowCompleteResult.Error -> { + emitAll(result.v1.onSignUpError()) + } + + is SignupFlowCompleteResult.Ok -> { + val userId = result.v1.userId + getSession(getAccount(userId))?.firstOrNull() + emit(LoginSuccess(userId)) + clearUp() + } + } + } + + private fun SignupException.onSignUpError() = flow { + getSignUpFlow().stepBack() + emit(SignUpError(message = getErrorMessage())) + } + + private suspend fun getSession(account: StoredAccount?): List? { + if (account == null) { + return null + } + + return when (val result = sessionInterface.getAccountSessions(account)) { + is MailSessionGetAccountSessionsResult.Error -> { + CoreLogger.e(LogTag.SIGNUP, "Failed to get account sessions: ${result.v1}") + null + } + + is MailSessionGetAccountSessionsResult.Ok -> result.v1 + } + } + + private suspend fun getAccount(userId: String): StoredAccount? = + when (val result = sessionInterface.getAccount(userId)) { + is MailSessionGetAccountResult.Error -> { + CoreLogger.e(LogTag.SIGNUP, "Failed to get account: ${result.v1}") + null + } + + is MailSessionGetAccountResult.Ok -> result.v1 + } + + private suspend fun clearUp() { + try { + getSignUpFlow().destroy() + } catch (e: Exception) { + CoreLogger.e(LogTag.SIGNUP, "Error destroying signup flow: ${e.message}") + } + } +} + +interface ErrorHandler { + + fun handleError(throwable: Throwable): SignUpState +} + +fun SignupException.getErrorMessage(): String? { + return when (this) { + is SignupException.Api -> v1.takeIf { it.isNotEmpty() } ?: message + is SignupException.Crypto -> v1.takeIf { it.isNotEmpty() } ?: message + else -> message + } +} diff --git a/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/UsernameHandler.kt b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/UsernameHandler.kt new file mode 100644 index 0000000000..65a5b73699 --- /dev/null +++ b/shared/core/auth/presentation/src/main/kotlin/me/proton/android/core/auth/presentation/signup/viewmodel/UsernameHandler.kt @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2022 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package me.proton.android.core.auth.presentation.signup.viewmodel + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.CreateExternalAccount +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.CreateInternalAccount +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.CreateUsernameClosed +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.LoadData +import me.proton.android.core.auth.presentation.signup.CreateUsernameAction.Perform +import me.proton.android.core.auth.presentation.signup.CreateUsernameState +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.Closed +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.Creating +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.Error +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.Load +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.LoadingComplete +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.Success +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.ValidationError.EmailEmpty +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.ValidationError.InternalUsernameEmpty +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.ValidationError.Other +import me.proton.android.core.auth.presentation.signup.CreateUsernameState.ValidationError.UsernameEmpty +import me.proton.android.core.auth.presentation.signup.SignUpState +import me.proton.android.core.auth.presentation.signup.ValidationField +import me.proton.android.core.auth.presentation.signup.mapToNavigationRoute +import me.proton.core.account.domain.entity.AccountType +import me.proton.core.challenge.domain.entity.ChallengeFrameDetails +import uniffi.proton_account_uniffi.SignupException +import uniffi.proton_account_uniffi.SignupFlow +import uniffi.proton_account_uniffi.SignupFlowAvailableDomainsResult +import uniffi.proton_account_uniffi.SignupFlowSubmitExternalUsernameResult +import uniffi.proton_account_uniffi.SignupFlowSubmitInternalUsernameResult + +/** + * Handler responsible for username-related actions during signup process. + */ +class UsernameHandler private constructor( + private val getFlow: suspend () -> SignupFlow, + private val getCurrentAccountType: () -> AccountType, + private val updateAccountType: (AccountType) -> Unit, + private val coroutineDispatcher: CoroutineDispatcher +) : ErrorHandler { + + fun handleAction(action: CreateUsernameAction) = when (action) { + is LoadData -> handleUsernameLoad(action.accountType) + is CreateExternalAccount -> handleCreateExternalAccount() + is CreateInternalAccount -> handleCreateInternalAccount() + is Perform -> handleUsernameSubmit( + accountType = action.accountType, + username = action.value, + domain = action.domain, + usernameFrameDetails = action.usernameFrameDetails + ) + + is CreateUsernameClosed -> handleClose(action.back) + } + + private fun handleUsernameLoad(accountType: AccountType) = flow { + emit(Load(accountType = accountType, isLoading = true)) + emitAll( + when (accountType) { + AccountType.Internal -> handleCreateInternalAccount() + AccountType.External -> handleCreateExternalAccount() + AccountType.Username -> handleCreateUsernameAccount() + } + ) + } + + private fun handleCreateUsernameAccount() = flow { + updateAccountType(AccountType.External) + emit(LoadingComplete(AccountType.Username)) + } + + private fun handleCreateInternalAccount() = flow { + updateAccountType(AccountType.Internal) + when (val result = getFlow().availableDomains()) { + is SignupFlowAvailableDomainsResult.Error -> Error( + accountType = getCurrentAccountType(), + isLoading = false, + message = result.v1.getErrorMessage() + ) + is SignupFlowAvailableDomainsResult.Ok -> emit(LoadingComplete(AccountType.Internal, domains = result.v1)) + } + } + + private fun handleCreateExternalAccount() = flow { + updateAccountType(AccountType.External) + emit(LoadingComplete(AccountType.External)) + } + + @Suppress("ForbiddenComment") + private fun handleUsernameSubmit( + accountType: AccountType, + username: String, + domain: String?, + usernameFrameDetails: ChallengeFrameDetails + ) = flow { + emit(Creating(accountType, isLoading = true)) + + val result = when (accountType) { + AccountType.Username, AccountType.Internal -> { // currently no difference between username and internal + requireNotNull(domain) { "Domain must be set for Internal Account type." } + if (username.isEmpty()) { + emit(InternalUsernameEmpty) + return@flow + } + handleInternalUsernameSubmission(accountType, username, domain) + } + + AccountType.External -> { + if (username.isEmpty()) { + emit(EmailEmpty) + return@flow + } + handleExternalUsernameSubmission(accountType, username) + } + } + + // TODO: add challenge-fingerprinting for + emit(result) + } + + private suspend fun handleInternalUsernameSubmission( + accountType: AccountType, + username: String, + domain: String + ): CreateUsernameState { + return when (val result = getFlow().submitInternalUsername(username, domain)) { + is SignupFlowSubmitInternalUsernameResult.Error -> { + when (result.v1) { + is SignupException.UsernameUnavailable -> + Other( + accountType = accountType, + field = ValidationField.USERNAME, + message = result.v1.getErrorMessage() + ) + + else -> Error( + accountType = accountType, + isLoading = false, + message = result.v1.getErrorMessage() + ) + } + } + + is SignupFlowSubmitInternalUsernameResult.Ok -> + Success( + accountType = accountType, + username = username, + domain = domain, + route = result.v1.mapToNavigationRoute() + ) + } + } + + private suspend fun handleExternalUsernameSubmission(accountType: AccountType, email: String): CreateUsernameState { + return when (val result = getFlow().submitExternalUsername(email)) { + is SignupFlowSubmitExternalUsernameResult.Error -> { + when (result.v1) { + is SignupException.UsernameUnavailable -> + Other( + accountType = accountType, + field = ValidationField.EMAIL, + message = result.v1.getErrorMessage() + ) + + is SignupException.UsernameEmpty -> UsernameEmpty + + else -> Error( + accountType = accountType, + message = result.v1.getErrorMessage() + ) + } + } + + is SignupFlowSubmitExternalUsernameResult.Ok -> + Success( + accountType = accountType, + username = email, + route = result.v1.mapToNavigationRoute() + ) + } + } + + private fun handleClose(back: Boolean) = flow { + if (back) { + getFlow().stepBack() + } + emit(Closed(getCurrentAccountType())) + } + + override fun handleError(throwable: Throwable): SignUpState = + Error(accountType = getCurrentAccountType(), message = throwable.message) + + companion object { + + fun create( + getFlow: suspend () -> SignupFlow, + getCurrentAccountType: () -> AccountType, + updateAccountType: (AccountType) -> Unit, + coroutineDispatcher: CoroutineDispatcher + ): UsernameHandler = UsernameHandler( + getFlow, getCurrentAccountType, updateAccountType, coroutineDispatcher + ) + } +} diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ad.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ad.xml new file mode 100644 index 0000000000..40a1a113fc --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ad.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ae.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ae.xml new file mode 100644 index 0000000000..f87ed98d3c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ae.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_af.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_af.xml new file mode 100644 index 0000000000..84b48ae639 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_af.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ag.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ag.xml new file mode 100644 index 0000000000..4394dcfc5f --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ag.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ai.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ai.xml new file mode 100644 index 0000000000..c72db63109 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ai.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_al.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_al.xml new file mode 100644 index 0000000000..44d667c79e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_al.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_am.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_am.xml new file mode 100644 index 0000000000..5b04a83add --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_am.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ao.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ao.xml new file mode 100644 index 0000000000..dc0a763bc5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ao.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ar.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ar.xml new file mode 100644 index 0000000000..310105add0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ar.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_as.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_as.xml new file mode 100644 index 0000000000..9ec82f1f12 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_as.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_at.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_at.xml new file mode 100644 index 0000000000..3d3f883f62 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_at.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_au.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_au.xml new file mode 100644 index 0000000000..edb20ed616 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_au.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_aw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_aw.xml new file mode 100644 index 0000000000..19e8ecda85 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_aw.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_az.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_az.xml new file mode 100644 index 0000000000..219a790f8b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_az.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ba.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ba.xml new file mode 100644 index 0000000000..2e94466b27 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ba.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bb.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bb.xml new file mode 100644 index 0000000000..c4b1155df9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bb.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bd.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bd.xml new file mode 100644 index 0000000000..5649150fdf --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bd.xml @@ -0,0 +1,12 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_be.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_be.xml new file mode 100644 index 0000000000..b592e03d00 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_be.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bf.xml new file mode 100644 index 0000000000..8a559ec794 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bf.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bg.xml new file mode 100644 index 0000000000..3a677716c0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bh.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bh.xml new file mode 100644 index 0000000000..7b54cac1b6 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bh.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bi.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bi.xml new file mode 100644 index 0000000000..f17ff3d078 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bi.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bj.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bj.xml new file mode 100644 index 0000000000..2e0a776937 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bj.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bl.xml new file mode 100644 index 0000000000..f6916c9da5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bl.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bm.xml new file mode 100644 index 0000000000..7f7863b0cd --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bn.xml new file mode 100644 index 0000000000..65f50f51be --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bn.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bo.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bo.xml new file mode 100644 index 0000000000..dc87c1df8c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bo.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bq.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bq.xml new file mode 100644 index 0000000000..e82de513ca --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bq.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_br.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_br.xml new file mode 100644 index 0000000000..6910d40aa4 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_br.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bs.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bs.xml new file mode 100644 index 0000000000..ddba952cdb --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bs.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bt.xml new file mode 100644 index 0000000000..278a0eabea --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bt.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bw.xml new file mode 100644 index 0000000000..0254fb596c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bw.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_by.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_by.xml new file mode 100644 index 0000000000..ad1ab46706 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_by.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_bz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_bz.xml new file mode 100644 index 0000000000..dd62c5610d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_bz.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ca.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ca.xml new file mode 100644 index 0000000000..1b2ecfba96 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ca.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cd.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cd.xml new file mode 100644 index 0000000000..feb22f1034 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cd.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cf.xml new file mode 100644 index 0000000000..34fd39532e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cf.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cg.xml new file mode 100644 index 0000000000..74fe872ce7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ch.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ch.xml new file mode 100644 index 0000000000..faeaeaeed9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ch.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ci.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ci.xml new file mode 100644 index 0000000000..9fae1f2ab5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ci.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ck.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ck.xml new file mode 100644 index 0000000000..2d34562a8d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ck.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cl.xml new file mode 100644 index 0000000000..7900ec95c9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cl.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cm.xml new file mode 100644 index 0000000000..4d3cfa3d7d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cm.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cn.xml new file mode 100644 index 0000000000..951dda2334 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cn.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_co.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_co.xml new file mode 100644 index 0000000000..eafae3516e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_co.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cr.xml new file mode 100644 index 0000000000..b6dac846ff --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cr.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cu.xml new file mode 100644 index 0000000000..26ec2156e7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cu.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cv.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cv.xml new file mode 100644 index 0000000000..dd447a8c2c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cv.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cw.xml new file mode 100644 index 0000000000..2038d5a020 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cw.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cy.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cy.xml new file mode 100644 index 0000000000..edee11b7ee --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cy.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_cz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_cz.xml new file mode 100644 index 0000000000..5baab8209f --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_cz.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_de.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_de.xml new file mode 100644 index 0000000000..973b91af6e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_de.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_dj.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_dj.xml new file mode 100644 index 0000000000..b05f5bc6ba --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_dj.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_dk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_dk.xml new file mode 100644 index 0000000000..ab8f8d043a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_dk.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_dm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_dm.xml new file mode 100644 index 0000000000..9caa897a95 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_dm.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_do.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_do.xml new file mode 100644 index 0000000000..be01f0d17e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_do.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_dz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_dz.xml new file mode 100644 index 0000000000..90345b3dd6 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_dz.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ec.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ec.xml new file mode 100644 index 0000000000..341822dd9e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ec.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ee.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ee.xml new file mode 100644 index 0000000000..f130603c10 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ee.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_eg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_eg.xml new file mode 100644 index 0000000000..5cb975cde0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_eg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_eh.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_eh.xml new file mode 100644 index 0000000000..8033da31f7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_eh.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_er.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_er.xml new file mode 100644 index 0000000000..79fb8cde0e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_er.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_es.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_es.xml new file mode 100644 index 0000000000..62c28dcf96 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_es.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_et.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_et.xml new file mode 100644 index 0000000000..cc119a4dba --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_et.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_fi.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_fi.xml new file mode 100644 index 0000000000..ca298de2a9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_fi.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_fj.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_fj.xml new file mode 100644 index 0000000000..d2e054e8d0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_fj.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_fk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_fk.xml new file mode 100644 index 0000000000..1d9c7fc2ae --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_fk.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_fm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_fm.xml new file mode 100644 index 0000000000..6a19d97f5e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_fm.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_fo.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_fo.xml new file mode 100644 index 0000000000..36cef52611 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_fo.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_fr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_fr.xml new file mode 100644 index 0000000000..429527313e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_fr.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ga.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ga.xml new file mode 100644 index 0000000000..2d182cce10 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ga.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gb.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gb.xml new file mode 100644 index 0000000000..cb6e5c3373 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gb.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gd.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gd.xml new file mode 100644 index 0000000000..dc2235ddad --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gd.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ge.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ge.xml new file mode 100644 index 0000000000..eb3f61f957 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ge.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gf.xml new file mode 100644 index 0000000000..4752369ea5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gf.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gg.xml new file mode 100644 index 0000000000..ca881e0507 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gh.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gh.xml new file mode 100644 index 0000000000..fddda17125 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gh.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gi.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gi.xml new file mode 100644 index 0000000000..5b2ce98f28 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gi.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gl.xml new file mode 100644 index 0000000000..8b450a7267 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gl.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gm.xml new file mode 100644 index 0000000000..4b0cf3b063 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gm.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gn.xml new file mode 100644 index 0000000000..395b643af5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gn.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gp.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gp.xml new file mode 100644 index 0000000000..0e09c00a6c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gp.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gq.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gq.xml new file mode 100644 index 0000000000..376107f7ad --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gq.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gr.xml new file mode 100644 index 0000000000..652613cf82 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gr.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gt.xml new file mode 100644 index 0000000000..f65070912b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gt.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gu.xml new file mode 100644 index 0000000000..891fa0b745 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gu.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gw.xml new file mode 100644 index 0000000000..48382ca760 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gw.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_gy.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_gy.xml new file mode 100644 index 0000000000..b1602e0096 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_gy.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_hk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_hk.xml new file mode 100644 index 0000000000..d76256aea3 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_hk.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_hn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_hn.xml new file mode 100644 index 0000000000..4caaa4f4b0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_hn.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_hr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_hr.xml new file mode 100644 index 0000000000..49a55e797b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_hr.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ht.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ht.xml new file mode 100644 index 0000000000..b3ad859eac --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ht.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_hu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_hu.xml new file mode 100644 index 0000000000..c0ee9736d7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_hu.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_id.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_id.xml new file mode 100644 index 0000000000..cf4c272af6 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_id.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ie.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ie.xml new file mode 100644 index 0000000000..262f462b00 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ie.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_il.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_il.xml new file mode 100644 index 0000000000..2c69e893fb --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_il.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_im.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_im.xml new file mode 100644 index 0000000000..7a029c0c91 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_im.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_in.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_in.xml new file mode 100644 index 0000000000..bc45b93f3a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_in.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_io.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_io.xml new file mode 100644 index 0000000000..2f1b2aeccf --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_io.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_iq.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_iq.xml new file mode 100644 index 0000000000..6ebf3eb5a6 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_iq.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ir.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ir.xml new file mode 100644 index 0000000000..5e00cbe222 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ir.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_is.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_is.xml new file mode 100644 index 0000000000..9f1379a1c3 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_is.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_it.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_it.xml new file mode 100644 index 0000000000..4831617015 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_it.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_je.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_je.xml new file mode 100644 index 0000000000..d976fbeaa7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_je.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_jm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_jm.xml new file mode 100644 index 0000000000..94f7cc70fb --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_jm.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_jo.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_jo.xml new file mode 100644 index 0000000000..89d7299f23 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_jo.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_jp.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_jp.xml new file mode 100644 index 0000000000..1e4245b094 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_jp.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ke.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ke.xml new file mode 100644 index 0000000000..ffc6205cf4 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ke.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kg.xml new file mode 100644 index 0000000000..b1dee98e13 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kg.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kh.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kh.xml new file mode 100644 index 0000000000..475c2ae5de --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ki.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ki.xml new file mode 100644 index 0000000000..8a9150a0c5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ki.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_km.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_km.xml new file mode 100644 index 0000000000..404bc77b9f --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_km.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kn.xml new file mode 100644 index 0000000000..49d5c45629 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kn.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kp.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kp.xml new file mode 100644 index 0000000000..1d65ae7d69 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kp.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kr.xml new file mode 100644 index 0000000000..f6017e6253 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kr.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kw.xml new file mode 100644 index 0000000000..244d591d00 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kw.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ky.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ky.xml new file mode 100644 index 0000000000..f35d139817 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ky.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_kz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_kz.xml new file mode 100644 index 0000000000..1a217f7b10 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_kz.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_la.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_la.xml new file mode 100644 index 0000000000..6a06ad6a85 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_la.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lb.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lb.xml new file mode 100644 index 0000000000..54cfa9c366 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lb.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lc.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lc.xml new file mode 100644 index 0000000000..c428c7865e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lc.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_li.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_li.xml new file mode 100644 index 0000000000..29153b21f2 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_li.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lk.xml new file mode 100644 index 0000000000..835207dbe0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lk.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lr.xml new file mode 100644 index 0000000000..7da15f600e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lr.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ls.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ls.xml new file mode 100644 index 0000000000..30d793b2fe --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ls.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lt.xml new file mode 100644 index 0000000000..710547317e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lt.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lu.xml new file mode 100644 index 0000000000..2ec5baeb27 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lu.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_lv.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_lv.xml new file mode 100644 index 0000000000..d13722c72d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_lv.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ly.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ly.xml new file mode 100644 index 0000000000..2b524a7e73 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ly.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ma.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ma.xml new file mode 100644 index 0000000000..5f24902686 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ma.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mc.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mc.xml new file mode 100644 index 0000000000..3109df807e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mc.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_md.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_md.xml new file mode 100644 index 0000000000..46822d49f8 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_md.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_me.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_me.xml new file mode 100644 index 0000000000..d9a162f19e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_me.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mf.xml new file mode 100644 index 0000000000..429527313e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mf.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mg.xml new file mode 100644 index 0000000000..f45ad6cfb8 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mh.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mh.xml new file mode 100644 index 0000000000..9a40cd6095 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mk.xml new file mode 100644 index 0000000000..877b664e9d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mk.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ml.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ml.xml new file mode 100644 index 0000000000..c110a6bde1 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ml.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mm.xml new file mode 100644 index 0000000000..d27f284439 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mm.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mn.xml new file mode 100644 index 0000000000..1a1a7482d2 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mn.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mo.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mo.xml new file mode 100644 index 0000000000..10b3285fa8 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mo.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mp.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mp.xml new file mode 100644 index 0000000000..3d4191bf7d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mp.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mq.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mq.xml new file mode 100644 index 0000000000..e18b85e0cc --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mq.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mr.xml new file mode 100644 index 0000000000..3dc2dcaf99 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mr.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ms.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ms.xml new file mode 100644 index 0000000000..8752d90ddd --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ms.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mt.xml new file mode 100644 index 0000000000..a51a3ab380 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mt.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mu.xml new file mode 100644 index 0000000000..b71ef9bdf9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mu.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mv.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mv.xml new file mode 100644 index 0000000000..87ab58799c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mv.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mw.xml new file mode 100644 index 0000000000..5bd39d3557 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mw.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mx.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mx.xml new file mode 100644 index 0000000000..e80806b248 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mx.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_my.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_my.xml new file mode 100644 index 0000000000..b2e9e5178f --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_my.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_mz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_mz.xml new file mode 100644 index 0000000000..194bc3fcd5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_mz.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_na.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_na.xml new file mode 100644 index 0000000000..c6e95a0632 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_na.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_nc.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_nc.xml new file mode 100644 index 0000000000..5bd29a200d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_nc.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ne.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ne.xml new file mode 100644 index 0000000000..d51e758155 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ne.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_nf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_nf.xml new file mode 100644 index 0000000000..49437ba825 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_nf.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ng.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ng.xml new file mode 100644 index 0000000000..fbb72a6f69 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ng.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ni.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ni.xml new file mode 100644 index 0000000000..d2ded19912 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ni.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_nl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_nl.xml new file mode 100644 index 0000000000..976f92502d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_nl.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_no.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_no.xml new file mode 100644 index 0000000000..870ab8f1f2 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_no.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_np.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_np.xml new file mode 100644 index 0000000000..7b5f8b6ff0 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_np.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_nr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_nr.xml new file mode 100644 index 0000000000..be2369e18c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_nr.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_nu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_nu.xml new file mode 100644 index 0000000000..6f6c5e059b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_nu.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_nz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_nz.xml new file mode 100644 index 0000000000..6096bf9e14 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_nz.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_om.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_om.xml new file mode 100644 index 0000000000..3c846d19ab --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_om.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pa.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pa.xml new file mode 100644 index 0000000000..9423cfb341 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pa.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pe.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pe.xml new file mode 100644 index 0000000000..75799acccc --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pe.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pf.xml new file mode 100644 index 0000000000..c594d4055d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pf.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pg.xml new file mode 100644 index 0000000000..f4548e82ea --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ph.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ph.xml new file mode 100644 index 0000000000..59363bc315 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ph.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pk.xml new file mode 100644 index 0000000000..a5cc4a1010 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pk.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pl.xml new file mode 100644 index 0000000000..aba5a5346e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pl.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pm.xml new file mode 100644 index 0000000000..f6da70a76a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pm.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pr.xml new file mode 100644 index 0000000000..e014d0d9d2 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pr.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ps.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ps.xml new file mode 100644 index 0000000000..b5c82e4757 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ps.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pt.xml new file mode 100644 index 0000000000..64e5109d05 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pt.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_pw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_pw.xml new file mode 100644 index 0000000000..0792508385 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_pw.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_py.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_py.xml new file mode 100644 index 0000000000..0100b1924b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_py.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_qa.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_qa.xml new file mode 100644 index 0000000000..aa6d01dfcc --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_qa.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_re.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_re.xml new file mode 100644 index 0000000000..cb304ca6fa --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_re.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ro.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ro.xml new file mode 100644 index 0000000000..78c93770ae --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ro.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_rs.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_rs.xml new file mode 100644 index 0000000000..0b0c28128a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_rs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ru.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ru.xml new file mode 100644 index 0000000000..a53f5da7ca --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ru.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_rw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_rw.xml new file mode 100644 index 0000000000..89ea9a8295 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_rw.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sa.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sa.xml new file mode 100644 index 0000000000..056e6a1d20 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sa.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sb.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sb.xml new file mode 100644 index 0000000000..3b10d1db3e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sb.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sc.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sc.xml new file mode 100644 index 0000000000..b853061b29 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sc.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sd.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sd.xml new file mode 100644 index 0000000000..7ac4f6bc46 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sd.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_se.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_se.xml new file mode 100644 index 0000000000..05c6ee13a9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_se.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sg.xml new file mode 100644 index 0000000000..e7a4274d7b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sh.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sh.xml new file mode 100644 index 0000000000..3789ef88b9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sh.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_si.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_si.xml new file mode 100644 index 0000000000..26d8b2d7d4 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_si.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sk.xml new file mode 100644 index 0000000000..f7c4246066 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sk.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sl.xml new file mode 100644 index 0000000000..a9c7a5ac27 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sl.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sm.xml new file mode 100644 index 0000000000..d34656ac27 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sm.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sn.xml new file mode 100644 index 0000000000..d8e7bc98a3 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sn.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_so.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_so.xml new file mode 100644 index 0000000000..aa10af3c77 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_so.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sr.xml new file mode 100644 index 0000000000..6deb2dbe46 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sr.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ss.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ss.xml new file mode 100644 index 0000000000..98ba2807da --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ss.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_st.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_st.xml new file mode 100644 index 0000000000..3ee8043280 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_st.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sv.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sv.xml new file mode 100644 index 0000000000..e3f8a75aca --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sv.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sx.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sx.xml new file mode 100644 index 0000000000..768028744c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sx.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sy.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sy.xml new file mode 100644 index 0000000000..2fd4bc0f91 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sy.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_sz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_sz.xml new file mode 100644 index 0000000000..d727d217f7 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_sz.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tc.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tc.xml new file mode 100644 index 0000000000..c459642ee6 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_td.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_td.xml new file mode 100644 index 0000000000..78c93770ae --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_td.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tg.xml new file mode 100644 index 0000000000..dce1bad56f --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_th.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_th.xml new file mode 100644 index 0000000000..5fa1af5522 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_th.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tj.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tj.xml new file mode 100644 index 0000000000..98bce68a35 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tj.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tk.xml new file mode 100644 index 0000000000..52a0d9bf5d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tk.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tl.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tl.xml new file mode 100644 index 0000000000..a2d0731afa --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tl.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tm.xml new file mode 100644 index 0000000000..28c99336fa --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tm.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tn.xml new file mode 100644 index 0000000000..e3dad897ea --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tn.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_to.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_to.xml new file mode 100644 index 0000000000..041f50e8bb --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_to.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tr.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tr.xml new file mode 100644 index 0000000000..e1112223c2 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tr.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tt.xml new file mode 100644 index 0000000000..07950c108b --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tt.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tv.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tv.xml new file mode 100644 index 0000000000..8539489c19 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tv.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tw.xml new file mode 100644 index 0000000000..b3421d1c0c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tw.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_tz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_tz.xml new file mode 100644 index 0000000000..9febb330be --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_tz.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ua.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ua.xml new file mode 100644 index 0000000000..4d4e16699e --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ua.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ug.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ug.xml new file mode 100644 index 0000000000..783f4bbd2a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ug.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_us.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_us.xml new file mode 100644 index 0000000000..f92b6c5f69 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_us.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_uy.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_uy.xml new file mode 100644 index 0000000000..03e382068a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_uy.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_uz.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_uz.xml new file mode 100644 index 0000000000..87e5438874 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_uz.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_va.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_va.xml new file mode 100644 index 0000000000..fe8548552f --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_va.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_vc.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_vc.xml new file mode 100644 index 0000000000..beb54ea326 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_vc.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ve.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ve.xml new file mode 100644 index 0000000000..03311fd2a4 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ve.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_vg.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_vg.xml new file mode 100644 index 0000000000..ff774479b9 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_vg.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_vi.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_vi.xml new file mode 100644 index 0000000000..91567972f3 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_vi.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_vn.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_vn.xml new file mode 100644 index 0000000000..d3d9ff162d --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_vn.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_vu.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_vu.xml new file mode 100644 index 0000000000..56c3952cb3 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_vu.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_wf.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_wf.xml new file mode 100644 index 0000000000..3eb0ab9898 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_wf.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ws.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ws.xml new file mode 100644 index 0000000000..bf2750eaff --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ws.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_xk.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_xk.xml new file mode 100644 index 0000000000..c1f10e0c8a --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_xk.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_ye.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_ye.xml new file mode 100644 index 0000000000..1b84ab6a47 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_ye.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_yt.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_yt.xml new file mode 100644 index 0000000000..8c3f547d49 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_yt.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_za.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_za.xml new file mode 100644 index 0000000000..40faf1f34c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_za.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_zm.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_zm.xml new file mode 100644 index 0000000000..37e7cc90c5 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_zm.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/flag_zw.xml b/shared/core/auth/presentation/src/main/res/drawable/flag_zw.xml new file mode 100644 index 0000000000..1eb9b3ff0c --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/flag_zw.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/drawable/ic_congratulations.xml b/shared/core/auth/presentation/src/main/res/drawable/ic_congratulations.xml new file mode 100644 index 0000000000..e6a377e9b4 --- /dev/null +++ b/shared/core/auth/presentation/src/main/res/drawable/ic_congratulations.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/core/auth/presentation/src/main/res/values/strings.xml b/shared/core/auth/presentation/src/main/res/values/strings.xml index 155aa2528a..7e73c9a703 100644 --- a/shared/core/auth/presentation/src/main/res/values/strings.xml +++ b/shared/core/auth/presentation/src/main/res/values/strings.xml @@ -75,6 +75,7 @@ Use your current email instead Get a new encrypted email address With a Proton address, you can use all Proton services + Skip Set recovery method (optional) We will send recovery instructions to this email or phone number if you get locked out of your account. @@ -82,7 +83,28 @@ Phone Recovery email Recovery phone number + Select Country XX XXX XX XX + + Skip recovery method? + A recovery method will help you access your account in case you forget your password or get locked out of your account. + Skip + Set recovery method + Your account is being created… It usually takes no more than a minute. + + Your Proton account was successfully created.\nEnjoy the world of privacy. + Congratulations! + Start using Proton + + Password must not be empty. + Passwords do not match. + Password input invalid. + Username must not be blank. + Email must not be blank. + Username input invalid. + Username input invalid. + + Search country diff --git a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordScreenTest.kt b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordStateScreenTest.kt similarity index 93% rename from shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordScreenTest.kt rename to shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordStateScreenTest.kt index f8635f292c..ae2e5919a5 100644 --- a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordScreenTest.kt +++ b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreatePasswordStateScreenTest.kt @@ -13,6 +13,7 @@ import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import me.proton.android.core.auth.presentation.secondfactor.otp.OneTimePasswordInputState import me.proton.android.core.auth.presentation.secondfactor.otp.OneTimePasswordInputViewModel +import me.proton.android.core.auth.presentation.signup.ui.CreatePasswordContent import me.proton.core.compose.theme.ProtonTheme import org.junit.Rule import org.junit.runner.RunWith @@ -21,7 +22,7 @@ import org.junit.runners.Parameterized.Parameters import kotlin.test.Test @RunWith(Parameterized::class) -class CreatePasswordScreenTest( +class CreatePasswordStateScreenTest( config: DeviceConfig ) { @@ -45,7 +46,7 @@ class CreatePasswordScreenTest( paparazzi.snapshot { CompositionLocalProvider(LocalViewModelStoreOwner provides fakeViewModelStoreOwner) { ProtonTheme { - CreatePasswordScreen( + CreatePasswordContent( state = CreatePasswordState.Idle ) } diff --git a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryScreenTest.kt b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryStateScreenTest.kt similarity index 90% rename from shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryScreenTest.kt rename to shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryStateScreenTest.kt index ebb35d9f1d..5df24e4003 100644 --- a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryScreenTest.kt +++ b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateRecoveryStateScreenTest.kt @@ -13,6 +13,7 @@ import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import me.proton.android.core.auth.presentation.secondfactor.otp.OneTimePasswordInputState import me.proton.android.core.auth.presentation.secondfactor.otp.OneTimePasswordInputViewModel +import me.proton.android.core.auth.presentation.signup.ui.CreateRecoveryScreen import me.proton.core.compose.theme.ProtonTheme import org.junit.Rule import org.junit.runner.RunWith @@ -21,7 +22,7 @@ import org.junit.runners.Parameterized.Parameters import kotlin.test.Test @RunWith(Parameterized::class) -class CreateRecoveryScreenTest( +class CreateRecoveryStateScreenTest( config: DeviceConfig ) { @@ -46,7 +47,7 @@ class CreateRecoveryScreenTest( CompositionLocalProvider(LocalViewModelStoreOwner provides fakeViewModelStoreOwner) { ProtonTheme { CreateRecoveryScreen( - state = CreateRecoveryState.Idle(RecoveryMethod.Email) + state = CreateRecoveryState.Idle(recoveryMethod = RecoveryMethod.Email, defaultCountry = null) ) } } diff --git a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameScreenTest.kt b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameStateScreenTest.kt similarity index 93% rename from shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameScreenTest.kt rename to shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameStateScreenTest.kt index 78eeae8b17..b8b3609a78 100644 --- a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameScreenTest.kt +++ b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/CreateUsernameStateScreenTest.kt @@ -13,6 +13,8 @@ import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import me.proton.android.core.auth.presentation.secondfactor.otp.OneTimePasswordInputState import me.proton.android.core.auth.presentation.secondfactor.otp.OneTimePasswordInputViewModel +import me.proton.android.core.auth.presentation.signup.ui.CreateUsernameScreen +import me.proton.core.account.domain.entity.AccountType import me.proton.core.compose.theme.ProtonTheme import org.junit.Rule import org.junit.runner.RunWith @@ -21,7 +23,7 @@ import org.junit.runners.Parameterized.Parameters import kotlin.test.Test @RunWith(Parameterized::class) -class CreateUsernameScreenTest( +class CreateUsernameStateScreenTest( config: DeviceConfig ) { diff --git a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadingScreenTest.kt b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadScreenTest.kt similarity index 70% rename from shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadingScreenTest.kt rename to shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadScreenTest.kt index 4ea624557b..c1194f10cd 100644 --- a/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadingScreenTest.kt +++ b/shared/core/auth/presentation/src/test/kotlin/me/proton/android/core/auth/presentation/signup/SignUpLoadScreenTest.kt @@ -4,6 +4,8 @@ import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import com.android.resources.NightMode import com.android.resources.ScreenOrientation +import me.proton.android.core.auth.presentation.signup.ui.SignUpLoadingScreen +import me.proton.core.account.domain.entity.AccountType import org.junit.Rule import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -11,7 +13,7 @@ import org.junit.runners.Parameterized.Parameters import kotlin.test.Test @RunWith(Parameterized::class) -class SignUpLoadingScreenTest( +class SignUpLoadScreenTest( config: DeviceConfig ) { @@ -21,7 +23,12 @@ class SignUpLoadingScreenTest( @Test fun signUpLoadingScreen() { paparazzi.snapshot { - SignUpLoading() + SignUpLoadingScreen( + state = CreateUsernameState.Load( + accountType = AccountType.Internal, + isLoading = false + ) + ) } } diff --git a/shared/core/gradle/wrapper/gradle-wrapper.properties b/shared/core/gradle/wrapper/gradle-wrapper.properties index 4fb963073d..a9a7ff593f 100644 --- a/shared/core/gradle/wrapper/gradle-wrapper.properties +++ b/shared/core/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jul 23 17:11:41 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists