Skip to content

Commit

Permalink
Merge pull request #70 from CrisisCleanup/forgot-password
Browse files Browse the repository at this point in the history
Refactor auth flows and add forgot password
  • Loading branch information
hueachilles authored Aug 26, 2023
2 parents 369955d + 3d7ae9d commit 826f01a
Show file tree
Hide file tree
Showing 31 changed files with 1,207 additions and 129 deletions.
4 changes: 4 additions & 0 deletions app/src/main/java/com/crisiscleanup/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ class MainActivity : ComponentActivity() {
viewModel.authState.collect { authState = it }
}
}

intent?.let {
viewModel.processMainIntent(it)
}
}

override fun onResume() {
Expand Down
42 changes: 40 additions & 2 deletions app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.crisiscleanup

import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.crisiscleanup.core.appheader.AppHeaderUiState
import com.crisiscleanup.core.common.AppEnv
import com.crisiscleanup.core.common.AppVersionProvider
import com.crisiscleanup.core.common.KeyResourceTranslator
import com.crisiscleanup.core.common.event.AuthEventBus
import com.crisiscleanup.core.common.log.AppLogger
import com.crisiscleanup.core.common.log.CrisisCleanupLoggers
import com.crisiscleanup.core.common.log.Logger
import com.crisiscleanup.core.common.network.CrisisCleanupDispatchers.IO
import com.crisiscleanup.core.common.network.Dispatcher
import com.crisiscleanup.core.common.sync.SyncPuller
Expand Down Expand Up @@ -60,10 +66,10 @@ class MainActivityViewModel @Inject constructor(
private val appVersionProvider: AppVersionProvider,
private val appEnv: AppEnv,
firebaseAnalytics: FirebaseAnalytics,
private val authEventBus: AuthEventBus,
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
@Logger(CrisisCleanupLoggers.App) private val logger: AppLogger,
) : ViewModel() {
val isDebuggable = appEnv.isDebuggable

/**
* Previous app open
*
Expand Down Expand Up @@ -145,6 +151,9 @@ class MainActivityViewModel @Inject constructor(
return null
}

// TODO Build route to auth/forgot-password rather than switches through the hierarchy
val showPasswordReset = authEventBus.showResetPassword

init {
accountDataRepository.accountData
.onEach {
Expand Down Expand Up @@ -189,6 +198,35 @@ class MainActivityViewModel @Inject constructor(
}
}
}

fun processMainIntent(intent: Intent) {
when (val action = intent.action) {
Intent.ACTION_VIEW -> {
intent.data?.let { intentUri ->
intentUri.path?.let { urlPath ->
processMainIntent(intentUri, urlPath)
}
}
}

else -> {
logger.logDebug("Main intent action not handled $action")
}
}
}

private fun processMainIntent(url: Uri, urlPath: String) {
if (urlPath.startsWith("/o/callback")) {
url.getQueryParameter("code")?.let { code ->
authEventBus.onEmailLoginLink(code)
}
} else if (urlPath.startsWith("/password/reset/")) {
val code = urlPath.replace("/password/reset/", "")
if (code.isNotBlank()) {
authEventBus.onResetPassword(code)
}
}
}
}

sealed interface MainActivityUiState {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.crisiscleanup.navigation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.crisiscleanup.core.appnav.RouteConstant
import com.crisiscleanup.feature.authentication.navigation.authGraph
import com.crisiscleanup.feature.authentication.navigation.emailLoginLinkScreen
import com.crisiscleanup.feature.authentication.navigation.forgotPasswordScreen
import com.crisiscleanup.feature.authentication.navigation.navigateToEmailLoginLink
import com.crisiscleanup.feature.authentication.navigation.navigateToForgotPassword

@Composable
fun CrisisCleanupAuthNavHost(
navController: NavHostController,
enableBackHandler: Boolean,
closeAuthentication: () -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
startDestination: String = RouteConstant.authGraphRoutePattern,
) {
val navToForgotPassword =
remember(navController) { { navController.navigateToForgotPassword() } }
val navToEmailMagicLink =
remember(navController) { { navController.navigateToEmailLoginLink() } }

NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
authGraph(
nestedGraphs = {
forgotPasswordScreen(onBack)
emailLoginLinkScreen(onBack)
},
enableBackHandler = enableBackHandler,
closeAuthentication = closeAuthentication,
openForgotPassword = navToForgotPassword,
openEmailMagicLink = navToEmailMagicLink,
)
}
}
49 changes: 25 additions & 24 deletions app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,9 @@ import com.crisiscleanup.core.designsystem.icon.Icon.DrawableResourceIcon
import com.crisiscleanup.core.designsystem.icon.Icon.ImageVectorIcon
import com.crisiscleanup.core.ui.AppLayoutArea
import com.crisiscleanup.core.ui.LocalAppLayout
import com.crisiscleanup.core.ui.ScreenKeyboardVisibility
import com.crisiscleanup.core.ui.screenKeyboardVisibility
import com.crisiscleanup.feature.authentication.AuthenticateScreen
import com.crisiscleanup.core.ui.rememberIsKeyboardOpen
import com.crisiscleanup.feature.cases.ui.SelectIncidentDialog
import com.crisiscleanup.navigation.CrisisCleanupAuthNavHost
import com.crisiscleanup.navigation.CrisisCleanupNavHost
import com.crisiscleanup.navigation.TopLevelDestination
import com.crisiscleanup.feature.authentication.R as authenticationR
Expand Down Expand Up @@ -147,17 +146,18 @@ private fun LoadedContent(
) {
val isAccountExpired by viewModel.isAccountExpired

val showPasswordReset by viewModel.showPasswordReset.collectAsStateWithLifecycle(false)
val isNotAuthenticatedState = authState !is AuthState.Authenticated
var openAuthentication by rememberSaveable { mutableStateOf(isNotAuthenticatedState) }
if (openAuthentication || isNotAuthenticatedState) {
if (openAuthentication || isNotAuthenticatedState || showPasswordReset) {
val toggleAuthentication = remember(authState) {
{ open: Boolean -> openAuthentication = open }
}
AuthenticateContent(
snackbarHostState,
appState,
!isNotAuthenticatedState,
toggleAuthentication,
viewModel.isDebuggable,
)
} else {
val accountData = (authState as AuthState.Authenticated).accountData
Expand Down Expand Up @@ -208,9 +208,9 @@ private fun LoadedContent(
@Composable
private fun AuthenticateContent(
snackbarHostState: SnackbarHostState,
appState: CrisisCleanupAppState,
enableBackHandler: Boolean,
toggleAuthentication: (Boolean) -> Unit,
isDebuggable: Boolean = false,
) {
Scaffold(
modifier = Modifier.semantics {
Expand All @@ -220,23 +220,23 @@ private fun AuthenticateContent(
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets.systemBars,
snackbarHost = { SnackbarHost(snackbarHostState) },
content = { padding ->
AuthenticateScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal,
),
) { padding ->
CrisisCleanupAuthNavHost(
navController = appState.navController,
enableBackHandler = enableBackHandler,
closeAuthentication = { toggleAuthentication(false) },
onBack = appState::onBack,
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal,
),
enableBackHandler = enableBackHandler,
closeAuthentication = { toggleAuthentication(false) },
isDebug = isDebuggable,
)
},
)
),
)
}
}

@OptIn(
Expand Down Expand Up @@ -339,12 +339,12 @@ private fun NavigableContent(
)
}

val keyboardVisibility by screenKeyboardVisibility()
val isKeyboardOpen = rememberIsKeyboardOpen()
Column(Modifier.fillMaxSize()) {
val snackbarAreaHeight =
if (!showNavigation &&
snackbarHostState.currentSnackbarData != null &&
keyboardVisibility == ScreenKeyboardVisibility.NotVisible
!isKeyboardOpen
) {
64.dp
} else {
Expand All @@ -358,6 +358,7 @@ private fun NavigableContent(
navController = appState.navController,
onBack = appState::onBack,
modifier = Modifier.weight(1f),
startDestination = appState.lastTopLevelRoute(),
)
}

Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.tracing.trace
import com.crisiscleanup.core.appnav.RouteConstant.casesGraphRoutePattern
import com.crisiscleanup.core.appnav.RouteConstant.casesRoute
import com.crisiscleanup.core.appnav.RouteConstant.dashboardRoute
import com.crisiscleanup.core.appnav.RouteConstant.menuRoute
Expand Down Expand Up @@ -112,6 +113,18 @@ class CrisisCleanupAppState(
MENU,
)

private var priorTopLevelDestination: TopLevelDestination = CASES

@Composable
fun lastTopLevelRoute(): String {
return when (priorTopLevelDestination) {
DASHBOARD -> dashboardRoute
TEAM -> teamRoute
MENU -> menuRoute
else -> casesGraphRoutePattern
}
}

/**
* UI logic for navigating to a top level destination in the app. Top level destinations have
* only one copy of the destination of the back stack, and save and restore state whenever you
Expand All @@ -135,6 +148,8 @@ class CrisisCleanupAppState(
restoreState = true
}

priorTopLevelDestination = topLevelDestination

when (topLevelDestination) {
CASES -> navController.navigateToCases(topLevelNavOptions)
DASHBOARD -> navController.navigateToDashboard(topLevelNavOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package com.crisiscleanup.core.appnav

object RouteConstant {
const val casesGraphRoutePattern = "cases_graph"
const val authGraphRoutePattern = "auth_graph"

const val authRoute = "auth_route"
const val forgotPasswordRoute = "forgot_password_route"
const val emailLoginLinkRoute = "email_login_link_route"
// const val resetPasswordRoute = "reset_password_route"

// This cannot be used as the navHost startDestination
const val casesRoute = "cases_route"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object LoggersModule {
@Provides
@Logger(CrisisCleanupLoggers.Account)
fun providesAccountLogger(logger: TagLogger): AppLogger {
logger.tag = "account"
return logger
}

@Provides
@Logger(CrisisCleanupLoggers.App)
fun providesAppLogger(logger: TagLogger): AppLogger {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.crisiscleanup.core.common.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -12,9 +15,17 @@ interface AuthEventBus {
val logouts: Flow<Boolean>
val refreshedTokens: Flow<Boolean>

val showResetPassword: Flow<Boolean>
val resetPasswords: StateFlow<String>
val emailLoginLinks: Flow<String>

fun onLogout()

fun onTokensRefreshed()

fun onResetPassword(code: String)

fun onEmailLoginLink(code: String)
}

@Singleton
Expand All @@ -24,6 +35,10 @@ class CrisisCleanupAuthEventBus @Inject constructor(
override val logouts = MutableSharedFlow<Boolean>(0)
override val refreshedTokens = MutableSharedFlow<Boolean>(0)

override val resetPasswords = MutableStateFlow("")
override val showResetPassword = resetPasswords.map { it.isNotBlank() }
override val emailLoginLinks = MutableSharedFlow<String>(1)

override fun onLogout() {
externalScope.launch {
logouts.emit(true)
Expand All @@ -35,4 +50,16 @@ class CrisisCleanupAuthEventBus @Inject constructor(
refreshedTokens.emit(true)
}
}

override fun onResetPassword(code: String) {
externalScope.launch {
resetPasswords.value = code
}
}

override fun onEmailLoginLink(code: String) {
externalScope.launch {
emailLoginLinks.emit(code)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface TagLogger : AppLogger {
annotation class Logger(val loggers: CrisisCleanupLoggers)

enum class CrisisCleanupLoggers {
Account,
App,
Auth,
Cases,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,23 @@ interface DataModule {

@Singleton
@Binds
fun casesFiltersRepository(
fun bindsCasesFiltersRepository(
repository: CrisisCleanupCasesFilterRepository,
): CasesFilterRepository

@Singleton
@Binds
fun caseHistoryRepository(
fun bindsCaseHistoryRepository(
repository: OfflineFirstCaseHistoryRepository,
): CaseHistoryRepository

@Binds
fun endOfLifeRepository(
fun bindsAccountUpdateRepository(
repository: CrisisCleanupAccountUpdateRepository,
): AccountUpdateRepository

@Binds
fun bindsEndOfLifeRepository(
repository: AppEndOfLifeRepository,
): EndOfLifeRepository
}
Expand Down
Loading

0 comments on commit 826f01a

Please sign in to comment.