import "package:amplify_auth_cognito/amplify_auth_cognito.dart"; import "package:amplify_flutter/amplify_flutter.dart"; import "package:arcane_framework/arcane_framework.dart"; import "package:arcane_helper_utils/arcane_helper_utils.dart"; import "package:flutter/widgets.dart"; typedef Credentials = ({String email, String password}); class AmplifyInterface with ArcaneAuthAccountRegistration, ArcaneAuthPasswordManagement implements ArcaneAuthInterface { AmplifyInterface._internal(); static bool _mocked = false; static final ArcaneAuthInterface _instance = AmplifyInterface._internal(); static ArcaneAuthInterface get I => _instance; AmplifyAuthCognito get _cognito => Amplify.Auth.getPlugin( AmplifyAuthCognito.pluginKey, ); Future get _session async { try { return await _cognito.fetchAuthSession(); } on AuthException { return null; } } @override Future get isSignedIn => _session.then((value) => value?.isSignedIn == true); @override Future get accessToken => isSignedIn.then( (loggedIn) => loggedIn ? _session.then( (value) => value?.userPoolTokensResult.value.accessToken.raw, ) : null, ); @override Future get refreshToken => isSignedIn.then( (loggedIn) => loggedIn ? _session.then( (value) => value?.userPoolTokensResult.value.refreshToken, ) : null, ); @override Future> logout() async { final result = await _cognito.signOut(); if (result is CognitoFailedSignOut) { return Result.error(result.exception.message); } return Result.ok(null); } @override Future> login({ LoginInput? input, Future Function()? onLoggedIn, }) async { final bool alreadyLoggedIn = await isSignedIn; if (alreadyLoggedIn) return Result.ok(null); if (input == null) return Result.error("No input provided"); final credentials = input as ({String email, String password}); final String email = credentials.email; final String password = credentials.password; try { final CognitoSignInResult result = await _cognito.signIn( username: email, password: password, ); return await _handleSignInResult(result, email); } on AuthException catch (e) { return Result.error("Error signing in: ${e.message}"); } catch (e) { return Result.error("Error signing in: $e"); } } Future> _handleSignInResult( SignInResult result, String email, ) async { switch (result.nextStep.signInStep) { case AuthSignInStep.confirmSignInWithSmsMfaCode: final codeDeliveryDetails = result.nextStep.codeDeliveryDetails!; return Result.error(_handleCodeDelivery(codeDeliveryDetails)); case AuthSignInStep.confirmSignInWithNewPassword: return Result.error("Enter a new password to continue signing in"); case AuthSignInStep.confirmSignInWithCustomChallenge: final parameters = result.nextStep.additionalInfo; final prompt = parameters["prompt"]!; return Result.error(prompt); case AuthSignInStep.resetPassword: final resetResult = await _cognito.resetPassword( username: email, ); return Result.error("Reset password result: $resetResult"); case AuthSignInStep.confirmSignUp: // Resend the sign up code to the registered device. final resendResult = await _cognito.resendSignUpCode( username: email, ); return Result.error( _handleCodeDelivery(resendResult.codeDeliveryDetails), ); case AuthSignInStep.done: return Result.ok(null); default: Arcane.log( "Sign-in failed", level: Level.warning, metadata: { "result": result.toString(), }, ); return Result.error( "Unexpected sign-in result: ${result.nextStep.signInStep}", ); } } String _handleCodeDelivery(AuthCodeDeliveryDetails codeDeliveryDetails) { // TODO(any): localize this return "A confirmation code has been sent to ${codeDeliveryDetails.destination}. " "Please check your ${codeDeliveryDetails.deliveryMedium.name} for the code."; } @override Future> resendVerificationCode({T? input}) async { try { final String? email = input as String?; if (email == null) return Result.error("No email address provided."); final result = await _cognito.resendSignUpCode( username: email.toLowerCase(), ); final codeDeliveryDetails = result.codeDeliveryDetails; final String returnValue = _handleCodeDelivery(codeDeliveryDetails); return Result.ok(returnValue); } on AuthException catch (e) { return Result.error("Error resending verification code: ${e.message}"); } } @override Future> register({ Credentials? input, }) async { try { if (input == null) { return Result.error("Unable to create an account with no credentials"); } final credentials = input as ({String email, String password}); final String email = credentials.email; final String password = credentials.password; final String accountEmail = email.toLowerCase(); final userAttributes = { AuthUserAttributeKey.email: accountEmail, }; final SignUpResult result = await _cognito.signUp( username: accountEmail, password: password, options: SignUpOptions( userAttributes: userAttributes, ), ); if (result.nextStep.signUpStep == AuthSignUpStep.confirmSignUp) { return Result.ok(SignUpStep.confirmSignUp); } return Result.ok(SignUpStep.done); } on AuthException catch (e) { return Result.error("Error signing up user: ${e.message}"); } } @override Future> confirmSignup({ String? username, String? confirmationCode, }) async { if (username.isNullOrEmpty) { return Result.error( "Unable to confirm account due to an empty username.", ); } if (confirmationCode.isNullOrEmpty) { return Result.error( "Unable to confirm account due to an empty confirmation code.", ); } try { final CognitoSignUpResult result = await _cognito.confirmSignUp( username: username!.toLowerCase(), confirmationCode: confirmationCode!, ); return Result.ok(result.isSignUpComplete); } on AuthException catch (e) { return Result.error("Error confirming user: ${e.message}"); } } @override Future> resetPassword({ String? email, String? newPassword, String? code, }) async { try { if (email.isNullOrEmpty) { return Result.error("Email address is empty."); } late ResetPasswordResult result; if (newPassword != null && code != null) { result = await _cognito.confirmResetPassword( username: email!, newPassword: newPassword, confirmationCode: code, ); } if (newPassword == null && code == null) { result = await _cognito.resetPassword( username: email!, ); } return Result.ok(result.isPasswordReset); } on AuthException catch (e) { return Result.error("Error resetting the password: ${e.message}"); } } @override Future init() async { if (_mocked) return; if (Amplify.isConfigured) return; final plugin = AmplifyAuthCognito(); await Amplify.addPlugin(plugin); await Amplify.configure(_amplifyconfig); } @visibleForTesting static void setMocked() { _mocked = true; } static final String _amplifyconfig = ''' { "UserAgent": "aws-amplify-cli/2.0", "Version": "1.0", "auth": { "plugins": { "awsCognitoAuthPlugin": { "IdentityManager": { "Default": {} }, "CredentialsProvider": { "CognitoIdentity": { "Default": { "PoolId": "${EnvVar.cognitoPoolId.value}", "Region": "${EnvVar.cognitoRegion.value}" } } }, "CognitoUserPool": { "Default": { "PoolId": "${EnvVar.cognitoPoolId.value}", "AppClientId": "${EnvVar.cognitoClientId.value}", "Region": "${EnvVar.cognitoRegion.value}" } }, "Auth": { "Default": { "authenticationFlowType": "USER_SRP_AUTH", "OAuth": { "WebDomain": "${EnvVar.authUrl.value}", "AppClientId": "${EnvVar.cognitoClientId.value}", "SignInRedirectURI": "${EnvVar.redirectUri.value}", "SignOutRedirectURI": "${EnvVar.redirectUri.value}", "Scopes": [ "phone", "email", "openid", "profile", "aws.cognito.signin.user.admin" ] } } } } } } }'''; }