Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bdad7141 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix: harden login error handling and preserve rollback failure state

parent 54ac01f9
Loading
Loading
Loading
Loading
+19 −9
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package foundation.e.apps.feature.auth.login
import android.content.Context
import androidx.annotation.StringRes
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.domain.auth.AuthError
import javax.inject.Inject

@@ -36,20 +37,29 @@ class LoginFailureMessageResolver @Inject constructor(
    ): String {
        val fallbackMessage = fallbackMessage(fallbackMessageRes)
        return when (failure) {
            is LoginWorkflowFailure.RefreshValidationFailed ->
                failure.authError?.messageOrNull() ?: fallbackMessage

            is LoginWorkflowFailure.RefreshValidationFailed -> {
                failure.authError?.messageRes()?.let { context.getString(it) } ?: fallbackMessage
            }
            is LoginWorkflowFailure.SubmissionFailed -> fallbackMessage
            is LoginWorkflowFailure.RollbackFailed ->
                context.getString(R.string.sign_in_recovery_failed)
        }
    }

    private fun AuthError.messageOrNull(): String? {
    @StringRes
    private fun AuthError.messageRes(): Int? {
        return when (this) {
            is AuthError.InvalidToken -> message
            is AuthError.LoginRequired -> message
            is AuthError.NetworkFailure -> message
            is AuthError.RateLimited -> message
            is AuthError.Unknown -> message
            is AuthError.InvalidToken -> R.string.sign_in_session_invalid
            is AuthError.LoginRequired -> R.string.sign_in_login_required
            is AuthError.NetworkFailure ->
                if (isTimeout) {
                    R.string.sign_in_network_timeout
                } else {
                    R.string.sign_in_network_failure
                }

            is AuthError.RateLimited -> R.string.sign_in_rate_limited
            is AuthError.Unknown -> null
        }
    }
}
+6 −16
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import foundation.e.apps.domain.auth.AuthSession
import foundation.e.apps.domain.auth.AuthSessionRepository
import foundation.e.apps.domain.auth.AuthStore
import foundation.e.apps.domain.auth.PlayStoreLoginMode
import foundation.e.apps.domain.auth.describe
import foundation.e.apps.domain.login.AnonymousLoginUseCase
import foundation.e.apps.domain.login.GoogleLoginUseCase
import foundation.e.apps.domain.login.MicrogLoginUseCase
@@ -265,8 +264,8 @@ class LoginWorkflowCoordinator @Inject constructor(
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: Exception) {
            // Rollback must report any unexpected non-cancellation failure as a submission
            // failure rather than letting it escape and leave the flow in an inconsistent state.
            // Rollback must report any unexpected non-cancellation failure as a typed
            // workflow failure rather than letting it escape and leave the flow inconsistent.
            IllegalStateException("Failed to rollback login after $rollbackContext", exception)
        }

@@ -275,22 +274,13 @@ class LoginWorkflowCoordinator @Inject constructor(
        }

        Timber.e(rollbackFailure, "Login rollback failed after %s", rollbackContext)
        rollbackFailure.addSuppressed(failure.toThrowable())
        return LoginWorkflowResult.Failure(
            LoginWorkflowFailure.SubmissionFailed(rollbackFailure)
            LoginWorkflowFailure.RollbackFailed(
                originalFailure = failure,
                rollbackThrowable = rollbackFailure,
            )
    }

    private fun LoginWorkflowFailure.toThrowable(): Throwable {
        return when (this) {
            is LoginWorkflowFailure.SubmissionFailed -> throwable
            is LoginWorkflowFailure.RefreshValidationFailed ->
                IllegalStateException(
                    "Original login refresh validation failed for $preferredStore: " +
                        (authError?.describe() ?: "unknown auth error")
        )
    }
    }

    private suspend fun captureRollbackState(): LoginRollbackState {
        return LoginRollbackState(
+5 −0
Original line number Diff line number Diff line
@@ -37,4 +37,9 @@ sealed interface LoginWorkflowFailure {
        val preferredStore: AuthStore,
        val authError: AuthError?,
    ) : LoginWorkflowFailure

    data class RollbackFailed(
        val originalFailure: LoginWorkflowFailure,
        val rollbackThrowable: Throwable,
    ) : LoginWorkflowFailure
}
+17 −3
Original line number Diff line number Diff line
@@ -17,6 +17,20 @@

package foundation.e.apps.feature.auth.login

import androidx.annotation.StringRes
import foundation.e.apps.R

internal enum class SystemWideGoogleLoginFailureReason {
    AccountFetchFailed,
}

@StringRes
internal fun SystemWideGoogleLoginFailureReason.messageRes(): Int {
    return when (this) {
        SystemWideGoogleLoginFailureReason.AccountFetchFailed -> R.string.sign_in_microg_login_failed
    }
}

internal class SystemWideGoogleLoginCoordinator {
    private var pendingAccountName: String? = null

@@ -71,8 +85,8 @@ internal class SystemWideGoogleLoginCoordinator {
        return Action.LaunchConsent
    }

    fun onFetchFailure(message: String?): Action {
        return Action.FetchFailed(message)
    fun onFetchFailure(): Action {
        return Action.FetchFailed(SystemWideGoogleLoginFailureReason.AccountFetchFailed)
    }

    internal sealed interface Action {
@@ -94,7 +108,7 @@ internal class SystemWideGoogleLoginCoordinator {
        ) : Action

        data class FetchFailed(
            val message: String?,
            val reason: SystemWideGoogleLoginFailureReason,
        ) : Action
    }
}
+2 −5
Original line number Diff line number Diff line
@@ -109,7 +109,7 @@ class SystemWideGoogleLoginLauncher(

                is MicrogAccountFetchResult.Error -> {
                    pendingConsentIntent = null
                    handleAction(coordinator.onFetchFailure(fetchResult.throwable.localizedMessage))
                    handleAction(coordinator.onFetchFailure())
                }
            }
        }
@@ -183,10 +183,7 @@ class SystemWideGoogleLoginLauncher(
            }

            is SystemWideGoogleLoginCoordinator.Action.FetchFailed -> {
                onError(
                    action.message
                        ?: fragment.getString(R.string.sign_in_microg_login_failed),
                )
                onError(fragment.getString(action.reason.messageRes()))
            }
        }
    }
Loading