Skip to content

Commit 7ec0b99

Browse files
committed
Allow retry if fetchAuthSession fails
1 parent 36f1ac1 commit 7ec0b99

File tree

7 files changed

+110
-34
lines changed

7 files changed

+110
-34
lines changed

authenticator/api/authenticator.api

+13-3
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ public final class com/amplifyframework/ui/authenticator/BuildConfig {
2222

2323
public final class com/amplifyframework/ui/authenticator/ErrorState : com/amplifyframework/ui/authenticator/AuthenticatorStepState {
2424
public static final field $stable I
25-
public fun <init> (Lcom/amplifyframework/auth/AuthException;)V
25+
public fun <init> (Lcom/amplifyframework/auth/AuthException;Lkotlin/jvm/functions/Function1;)V
26+
public synthetic fun <init> (Lcom/amplifyframework/auth/AuthException;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
2627
public final fun component1 ()Lcom/amplifyframework/auth/AuthException;
27-
public final fun copy (Lcom/amplifyframework/auth/AuthException;)Lcom/amplifyframework/ui/authenticator/ErrorState;
28-
public static synthetic fun copy$default (Lcom/amplifyframework/ui/authenticator/ErrorState;Lcom/amplifyframework/auth/AuthException;ILjava/lang/Object;)Lcom/amplifyframework/ui/authenticator/ErrorState;
28+
public final fun copy (Lcom/amplifyframework/auth/AuthException;Lkotlin/jvm/functions/Function1;)Lcom/amplifyframework/ui/authenticator/ErrorState;
29+
public static synthetic fun copy$default (Lcom/amplifyframework/ui/authenticator/ErrorState;Lcom/amplifyframework/auth/AuthException;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/amplifyframework/ui/authenticator/ErrorState;
2930
public fun equals (Ljava/lang/Object;)Z
31+
public final fun getCanRetry ()Z
3032
public final fun getError ()Lcom/amplifyframework/auth/AuthException;
3133
public fun getStep ()Lcom/amplifyframework/ui/authenticator/enums/AuthenticatorStep$Error;
3234
public synthetic fun getStep ()Lcom/amplifyframework/ui/authenticator/enums/AuthenticatorStep;
3335
public fun hashCode ()I
36+
public final fun retry (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
3437
public fun toString ()Ljava/lang/String;
3538
}
3639

@@ -674,6 +677,13 @@ public final class com/amplifyframework/ui/authenticator/ui/AuthenticatorLoading
674677
public static final fun AuthenticatorLoading (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
675678
}
676679

680+
public final class com/amplifyframework/ui/authenticator/ui/ComposableSingletons$AuthenticatorErrorKt {
681+
public static final field INSTANCE Lcom/amplifyframework/ui/authenticator/ui/ComposableSingletons$AuthenticatorErrorKt;
682+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
683+
public fun <init> ()V
684+
public final fun getLambda-1$authenticator_release ()Lkotlin/jvm/functions/Function3;
685+
}
686+
677687
public final class com/amplifyframework/ui/authenticator/ui/ComposableSingletons$AuthenticatorFormKt {
678688
public static final field INSTANCE Lcom/amplifyframework/ui/authenticator/ui/ComposableSingletons$AuthenticatorFormKt;
679689
public static field lambda-1 Lkotlin/jvm/functions/Function2;

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorStepState.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ object LoadingState : AuthenticatorStepState {
5151
* @param error The error that occurred.
5252
*/
5353
@Immutable
54-
data class ErrorState(val error: AuthException) : AuthenticatorStepState {
54+
data class ErrorState(
55+
val error: AuthException,
56+
private val onRetry: (suspend () -> Unit)? = null
57+
) : AuthenticatorStepState {
5558
override val step = AuthenticatorStep.Error
59+
val canRetry = onRetry != null
60+
61+
suspend fun retry() = onRetry?.invoke()
5662
}
5763

5864
/**

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt

+20-7
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,7 @@ internal class AuthenticatorViewModel(
148148
::moveTo
149149
)
150150

151-
// Fetch the current session to determine if the user is already authenticated
152-
val result = authProvider.fetchAuthSession()
153-
when {
154-
result is AmplifyResult.Error -> handleGeneralFailure(result.error)
155-
result is AmplifyResult.Success && result.data.isSignedIn -> handleSignedIn()
156-
else -> moveTo(configuration.initialStep)
157-
}
151+
checkInitialLogin()
158152
}
159153

160154
// Respond to any events from Amplify Auth
@@ -168,6 +162,20 @@ internal class AuthenticatorViewModel(
168162
}
169163
}
170164

165+
private suspend fun checkInitialLogin() {
166+
// Fetch the current session to determine if the user is already authenticated
167+
val result = authProvider.fetchAuthSession()
168+
when {
169+
// Allow user to retry a failure from fetchAuthSession
170+
result is AmplifyResult.Error -> handleRetryableGeneralFailure(
171+
error = result.error,
172+
onRetry = { viewModelScope.launch { checkInitialLogin() }.join() }
173+
)
174+
result is AmplifyResult.Success && result.data.isSignedIn -> handleSignedIn()
175+
else -> moveTo(configuration.initialStep)
176+
}
177+
}
178+
171179
fun moveTo(initialStep: AuthenticatorInitialStep) {
172180
logger.debug("Moving to initial step: $initialStep")
173181
val state = when (initialStep) {
@@ -640,6 +648,11 @@ internal class AuthenticatorViewModel(
640648
moveTo(ErrorState(error))
641649
}
642650

651+
private fun handleRetryableGeneralFailure(error: AuthException, onRetry: suspend () -> Unit) {
652+
logger.error(error.toString())
653+
moveTo(ErrorState(error, onRetry))
654+
}
655+
643656
private suspend fun sendMessage(event: AuthenticatorMessage) {
644657
logger.debug("Sending message: $event")
645658
_events.emit(event)

authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/Authenticator.kt

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ fun Authenticator(
134134
Box(modifier = modifier) {
135135
AnimatedContent(
136136
targetState = stepState,
137+
contentKey = { targetState -> targetState::class },
137138
transitionSpec = { defaultTransition() },
138139
label = "AuthenticatorContentTransition"
139140
) { targetState ->

authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/AuthenticatorError.kt

+43-17
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,65 @@
1515

1616
package com.amplifyframework.ui.authenticator.ui
1717

18+
import androidx.compose.animation.AnimatedVisibility
1819
import androidx.compose.foundation.background
1920
import androidx.compose.foundation.layout.Box
21+
import androidx.compose.foundation.layout.Column
2022
import androidx.compose.foundation.layout.fillMaxWidth
2123
import androidx.compose.foundation.layout.padding
2224
import androidx.compose.material3.MaterialTheme
2325
import androidx.compose.material3.Text
26+
import androidx.compose.material3.TextButton
2427
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.runtime.mutableStateOf
30+
import androidx.compose.runtime.remember
31+
import androidx.compose.runtime.rememberCoroutineScope
32+
import androidx.compose.runtime.setValue
2533
import androidx.compose.ui.Alignment
2634
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.res.stringResource
2736
import androidx.compose.ui.unit.dp
2837
import com.amplifyframework.ui.authenticator.ErrorState
38+
import com.amplifyframework.ui.authenticator.R
2939
import com.amplifyframework.ui.authenticator.strings.StringResolver
40+
import kotlinx.coroutines.launch
3041

3142
/**
3243
* The content displayed when Authenticator is in the ErrorState
3344
*/
3445
@Composable
35-
fun AuthenticatorError(
36-
state: ErrorState,
37-
modifier: Modifier = Modifier
38-
) {
39-
Box(
40-
modifier = modifier
41-
.fillMaxWidth()
42-
.padding(16.dp)
43-
.background(MaterialTheme.colorScheme.errorContainer)
44-
.padding(16.dp),
45-
contentAlignment = Alignment.Center
46-
) {
47-
val message = StringResolver.error(state.error)
48-
Text(
49-
text = message,
50-
color = MaterialTheme.colorScheme.onErrorContainer
51-
)
46+
fun AuthenticatorError(state: ErrorState, modifier: Modifier = Modifier) {
47+
val scope = rememberCoroutineScope()
48+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
49+
Box(
50+
modifier = modifier
51+
.fillMaxWidth()
52+
.padding(horizontal = 16.dp)
53+
.background(MaterialTheme.colorScheme.errorContainer)
54+
.padding(16.dp),
55+
contentAlignment = Alignment.Center
56+
) {
57+
val message = StringResolver.error(state.error)
58+
Text(
59+
text = message,
60+
color = MaterialTheme.colorScheme.onErrorContainer
61+
)
62+
}
63+
AnimatedVisibility(state.canRetry) {
64+
var retrying by remember { mutableStateOf(false) }
65+
TextButton(
66+
onClick = {
67+
scope.launch {
68+
retrying = true
69+
state.retry()
70+
retrying = false
71+
}
72+
},
73+
enabled = !retrying
74+
) {
75+
Text(stringResource(R.string.amplify_ui_authenticator_button_retry))
76+
}
77+
}
5278
}
5379
}

authenticator/src/main/res/values/buttons.xml

+1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@
2626
<string name="amplify_ui_authenticator_button_resend_code">Send Code</string>
2727
<string name="amplify_ui_authenticator_button_skip">Skip</string>
2828
<string name="amplify_ui_authenticator_button_copy_key">Copy Key</string>
29+
<string name="amplify_ui_authenticator_button_retry">Retry</string>
2930
</resources>

authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt

+25-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import com.amplifyframework.auth.result.step.AuthResetPasswordStep
2929
import com.amplifyframework.auth.result.step.AuthSignInStep
3030
import com.amplifyframework.ui.authenticator.auth.VerificationMechanism
3131
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
32-
import com.amplifyframework.ui.authenticator.util.AmplifyResult
3332
import com.amplifyframework.ui.authenticator.util.AmplifyResult.Error
3433
import com.amplifyframework.ui.authenticator.util.AmplifyResult.Success
3534
import com.amplifyframework.ui.authenticator.util.AuthConfigurationResult
@@ -38,6 +37,7 @@ import com.amplifyframework.ui.authenticator.util.LimitExceededMessage
3837
import com.amplifyframework.ui.authenticator.util.NetworkErrorMessage
3938
import com.amplifyframework.ui.testing.CoroutineTestRule
4039
import io.kotest.matchers.shouldBe
40+
import io.kotest.matchers.types.shouldBeInstanceOf
4141
import io.mockk.coEvery
4242
import io.mockk.coVerify
4343
import io.mockk.every
@@ -108,7 +108,7 @@ class AuthenticatorViewModelTest {
108108

109109
@Test
110110
fun `fetchAuthSession error during start results in an error`() = runTest {
111-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Error(mockAuthException())
111+
coEvery { authProvider.fetchAuthSession() } returns Error(mockAuthException())
112112

113113
viewModel.start(mockAuthenticatorConfiguration())
114114
advanceUntilIdle()
@@ -117,10 +117,28 @@ class AuthenticatorViewModelTest {
117117
viewModel.currentStep shouldBe AuthenticatorStep.Error
118118
}
119119

120+
@Test
121+
fun `fetchAuthSession error can be retried`() = runTest {
122+
coEvery { authProvider.fetchAuthSession() } returns
123+
Error(mockAuthException()) andThen Success(mockAuthSession())
124+
125+
viewModel.start(mockAuthenticatorConfiguration())
126+
advanceUntilIdle()
127+
128+
val state = viewModel.stepState.value.shouldBeInstanceOf<ErrorState>()
129+
state.retry()
130+
advanceUntilIdle()
131+
132+
viewModel.currentStep shouldBe AuthenticatorStep.SignIn
133+
coVerify(exactly = 2) {
134+
authProvider.fetchAuthSession()
135+
}
136+
}
137+
120138
@Test
121139
fun `getCurrentUser error during start results in an error`() = runTest {
122140
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = true))
123-
coEvery { authProvider.getCurrentUser() } returns AmplifyResult.Error(mockAuthException())
141+
coEvery { authProvider.getCurrentUser() } returns Error(mockAuthException())
124142

125143
viewModel.start(mockAuthenticatorConfiguration())
126144
advanceUntilIdle()
@@ -135,7 +153,7 @@ class AuthenticatorViewModelTest {
135153
@Test
136154
fun `getCurrentUser error with session expired exception during start results in being signed out`() = runTest {
137155
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = true))
138-
coEvery { authProvider.getCurrentUser() } returns AmplifyResult.Error(SessionExpiredException())
156+
coEvery { authProvider.getCurrentUser() } returns Error(SessionExpiredException())
139157

140158
viewModel.start(mockAuthenticatorConfiguration())
141159
advanceUntilIdle()
@@ -265,7 +283,7 @@ class AuthenticatorViewModelTest {
265283
coEvery { authProvider.signIn(any(), any()) } returns Success(
266284
mockSignInResult(signInStep = AuthSignInStep.CONFIRM_SIGN_UP)
267285
)
268-
coEvery { authProvider.resendSignUpCode(any()) } returns AmplifyResult.Error(mockAuthException())
286+
coEvery { authProvider.resendSignUpCode(any()) } returns Error(mockAuthException())
269287

270288
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
271289

@@ -394,7 +412,7 @@ class AuthenticatorViewModelTest {
394412
verificationMechanisms = setOf(VerificationMechanism.Email)
395413
)
396414
// cannot fetch user attributes
397-
coEvery { authProvider.fetchUserAttributes() } returns AmplifyResult.Error(mockk(relaxed = true))
415+
coEvery { authProvider.fetchUserAttributes() } returns Error(mockk(relaxed = true))
398416

399417
viewModel.start(mockAuthenticatorConfiguration())
400418
viewModel.signIn("username", "password")
@@ -571,6 +589,7 @@ class AuthenticatorViewModelTest {
571589
viewModel.resetPassword("username")
572590
}
573591
}
592+
574593
//endregion
575594
//region helpers
576595
private val AuthenticatorViewModel.currentStep: AuthenticatorStep

0 commit comments

Comments
 (0)