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
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[*.{kt,kts}]
#this is to match java checkstyle
max_line_length=120
ktlint_code_style=android_studio
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.VisibleForTesting

internal class AuthenticatorViewModel(
application: Application,
private val authProvider: AuthProvider
) : AndroidViewModel(application) {
internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) :
AndroidViewModel(application) {

// Constructor for compose viewModels provider
constructor(application: Application) : this(application, RealAuthProvider())
Expand Down Expand Up @@ -185,7 +183,8 @@ internal class AuthenticatorViewModel(

//region SignUp

private suspend fun signUp(username: String, password: String, attributes: List<AuthUserAttribute>) {
@VisibleForTesting
suspend fun signUp(username: String, password: String, attributes: List<AuthUserAttribute>) {
viewModelScope.launch {
val options = AuthSignUpOptions.builder().userAttributes(attributes).build()

Expand Down Expand Up @@ -229,6 +228,7 @@ internal class AuthenticatorViewModel(
moveTo(newState)
}
AuthSignUpStep.DONE -> handleSignedUp(username, password)
AuthSignUpStep.COMPLETE_AUTO_SIGN_IN -> handleAutoSignIn(username, password)
else -> {
// Generic error for any other next steps that may be added in the future
val exception = AuthException(
Expand All @@ -241,6 +241,18 @@ internal class AuthenticatorViewModel(
}
}

private suspend fun handleAutoSignIn(username: String, password: String) {
when (val result = authProvider.autoSignIn()) {
is AmplifyResult.Error -> {
// If auto sign in fails then proceed with manually trying to sign in the user. If this also fails the
// user will end up back on the sign in screen.
logger.warn("Unable to complete auto-signIn")
handleSignedUp(username, password)
}
is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data)
}
}

private suspend fun handleSignedUp(username: String, password: String) {
when (val result = authProvider.signIn(username, password)) {
is AmplifyResult.Error -> {
Expand Down Expand Up @@ -355,22 +367,15 @@ internal class AuthenticatorViewModel(
)
}

private suspend fun handleEmailMfaSetupRequired(
username: String,
password: String
) {
private suspend fun handleEmailMfaSetupRequired(username: String, password: String) {
moveTo(
stateFactory.newSignInContinueWithEmailSetupState(
onSubmit = { mfaType -> confirmSignIn(username, password, mfaType) }
)
)
}

private suspend fun handleMfaSelectionRequired(
username: String,
password: String,
allowedMfaTypes: Set<MFAType>?
) {
private suspend fun handleMfaSelectionRequired(username: String, password: String, allowedMfaTypes: Set<MFAType>?) {
if (allowedMfaTypes.isNullOrEmpty()) {
handleGeneralFailure(AuthException("Missing allowedMfaTypes", "Please open a bug with Amplify"))
return
Expand Down Expand Up @@ -492,10 +497,7 @@ internal class AuthenticatorViewModel(
}.join()
}

private suspend fun handleResetPasswordSuccess(
username: String,
result: AuthResetPasswordResult
) {
private suspend fun handleResetPasswordSuccess(username: String, result: AuthResetPasswordResult) {
when (result.nextStep.resetPasswordStep) {
AuthResetPasswordStep.DONE -> handlePasswordResetComplete()
AuthResetPasswordStep.CONFIRM_RESET_PASSWORD_WITH_CODE -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,19 @@ import kotlinx.coroutines.flow.callbackFlow
* An abstraction of the Amplify.Auth API that allows us to use coroutines with no exceptions
*/
internal interface AuthProvider {
suspend fun signIn(
username: String,
password: String
): AmplifyResult<AuthSignInResult>
suspend fun signIn(username: String, password: String): AmplifyResult<AuthSignInResult>

suspend fun confirmSignIn(
challengeResponse: String
): AmplifyResult<AuthSignInResult>
suspend fun confirmSignIn(challengeResponse: String): AmplifyResult<AuthSignInResult>

suspend fun signUp(
username: String,
password: String,
options: AuthSignUpOptions
): AmplifyResult<AuthSignUpResult>
suspend fun signUp(username: String, password: String, options: AuthSignUpOptions): AmplifyResult<AuthSignUpResult>

suspend fun confirmSignUp(
username: String,
code: String
): AmplifyResult<AuthSignUpResult>
suspend fun confirmSignUp(username: String, code: String): AmplifyResult<AuthSignUpResult>

suspend fun resendSignUpCode(
username: String
): AmplifyResult<AuthCodeDeliveryDetails>
suspend fun resendSignUpCode(username: String): AmplifyResult<AuthCodeDeliveryDetails>

suspend fun resetPassword(
username: String
): AmplifyResult<AuthResetPasswordResult>
suspend fun autoSignIn(): AmplifyResult<AuthSignInResult>

suspend fun resetPassword(username: String): AmplifyResult<AuthResetPasswordResult>

suspend fun confirmResetPassword(
username: String,
Expand All @@ -92,10 +78,7 @@ internal interface AuthProvider {

suspend fun fetchUserAttributes(): AmplifyResult<List<AuthUserAttribute>>

suspend fun confirmUserAttribute(
key: AuthUserAttributeKey,
confirmationCode: String
): AmplifyResult<Unit>
suspend fun confirmUserAttribute(key: AuthUserAttributeKey, confirmationCode: String): AmplifyResult<Unit>

suspend fun resendUserAttributeConfirmationCode(key: AuthUserAttributeKey): AmplifyResult<AuthCodeDeliveryDetails>

Expand All @@ -108,15 +91,16 @@ internal interface AuthProvider {

internal sealed interface AuthConfigurationResult {
data class Valid(val configuration: AmplifyAuthConfiguration) : AuthConfigurationResult

data class Invalid(val message: String, val cause: Exception? = null) : AuthConfigurationResult

object Missing : AuthConfigurationResult
}

/**
* The [AuthProvider] implementation that calls through to [Amplify.Auth]
*/
internal class RealAuthProvider : AuthProvider {

init {
val cognitoPlugin = getCognitoPlugin()
cognitoPlugin?.addToUserAgent(AWSCognitoAuthMetadataType.Authenticator, BuildConfig.VERSION_NAME)
Expand All @@ -139,24 +123,18 @@ internal class RealAuthProvider : AuthProvider {
)
}

override suspend fun signUp(
username: String,
password: String,
options: AuthSignUpOptions
) = suspendCoroutine { continuation ->
Amplify.Auth.signUp(
username,
password,
options,
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}
override suspend fun signUp(username: String, password: String, options: AuthSignUpOptions) =
suspendCoroutine { continuation ->
Amplify.Auth.signUp(
username,
password,
options,
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun confirmSignUp(
username: String,
code: String
) = suspendCoroutine { continuation ->
override suspend fun confirmSignUp(username: String, code: String) = suspendCoroutine { continuation ->
Amplify.Auth.confirmSignUp(
username,
code,
Expand All @@ -165,40 +143,40 @@ internal class RealAuthProvider : AuthProvider {
)
}

override suspend fun resendSignUpCode(
username: String
) = suspendCoroutine { continuation ->
override suspend fun resendSignUpCode(username: String) = suspendCoroutine { continuation ->
Amplify.Auth.resendSignUpCode(
username,
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun resetPassword(
username: String
) = suspendCoroutine { continuation ->
Amplify.Auth.resetPassword(
username,
override suspend fun autoSignIn() = suspendCoroutine { continuation ->
Amplify.Auth.autoSignIn(
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun confirmResetPassword(
username: String,
newPassword: String,
confirmationCode: String
) = suspendCoroutine { continuation ->
Amplify.Auth.confirmResetPassword(
override suspend fun resetPassword(username: String) = suspendCoroutine { continuation ->
Amplify.Auth.resetPassword(
username,
newPassword,
confirmationCode,
{ continuation.resume(AmplifyResult.Success(Unit)) },
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun confirmResetPassword(username: String, newPassword: String, confirmationCode: String) =
suspendCoroutine { continuation ->
Amplify.Auth.confirmResetPassword(
username,
newPassword,
confirmationCode,
{ continuation.resume(AmplifyResult.Success(Unit)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun signOut() = suspendCoroutine { continuation ->
Amplify.Auth.signOut { continuation.resume(it) }
}
Expand All @@ -217,27 +195,24 @@ internal class RealAuthProvider : AuthProvider {
)
}

override suspend fun confirmUserAttribute(
key: AuthUserAttributeKey,
confirmationCode: String
) = suspendCoroutine { continuation ->
Amplify.Auth.confirmUserAttribute(
key,
confirmationCode,
{ continuation.resume(AmplifyResult.Success(Unit)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}
override suspend fun confirmUserAttribute(key: AuthUserAttributeKey, confirmationCode: String) =
suspendCoroutine { continuation ->
Amplify.Auth.confirmUserAttribute(
key,
confirmationCode,
{ continuation.resume(AmplifyResult.Success(Unit)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun resendUserAttributeConfirmationCode(
key: AuthUserAttributeKey
) = suspendCoroutine { continuation ->
Amplify.Auth.resendUserAttributeConfirmationCode(
key,
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}
override suspend fun resendUserAttributeConfirmationCode(key: AuthUserAttributeKey) =
suspendCoroutine { continuation ->
Amplify.Auth.resendUserAttributeConfirmationCode(
key,
{ continuation.resume(AmplifyResult.Success(it)) },
{ continuation.resume(AmplifyResult.Error(it)) }
)
}

override suspend fun getCurrentUser() = suspendCoroutine { continuation ->
Amplify.Auth.getCurrentUser(
Expand Down Expand Up @@ -282,13 +257,10 @@ internal class RealAuthProvider : AuthProvider {
return AuthConfigurationResult.Valid(amplifyAuthConfiguration)
}

private fun getCognitoPlugin(): AWSCognitoAuthPlugin? {
return try {
Amplify.Auth.getPlugin("awsCognitoAuthPlugin")
as AWSCognitoAuthPlugin
} catch (e: Throwable) {
null
}
private fun getCognitoPlugin(): AWSCognitoAuthPlugin? = try {
Amplify.Auth.getPlugin("awsCognitoAuthPlugin") as AWSCognitoAuthPlugin
} catch (e: Throwable) {
null
}

private fun getSignInMethod(attributes: List<UsernameAttribute>) = when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.amplifyframework.auth.result.AuthResetPasswordResult
import com.amplifyframework.auth.result.step.AuthNextResetPasswordStep
import com.amplifyframework.auth.result.step.AuthResetPasswordStep
import com.amplifyframework.auth.result.step.AuthSignInStep
import com.amplifyframework.auth.result.step.AuthSignUpStep
import com.amplifyframework.ui.authenticator.auth.VerificationMechanism
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
import com.amplifyframework.ui.authenticator.util.AmplifyResult
Expand Down Expand Up @@ -440,6 +441,22 @@ class AuthenticatorViewModelTest {
viewModel.currentStep shouldBe AuthenticatorStep.SignIn
}

//endregion
//region sign up tests

@Test
fun `user can autoSignIn after sign up`() = runTest {
val result = mockSignUpResult(nextStep = mockNextSignUpStep(signUpStep = AuthSignUpStep.COMPLETE_AUTO_SIGN_IN))
coEvery { authProvider.signUp("username", "password", any()) } returns Success(result)
coEvery { authProvider.autoSignIn() } returns Success(mockSignInResult())

viewModel.start(mockAuthenticatorConfiguration())
viewModel.signUp("username", "password", emptyList())
advanceUntilIdle()

viewModel.currentStep shouldBe AuthenticatorStep.SignedIn
}

//endregion
//region password reset tests

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ import com.amplifyframework.auth.AuthUserAttributeKey
import com.amplifyframework.auth.MFAType
import com.amplifyframework.auth.TOTPSetupDetails
import com.amplifyframework.auth.result.AuthSignInResult
import com.amplifyframework.auth.result.AuthSignUpResult
import com.amplifyframework.auth.result.step.AuthNextSignInStep
import com.amplifyframework.auth.result.step.AuthNextSignUpStep
import com.amplifyframework.auth.result.step.AuthSignInStep
import com.amplifyframework.auth.result.step.AuthSignUpStep
import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration
import com.amplifyframework.ui.authenticator.auth.PasswordCriteria
import com.amplifyframework.ui.authenticator.auth.SignInMethod
Expand Down Expand Up @@ -112,6 +115,25 @@ internal fun mockNextSignInStep(
availableFactors
)

internal fun mockSignUpResult(
nextStep: AuthNextSignUpStep,
userId: String = "userId"
) = AuthSignUpResult(
nextStep.signUpStep != AuthSignUpStep.CONFIRM_SIGN_UP_STEP,
nextStep,
userId
)

internal fun mockNextSignUpStep(
signUpStep: AuthSignUpStep = AuthSignUpStep.DONE,
additionalInfo: Map<String, String> = emptyMap(),
codeDeliveryDetails: AuthCodeDeliveryDetails? = null
) = AuthNextSignUpStep(
signUpStep,
additionalInfo,
codeDeliveryDetails
)

internal fun mockUserAttributes(vararg attribute: Pair<AuthUserAttributeKey, String>) =
attribute.map { AuthUserAttribute(it.first, it.second) }

Expand Down