diff --git a/android/app-newm/build.gradle.kts b/android/app-newm/build.gradle.kts index f5234b899..77ef1ea41 100644 --- a/android/app-newm/build.gradle.kts +++ b/android/app-newm/build.gradle.kts @@ -117,12 +117,12 @@ dependencies { implementation(libs.cmp.image.pick.n.crop) implementation(platform(libs.firebase.bom)) implementation(libs.androidx.material.icons.extended) + implementation(compose.components.resources) implementation(project(Modules.BARCODE_SCANNER)) implementation(project(Modules.CORE_ANDROID_IMPLEMENTATIONS)) implementation(project(Modules.CORE_RESOURCES)) implementation(project(Modules.CORE_THEME)) implementation(project(Modules.CORE_UI_UTILS)) - implementation(project(Modules.LOGIN)) implementation(project(Modules.MUSIC_PLAYER)) implementation(project(Modules.SHARED)) implementation(project(Modules.SHARED_COMPOSE_FEATURES)) diff --git a/android/app-newm/src/main/java/io/newm/HomeActivity.kt b/android/app-newm/src/main/java/io/newm/HomeActivity.kt index ca2674d0a..60dcb62a7 100644 --- a/android/app-newm/src/main/java/io/newm/HomeActivity.kt +++ b/android/app-newm/src/main/java/io/newm/HomeActivity.kt @@ -56,6 +56,14 @@ import io.newm.shared.commonPublic.featureflags.FeatureFlags import io.newm.shared.config.NewmSharedBuildConfig import io.newm.sharedfeatures.screens.DevMenuMainScreen import io.newm.sharedfeatures.screens.FeatureFlagsListScreen +import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreen +import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreenPresenter +import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreenUi +import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreenUiState +import io.newm.sharedfeatures.screens.auth.signup.CreateAccountScreen +import io.newm.sharedfeatures.screens.auth.signup.CreateAccountScreenPresenter +import io.newm.sharedfeatures.screens.auth.signup.CreateAccountUi +import io.newm.sharedfeatures.screens.auth.signup.CreateAccountUiState import io.newm.sharedfeatures.screens.devmenu.DevMenuPresenter import io.newm.sharedfeatures.screens.devmenu.DevMenuUi import io.newm.sharedfeatures.screens.devmenu.featureflaglist.FeatureFlagsListPresenter @@ -217,6 +225,16 @@ class HomeActivity : ComponentActivity() { } } + is CreateAccountScreen -> { + ui { state, modifier -> CreateAccountUi(state, modifier) } + } + + is ResetPasswordScreen -> { + ui { state, modifier -> + ResetPasswordScreenUi(state, eventLogger, modifier) + } + } + is DevMenuMainScreen -> { ui { state, modifier -> DevMenuUi(state, modifier) } } @@ -278,6 +296,14 @@ class HomeActivity : ComponentActivity() { inject { parametersOf(navigator) }.value } + is CreateAccountScreen -> { + inject { parametersOf(navigator) }.value + } + + is ResetPasswordScreen -> { + inject { parametersOf(screen, navigator) }.value + } + is DevMenuMainScreen -> { inject { parametersOf(navigator) }.value } diff --git a/android/app-newm/src/main/java/io/newm/screens/profile/ProfileForm.kt b/android/app-newm/src/main/java/io/newm/screens/profile/ProfileForm.kt index 3b73f6636..6e2f3cd08 100644 --- a/android/app-newm/src/main/java/io/newm/screens/profile/ProfileForm.kt +++ b/android/app-newm/src/main/java/io/newm/screens/profile/ProfileForm.kt @@ -20,9 +20,12 @@ import io.newm.core.theme.Gray16 import io.newm.core.theme.Gray23 import io.newm.core.ui.text.TextFieldWithLabel import io.newm.core.ui.text.formTitleStyle -import io.newm.feature.login.screen.TextFieldState -import io.newm.feature.login.screen.email.Email -import io.newm.feature.login.screen.password.Password +import io.newm.sharedfeatures.screens.auth.login.Password +import io.newm.sharedfeatures.screens.auth.login.TextFieldState +import newm_mobile.sharedfeatures.generated.resources.Res +import newm_mobile.sharedfeatures.generated.resources.profile_form_password_confirm_password +import newm_mobile.sharedfeatures.generated.resources.profile_form_password_current_password +import newm_mobile.sharedfeatures.generated.resources.profile_form_password_new_password @Composable fun ProfileForm( @@ -39,15 +42,17 @@ fun ProfileForm( Box(modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(Gray16)) { Column(modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp)) { if (canUserEditName) { - Email( - label = R.string.profile_form_first_name, - emailState = firstName, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + TextFieldWithLabel( + labelResId = R.string.profile_form_first_name, + value = firstName.text, + onValueChange = { firstName.text = it }, + textfieldBackgroundColor = Gray16, ) - Email( - label = R.string.profile_form_last_name, - emailState = lastName, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + TextFieldWithLabel( + labelResId = R.string.profile_form_last_name, + value = lastName.text, + onValueChange = { lastName.text = it }, + textfieldBackgroundColor = Gray16, ) } else { TextFieldWithLabel( @@ -83,18 +88,23 @@ fun ProfileForm( ) Spacer(modifier = Modifier.height(16.dp)) Password( - label = R.string.profile_form_password_current_password, - passwordState = currentPasswordState, + label = Res.string.profile_form_password_current_password, + passwordState = + currentPasswordState + as io.newm.sharedfeatures.screens.auth.login.PasswordState, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), ) Password( - label = R.string.profile_form_password_new_password, - passwordState = newPasswordState, + label = Res.string.profile_form_password_new_password, + passwordState = + newPasswordState as io.newm.sharedfeatures.screens.auth.login.PasswordState, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), ) Password( - label = R.string.profile_form_password_confirm_password, - passwordState = confirmNewPasswordState, + label = Res.string.profile_form_password_confirm_password, + passwordState = + confirmNewPasswordState + as io.newm.sharedfeatures.screens.auth.login.PasswordState, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), ) } diff --git a/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditPresenter.kt b/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditPresenter.kt index bd488e9fd..2652d1273 100644 --- a/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditPresenter.kt +++ b/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditPresenter.kt @@ -13,9 +13,6 @@ import com.slack.circuit.runtime.internal.rememberStableCoroutineScope import com.slack.circuit.runtime.presenter.Presenter import io.newm.Logout import io.newm.core.resources.R -import io.newm.feature.login.screen.TextFieldState -import io.newm.feature.login.screen.password.isPasswordValid -import io.newm.feature.login.screen.password.passwordValidationError import io.newm.screens.Screen.PrivacyPolicy import io.newm.screens.Screen.TermsOfService import io.newm.screens.profile.OnBack @@ -36,6 +33,9 @@ import io.newm.shared.commonPublic.usecases.ConnectWalletUseCase import io.newm.shared.commonPublic.usecases.HasWalletConnectionsUseCase import io.newm.shared.commonPublic.usecases.UpdateProfilePictureUseCase import io.newm.shared.commonPublic.usecases.UserDetailsUseCase +import io.newm.sharedfeatures.screens.auth.login.PasswordState +import io.newm.sharedfeatures.screens.auth.login.TextFieldState +import io.newm.sharedfeatures.screens.auth.login.isPasswordValid import io.newm.utils.toTempFile import kotlinx.coroutines.launch @@ -81,11 +81,11 @@ class ProfileEditPresenter( val lastNameState = remember(profile?.lastName) { TextFieldState(profile?.lastName.orEmpty()) } - val currentPasswordState = remember { TextFieldState() } + val currentPasswordState = remember { PasswordState() } - val newPasswordState = remember { TextFieldState() } + val newPasswordState = remember { PasswordState() } - val confirmPasswordState = remember { TextFieldState() } + val confirmPasswordState = remember { PasswordState() } var errorMessage by remember { mutableStateOf(null) } @@ -268,7 +268,7 @@ class ProfileEditPresenter( lastNameState: TextFieldState, ): String? { if (newPasswordState.text.isNotEmpty() && isPasswordValid(newPasswordState.text).not()) { - return passwordValidationError(context) + return context.getString(R.string.password_validation_error_message) } if ( diff --git a/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUi.kt b/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUi.kt index 909c1d0e0..2182dab0e 100644 --- a/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUi.kt +++ b/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUi.kt @@ -34,7 +34,6 @@ import io.newm.core.resources.R import io.newm.core.theme.NewmTheme import io.newm.core.ui.LoadingScreen import io.newm.core.ui.buttons.PrimaryButton -import io.newm.feature.login.screen.TextFieldState import io.newm.screens.profile.OnBack import io.newm.screens.profile.OnBottomSheetVisible import io.newm.screens.profile.OnLogout @@ -53,6 +52,7 @@ import io.newm.screens.profile.edit.ProfileEditUiState.Loading import io.newm.shared.commonPublic.models.User import io.newm.shared.commonPublic.models.canEditName import io.newm.shared.commonPublic.models.mocks.mockUsers +import io.newm.sharedfeatures.screens.auth.login.TextFieldState import kotlinx.coroutines.launch import network.chaintech.cmpimagepickncrop.CMPImagePickNCropDialog diff --git a/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUiState.kt b/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUiState.kt index 7df989e83..ac3d03859 100644 --- a/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUiState.kt +++ b/android/app-newm/src/main/java/io/newm/screens/profile/edit/ProfileEditUiState.kt @@ -1,8 +1,8 @@ package io.newm.screens.profile.edit import com.slack.circuit.runtime.CircuitUiState -import io.newm.feature.login.screen.TextFieldState import io.newm.screens.profile.ProfileEditUiEvent +import io.newm.sharedfeatures.screens.auth.login.TextFieldState sealed class ProfileEditUiState : CircuitUiState { data object Loading : ProfileEditUiState() diff --git a/android/features/login/.gitignore b/android/features/login/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/android/features/login/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/android/features/login/build.gradle.kts b/android/features/login/build.gradle.kts deleted file mode 100644 index e3059630d..000000000 --- a/android/features/login/build.gradle.kts +++ /dev/null @@ -1,73 +0,0 @@ -@file:OptIn(ExperimentalWasmDsl::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlin.plugin.parcelize) - alias(libs.plugins.paparazzi) - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.compose.multiplatform) -} - -apply(from = "../../../gradle_include/compose.gradle") - -apply(from = "../../../gradle_include/circuit.gradle") - -android { - compileSdk = libs.versions.android.compileSdk.get().toInt() - - namespace = "io.newm.feature.login" - - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - resourcePrefix = "login" - } - - lint { baseline = file("lint-baseline.xml") } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - androidTarget() - - wasmJs { browser {} } - - jvm("desktop") - - sourceSets { - androidMain.dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.material) - implementation(libs.androidx.material.icons.extended) - implementation(libs.koin.android) - implementation(libs.recaptcha) - implementation(project(Modules.CORE_RESOURCES)) - implementation(project(Modules.CORE_THEME)) - implementation(project(Modules.CORE_UI_UTILS)) - implementation(project(Modules.SHARED)) - implementation(project(Modules.SHARED_COMPOSE_FEATURES)) - - implementation(compose.material) - } - } -} - -tasks.withType { - compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } -} - -dependencies { - testImplementation(libs.junit) - testImplementation(libs.test.parameter.injector) - testImplementation(project(Modules.TEST_UTILS)) - testImplementation(compose.components.resources) - - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(libs.androidx.test.junit) -} diff --git a/android/features/login/lint-baseline.xml b/android/features/login/lint-baseline.xml deleted file mode 100644 index 81e52111c..000000000 --- a/android/features/login/lint-baseline.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/NewArtistBackground.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/NewArtistBackground.kt deleted file mode 100644 index 1f89b2c30..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/NewArtistBackground.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.newm.feature.login.screen - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -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.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.MaterialTheme -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.unit.dp -import io.newm.core.resources.R -import io.newm.core.ui.OnboardingMainImage - -@Composable -fun PreLoginArtistBackgroundContentTemplate( - modifier: Modifier = Modifier, - isLoading: Boolean = false, - header: @Composable ColumnScope.() -> Unit = {}, - content: @Composable ColumnScope.() -> Unit, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()), - ) { - header() - Spacer(modifier = Modifier.height(70.dp)) - OnboardingMainImage(painterResource(R.drawable.ic_newm_logo)) - Spacer(modifier = Modifier.height(16.dp)) - content() - } - - if (isLoading) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/TextFieldError.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/TextFieldError.kt deleted file mode 100644 index e2fbdd80e..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/TextFieldError.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.newm.feature.login.screen - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun TextFieldError(textError: String) { - Row(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = textError, - modifier = Modifier.fillMaxWidth(), - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.error), - ) - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/TextFieldState.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/TextFieldState.kt deleted file mode 100644 index 34267050c..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/TextFieldState.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.newm.feature.login.screen - -import android.content.Context -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue - -open class TextFieldState( - defaultValue: String = "", - private val validator: (String) -> Boolean = { true }, - private val errorFor: (Context, String) -> String = { _, _ -> "" }, -) { - var text: String by mutableStateOf(defaultValue) - - // was the TextField ever focused - var isFocusedDirty: Boolean by mutableStateOf(false) - var isFocused: Boolean by mutableStateOf(false) - private var displayErrors: Boolean by mutableStateOf(false) - - open val isValid: Boolean - get() = validator(text) - - fun onFocusChange(focused: Boolean) { - isFocused = focused - if (focused) isFocusedDirty = true - } - - fun enableShowErrors() { - // only show errors if the text was at least once focused - if (isFocusedDirty) { - displayErrors = true - } - } - - fun showErrors() = !isValid && displayErrors - - /** - * Gets error string - * - * @param context needed for fetching string resource - */ - open fun getError(context: Context): String? = - if (showErrors()) { - errorFor(context, text) - } else { - null - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreen.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreen.kt deleted file mode 100644 index e0ef11fc6..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreen.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.newm.feature.login.screen.createaccount - -import com.slack.circuit.runtime.screen.Screen -import kotlinx.parcelize.Parcelize - -@Parcelize object CreateAccountScreen : Screen diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreenPresenter.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreenPresenter.kt deleted file mode 100644 index 4b74a8151..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreenPresenter.kt +++ /dev/null @@ -1,178 +0,0 @@ -package io.newm.feature.login.screen.createaccount - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import com.google.android.recaptcha.RecaptchaAction -import com.slack.circuit.retained.rememberRetained -import com.slack.circuit.runtime.presenter.Presenter -import io.newm.core.resources.R -import io.newm.feature.login.screen.createaccount.CreateAccountUiState.EmailAndPasswordUiState -import io.newm.feature.login.screen.createaccount.CreateAccountUiState.EmailVerificationUiState -import io.newm.feature.login.screen.email.EmailState -import io.newm.feature.login.screen.password.ConfirmPasswordState -import io.newm.feature.login.screen.password.PasswordState -import io.newm.feature.login.screen.password.VerificationCodeState -import io.newm.shared.NewmAppLogger -import io.newm.shared.commonPublic.analytics.NewmAppEventLogger -import io.newm.shared.commonPublic.analytics.events.AppScreens -import io.newm.shared.commonPublic.usecases.LoginUseCase -import io.newm.shared.commonPublic.usecases.SignupUseCase -import io.newm.sharedfeatures.screens.auth.login.RecaptchaClientProvider -import kotlinx.coroutines.launch - -class CreateAccountScreenPresenter( - private val navigateHome: () -> Unit, - private val signupUseCase: SignupUseCase, - private val loginUseCase: LoginUseCase, - private val recaptchaClientProvider: RecaptchaClientProvider, - private val appLogger: NewmAppLogger, - private val eventLogger: NewmAppEventLogger, -) : Presenter { - @Composable - override fun present(): CreateAccountUiState { - var step by rememberRetained { mutableStateOf(Step.EmailAndPassword) } - val userEmail = rememberRetained { EmailState() } - val password = rememberRetained { PasswordState() } - val passwordConfirmation = rememberRetained { ConfirmPasswordState(password) } - val verificationCode = rememberRetained { VerificationCodeState() } - - val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - - val emailAndPasswordValid = - remember(userEmail.isValid, password.isValid, passwordConfirmation.isValid) { - userEmail.isValid && password.isValid && passwordConfirmation.isValid - } - - var errorMessage by remember { mutableStateOf(null) } - - return when (step) { - Step.EmailAndPassword -> { - EmailAndPasswordUiState( - emailState = userEmail, - passwordState = password, - submitButtonEnabled = emailAndPasswordValid, - passwordConfirmationState = passwordConfirmation, - errorMessage = errorMessage, - ) { event -> - when (event) { - SignupFormUiEvent.Next -> { - eventLogger.logClickEvent(AppScreens.CreateAccountScreen.NEXT_BUTTON) - require(emailAndPasswordValid) { - "Email and password - next button should not be enabled if any of the fields are invalid" - } - - coroutineScope.launch { - step = Step.Loading - step = - try { - recaptchaClientProvider - .get() - .execute(RecaptchaAction.custom("auth_code")) - .onSuccess { token -> - signupUseCase.requestEmailConfirmationCode( - email = userEmail.text, - humanVerificationCode = token, - ) - }.onFailure { - errorMessage = - context.getString( - R.string.invalid_recaptcha_message, - ) - } - Step.EmailVerification - } catch (e: Throwable) { - appLogger.error( - tag = "Sign up", - message = "${e.message}", - exception = e, - ) - errorMessage = e.message - Step.EmailAndPassword - } - } - } - } - } - } - - Step.EmailVerification -> { - eventLogger.logPageLoad(AppScreens.EmailVerificationScreen.name) - EmailVerificationUiState( - verificationCode = verificationCode, - nextButtonEnabled = verificationCode.isValid, - errorMessage = errorMessage, - ) { event -> - when (event) { - is EmailVerificationUiEvent.Next -> { - require(emailAndPasswordValid && verificationCode.isValid) { - "Email verification - next button should not be enabled if any of the fields are invalid" - } - eventLogger.logClickEvent( - AppScreens.EmailVerificationScreen.CONTINUE_BUTTON, - ) - - coroutineScope.launch { - step = Step.Loading - try { - recaptchaClientProvider - .get() - .execute(RecaptchaAction.SIGNUP) - .onSuccess { token -> - signupUseCase.registerUser( - email = userEmail.text, - verificationCode = verificationCode.text, - password = password.text, - passwordConfirmation = passwordConfirmation.text, - humanVerificationCode = token, - ) - recaptchaClientProvider - .get() - .execute(RecaptchaAction.LOGIN) - .onSuccess { newToken -> - loginUseCase.logIn( - userEmail.text, - password.text, - humanVerificationCode = newToken, - ) - navigateHome() - } - }.onFailure { - errorMessage = - context.getString( - R.string.invalid_recaptcha_message, - ) - } - } catch (e: Throwable) { - appLogger.error( - tag = "Create Account", - message = "Email verification: ${e.message}", - exception = e, - ) - errorMessage = e.message - step = Step.EmailVerification - } - } - } - } - } - } - - Step.Loading -> { - eventLogger.logPageLoad(AppScreens.LoadingScreen.name) - CreateAccountUiState.Loading - } - } - } -} - -private enum class Step { - Loading, - EmailAndPassword, - EmailVerification, -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreenUi.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreenUi.kt deleted file mode 100644 index 212395888..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountScreenUi.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.newm.feature.login.screen.createaccount - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import io.newm.core.ui.LoadingScreen -import io.newm.feature.login.screen.createaccount.CreateAccountUiState.EmailAndPasswordUiState -import io.newm.feature.login.screen.createaccount.CreateAccountUiState.EmailVerificationUiState - -@Composable -fun CreateAccountUi( - state: CreateAccountUiState, - modifier: Modifier, -) { - when (state) { - is EmailAndPasswordUiState -> { - EmailAndPasswordUi(modifier, state) - } - - is EmailVerificationUiState -> { - EmailVerificationUi(modifier, state) - } - - CreateAccountUiState.Loading -> { - LoadingScreen() - } - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountUiState.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountUiState.kt deleted file mode 100644 index ab77e0e5d..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/CreateAccountUiState.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.newm.feature.login.screen.createaccount - -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState -import io.newm.feature.login.screen.TextFieldState - -sealed interface CreateAccountUiState : CircuitUiState { - data object Loading : CreateAccountUiState - - data class EmailAndPasswordUiState( - val passwordConfirmationState: TextFieldState, - val passwordState: TextFieldState, - val emailState: TextFieldState, - val submitButtonEnabled: Boolean, - val errorMessage: String?, - val eventSink: (SignupFormUiEvent) -> Unit, - ) : CreateAccountUiState - - data class EmailVerificationUiState( - val verificationCode: TextFieldState, - val errorMessage: String?, - val nextButtonEnabled: Boolean, - val eventSink: (EmailVerificationUiEvent) -> Unit, - ) : CreateAccountUiState -} - -sealed interface EmailVerificationUiEvent : CircuitUiEvent { - data object Next : EmailVerificationUiEvent -} - -sealed interface SignupFormUiEvent : CircuitUiEvent { - data object Next : SignupFormUiEvent -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/EmailAndPasswordUi.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/EmailAndPasswordUi.kt deleted file mode 100644 index 77db98209..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/EmailAndPasswordUi.kt +++ /dev/null @@ -1,97 +0,0 @@ -package io.newm.feature.login.screen.createaccount - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.newm.core.resources.R -import io.newm.core.ui.ToastSideEffect -import io.newm.core.ui.buttons.PrimaryButton -import io.newm.core.ui.text.TextFieldWithLabelDefaults -import io.newm.feature.login.screen.PreLoginArtistBackgroundContentTemplate -import io.newm.feature.login.screen.createaccount.CreateAccountUiState.EmailAndPasswordUiState -import io.newm.feature.login.screen.email.Email -import io.newm.feature.login.screen.email.EmailState -import io.newm.feature.login.screen.password.ConfirmPasswordState -import io.newm.feature.login.screen.password.Password -import io.newm.feature.login.screen.password.PasswordState - -@Composable -fun EmailAndPasswordUi( - modifier: Modifier, - state: EmailAndPasswordUiState, -) { - val onEvent = state.eventSink - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - ToastSideEffect(message = state.errorMessage) - - PreLoginArtistBackgroundContentTemplate(modifier = modifier) { - Email( - modifier = Modifier.focusRequester(focusRequester), - emailState = state.emailState, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.EMAIL.copy(imeAction = ImeAction.Next), - ) - - Password( - label = R.string.password, - passwordState = state.passwordState, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.PASSWORD.copy(imeAction = ImeAction.Next), - ) - - Password( - label = R.string.confirm_password, - passwordState = state.passwordConfirmationState, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.PASSWORD.copy(imeAction = ImeAction.Go), - keyboardActions = - KeyboardActions( - onGo = { - keyboardController?.hide() - if (state.submitButtonEnabled) { - onEvent(SignupFormUiEvent.Next) - } - }, - ), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.next), - onClick = { onEvent(SignupFormUiEvent.Next) }, - enabled = state.submitButtonEnabled, - ) - } -} - -@Preview -@Composable -fun EmailAndPasswordUiPreview() { - EmailAndPasswordUi( - modifier = Modifier.fillMaxSize(), - state = - EmailAndPasswordUiState( - emailState = EmailState(), - passwordState = PasswordState(), - passwordConfirmationState = ConfirmPasswordState(PasswordState()), - submitButtonEnabled = true, - errorMessage = null, - ) {}, - ) -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/EmailVerificationUi.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/EmailVerificationUi.kt deleted file mode 100644 index a5b3d5cff..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/createaccount/EmailVerificationUi.kt +++ /dev/null @@ -1,112 +0,0 @@ -package io.newm.feature.login.screen.createaccount - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.material.MaterialTheme -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.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import io.newm.core.resources.R -import io.newm.core.ui.ToastSideEffect -import io.newm.core.ui.buttons.PrimaryButton -import io.newm.core.ui.text.TextFieldWithLabel -import io.newm.core.ui.text.TextFieldWithLabelDefaults -import io.newm.feature.login.screen.TextFieldState - -@Composable -internal fun EmailVerificationUi( - modifier: Modifier = Modifier, - state: CreateAccountUiState.EmailVerificationUiState, -) { - val onEvent = state.eventSink - - EmailVerificationContent( - modifier = modifier, - verificationCode = state.verificationCode, - errorMessage = state.errorMessage, - nextButtonEnabled = state.nextButtonEnabled, - onNextClicked = { onEvent(EmailVerificationUiEvent.Next) }, - ) -} - -@Composable -internal fun EmailVerificationContent( - verificationCode: TextFieldState, - errorMessage: String?, - nextButtonEnabled: Boolean, - onNextClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - ToastSideEffect(errorMessage) - - Box( - modifier = - modifier.fillMaxHeight().fillMaxWidth().background(MaterialTheme.colors.background), - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 16.dp), - ) { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.login_check_your_email), - color = MaterialTheme.colors.onBackground, - style = MaterialTheme.typography.h1, - ) - Text( - text = stringResource(id = R.string.login_enter_verification_code_below), - color = MaterialTheme.colors.primary, - style = MaterialTheme.typography.h1, - textAlign = TextAlign.Center, - fontStyle = FontStyle.Italic, - ) - Spacer(modifier = Modifier.height(100.dp)) - Text( - text = stringResource(id = R.string.login_receive_email), - color = MaterialTheme.colors.primary, - style = MaterialTheme.typography.caption, - ) - Spacer(modifier = Modifier.height(36.dp)) - - TextFieldWithLabel( - labelResId = R.string.login_enter_verification_code, - value = verificationCode.text, - onValueChange = verificationCode::text::set, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.Digits.copy( - imeAction = ImeAction.Go, - ), - keyboardActions = - KeyboardActions( - onGo = { - if (nextButtonEnabled) { - onNextClicked() - } - }, - ), - ) - - Spacer(modifier = Modifier.height(16.dp)) - PrimaryButton( - text = stringResource(id = R.string.login_continue), - enabled = nextButtonEnabled, - onClick = { onNextClicked() }, - ) - } - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/email/Email.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/email/Email.kt deleted file mode 100644 index 01e70b7b8..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/email/Email.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.newm.feature.login.screen.email - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalContext -import io.newm.core.resources.R -import io.newm.core.ui.text.TextFieldWithLabel -import io.newm.core.ui.text.TextFieldWithLabelDefaults -import io.newm.feature.login.screen.TextFieldState - -@Composable -fun Email( - emailState: TextFieldState, - modifier: Modifier = Modifier, - label: Int = R.string.email, - keyboardOptions: KeyboardOptions = TextFieldWithLabelDefaults.KeyboardOptions.EMAIL, - keyboardActions: KeyboardActions = KeyboardActions.Default, -) { - val context = LocalContext.current - - TextFieldWithLabel( - modifier = - modifier.fillMaxWidth().onFocusChanged { focusState -> - emailState.onFocusChange(focusState.isFocused) - if (!focusState.isFocused) { - emailState.enableShowErrors() - } - }, - onValueChange = { emailState.text = it }, - value = emailState.text, - labelResId = label, - isError = emailState.showErrors(), - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - helperText = emailState.getError(context), - singleLine = true, - ) -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/email/EmailState.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/email/EmailState.kt deleted file mode 100644 index 6b417e24d..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/email/EmailState.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.newm.feature.login.screen.email - -import android.content.Context -import androidx.core.util.PatternsCompat.EMAIL_ADDRESS -import io.newm.core.resources.R -import io.newm.feature.login.screen.TextFieldState - -class EmailState : TextFieldState(validator = ::isEmailValid, errorFor = ::emailValidationError) - -/** - * Returns an error to be displayed or null if no error was found - * - * @param context needed to get string resource - * @param email invalid email we show the user - */ -private fun emailValidationError( - context: Context, - email: String, -): String = context.getString(R.string.email_validation_error_message, email) - -private fun isEmailValid(email: String): Boolean = EMAIL_ADDRESS.matcher(email).matches() diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/password/Password.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/password/Password.kt deleted file mode 100644 index 635608470..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/password/Password.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.newm.feature.login.screen.password - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalContext -import io.newm.core.ui.text.TextFieldWithLabel -import io.newm.core.ui.text.TextFieldWithLabelDefaults -import io.newm.feature.login.screen.TextFieldState - -@Composable -fun Password( - @StringRes label: Int, - passwordState: TextFieldState, - modifier: Modifier = Modifier, - keyboardOptions: KeyboardOptions = TextFieldWithLabelDefaults.KeyboardOptions.PASSWORD, - keyboardActions: KeyboardActions = KeyboardActions.Default, -) { - val context = LocalContext.current - - TextFieldWithLabel( - modifier = - modifier.fillMaxWidth().onFocusChanged { focusState -> - passwordState.onFocusChange(focusState.isFocused) - if (!focusState.isFocused) { - passwordState.enableShowErrors() - } - }, - onValueChange = { passwordState.text = it }, - value = passwordState.text, - isPassword = true, - labelResId = label, - isError = passwordState.showErrors(), - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - helperText = passwordState.getError(context), - singleLine = true, - ) -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/password/PasswordState.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/password/PasswordState.kt deleted file mode 100644 index 9bba72f99..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/password/PasswordState.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.newm.feature.login.screen.password - -import android.content.Context -import io.newm.core.resources.R -import io.newm.feature.login.screen.TextFieldState -import io.newm.shared.login.util.LoginFieldValidator - -class PasswordState : - TextFieldState( - validator = ::isPasswordValid, - errorFor = { cxt, _ -> passwordValidationError(cxt) }, - ) - -class ConfirmPasswordState( - private val passwordState: PasswordState, -) : TextFieldState() { - override val isValid - get() = passwordAndConfirmationValid(passwordState.text, text) - - override fun getError(context: Context): String? = - if (showErrors()) { - passwordConfirmationError(context) - } else { - null - } -} - -private const val MINIMUM_VERIFICATION_CODE_LENGTH = 6 - -class VerificationCodeState : - TextFieldState( - validator = { it.length >= MINIMUM_VERIFICATION_CODE_LENGTH }, - errorFor = { cxt, _ -> - cxt.getString( - R.string.code_verification_error_message, - MINIMUM_VERIFICATION_CODE_LENGTH, - ) - }, - ) - -private fun passwordAndConfirmationValid( - password: String, - confirmedPassword: String, -): Boolean = isPasswordValid(password) && password == confirmedPassword - -fun isPasswordValid(password: String): Boolean = LoginFieldValidator.isPasswordValid(password) - -fun passwordValidationError(context: Context): String = context.getString(R.string.password_validation_error_message) - -private fun passwordConfirmationError(context: Context): String = context.getString(R.string.password_confirmation_error_message) diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenPresenter.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenPresenter.kt deleted file mode 100644 index a3ed92b08..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenPresenter.kt +++ /dev/null @@ -1,199 +0,0 @@ -package io.newm.feature.login.screen.resetpassword - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import com.google.android.recaptcha.RecaptchaAction -import com.slack.circuit.retained.rememberRetained -import com.slack.circuit.runtime.Navigator -import com.slack.circuit.runtime.presenter.Presenter -import io.newm.core.resources.R -import io.newm.feature.login.screen.email.EmailState -import io.newm.feature.login.screen.password.ConfirmPasswordState -import io.newm.feature.login.screen.password.PasswordState -import io.newm.feature.login.screen.password.VerificationCodeState -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterEmailUiEvent -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterNewPasswordUiEvent -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterVerificationCodeUiEvent -import io.newm.shared.NewmAppLogger -import io.newm.shared.commonPublic.analytics.NewmAppEventLogger -import io.newm.shared.commonPublic.analytics.events.AppScreens -import io.newm.shared.commonPublic.usecases.LoginUseCase -import io.newm.shared.commonPublic.usecases.ResetPasswordUseCase -import io.newm.shared.commonPublic.usecases.SignupUseCase -import io.newm.sharedfeatures.screens.HomeScreen -import io.newm.sharedfeatures.screens.auth.login.RecaptchaClientProvider -import kotlinx.coroutines.launch - -private enum class ResetPasswordStep { - EnterEmail, - EnterVerificationCode, - EnterNewPassword, -} - -class ResetPasswordScreenPresenter( - private val navigator: Navigator, - private val signupUseCase: SignupUseCase, - private val loginUseCase: LoginUseCase, - private val resetPasswordUseCase: ResetPasswordUseCase, - private val recaptchaClientProvider: RecaptchaClientProvider, - private val logger: NewmAppLogger, - private val analyticsTracker: NewmAppEventLogger, -) : Presenter { - @Composable - override fun present(): ResetPasswordScreenUiState { - var step by remember { mutableStateOf(ResetPasswordStep.EnterEmail) } - var isLoading by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf(null) } - val email = rememberRetained { EmailState() } - val authCode = rememberRetained { VerificationCodeState() } - val password = remember { PasswordState() } - val passwordConfirmation = remember(password) { ConfirmPasswordState(password) } - val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - - return when (step) { - ResetPasswordStep.EnterEmail -> { - ResetPasswordScreenUiState.EnterEmail( - email = email, - errorMessage = errorMessage, - isLoading = isLoading, - submitButtonEnabled = email.isValid && !isLoading, - eventSink = { event -> - when (event) { - EnterEmailUiEvent.OnSubmit -> { - analyticsTracker.logClickEvent( - AppScreens.ResetPasswordEnterEmailScreen.CONTINUE_BUTTON, - ) - errorMessage = null - isLoading = true - coroutineScope.launch { - try { - recaptchaClientProvider - .get() - .execute(RecaptchaAction.custom("auth_code")) - .onSuccess { token -> - signupUseCase.requestEmailConfirmationCode( - email.text, - humanVerificationCode = token, - mustExists = true, - ) - step = ResetPasswordStep.EnterVerificationCode - }.onFailure { - logger.error( - "ResetPasswordScreenPresenter", - "Human verification error", - it, - ) - } - } catch (e: Throwable) { - logger.error( - "ResetPasswordScreenPresenter", - "Requesting email confirmation code failed", - e, - ) - errorMessage = e.message - } - - isLoading = false - } - } - } - }, - ) - } - - ResetPasswordStep.EnterVerificationCode -> { - ResetPasswordScreenUiState.EnterVerificationCode( - code = authCode, - errorMessage = errorMessage, - isLoading = isLoading, - submitButtonEnabled = authCode.isValid && !isLoading, - eventSink = { event -> - when (event) { - EnterVerificationCodeUiEvent.OnSubmit -> { - analyticsTracker.logClickEvent( - AppScreens.ResetPasswordEnterCodeScreen.CONTINUE_BUTTON, - ) - step = ResetPasswordStep.EnterNewPassword - } - } - }, - ) - } - - ResetPasswordStep.EnterNewPassword -> { - val submitEnabled = password.isValid && passwordConfirmation.isValid && !isLoading - - ResetPasswordScreenUiState.EnterNewPassword( - password = password, - confirmPasswordState = passwordConfirmation, - errorMessage = errorMessage, - isLoading = isLoading, - submitButtonEnabled = submitEnabled, - eventSink = { event -> - when (event) { - EnterNewPasswordUiEvent.OnSubmit -> { - analyticsTracker.logClickEvent( - AppScreens.NewPasswordScreen.CONFIRM_BUTTON, - ) - errorMessage = null - isLoading = true - coroutineScope.launch { - try { - recaptchaClientProvider - .get() - .execute(RecaptchaAction.custom("password_reset")) - .onSuccess { token -> - resetPasswordUseCase.resetPassword( - email = email.text, - code = authCode.text, - newPassword = password.text, - confirmPassword = passwordConfirmation.text, - humanVerificationCode = token, - ) - errorMessage = - context.getString( - R.string.password_reset_successfully_message, - ) - recaptchaClientProvider - .get() - .execute(RecaptchaAction.LOGIN) - .onSuccess { newToken -> - loginUseCase.logIn( - email.text, - password.text, - humanVerificationCode = newToken, - ) - navigator.goTo(HomeScreen) - } - }.onFailure { - logger.error( - "ResetPasswordScreenPresenter", - "Human verification error", - it, - ) - } - } catch (e: Throwable) { - logger.error( - "ResetPasswordScreenPresenter", - "Resetting password failed", - e, - ) - errorMessage = e.message - } - - isLoading = false - } - } - } - }, - ) - } - } - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenUi.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenUi.kt deleted file mode 100644 index ee2576d56..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenUi.kt +++ /dev/null @@ -1,289 +0,0 @@ -package io.newm.feature.login.screen.resetpassword - -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -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.text.KeyboardActions -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.slack.circuit.runtime.ui.Ui -import io.newm.core.resources.R -import io.newm.core.theme.NewmTheme -import io.newm.core.ui.ToastSideEffect -import io.newm.core.ui.buttons.PrimaryButton -import io.newm.core.ui.text.TextFieldWithLabelDefaults -import io.newm.feature.login.screen.TextFieldState -import io.newm.feature.login.screen.createaccount.EmailVerificationContent -import io.newm.feature.login.screen.email.Email -import io.newm.feature.login.screen.email.EmailState -import io.newm.feature.login.screen.password.Password -import io.newm.feature.login.screen.password.PasswordState -import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenUiState.EnterEmail -import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenUiState.EnterNewPassword -import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenUiState.EnterVerificationCode -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterEmailUiEvent -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterNewPasswordUiEvent -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterVerificationCodeUiEvent -import io.newm.shared.commonPublic.analytics.NewmAppEventLogger -import io.newm.shared.commonPublic.analytics.events.AppScreens - -class ResetPasswordScreenUi( - val eventLogger: NewmAppEventLogger, -) : Ui { - @Composable - override fun Content( - state: ResetPasswordScreenUiState, - modifier: Modifier, - ) { - ResetPasswordScreenContent(modifier = modifier, state = state, eventLogger = eventLogger) - } -} - -@Composable -internal fun ResetPasswordScreenContent( - state: ResetPasswordScreenUiState, - modifier: Modifier = Modifier, - eventLogger: NewmAppEventLogger, -) { - ToastSideEffect(message = state.errorMessage) - - Scaffold(modifier = modifier.fillMaxSize()) { padding -> - when (state) { - is EnterEmail -> { - EnterEmailContent(state, modifier = Modifier.padding(padding), eventLogger) - } - - is EnterVerificationCode -> { - EnterCodeContent(state, modifier = Modifier.padding(padding), eventLogger) - } - - is EnterNewPassword -> { - SetNewPasswordContent(state, modifier = Modifier.padding(padding), eventLogger) - } - } - if (state.isLoading) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } - } -} - -@Composable -private fun EnterEmailContent( - state: EnterEmail, - modifier: Modifier = Modifier, - eventLogger: NewmAppEventLogger, -) { - val eventSink = state.eventSink - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - LaunchedEffect(Unit) { eventLogger.logPageLoad(AppScreens.ResetPasswordEnterEmailScreen.name) } - Column(modifier = modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text( - stringResource(R.string.reset_password_forgot_your_password), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.h1, - ) - Text( - stringResource(R.string.reset_password_enter_email), - style = MaterialTheme.typography.h2, - color = MaterialTheme.colors.primary, - textAlign = TextAlign.Center, - fontStyle = FontStyle.Italic, - ) - Spacer(modifier = Modifier.weight(1f)) - Email( - modifier = Modifier.focusRequester(focusRequester), - emailState = state.email, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.EMAIL.copy(imeAction = ImeAction.Go), - keyboardActions = - KeyboardActions( - onGo = { - keyboardController?.hide() - if (state.submitButtonEnabled) { - eventSink(EnterEmailUiEvent.OnSubmit) - } - }, - ), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - PrimaryButton( - text = stringResource(R.string.reset_password_enter_email_continue), - onClick = { eventSink(EnterEmailUiEvent.OnSubmit) }, - enabled = state.submitButtonEnabled, - ) - - Spacer(modifier = Modifier.weight(1f)) - } -} - -@Composable -private fun EnterCodeContent( - state: EnterVerificationCode, - modifier: Modifier, - eventLogger: NewmAppEventLogger, -) { - val eventSink = state.eventSink - LaunchedEffect(Unit) { eventLogger.logPageLoad(AppScreens.ResetPasswordEnterCodeScreen.name) } - EmailVerificationContent( - modifier = modifier, - verificationCode = state.code, - errorMessage = state.errorMessage, - nextButtonEnabled = state.submitButtonEnabled, - onNextClicked = { eventSink(EnterVerificationCodeUiEvent.OnSubmit) }, - ) -} - -@Composable -private fun SetNewPasswordContent( - state: EnterNewPassword, - modifier: Modifier = Modifier, - eventLogger: NewmAppEventLogger, -) { - val onEvent = state.eventSink - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - LaunchedEffect(Unit) { eventLogger.logPageLoad(AppScreens.NewPasswordScreen.name) } - - ToastSideEffect(message = state.errorMessage) - - Column(modifier = modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(R.string.reset_password_enter_new_password), - style = MaterialTheme.typography.h1, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.weight(1f)) - Password( - modifier = Modifier.focusRequester(focusRequester), - label = R.string.reset_password_new_password, - passwordState = state.password, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.PASSWORD.copy(imeAction = ImeAction.Next), - ) - - Password( - label = R.string.reset_password_confirm_new_password, - passwordState = state.confirmPasswordState, - keyboardOptions = - TextFieldWithLabelDefaults.KeyboardOptions.PASSWORD.copy(imeAction = ImeAction.Go), - keyboardActions = - KeyboardActions( - onGo = { - keyboardController?.hide() - if (state.submitButtonEnabled) { - onEvent(EnterNewPasswordUiEvent.OnSubmit) - } - }, - ), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.confirm), - onClick = { onEvent(EnterNewPasswordUiEvent.OnSubmit) }, - enabled = state.submitButtonEnabled, - ) - Spacer(modifier = Modifier.weight(1f)) - } -} - -@Composable -@Preview(showBackground = true) -@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) -private fun PreviewEnterEmail() { - NewmTheme { - ResetPasswordScreenContent( - EnterEmail( - email = EmailState(), - errorMessage = null, - isLoading = false, - submitButtonEnabled = true, - eventSink = {}, - ), - eventLogger = NewmAppEventLogger(), - ) - } -} - -@Composable -@Preview(showBackground = true) -@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) -private fun PreviewVerificationCode() { - NewmTheme { - ResetPasswordScreenContent( - EnterVerificationCode( - code = TextFieldState(), - errorMessage = null, - isLoading = false, - submitButtonEnabled = true, - eventSink = {}, - ), - eventLogger = NewmAppEventLogger(), - ) - } -} - -@Composable -@Preview(showBackground = true) -@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) -private fun PreviewSetNewPassword() { - NewmTheme { - ResetPasswordScreenContent( - EnterNewPassword( - password = PasswordState(), - confirmPasswordState = PasswordState(), - errorMessage = null, - isLoading = false, - submitButtonEnabled = true, - eventSink = {}, - ), - eventLogger = NewmAppEventLogger(), - ) - } -} - -@Composable -@Preview(showBackground = true) -@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) -private fun PreviewLoading() { - NewmTheme { - ResetPasswordScreenContent( - EnterNewPassword( - password = PasswordState(), - confirmPasswordState = PasswordState(), - errorMessage = null, - isLoading = true, - submitButtonEnabled = true, - eventSink = {}, - ), - eventLogger = NewmAppEventLogger(), - ) - } -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenUiState.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenUiState.kt deleted file mode 100644 index ad201a311..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordScreenUiState.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.newm.feature.login.screen.resetpassword - -import com.slack.circuit.runtime.CircuitUiState -import io.newm.feature.login.screen.TextFieldState -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterEmailUiEvent -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterNewPasswordUiEvent -import io.newm.feature.login.screen.resetpassword.ResetPasswordUiEvent.EnterVerificationCodeUiEvent - -sealed interface ResetPasswordScreenUiState : CircuitUiState { - val isLoading: Boolean - val errorMessage: String? - - data class EnterEmail( - val email: TextFieldState, - override val isLoading: Boolean, - override val errorMessage: String?, - val submitButtonEnabled: Boolean, - val eventSink: (EnterEmailUiEvent) -> Unit, - ) : ResetPasswordScreenUiState - - data class EnterVerificationCode( - val code: TextFieldState, - val submitButtonEnabled: Boolean, - override val isLoading: Boolean, - override val errorMessage: String?, - val eventSink: (EnterVerificationCodeUiEvent) -> Unit, - ) : ResetPasswordScreenUiState - - data class EnterNewPassword( - val password: TextFieldState, - val confirmPasswordState: TextFieldState, - val submitButtonEnabled: Boolean, - override val isLoading: Boolean, - override val errorMessage: String?, - val eventSink: (EnterNewPasswordUiEvent) -> Unit, - ) : ResetPasswordScreenUiState -} diff --git a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordUiEvent.kt b/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordUiEvent.kt deleted file mode 100644 index d62b0d519..000000000 --- a/android/features/login/src/androidMain/kotlin/io/newm/feature/login/screen/resetpassword/ResetPasswordUiEvent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.newm.feature.login.screen.resetpassword - -import com.slack.circuit.runtime.CircuitUiEvent - -sealed interface ResetPasswordUiEvent : CircuitUiEvent { - sealed interface EnterEmailUiEvent : ResetPasswordUiEvent { - data object OnSubmit : EnterEmailUiEvent - } - - sealed interface EnterVerificationCodeUiEvent : ResetPasswordUiEvent { - data object OnSubmit : EnterVerificationCodeUiEvent - } - - sealed interface EnterNewPasswordUiEvent : ResetPasswordUiEvent { - data object OnSubmit : EnterNewPasswordUiEvent - } -} diff --git a/buildSrc/src/main/java/Modules.kt b/buildSrc/src/main/java/Modules.kt index c7160fa71..62270dc4d 100644 --- a/buildSrc/src/main/java/Modules.kt +++ b/buildSrc/src/main/java/Modules.kt @@ -4,7 +4,6 @@ object Modules { const val CORE_RESOURCES = ":android:core:resources" const val CORE_ANDROID_IMPLEMENTATIONS = ":android:core:android:implementations" - const val LOGIN = ":android:features:login" const val MUSIC_PLAYER = ":android:features:music-player" const val BARCODE_SCANNER = ":android:features:barcode-scanner" const val SHARED = ":shared" diff --git a/conductor/tracks.md b/conductor/tracks.md index e14f4b42c..c234f4e3f 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -8,6 +8,6 @@ This file tracks all major tracks for the project. Each track has its own detail *Link: [./conductor/tracks/music_player_ux_20260122/](./conductor/tracks/music_player_ux_20260122/)* --- -- [~] **Track: Migrate CreateAccount and ResetPassword screens from android module to sharedfeatures module.** +- [x] **Track: Migrate CreateAccount and ResetPassword screens from android module to sharedfeatures module.** *Link: [./tracks/feature_login_migration_20260122/](./tracks/feature_login_migration_20260122/)* diff --git a/conductor/tracks/feature_login_migration_20260122/plan.md b/conductor/tracks/feature_login_migration_20260122/plan.md index 1cf7b95c0..cca85a1e1 100644 --- a/conductor/tracks/feature_login_migration_20260122/plan.md +++ b/conductor/tracks/feature_login_migration_20260122/plan.md @@ -1,14 +1,14 @@ # Implementation Plan - Login Feature Migration (Part 2) -## Phase 1: Preparation and Shared Components +## Phase 1: Preparation and Shared Components [checkpoint: ba1a8304] - [x] Task: Analyze `CreateAccount` and `ResetPassword` for Android-specific dependencies. - [x] Task: Migrate required resources (Strings, Drawables) to `sharedfeatures/src/commonMain/composeResources`. - [x] Task: Migrate shared UI components (`Email`, `Password`, `TextFieldState`, `TextFieldError`) if they are not already properly shared/reused. - - [ ] Sub-task: Check for duplication with existing `sharedfeatures` components. - - [ ] Sub-task: Refactor to use common components if duplicates exist. -- [ ] Task: Conductor - User Manual Verification 'Preparation and Shared Components' (Protocol in workflow.md) + - [x] Sub-task: Check for duplication with existing `sharedfeatures` components. + - [x] Sub-task: Refactor to use common components if duplicates exist. +- [x] Task: Conductor - User Manual Verification 'Preparation and Shared Components' (Protocol in workflow.md) -## Phase 2: Create Account Screen Migration +## Phase 2: Create Account Screen Migration [checkpoint: ba1a8304] - [x] Task: Migrate `CreateAccountScreen` class definition to `sharedfeatures`. - [x] Task: Migrate `CreateAccountScreenPresenter` to `sharedfeatures`. - [x] Sub-task: Update imports to use shared UseCases (`SignupUseCase`). @@ -17,21 +17,23 @@ - [x] Task: Migrate `CreateAccountScreenUi` to `sharedfeatures`. - [x] Sub-task: Replace Android resources with `compose.resources`. - [x] Sub-task: Ensure `EmailVerificationUi` and `EmailAndPasswordUi` are included/migrated. -- [ ] Task: Verify `CreateAccount` flow on Android emulator. -- [ ] Task: Conductor - User Manual Verification 'Create Account Screen Migration' (Protocol in workflow.md) +- [x] Task: Verify `CreateAccount` flow on Android emulator. +- [x] Task: Conductor - User Manual Verification 'Create Account Screen Migration' (Protocol in workflow.md) -## Phase 3: Reset Password Screen Migration +## Phase 3: Reset Password Screen Migration [checkpoint: ba1a8304] - [x] Task: Migrate `ResetPasswordScreen` class definition to `sharedfeatures`. - [x] Task: Migrate `ResetPasswordScreenPresenter` to `sharedfeatures`. - [x] Sub-task: Update imports to use shared UseCases (`ResetPasswordUseCase`). + - [x] Sub-task: Replace Android-specific implementations with KMP equivalents. - [x] Sub-task: Create/Migrate Unit Tests for Presenter. - [x] Task: Migrate `ResetPasswordScreenUi` to `sharedfeatures`. - [x] Sub-task: Replace Android resources with `compose.resources`. - [x] Task: Verify `ResetPassword` flow on Android emulator. -- [ ] Task: Conductor - User Manual Verification 'Reset Password Screen Migration' (Protocol in workflow.md) +- [x] Task: Conductor - User Manual Verification 'Reset Password Screen Migration' (Protocol in workflow.md) -## Phase 4: Finalization -- [ ] Task: Update Circuit configuration to register the new shared screens. -- [ ] Task: Remove migrated files from `android/features/login`. -- [ ] Task: Run full regression test on Login, Create Account, and Reset Password flows. -- [ ] Task: Conductor - User Manual Verification 'Finalization' (Protocol in workflow.md) +## Phase 4: Finalization [checkpoint: 5938c5b] +- [x] Task: Update Circuit configuration to register the new shared screens. +- [x] Task: Write new paparazzi tests and record snapshots +- [x] Task: Remove migrated files from `android/features/login`. +- [x] Task: Run full regression test on Login, Create Account, and Reset Password flows. +- [x] Task: Conductor - User Manual Verification 'Finalization' (Protocol in workflow.md) \ No newline at end of file diff --git a/conductor/workflow.md b/conductor/workflow.md index e3d85d63e..bf042b36e 100644 --- a/conductor/workflow.md +++ b/conductor/workflow.md @@ -21,11 +21,13 @@ All tasks follow a strict lifecycle: 3. **Write Failing Tests (Red Phase):** - Create a new test file for the feature or bug fix. + - For UI changes, write new Paparazzi tests or identify existing ones that must be verified. - Write one or more unit tests that clearly define the expected behavior and acceptance criteria for the task. - **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests. 4. **Implement to Pass Tests (Green Phase):** - Write the minimum amount of application code necessary to make the failing tests pass. + - For UI changes, run `./gradlew recordPaparazziDebug` to record new snapshots if visual changes are intended, or `./gradlew verifyPaparazziDebug` to ensure no regression if no changes are intended. - Run the test suite again and confirm that all tests now pass. This is the "Green" phase. 5. **Refactor (Optional but Recommended):** @@ -122,13 +124,18 @@ All tasks follow a strict lifecycle: - **Action:** Stage the modified `plan.md` file. - **Action:** Commit this change with a descriptive message following the format `conductor(plan): Mark phase '' as complete`. -10. **Announce Completion:** Inform the user that the phase is complete and the checkpoint has been created, with the detailed verification report attached as a git note. +10. **Create Branch (Charcoal):** + - **Action:** Always commit and create a branch after every phase using charcoal cli. + - **Command:** `gt b c sky/{branch name} -m "{commit message}"` + +11. **Announce Completion:** Inform the user that the phase is complete and the checkpoint has been created, with the detailed verification report attached as a git note. ### Quality Gates Before marking any task complete, verify: - [ ] All tests pass +- [ ] For UI changes: New snapshots recorded (if changes intended) OR existing snapshots verified (if no changes intended) using Paparazzi - [ ] Code coverage meets requirements (>80%) - [ ] Code follows project's code style guidelines (as defined in `code_styleguides/`) - [ ] All public functions/methods are documented (e.g., docstrings, JSDoc, GoDoc) @@ -177,6 +184,13 @@ Before marking any task complete, verify: - Test authentication and authorization - Check form submissions +### UI Snapshot Testing +- Any UI change must record and/or write new Paparazzi tests for intended changes. +- If there are no intended visual differences, verify existing snapshots. +- Commands: + - Record: `./gradlew recordPaparazziDebug` + - Verify: `./gradlew verifyPaparazziDebug` + ### Mobile Testing - Test on actual iPhone when possible - Use Safari developer tools @@ -256,7 +270,8 @@ A task is complete when: 1. All code implemented to specification 2. Unit tests written and passing -3. Code coverage meets project requirements +3. For UI changes: Paparazzi snapshots recorded/verified +4. Code coverage meets project requirements 4. Documentation complete (if applicable) 5. Code passes all configured linting and static analysis checks 6. Works beautifully on mobile (if applicable) diff --git a/settings.gradle.kts b/settings.gradle.kts index 502629c8d..138c0fbf4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,8 +30,6 @@ include(":android:core:ui-utils") include(":android:features:barcode-scanner") -include(":android:features:login") - include(":android:features:music-player") include(":composeapp") diff --git a/sharedfeatures/build.gradle.kts b/sharedfeatures/build.gradle.kts index 677bc9a54..377d2bc0f 100644 --- a/sharedfeatures/build.gradle.kts +++ b/sharedfeatures/build.gradle.kts @@ -11,6 +11,8 @@ plugins { alias(libs.plugins.paparazzi) } +compose.resources { publicResClass = true } + android { namespace = "io.newm.sharedfeatures" diff --git a/sharedfeatures/src/test/kotlin/io/newm/sharedfeatures/paparazzi/CreateAccountUiTest.kt b/sharedfeatures/src/test/kotlin/io/newm/sharedfeatures/paparazzi/CreateAccountUiTest.kt new file mode 100644 index 000000000..3d345a387 --- /dev/null +++ b/sharedfeatures/src/test/kotlin/io/newm/sharedfeatures/paparazzi/CreateAccountUiTest.kt @@ -0,0 +1,53 @@ +package io.newm.sharedfeatures.paparazzi + +import androidx.compose.ui.Modifier +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import io.newm.core.test.utils.SnapshotTest +import io.newm.core.test.utils.SnapshotTestConfiguration +import io.newm.sharedfeatures.screens.auth.login.EmailState +import io.newm.sharedfeatures.screens.auth.login.PasswordState +import io.newm.sharedfeatures.screens.auth.login.VerificationCodeState +import io.newm.sharedfeatures.screens.auth.signup.CreateAccountUi +import io.newm.sharedfeatures.screens.auth.signup.CreateAccountUiState +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class CreateAccountUiTest( + @TestParameter configuration: SnapshotTestConfiguration, +) : SnapshotTest(configuration) { + @Test + fun emailAndPasswordStep() { + snapshot { + CreateAccountUi( + state = + CreateAccountUiState.EmailAndPasswordUiState( + emailState = EmailState(), + passwordState = PasswordState(), + passwordConfirmationState = PasswordState(), + submitButtonEnabled = false, + errorMessage = null, + eventSink = {}, + ), + modifier = Modifier, + ) + } + } + + @Test + fun emailVerificationStep() { + snapshot { + CreateAccountUi( + state = + CreateAccountUiState.EmailVerificationUiState( + verificationCode = VerificationCodeState(), + errorMessage = null, + nextButtonEnabled = false, + eventSink = {}, + ), + modifier = Modifier, + ) + } + } +} diff --git a/sharedfeatures/src/test/kotlin/io/newm/sharedfeatures/paparazzi/ResetPasswordUiTest.kt b/sharedfeatures/src/test/kotlin/io/newm/sharedfeatures/paparazzi/ResetPasswordUiTest.kt new file mode 100644 index 000000000..3730172c2 --- /dev/null +++ b/sharedfeatures/src/test/kotlin/io/newm/sharedfeatures/paparazzi/ResetPasswordUiTest.kt @@ -0,0 +1,77 @@ +package io.newm.sharedfeatures.paparazzi + +import androidx.compose.ui.Modifier +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import io.newm.core.test.utils.SnapshotTest +import io.newm.core.test.utils.SnapshotTestConfiguration +import io.newm.shared.commonPublic.analytics.NewmAppEventLogger +import io.newm.sharedfeatures.screens.auth.login.EmailState +import io.newm.sharedfeatures.screens.auth.login.PasswordState +import io.newm.sharedfeatures.screens.auth.login.VerificationCodeState +import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreenUi +import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreenUiState +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class ResetPasswordUiTest( + @TestParameter configuration: SnapshotTestConfiguration, +) : SnapshotTest(configuration) { + private val eventLogger = NewmAppEventLogger() + + @Test + fun enterEmailStep() { + snapshot { + ResetPasswordScreenUi( + state = + ResetPasswordScreenUiState.EnterEmail( + email = EmailState(), + isLoading = false, + errorMessage = null, + submitButtonEnabled = false, + eventSink = {}, + ), + eventLogger = eventLogger, + modifier = Modifier, + ) + } + } + + @Test + fun enterVerificationCodeStep() { + snapshot { + ResetPasswordScreenUi( + state = + ResetPasswordScreenUiState.EnterVerificationCode( + code = VerificationCodeState(), + submitButtonEnabled = false, + isLoading = false, + errorMessage = null, + eventSink = {}, + ), + eventLogger = eventLogger, + modifier = Modifier, + ) + } + } + + @Test + fun enterNewPasswordStep() { + snapshot { + ResetPasswordScreenUi( + state = + ResetPasswordScreenUiState.EnterNewPassword( + password = PasswordState(), + confirmPasswordState = PasswordState(), + submitButtonEnabled = false, + isLoading = false, + errorMessage = null, + eventSink = {}, + ), + eventLogger = eventLogger, + modifier = Modifier, + ) + } + } +} diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[NormalDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[NormalDark].png new file mode 100644 index 000000000..3ca96e6e2 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[NormalDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7db2d45aae4d78da762965d9372e791b3ea11c014814daa5d49f51de4d97ee8 +size 29535 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[NormalLAccessibilityLight].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[NormalLAccessibilityLight].png new file mode 100644 index 000000000..6a543481a --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[NormalLAccessibilityLight].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a832a1981c3600e4a078ee5bf9c345f3d4a4446ba8333f1930660be107bf0074 +size 34507 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[TabletDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[TabletDark].png new file mode 100644 index 000000000..4f175a100 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailAndPasswordStep[TabletDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f489c26492fbcb4efc9609393b301cd8d9d77e01b5f83ca9877f7b2b0b0eade +size 29597 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[NormalDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[NormalDark].png new file mode 100644 index 000000000..188e82c07 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[NormalDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:178734cbbe2611db4f259c652d2559b5be6ebcd5db8c32fecd6449a1047d72cd +size 33749 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[NormalLAccessibilityLight].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[NormalLAccessibilityLight].png new file mode 100644 index 000000000..307a2027f --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[NormalLAccessibilityLight].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:109bbf22827476933d69b66c8d910f0d82d0dd16f3c9edeeed09f342fe1ade62 +size 44178 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[TabletDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[TabletDark].png new file mode 100644 index 000000000..b990aa859 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_CreateAccountUiTest_emailVerificationStep[TabletDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd3bedc3b6d9f578bbcbcbc02240d8b99c9f0886194a7d32af09a85d77170b1f +size 39693 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[NormalDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[NormalDark].png new file mode 100644 index 000000000..9d310f605 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[NormalDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55556a0d3c091273ad24fecc73f4d6621a910a825c827e8eec33885d98246bb9 +size 29038 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[NormalLAccessibilityLight].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[NormalLAccessibilityLight].png new file mode 100644 index 000000000..39a9ac3b0 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[NormalLAccessibilityLight].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ce9a5b6b85cf50997898b1da7085f455310321019c39b3a625370f3d6447926 +size 37932 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[TabletDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[TabletDark].png new file mode 100644 index 000000000..f57fe9f25 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterEmailStep[TabletDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cfddf8b11b4ffc0d778c314b017c4015c6b4f3ad314729a49f277daaed29b65 +size 31985 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[NormalDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[NormalDark].png new file mode 100644 index 000000000..03366efbb --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[NormalDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ba4c91586634b39f027fbc7b2cc6c50479b498366c8004761ff94cfd3902151 +size 24445 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[NormalLAccessibilityLight].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[NormalLAccessibilityLight].png new file mode 100644 index 000000000..0a715074a --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[NormalLAccessibilityLight].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a666a1ce74d4cb6ae5c8d85399dce80c1f352b3707b02a25bd912753a4a3bb +size 30587 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[TabletDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[TabletDark].png new file mode 100644 index 000000000..5b4f1eaa4 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterNewPasswordStep[TabletDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dc2ba6c3950ba942c2ef6ec63597b6f392b0599108a4e9c056c5da29d5d7429 +size 27953 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[NormalDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[NormalDark].png new file mode 100644 index 000000000..188e82c07 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[NormalDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:178734cbbe2611db4f259c652d2559b5be6ebcd5db8c32fecd6449a1047d72cd +size 33749 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[NormalLAccessibilityLight].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[NormalLAccessibilityLight].png new file mode 100644 index 000000000..307a2027f --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[NormalLAccessibilityLight].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:109bbf22827476933d69b66c8d910f0d82d0dd16f3c9edeeed09f342fe1ade62 +size 44178 diff --git a/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[TabletDark].png b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[TabletDark].png new file mode 100644 index 000000000..b990aa859 --- /dev/null +++ b/sharedfeatures/src/test/snapshots/images/io.newm.sharedfeatures.paparazzi_ResetPasswordUiTest_enterVerificationCodeStep[TabletDark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd3bedc3b6d9f578bbcbcbc02240d8b99c9f0886194a7d32af09a85d77170b1f +size 39693