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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import org.groundplatform.android.model.User

sealed class SignInState {

/** Returns true if a sign-in attempt is allowed based on current state. */
fun shouldAllowSignIn(): Boolean = this is SignedOut || this is Error

data object SignedOut : SignInState()

data object SigningIn : SignInState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import org.groundplatform.android.ui.offlineareas.OfflineAreasViewModel
import org.groundplatform.android.ui.offlineareas.selector.OfflineAreaSelectorViewModel
import org.groundplatform.android.ui.offlineareas.viewer.OfflineAreaViewerViewModel
import org.groundplatform.android.ui.settings.SettingsViewModel
import org.groundplatform.android.ui.signin.SignInViewModel
import org.groundplatform.android.ui.surveyselector.SurveySelectorViewModel
import org.groundplatform.android.ui.syncstatus.SyncStatusViewModel
import org.groundplatform.android.ui.tos.TermsOfServiceViewModel
Expand Down Expand Up @@ -83,11 +82,6 @@ abstract class ViewModelModule {
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(SignInViewModel::class)
abstract fun bindSignInVideModel(viewModel: SignInViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(TermsOfServiceViewModel::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import org.groundplatform.android.system.ActivityStreams
import org.groundplatform.android.ui.common.AbstractActivity
import org.groundplatform.android.ui.common.BackPressListener
import org.groundplatform.android.ui.common.ViewModelFactory
import org.groundplatform.android.ui.common.modalSpinner
import org.groundplatform.android.ui.components.PermissionDeniedDialog
import org.groundplatform.android.ui.home.HomeScreenFragmentDirections
import org.groundplatform.android.ui.signin.SignInFragmentDirections
Expand All @@ -64,8 +63,6 @@ class MainActivity : AbstractActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var navHostFragment: NavHostFragment

private var signInProgressDialog: AlertDialog? = null

private var pendingDeepLink: Uri? = null

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -133,19 +130,12 @@ class MainActivity : AbstractActivity() {
MainUiState.ShowHomeScreen -> {
navigateTo(HomeScreenFragmentDirections.showHomeScreen())
}
MainUiState.OnUserSigningIn -> {
onSignInProgress(true)
}
is MainUiState.ActiveSurveyById -> {
val action = SurveySelectorFragmentDirections.showSurveySelectorScreen(false)
action.surveyId = uiState.surveyId
navigateTo(action)
}
}

if (uiState != MainUiState.OnUserSigningIn) {
onSignInProgress(false)
}
}

private fun showPermissionDeniedDialog() {
Expand Down Expand Up @@ -224,24 +214,6 @@ class MainActivity : AbstractActivity() {
return currentFragment is BackPressListener && currentFragment.onBack()
}

private fun onSignInProgress(visible: Boolean) {
if (visible) showSignInDialog() else dismissSignInDialog()
}

private fun showSignInDialog() {
if (signInProgressDialog == null) {
signInProgressDialog = modalSpinner(this, layoutInflater, R.string.signing_in)
}
signInProgressDialog?.show()
}

private fun dismissSignInDialog() {
if (signInProgressDialog != null) {
signInProgressDialog?.dismiss()
signInProgressDialog = null
}
}

private fun navigateTo(directions: NavDirections) {
val currentDestination = navHostFragment.navController.currentDestination
val action = currentDestination?.getAction(directions.actionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ sealed class MainUiState {

data object OnUserSignedOut : MainUiState()

data object OnUserSigningIn : MainUiState()

data object TosNotAccepted : MainUiState()

data object NoActiveSurvey : MainUiState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ constructor(
_deepLinkUri.value = uri
}

private suspend fun onSignInStateChange(signInState: SignInState): MainUiState =
private suspend fun onSignInStateChange(signInState: SignInState): MainUiState? =
when (signInState) {
is SignInState.Error -> onUserSignInError(signInState.error)
is SignInState.SignedIn -> onUserSignedIn(signInState.user)
is SignInState.SignedOut -> onUserSignedOut()
is SignInState.SigningIn -> MainUiState.OnUserSigningIn
else -> null
}

private fun onUserSignInError(error: Throwable): MainUiState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,58 +21,24 @@ import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import org.groundplatform.android.R
import org.groundplatform.android.ui.common.AbstractFragment
import org.groundplatform.android.ui.common.BackPressListener
import org.groundplatform.android.ui.theme.AppTheme

@AndroidEntryPoint
class SignInFragment : AbstractFragment(), BackPressListener {

private lateinit var viewModel: SignInViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModel(SignInViewModel::class.java)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View =
ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { AppTheme { SignInScreen(onSignInClick = { viewModel.onSignInButtonClick() }) } }
setContent { AppTheme { SignInScreen() } }
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

lifecycleScope.launch(Dispatchers.Main) {
viewModel.getNetworkFlow().filterNotNull().collect { connected ->
if (!connected) {
displayNetworkError()
}
}
}
}

private fun displayNetworkError() {
Snackbar.make(
requireView(),
getString(R.string.network_error_when_signing_in),
Snackbar.LENGTH_LONG,
)
.show()
}

override fun onBack(): Boolean {
// Workaround to exit on back from sign-in screen since for some reason
// popUpTo is not working on signOut action.
Expand Down
109 changes: 98 additions & 11 deletions app/src/main/java/org/groundplatform/android/ui/signin/SignInScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ 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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
Expand All @@ -40,15 +50,48 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.gms.common.SignInButton
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.groundplatform.android.R
import org.groundplatform.android.system.auth.SignInState
import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport
import org.groundplatform.android.ui.components.LoadingDialog
import org.groundplatform.android.ui.theme.AppTheme

const val BUTTON_TEST_TAG = "google_sign_in_button"

/**
* Displays the sign-in screen, handling network connectivity status and authentication state.
*
* @param viewModel the view model used to manage sign-in state and network connectivity.
*/
@Composable
fun SignInScreen(viewModel: SignInViewModel = hiltViewModel()) {
val connected by viewModel.networkAvailable.collectAsStateWithLifecycle()
val signInState by viewModel.signInState.collectAsStateWithLifecycle()

SignInContent(
connected = connected,
signInState = signInState,
onSignInClick = { viewModel.onSignInButtonClick() },
)
}

@Composable
fun SignInScreen(onSignInClick: () -> Unit) {
private fun SignInContent(connected: Boolean, signInState: SignInState, onSignInClick: () -> Unit) {
val snackbarHostState = remember { SnackbarHostState() }
val networkErrorMessage = stringResource(R.string.network_error_when_signing_in)

LaunchedEffect(connected) {
if (!connected) {
snackbarHostState.showSnackbar(networkErrorMessage)
}
}

if (signInState is SignInState.SigningIn) {
LoadingDialog(R.string.signing_in)
}

Box(modifier = Modifier.fillMaxSize()) {

// Background image
Expand All @@ -67,8 +110,10 @@ fun SignInScreen(onSignInClick: () -> Unit) {
verticalArrangement = Arrangement.SpaceAround,
) {
LogoAndTitle()
GoogleSignInButton { onSignInClick() }
GoogleSignInButton(enabled = connected && signInState.shouldAllowSignIn()) { onSignInClick() }
}

SnackbarHost(hostState = snackbarHostState, modifier = Modifier.align(Alignment.BottomCenter))
}
}

Expand Down Expand Up @@ -110,17 +155,59 @@ private fun LogoAndTitle(modifier: Modifier = Modifier) {
}

@Composable
private fun GoogleSignInButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
AndroidView(
private fun GoogleSignInButton(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier.wrapContentSize().testTag(BUTTON_TEST_TAG),
factory = { context -> SignInButton(context).apply { setSize(SignInButton.SIZE_WIDE) } },
update = { button -> button.setOnClickListener { onClick() } },
)
colors =
ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.DarkGray,
disabledContainerColor = Color.LightGray,
disabledContentColor = Color.Gray,
),
Comment thread
shobhitagarwal1612 marked this conversation as resolved.
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.ic_google_logo),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified,
)
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
Text(text = stringResource(id = R.string.sign_in_with_google), fontSize = 16.sp)
}
}
}

@Preview(showBackground = true)
@Composable
@ExcludeFromJacocoGeneratedReport
private fun SignInScreenSignedOutPreview() {
AppTheme {
SignInContent(connected = true, signInState = SignInState.SignedOut, onSignInClick = {})
}
}

@Preview(showBackground = true)
@Composable
@ExcludeFromJacocoGeneratedReport
private fun SignInScreenPreview() {
SignInScreen(onSignInClick = {})
private fun SignInScreenSigningInPreview() {
AppTheme {
SignInContent(connected = true, signInState = SignInState.SigningIn, onSignInClick = {})
}
}

@Preview(showBackground = true)
@Composable
@ExcludeFromJacocoGeneratedReport
private fun SignInScreenNotConnectedPreview() {
AppTheme {
SignInContent(connected = false, signInState = SignInState.SignedOut, onSignInClick = {})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,44 @@
package org.groundplatform.android.ui.signin

import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.groundplatform.android.repository.UserRepository
import org.groundplatform.android.system.NetworkManager
import org.groundplatform.android.system.NetworkStatus
import org.groundplatform.android.system.auth.SignInState
import org.groundplatform.android.ui.common.AbstractViewModel

/** View model responsible for handling the sign-in screen. */
@HiltViewModel
class SignInViewModel
@Inject
internal constructor(
private val networkManager: NetworkManager,
private val userRepository: UserRepository,
) : AbstractViewModel() {
internal constructor(networkManager: NetworkManager, private val userRepository: UserRepository) :
AbstractViewModel() {

@OptIn(ExperimentalCoroutinesApi::class)
fun getNetworkFlow(): Flow<Boolean> =
val signInState: StateFlow<SignInState> =
userRepository
.getSignInState()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = SignInState.SignedOut,
)

val networkAvailable: StateFlow<Boolean> =
networkManager.networkStatusFlow
.mapLatest { it == NetworkStatus.AVAILABLE }
.shareIn(viewModelScope, SharingStarted.Lazily, replay = 0)
.map { it == NetworkStatus.AVAILABLE }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = networkManager.isNetworkConnected(),
)

fun onSignInButtonClick() {
viewModelScope.launch {
val state = userRepository.getSignInState().first()
if (state is SignInState.SignedOut || state is SignInState.Error) {
userRepository.signIn()
}
}
userRepository.signIn()
}
}
Loading