UNPKG

@aws-amplify/ui

Version:

`@aws-amplify/ui` contains low-level logic & styles for stand-alone usage or re-use in framework-specific implementations.

478 lines (475 loc) • 18.8 kB
import { createMachine, sendUpdate } from 'xstate'; import { listWebAuthnCredentials, signInWithRedirect, confirmSignIn, resetPassword, fetchUserAttributes } from 'aws-amplify/auth'; import { runValidators } from '../../../validators/index.mjs'; import ACTIONS from '../actions.mjs'; import { defaultServices } from '../defaultServices.mjs'; import GUARDS from '../guards.mjs'; import { getFederatedSignInState, getConfirmSignInFormValuesKey } from './utils.mjs'; const handleSignInResponse = { onDone: [ { cond: 'hasCompletedSignIn', actions: 'setNextSignInStep', target: '#signInActor.fetchUserAttributes', }, { cond: 'shouldConfirmSignInWithNewPassword', actions: ['setMissingAttributes', 'setNextSignInStep'], target: '#signInActor.forceChangePassword', }, { cond: 'shouldResetPasswordFromSignIn', actions: 'setNextSignInStep', target: '#signInActor.resetPassword', }, { cond: 'shouldConfirmSignUpFromSignIn', actions: 'setNextSignInStep', target: '#signInActor.resendSignUpCode', }, { actions: [ 'setChallengeName', 'setMissingAttributes', 'setNextSignInStep', 'setTotpSecretCode', 'setAllowedMfaTypes', 'setCodeDeliveryDetails', ], target: '#signInActor.init', }, ], onError: { actions: 'setRemoteError', target: 'edit' }, }; const getDefaultConfirmSignInState = (exit) => ({ initial: 'edit', exit, states: { edit: { entry: 'sendUpdate', on: { SUBMIT: { actions: 'handleSubmit', target: 'submit' }, SIGN_IN: '#signInActor.signIn', CHANGE: { actions: 'handleInput' }, RESEND: { target: 'resend' }, }, }, submit: { tags: 'pending', entry: ['sendUpdate', 'clearError'], invoke: { src: 'confirmSignIn', ...handleSignInResponse }, }, resend: { tags: 'pending', entry: ['sendUpdate', 'clearError'], invoke: { src: 'resendSignInCode', onDone: { actions: ['setCodeDeliveryDetails', 'sendUpdate'], target: 'edit', }, onError: { actions: 'setRemoteError', target: 'edit', }, }, }, }, }); function signInActor({ services }) { return createMachine({ id: 'signInActor', initial: 'init', predictableActionArguments: true, states: { init: { always: [ { cond: 'shouldConfirmSignIn', target: 'confirmSignIn', }, { cond: 'shouldSetupTotp', target: 'setupTotp', }, { cond: 'shouldSetupEmail', target: 'setupEmail', }, { cond: 'shouldSelectMfaType', target: 'selectMfaType', }, { cond: ({ step }) => step === 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED', actions: 'setActorDoneData', target: 'forceChangePassword', }, { target: 'signIn' }, ], }, federatedSignIn: { ...getFederatedSignInState('signIn') }, fetchUserAttributes: { invoke: { src: 'fetchUserAttributes', onDone: [ { cond: 'hasPasskeyRegistrationPrompts', actions: 'setFetchedUserAttributes', target: 'checkPasskeys', }, { actions: 'setFetchedUserAttributes', target: 'evaluatePasskeyPrompt', }, ], onError: { actions: 'setConfirmAttributeCompleteStep', target: '#signInActor.resolved', }, }, }, checkPasskeys: { invoke: { src: async () => { try { const result = await listWebAuthnCredentials(); return result.credentials && result.credentials.length > 0; } catch { return false; } }, onDone: { actions: 'setHasExistingPasskeys', target: 'evaluatePasskeyPrompt', }, onError: { actions: 'clearHasExistingPasskeys', target: 'evaluatePasskeyPrompt', }, }, }, evaluatePasskeyPrompt: { always: [ { cond: 'shouldPromptPasskeyRegistration', target: '#signInActor.passkeyPrompt', }, { cond: 'shouldVerifyAttribute', actions: [ 'setShouldVerifyUserAttributeStep', 'setUnverifiedUserAttributes', ], target: '#signInActor.resolved', }, { actions: 'setConfirmAttributeCompleteStep', target: '#signInActor.resolved', }, ], }, resendSignUpCode: { invoke: { src: 'handleResendSignUpCode', onDone: { actions: 'setCodeDeliveryDetails', target: '#signInActor.resolved', }, onError: { actions: 'setRemoteError', target: '#signInActor.signIn', }, }, }, resetPassword: { invoke: { src: 'resetPassword', onDone: [ { actions: 'setCodeDeliveryDetails', target: '#signInActor.resolved', }, ], onError: { actions: ['setRemoteError', 'sendUpdate'] }, }, }, signIn: { initial: 'edit', exit: 'clearTouched', states: { edit: { entry: 'sendUpdate', on: { CHANGE: { actions: 'handleInput' }, FEDERATED_SIGN_IN: { target: '#signInActor.federatedSignIn' }, SHOW_AUTH_METHODS: { actions: 'setUsernameSignIn', target: 'selectMethod', }, SUBMIT: [ { cond: 'shouldSelectAuthMethod', actions: 'handleSubmit', target: 'selectMethod', }, { actions: 'handleSubmit', target: 'submit', }, ], }, }, selectMethod: { entry: [ 'sendUpdate', 'setSelectAuthMethodStep', 'setUsernameSignIn', ], on: { SELECT_METHOD: { actions: 'setSelectedAuthMethod', target: 'submit', }, SUBMIT: { actions: ['handleSubmit', 'setSelectedAuthMethodFromForm'], target: 'submit', }, SIGN_IN: { target: 'edit', }, }, }, submit: { tags: 'pending', entry: ['clearError', 'sendUpdate', 'setUsernameSignIn'], exit: 'clearFormValues', invoke: { src: 'handleSignIn', onDone: handleSignInResponse.onDone, onError: [ { cond: 'shouldReturnToSelectMethod', actions: 'setRemoteError', target: 'selectMethod', }, handleSignInResponse.onError, ], }, }, }, }, confirmSignIn: { ...getDefaultConfirmSignInState([ 'clearChallengeName', 'clearFormValues', 'clearError', 'clearTouched', ]), }, forceChangePassword: { entry: 'sendUpdate', type: 'parallel', exit: ['clearFormValues', 'clearError', 'clearTouched'], states: { validation: { initial: 'pending', states: { pending: { invoke: { src: 'validateFields', onDone: { target: 'valid', actions: 'clearValidationError', }, onError: { target: 'invalid', actions: 'setFieldErrors', }, }, }, valid: { entry: 'sendUpdate' }, invalid: { entry: 'sendUpdate' }, }, on: { SIGN_IN: { actions: 'setSignInStep', target: '#signInActor.resolved', }, CHANGE: { actions: 'handleInput', target: '.pending', }, BLUR: { actions: 'handleBlur', target: '.pending', }, }, }, submit: { initial: 'edit', entry: 'clearError', states: { edit: { entry: 'sendUpdate', on: { SUBMIT: { actions: 'handleSubmit', target: 'validate' }, }, }, validate: { entry: 'sendUpdate', invoke: { src: 'validateFields', onDone: { actions: 'clearValidationError', target: 'pending', }, onError: { actions: 'setFieldErrors', target: 'edit' }, }, }, pending: { tags: 'pending', entry: ['sendUpdate', 'clearError'], invoke: { src: 'handleForceChangePassword', ...handleSignInResponse, }, }, }, }, }, }, setupTotp: { ...getDefaultConfirmSignInState([ 'clearFormValues', 'clearError', 'clearTouched', ]), }, setupEmail: { ...getDefaultConfirmSignInState([ 'clearFormValues', 'clearError', 'clearTouched', ]), }, selectMfaType: { ...getDefaultConfirmSignInState([ 'clearFormValues', 'clearError', 'clearTouched', ]), }, passkeyPrompt: { entry: 'sendUpdate', on: { SUBMIT: { actions: 'setConfirmAttributeCompleteStep', target: 'resolved', }, SKIP: { actions: 'setConfirmAttributeCompleteStep', target: 'resolved', }, }, }, resolved: { type: 'final', data: (context) => ({ codeDeliveryDetails: context.codeDeliveryDetails, remoteError: context.remoteError, step: context.step, unverifiedUserAttributes: context.unverifiedUserAttributes, username: context.username, }), }, }, }, { // sendUpdate is a HOC actions: { ...ACTIONS, sendUpdate: sendUpdate() }, guards: GUARDS, services: { async fetchUserAttributes() { return fetchUserAttributes(); }, resetPassword({ username }) { return resetPassword({ username }); }, handleResendSignUpCode({ username }) { return services.handleResendSignUpCode({ username }); }, resendSignInCode({ username, selectedAuthMethod, availableAuthMethods, preferredChallenge, }) { // Resend code by calling signIn again with the same parameters const method = selectedAuthMethod ?? preferredChallenge ?? availableAuthMethods?.[0] ?? 'PASSWORD'; return services.handleSignIn({ username, options: { authFlowType: 'USER_AUTH', preferredChallenge: method, }, }); }, handleSignIn({ formValues, username, selectedAuthMethod, availableAuthMethods, preferredChallenge, }) { // Determine which method to use const method = selectedAuthMethod ?? preferredChallenge ?? availableAuthMethods?.[0] ?? 'PASSWORD'; if (method === 'PASSWORD') { // Traditional password flow const { password } = formValues; return services.handleSignIn({ username, password }); } else { // Passwordless flow using USER_AUTH return services.handleSignIn({ username, options: { authFlowType: 'USER_AUTH', preferredChallenge: method, }, }); } }, confirmSignIn({ formValues, step }) { const formValuesKey = getConfirmSignInFormValuesKey(step); const { [formValuesKey]: challengeResponse } = formValues; return services.handleConfirmSignIn({ challengeResponse }); }, async handleForceChangePassword({ formValues }) { let { password: challengeResponse, phone_number, country_code, // destructure and toss UI confirm_password field // to prevent error from sending to confirmSignIn confirm_password, username, ...userAttributes } = formValues; let phoneNumberWithCountryCode; if (phone_number) { phoneNumberWithCountryCode = `${country_code}${phone_number}`.replace(/[^A-Z0-9+]/gi, ''); userAttributes = { ...userAttributes, phone_number: phoneNumberWithCountryCode, }; } const input = { challengeResponse, options: { userAttributes }, }; return confirmSignIn(input); }, signInWithRedirect(_, { data }) { return signInWithRedirect(data); }, async validateFields(context) { return runValidators(context.formValues, context.touched, context.passwordSettings, [ defaultServices.validateFormPassword, defaultServices.validateConfirmPassword, ]); }, }, }); } export { signInActor };