Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions android/app-newm/src/main/java/io/newm/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import io.newm.core.theme.NewmTheme
import io.newm.core.ui.LocalSnackBarHostState
import io.newm.feature.login.screen.createaccount.CreateAccountScreenPresenter
import io.newm.feature.login.screen.createaccount.CreateAccountUi
import io.newm.feature.login.screen.createaccount.CreateAccountUiState
import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenPresenter
import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenUi
import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenUiState
import io.newm.screens.forceupdate.ForceAppUpdateState
import io.newm.screens.forceupdate.ForceAppUpdateUi
import io.newm.screens.forceupdate.openAppPlayStore
import io.newm.shared.NewmAppLogger
import io.newm.shared.commonPublic.analytics.NewmAppEventLogger
import io.newm.shared.commonPublic.analytics.events.AppScreens
import io.newm.sharedfeatures.screens.CreateAccountScreen
import io.newm.sharedfeatures.screens.DevMenuMainScreen
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen
import io.newm.sharedfeatures.screens.LoginScreen
import io.newm.sharedfeatures.screens.ResetPasswordScreen
import io.newm.sharedfeatures.screens.WelcomeScreen
import io.newm.sharedfeatures.screens.auth.login.LoginPresenter
import io.newm.sharedfeatures.screens.auth.login.LoginUi
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.auth.welcome.WelcomePresenter
import io.newm.sharedfeatures.screens.auth.welcome.WelcomeUi
import io.newm.sharedfeatures.screens.devmenu.DevMenuPresenter
Expand Down Expand Up @@ -78,8 +78,7 @@ class LoginActivity : ComponentActivity() {
Presenter.Factory { screen, navigator, _ ->
when (screen) {
is CreateAccountScreen -> {
inject<CreateAccountScreenPresenter> { parametersOf(::launchHomeActivity) }
.value
inject<CreateAccountScreenPresenter> { parametersOf(navigator) }.value
}

is WelcomeScreen -> {
Expand All @@ -91,7 +90,7 @@ class LoginActivity : ComponentActivity() {
}

is ResetPasswordScreen -> {
inject<ResetPasswordScreenPresenter> { parametersOf(navigator) }.value
inject<ResetPasswordScreenPresenter> { parametersOf(screen, navigator) }.value
}

is DevMenuMainScreen -> {
Expand Down Expand Up @@ -125,8 +124,7 @@ class LoginActivity : ComponentActivity() {

is ResetPasswordScreen -> {
ui<ResetPasswordScreenUiState> { state, modifier ->
ResetPasswordScreenUi(eventLogger)
.Content(state = state, modifier = modifier)
ResetPasswordScreenUi(state, eventLogger, modifier)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import com.google.android.gms.common.Scopes
import com.google.android.gms.common.api.Scope
import io.newm.Logout
import io.newm.RestartApp
import io.newm.feature.login.screen.createaccount.CreateAccountScreenPresenter
import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenPresenter
import io.newm.feature.musicplayer.service.DownloadManager
import io.newm.feature.musicplayer.service.DownloadManagerImpl
import io.newm.feature.musicplayer.service.DownloadStateManager
Expand All @@ -36,6 +34,8 @@ import io.newm.sharedfeatures.screens.auth.login.LoginPresenter
import io.newm.sharedfeatures.screens.auth.login.RecaptchaClientProvider
import io.newm.sharedfeatures.screens.auth.login.RecaptchaManager
import io.newm.sharedfeatures.screens.auth.login.RecaptchaManagerImpl
import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreenPresenter
import io.newm.sharedfeatures.screens.auth.signup.CreateAccountScreenPresenter
import io.newm.sharedfeatures.screens.auth.welcome.SocialLoginManager
import io.newm.sharedfeatures.screens.auth.welcome.SocialLoginManagerImpl
import io.newm.sharedfeatures.screens.auth.welcome.WelcomePresenter
Expand All @@ -61,7 +61,16 @@ val viewModule =
}

factory { params ->
ResetPasswordScreenPresenter(params.get(), get(), get(), get(), get(), get(), get())
ResetPasswordScreenPresenter(
screen = params[0],
navigator = params[1],
signupUseCase = get(),
loginUseCase = get(),
resetPasswordUseCase = get(),
recaptchaManager = get(),
logger = get(),
analyticsTracker = get(),
)
}
single {
val sharedBuildConfig = get<NewmSharedBuildConfig>()
Expand Down
5 changes: 5 additions & 0 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ This file tracks all major tracks for the project. Each track has its own detail

- [~] **Track: Enhance Music Player UI/UX and Performance**
*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.**
*Link: [./tracks/feature_login_migration_20260122/](./tracks/feature_login_migration_20260122/)*

5 changes: 5 additions & 0 deletions conductor/tracks/feature_login_migration_20260122/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Track feature_login_migration_20260122 Context

- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"track_id": "feature_login_migration_20260122",
"type": "feature",
"status": "new",
"created_at": "2026-01-22T12:00:00Z",
"updated_at": "2026-01-22T12:00:00Z",
"description": "Migrate CreateAccount and ResetPassword screens from android module to sharedfeatures module for Compose Multiplatform compatibility."
}
37 changes: 37 additions & 0 deletions conductor/tracks/feature_login_migration_20260122/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Implementation Plan - Login Feature Migration (Part 2)

## Phase 1: Preparation and Shared Components
- [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)

## Phase 2: Create Account Screen Migration
- [x] Task: Migrate `CreateAccountScreen` class definition to `sharedfeatures`.
- [x] Task: Migrate `CreateAccountScreenPresenter` to `sharedfeatures`.
- [x] Sub-task: Update imports to use shared UseCases (`SignupUseCase`).
- [x] Sub-task: Replace Android-specific implementations with KMP equivalents.
- [x] Sub-task: Create/Migrate Unit Tests for Presenter.
- [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)

## Phase 3: Reset Password Screen Migration
- [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: 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)

## 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)
29 changes: 29 additions & 0 deletions conductor/tracks/feature_login_migration_20260122/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Specification: Login Feature Migration (Part 2)

## Overview
This track focuses on completing the migration of the Login Feature by moving the `CreateAccount` and `ResetPassword` screens from the `android` module to the `sharedfeatures` module. This ensures the entire authentication flow is compatible with Compose Multiplatform, enabling future cross-platform deployment.

## Functional Requirements
1. **Create Account Migration:**
* Migrate `CreateAccountScreen`, `CreateAccountScreenPresenter`, and `CreateAccountScreenUi` to `sharedfeatures`.
* Migrate helper UI components: `EmailVerificationUi`, `EmailAndPasswordUi`.
* Migrate state management: `CreateAccountUiState`.
2. **Reset Password Migration:**
* Migrate `ResetPasswordScreen`, `ResetPasswordScreenPresenter`, and `ResetPasswordScreenUi` to `sharedfeatures`.
* Migrate state management: `ResetPasswordScreenUiState`, `ResetPasswordUiEvent`.
3. **Shared Components & Resources:**
* Migrate shared sub-components like `TextFieldState`, `TextFieldError`, `Email`, `EmailState`, `Password`, `PasswordState` if they are not already shared.
* Migrate strings and drawable resources used by these screens to `compose.resources` in `sharedfeatures`.
4. **Navigation:**
* Update Circuit configuration to use the new shared screens.

## Non-Functional Requirements
- **Architecture:** Maintain the Circuit architecture (Presenter + UI).
- **Compatibility:** Ensure the migrated code compiles for all shared targets (Android, iOS, Desktop, Wasm).
- **Parity:** Zero visual or functional regression on Android.

## Acceptance Criteria
- [ ] **Create Account:** User can sign up, verify email (if applicable in flow), and land on the main screen.
- [ ] **Reset Password:** User can request a password reset email.
- [ ] **Resources:** All text and icons render correctly from shared resources.
- [ ] **Tests:** Presenter unit tests are migrated/added and passing.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import io.newm.shared.commonInternal.implementations.utilities.mapErrorsSuspend
import io.newm.shared.commonInternal.repositories.LogInRepository
import io.newm.shared.commonPublic.models.error.KMMException
import io.newm.shared.commonPublic.usecases.ResetPasswordUseCase
import me.tatarka.inject.annotations.Inject
import kotlin.coroutines.cancellation.CancellationException

internal class ResetPasswordUseCaseImpl(
@Inject
class ResetPasswordUseCaseImpl(
private var repository: LogInRepository,
) : ResetPasswordUseCase {
@Throws(KMMException::class, CancellationException::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import io.newm.shared.commonInternal.implementations.utilities.mapErrorsSuspend
import io.newm.shared.commonInternal.repositories.LogInRepository
import io.newm.shared.commonPublic.models.error.KMMException
import io.newm.shared.commonPublic.usecases.SignupUseCase
import me.tatarka.inject.annotations.Inject
import kotlin.coroutines.cancellation.CancellationException

internal class SignupUseCaseImpl(
@Inject
class SignupUseCaseImpl(
private val repository: LogInRepository,
) : SignupUseCase {
@Throws(KMMException::class, CancellationException::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package io.newm.shared.di.dagger

import io.newm.shared.commonInternal.implementations.LoginUseCaseImpl
import io.newm.shared.commonInternal.implementations.ResetPasswordUseCaseImpl
import io.newm.shared.commonInternal.implementations.SignupUseCaseImpl
import io.newm.shared.commonPublic.usecases.LoginUseCase
import io.newm.shared.commonPublic.usecases.ResetPasswordUseCase
import io.newm.shared.commonPublic.usecases.SignupUseCase
import me.tatarka.inject.annotations.Provides

interface UseCaseComponent {
@Provides fun provideLoginUseCase(impl: LoginUseCaseImpl): LoginUseCase = impl

@Provides fun provideSignupUseCase(impl: SignupUseCaseImpl): SignupUseCase = impl

@Provides
fun provideResetPasswordUseCase(impl: ResetPasswordUseCaseImpl): ResetPasswordUseCase = impl
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@ actual class RecaptchaManagerImpl(
} catch (e: Exception) {
Result.failure(e)
}

actual override suspend fun execute(action: String): Result<String> =
try {
val token =
recaptchaClientProvider.get().execute(RecaptchaAction.custom(action)).getOrThrow()
Result.success(token)
} catch (e: Exception) {
Result.failure(e)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import io.newm.shared.di.dagger.ActivityScope
import io.newm.sharedfeatures.screens.auth.login.LoginComponent
import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordComponent
import io.newm.sharedfeatures.screens.auth.signup.CreateAccountComponent
import io.newm.sharedfeatures.screens.auth.welcome.WelcomeComponent
import io.newm.sharedfeatures.screens.devmenu.DevMenuComponent
import me.tatarka.inject.annotations.Provides

interface CircuitComponent :
WelcomeComponent,
DevMenuComponent,
LoginComponent {
LoginComponent,
CreateAccountComponent,
ResetPasswordComponent {
val circuit: Circuit

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import io.newm.shared.commonPublic.analytics.events.AppScreens
import io.newm.shared.commonPublic.usecases.LoginUseCase
import io.newm.sharedfeatures.screens.HomeScreen
import io.newm.sharedfeatures.screens.LoginScreen
import io.newm.sharedfeatures.screens.ResetPasswordScreen
import io.newm.sharedfeatures.screens.auth.resetpassword.ResetPasswordScreen
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ package io.newm.sharedfeatures.screens.auth.login

interface RecaptchaManager {
suspend fun executeLogin(): Result<String>

suspend fun execute(action: String): Result<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ import me.tatarka.inject.annotations.Inject
@Inject
expect class RecaptchaManagerImpl : RecaptchaManager {
override suspend fun executeLogin(): Result<String>

override suspend fun execute(action: String): Result<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ open class TextFieldState(
}
}

class EmailState : TextFieldState(validator = ::isEmailValid, errorFor = ::emailValidationError)
class EmailState(
defaultValue: String = "",
) : TextFieldState(
defaultValue = defaultValue,
validator = ::isEmailValid,
errorFor = ::emailValidationError,
)

private fun emailValidationError(email: String): StringResource {
// Note: The original android code passed the email into the string resource.
Expand All @@ -63,11 +69,11 @@ private val EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".toRegex()

private fun isEmailValid(email: String): Boolean = EMAIL_REGEX.matches(email)

class PasswordState : TextFieldState(validator = ::isPasswordValid, errorFor = { passwordValidationError() })
open class PasswordState : TextFieldState(validator = ::isPasswordValid, errorFor = { passwordValidationError() })

class ConfirmPasswordState(
private val passwordState: PasswordState,
) : TextFieldState() {
) : PasswordState() {
override val isValid
get() = passwordAndConfirmationValid(passwordState.text, text)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.newm.sharedfeatures.screens.auth.login

import org.jetbrains.compose.resources.StringResource

sealed interface UiMessage {
data class Resource(
val resId: StringResource,
) : UiMessage

data class Text(
val text: String,
) : UiMessage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.newm.sharedfeatures.screens.auth.resetpassword

import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import io.newm.shared.di.dagger.ActivityScope
import me.tatarka.inject.annotations.IntoSet
import me.tatarka.inject.annotations.Provides

interface ResetPasswordComponent {
@Provides
@IntoSet
@ActivityScope
fun ResetPasswordPresenterFactory.bind(): Presenter.Factory = this

@Provides @IntoSet @ActivityScope
fun ResetPasswordUiFactory.bind(): Ui.Factory = this
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.newm.sharedfeatures.screens
package io.newm.sharedfeatures.screens.auth.resetpassword

import com.slack.circuit.runtime.screen.Screen
import io.newm.sharedfeatures.parceling.CommonParcelize
Expand Down
Loading