@aws-amplify/ui 6.13.0 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/esm/helpers/authenticator/facade.mjs +9 -2
  2. package/dist/esm/helpers/authenticator/formFields/defaults.mjs +34 -8
  3. package/dist/esm/helpers/authenticator/getRoute.mjs +8 -0
  4. package/dist/esm/helpers/authenticator/textUtil.mjs +34 -0
  5. package/dist/esm/i18n/dictionaries/authenticator/defaultTexts.mjs +27 -0
  6. package/dist/esm/i18n/dictionaries/authenticator/es.mjs +1 -1
  7. package/dist/esm/i18n/dictionaries/authenticator/fr.mjs +1 -1
  8. package/dist/esm/machines/authenticator/actions.mjs +63 -4
  9. package/dist/esm/machines/authenticator/actors/signIn.mjs +199 -49
  10. package/dist/esm/machines/authenticator/actors/signUp.mjs +81 -27
  11. package/dist/esm/machines/authenticator/defaultServices.mjs +37 -0
  12. package/dist/esm/machines/authenticator/guards.mjs +49 -1
  13. package/dist/esm/machines/authenticator/index.mjs +29 -15
  14. package/dist/esm/machines/authenticator/utils.mjs +58 -6
  15. package/dist/index.js +625 -111
  16. package/dist/styles/StorageBrowser.css +0 -1
  17. package/dist/styles/StorageBrowser.layer.css +0 -1
  18. package/dist/styles/authenticator.css +17 -0
  19. package/dist/styles/authenticator.layer.css +17 -0
  20. package/dist/styles/modal.css +81 -0
  21. package/dist/styles/modal.layer.css +83 -0
  22. package/dist/styles.css +100 -1
  23. package/dist/styles.layer.css +100 -1
  24. package/dist/types/helpers/authenticator/facade.d.ts +6 -2
  25. package/dist/types/helpers/authenticator/textUtil.d.ts +24 -1
  26. package/dist/types/i18n/dictionaries/authenticator/defaultTexts.d.ts +27 -0
  27. package/dist/types/i18n/dictionaries/index.d.ts +27 -0
  28. package/dist/types/i18n/translations.d.ts +27 -0
  29. package/dist/types/machines/authenticator/defaultServices.d.ts +105 -0
  30. package/dist/types/machines/authenticator/types.d.ts +26 -2
  31. package/dist/types/machines/authenticator/utils.d.ts +14 -2
  32. package/dist/types/theme/components/index.d.ts +3 -1
  33. package/dist/types/theme/components/modal.d.ts +10 -0
  34. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1117,7 +1117,7 @@ const esDict$1 = {
1117
1117
  'Sign Up with Google': 'Crear cuenta con Google',
1118
1118
  'Sign Up': 'Crear cuenta',
1119
1119
  'User already exists': 'El usuario ya existe',
1120
- 'User does not exist': 'El usuario no existe',
1120
+ 'User does not exist.': 'El usuario no existe',
1121
1121
  'Username/client id combination not found.': 'El usuario no existe',
1122
1122
  'Username cannot be empty': 'El nombre de usuario no puede estar vacío',
1123
1123
  'Your passwords must match': 'Las contraseñas deben coincidir',
@@ -1244,7 +1244,7 @@ const frDict$1 = {
1244
1244
  'Sign Up': "S'inscrire",
1245
1245
  SMS: 'SMS',
1246
1246
  'User already exists': "L'utilisateur existe déjà",
1247
- 'User does not exist': "L'utilisateur n'existe pas",
1247
+ 'User does not exist.': "L'utilisateur n'existe pas",
1248
1248
  'Username cannot be empty': "Le nom d'utilisateur doit être renseigné",
1249
1249
  'Username/client id combination not found.': "L'utilisateur n'existe pas",
1250
1250
  'We Emailed You': 'Nous vous avons envoyé un code',
@@ -2770,7 +2770,13 @@ const defaultTexts$1 = {
2770
2770
  CONFIRM: 'Confirm',
2771
2771
  CONFIRMATION_CODE: 'Confirmation Code',
2772
2772
  CONFIRMING: 'Confirming',
2773
+ CONTINUE: 'Continue',
2774
+ CONTINUE_WITHOUT_PASSKEY: 'Continue without a Passkey',
2773
2775
  CREATE_ACCOUNT: 'Create Account',
2776
+ CREATE_ACCOUNT_WITH_EMAIL_OTP: 'Create account with Email OTP',
2777
+ CREATE_ACCOUNT_WITH_PASSWORD: 'Create account with Password',
2778
+ CREATE_ACCOUNT_WITH_SMS_OTP: 'Create account with SMS OTP',
2779
+ CREATE_PASSKEY: 'Create a Passkey',
2774
2780
  CREATING_ACCOUNT: 'Creating Account',
2775
2781
  EMAIL_ADDRESS: 'Email',
2776
2782
  EMAIL_OTP: 'Email Message',
@@ -2787,6 +2793,8 @@ const defaultTexts$1 = {
2787
2793
  ENTER_PHONE_NUMBER: 'Enter your Phone Number',
2788
2794
  ENTER_PREFERRED_USERNAME: 'Enter your Preferred Username',
2789
2795
  ENTER_USERNAME: 'Enter your username',
2796
+ ENTER_USERNAME_FIRST: 'Please enter your username first',
2797
+ EXISTING_PASSKEYS: 'Existing Passkeys',
2790
2798
  FAMILY_NAME: 'Family Name',
2791
2799
  GIVEN_NAME: 'Given Name',
2792
2800
  FORGOT_PASSWORD: 'Forgot Password?',
@@ -2801,16 +2809,30 @@ const defaultTexts$1 = {
2801
2809
  NICKNAME: 'Nickname',
2802
2810
  NEW_PASSWORD: 'New password',
2803
2811
  OR: 'or',
2812
+ OTHER_SIGN_IN_OPTIONS: 'Other sign-in options',
2813
+ PASSKEY_AUTHENTICATION_CANCELED: 'Passkey authentication was canceled. Please try again or use a different sign-in method.',
2814
+ PASSKEY_CREATED_SUCCESS: 'Passkey created successfully!',
2815
+ PASSKEY_LABEL: 'Passkey',
2816
+ PASSKEY_PROMPT_DESCRIPTION: 'Passkeys are WebAuthn credentials that validate your identity using biometric data like touch or facial recognition or device authentication like passwords or PINs, serving as a secure password replacement.',
2817
+ PASSKEY_PROMPT_HEADING: 'Sign in faster with Passkey',
2818
+ PASSKEY_REGISTERED: 'Your passkey has been successfully registered.',
2819
+ PASSKEY_REGISTRATION_FAILED: 'Failed to create passkey. Please try again.',
2804
2820
  PASSWORD: 'Password',
2821
+ PASSWORDLESS_NOT_ENABLED: 'Passwordless authentication is not enabled for this account. Please contact your administrator or use password sign-in.',
2805
2822
  PHONE_NUMBER: 'Phone Number',
2806
2823
  PREFERRED_USERNAME: 'Preferred Username',
2807
2824
  PROFILE: 'Profile',
2825
+ REGISTERING: 'Registering',
2808
2826
  RESEND_CODE: 'Resend Code',
2809
2827
  RESET_PASSWORD_HEADING: 'Reset your password',
2810
2828
  RESET_PASSWORD: 'Reset Password',
2811
2829
  SEND_CODE: 'Send code',
2830
+ CODE_DELIVERY_FAILED: 'Unable to send verification code. Please try again.',
2831
+ VERIFICATION_CODE_EXPIRED: 'Your verification code has expired. Please request a new code.',
2832
+ VERIFICATION_CODE_INVALID: 'The verification code you entered is incorrect. Please try again.',
2812
2833
  SENDING: 'Sending',
2813
2834
  SELECT_MFA_TYPE: 'Select MFA Type',
2835
+ SETUP_ANOTHER_PASSKEY: 'Setup another Passkey',
2814
2836
  SETUP_EMAIL: 'Setup Email',
2815
2837
  SETUP_TOTP: 'Setup TOTP',
2816
2838
  SHOW_PASSWORD: 'Show password',
@@ -2818,8 +2840,12 @@ const defaultTexts$1 = {
2818
2840
  SIGN_IN_TAB: 'Sign In',
2819
2841
  SIGN_IN_WITH_AMAZON: 'Sign In with Amazon',
2820
2842
  SIGN_IN_WITH_APPLE: 'Sign In with Apple',
2843
+ SIGN_IN_WITH_EMAIL: 'Sign In with Email',
2821
2844
  SIGN_IN_WITH_FACEBOOK: 'Sign In with Facebook',
2822
2845
  SIGN_IN_WITH_GOOGLE: 'Sign In with Google',
2846
+ SIGN_IN_WITH_PASSKEY: 'Sign In with Passkey',
2847
+ SIGN_IN_WITH_PASSWORD: 'Sign In with Password',
2848
+ SIGN_IN_WITH_SMS: 'Sign In with SMS',
2823
2849
  SIGN_IN: 'Sign in to your account',
2824
2850
  SIGN_UP_BUTTON: 'Create a new account',
2825
2851
  SIGNING_IN_BUTTON: 'Signing in',
@@ -2829,6 +2855,7 @@ const defaultTexts$1 = {
2829
2855
  SUBMITTING: 'Submitting',
2830
2856
  SOFTWARE_TOKEN_MFA: 'Authenticator App (TOTP)',
2831
2857
  UPPERCASE_COPY: 'COPY',
2858
+ USERNAME: 'Username',
2832
2859
  VERIFY_CONTACT: 'Verify Contact',
2833
2860
  VERIFY_HEADING: 'Account recovery requires verified contact information',
2834
2861
  VERIFY: 'Verify',
@@ -3100,6 +3127,14 @@ const getRoute = (state, actorState) => {
3100
3127
  case actorState?.matches('setupTotp.edit'):
3101
3128
  case actorState?.matches('setupTotp.submit'):
3102
3129
  return 'setupTotp';
3130
+ case actorState?.matches('signIn.submit'):
3131
+ return actorState?.context.selectedAuthMethod != null
3132
+ ? 'signInSelectAuthFactor'
3133
+ : 'signIn';
3134
+ case actorState?.matches('signIn.selectMethod'):
3135
+ return 'signInSelectAuthFactor';
3136
+ case actorState?.matches('passkeyPrompt'):
3137
+ return 'passkeyPrompt';
3103
3138
  case actorState?.matches('signIn'):
3104
3139
  return 'signIn';
3105
3140
  case actorState?.matches('signUp'):
@@ -3157,8 +3192,10 @@ const getSendEventAliases = (send) => {
3157
3192
  return {
3158
3193
  initializeMachine: sendToMachine('INIT'),
3159
3194
  resendCode: sendToMachine('RESEND'),
3195
+ selectAuthMethod: sendToMachine('SELECT_METHOD'),
3160
3196
  signOut: sendToMachine('SIGN_OUT'),
3161
3197
  submitForm: sendToMachine('SUBMIT'),
3198
+ toShowAuthMethods: sendToMachine('SHOW_AUTH_METHODS'),
3162
3199
  updateForm: sendToMachine('CHANGE'),
3163
3200
  updateBlur: sendToMachine('BLUR'),
3164
3201
  // Actions that don't immediately invoke a service but instead transition to a screen
@@ -3183,8 +3220,9 @@ const getNextSendEventAliases = (send) => {
3183
3220
  };
3184
3221
  const getServiceContextFacade = (state) => {
3185
3222
  const actorContext = (getActorContext$1(state) ?? {});
3186
- const { allowedMfaTypes, challengeName, codeDeliveryDetails, remoteError: error, validationError: validationErrors, totpSecretCode = null, unverifiedUserAttributes, username, } = actorContext;
3187
- const { socialProviders = [] } = state.context?.config ?? {};
3223
+ const { allowedMfaTypes, availableAuthMethods, challengeName, codeDeliveryDetails, preferredChallenge, remoteError: error, selectedAuthMethod, validationError: validationErrors, totpSecretCode = null, unverifiedUserAttributes, username, } = actorContext;
3224
+ const { socialProviders = [], loginMechanisms } = state.context?.config ?? {};
3225
+ const loginMechanism = loginMechanisms?.[0];
3188
3226
  // check for user in actorContext prior to state context. actorContext is more "up to date",
3189
3227
  // but is not available on all states
3190
3228
  const user = actorContext?.user ?? state.context?.user;
@@ -3208,12 +3246,16 @@ const getServiceContextFacade = (state) => {
3208
3246
  const facade = {
3209
3247
  allowedMfaTypes,
3210
3248
  authStatus,
3249
+ availableAuthMethods,
3211
3250
  challengeName,
3212
3251
  codeDeliveryDetails,
3213
3252
  error,
3214
3253
  hasValidationErrors,
3215
3254
  isPending,
3255
+ loginMechanism,
3256
+ preferredChallenge,
3216
3257
  route,
3258
+ selectedAuthMethod,
3217
3259
  socialProviders,
3218
3260
  totpSecretCode,
3219
3261
  unverifiedUserAttributes,
@@ -3682,16 +3724,32 @@ const getConfirmationCodeFormFields = (_) => ({
3682
3724
  placeholder: 'Code',
3683
3725
  },
3684
3726
  });
3685
- const getSignInFormFields = (state) => ({
3686
- username: { ...getAliasDefaultFormField(state) },
3687
- password: {
3688
- ...getDefaultFormField('password'),
3689
- autocomplete: 'current-password',
3690
- },
3691
- });
3727
+ const getSignInFormFields = (state) => {
3728
+ const actorContext = state.context.actorRef?.getSnapshot()?.context;
3729
+ const availableAuthMethods = actorContext?.availableAuthMethods;
3730
+ const preferredChallenge = actorContext?.preferredChallenge;
3731
+ const shouldShowPassword = !availableAuthMethods?.length ||
3732
+ (availableAuthMethods.length === 1 &&
3733
+ availableAuthMethods[0] === 'PASSWORD') ||
3734
+ (availableAuthMethods.length > 1 &&
3735
+ (!preferredChallenge || preferredChallenge === 'PASSWORD'));
3736
+ const fields = {
3737
+ username: { ...getAliasDefaultFormField(state) },
3738
+ };
3739
+ if (shouldShowPassword) {
3740
+ fields.password = {
3741
+ ...getDefaultFormField('password'),
3742
+ autocomplete: 'current-password',
3743
+ };
3744
+ }
3745
+ return fields;
3746
+ };
3692
3747
  const getSignUpFormFields = (state) => {
3693
3748
  const { loginMechanisms, signUpAttributes } = state.context.config;
3694
3749
  const primaryAlias = getPrimaryAlias(state);
3750
+ const actorContext = state.context.actorRef?.getSnapshot()?.context;
3751
+ const availableAuthMethods = actorContext?.availableAuthMethods;
3752
+ const hasMultipleMethods = availableAuthMethods && availableAuthMethods.length > 1;
3695
3753
  /**
3696
3754
  * @migration signUp Fields created here
3697
3755
  */
@@ -3707,7 +3765,17 @@ const getSignUpFormFields = (state) => {
3707
3765
  const fieldAttrs = fieldName === primaryAlias
3708
3766
  ? getAliasDefaultFormField(state)
3709
3767
  : getDefaultFormField(fieldName);
3710
- formField[fieldName] = { ...fieldAttrs };
3768
+ // Make email, phone_number, password, and confirm_password optional when multiple auth methods available
3769
+ // Validation will check based on selected method
3770
+ const isOptional = hasMultipleMethods &&
3771
+ (fieldName === 'email' ||
3772
+ fieldName === 'phone_number' ||
3773
+ fieldName === 'password' ||
3774
+ fieldName === 'confirm_password');
3775
+ formField[fieldName] = {
3776
+ ...fieldAttrs,
3777
+ ...(isOptional && { isRequired: false }),
3778
+ };
3711
3779
  }
3712
3780
  else {
3713
3781
  // There's a `custom:*` attribute or one we don't already have an implementation for
@@ -3903,6 +3971,17 @@ const getSelectMfaTypeByChallengeName = (challengeName) => {
3903
3971
  }
3904
3972
  return translate(DefaultTexts.MFA_SELECTION);
3905
3973
  };
3974
+ const getUsernameLabelByLoginMechanism = (loginMechanism) => {
3975
+ switch (loginMechanism) {
3976
+ case 'email':
3977
+ return translate(DefaultTexts.EMAIL_ADDRESS);
3978
+ case 'phone_number':
3979
+ return translate(DefaultTexts.PHONE_NUMBER);
3980
+ case 'username':
3981
+ default:
3982
+ return translate(DefaultTexts.USERNAME);
3983
+ }
3984
+ };
3906
3985
  const getMfaTypeLabelByValue = (mfaType) => {
3907
3986
  switch (mfaType) {
3908
3987
  case 'EMAIL':
@@ -3922,6 +4001,7 @@ const authenticatorTextUtil = {
3922
4001
  getChangingText: () => translate(DefaultTexts.CHANGING_PASSWORD),
3923
4002
  getConfirmText: () => translate(DefaultTexts.CONFIRM),
3924
4003
  getConfirmingText: () => translate(DefaultTexts.CONFIRMING),
4004
+ getContinueText: () => translate(DefaultTexts.CONTINUE),
3925
4005
  getCopyText: () => translate(DefaultTexts.UPPERCASE_COPY),
3926
4006
  getHidePasswordText: () => translate(DefaultTexts.HIDE_PASSWORD),
3927
4007
  getLoadingText: () => translate(DefaultTexts.LOADING),
@@ -3944,6 +4024,9 @@ const authenticatorTextUtil = {
3944
4024
  /** SignUp */
3945
4025
  getCreatingAccountText: () => translate(DefaultTexts.CREATING_ACCOUNT),
3946
4026
  getCreateAccountText: () => translate(DefaultTexts.CREATE_ACCOUNT),
4027
+ getCreateAccountWithEmailText: () => translate(DefaultTexts.CREATE_ACCOUNT_WITH_EMAIL_OTP),
4028
+ getCreateAccountWithPasswordText: () => translate(DefaultTexts.CREATE_ACCOUNT_WITH_PASSWORD),
4029
+ getCreateAccountWithSmsText: () => translate(DefaultTexts.CREATE_ACCOUNT_WITH_SMS_OTP),
3947
4030
  /** ConfirmSignUp */
3948
4031
  getDeliveryMessageText,
3949
4032
  getDeliveryMethodText,
@@ -3970,6 +4053,25 @@ const authenticatorTextUtil = {
3970
4053
  getVerifyText: () => translate(DefaultTexts.VERIFY),
3971
4054
  getVerifyContactText: () => translate(DefaultTexts.VERIFY_CONTACT),
3972
4055
  getAccountRecoveryInfoText: () => translate(DefaultTexts.VERIFY_HEADING),
4056
+ /** Passwordless */
4057
+ getPasskeyPromptHeadingText: () => translate(DefaultTexts.PASSKEY_PROMPT_HEADING),
4058
+ getPasskeyPromptDescriptionText: () => translate(DefaultTexts.PASSKEY_PROMPT_DESCRIPTION),
4059
+ getCreatePasskeyText: () => translate(DefaultTexts.CREATE_PASSKEY),
4060
+ getRegisteringText: () => translate(DefaultTexts.REGISTERING),
4061
+ getContinueWithoutPasskeyText: () => translate(DefaultTexts.CONTINUE_WITHOUT_PASSKEY),
4062
+ getPasskeyCreatedSuccessText: () => translate(DefaultTexts.PASSKEY_CREATED_SUCCESS),
4063
+ getPasskeyRegisteredText: () => translate(DefaultTexts.PASSKEY_REGISTERED),
4064
+ getPasskeyRegistrationFailedText: () => translate(DefaultTexts.PASSKEY_REGISTRATION_FAILED),
4065
+ getPasskeyLabelText: () => translate(DefaultTexts.PASSKEY_LABEL),
4066
+ getExistingPasskeysText: () => translate(DefaultTexts.EXISTING_PASSKEYS),
4067
+ getSetupAnotherPasskeyText: () => translate(DefaultTexts.SETUP_ANOTHER_PASSKEY),
4068
+ getSignInWithPasswordText: () => translate(DefaultTexts.SIGN_IN_WITH_PASSWORD),
4069
+ getSignInWithEmailText: () => translate(DefaultTexts.SIGN_IN_WITH_EMAIL),
4070
+ getSignInWithSmsText: () => translate(DefaultTexts.SIGN_IN_WITH_SMS),
4071
+ getSignInWithPasskeyText: () => translate(DefaultTexts.SIGN_IN_WITH_PASSKEY),
4072
+ getOtherSignInOptionsText: () => translate(DefaultTexts.OTHER_SIGN_IN_OPTIONS),
4073
+ getEnterUsernameFirstText: () => translate(DefaultTexts.ENTER_USERNAME_FIRST),
4074
+ getUsernameLabelByLoginMechanism,
3973
4075
  /** Validations */
3974
4076
  // TODO: add defaultText
3975
4077
  getInvalidEmailText: () => translate('Please enter a valid email'),
@@ -4162,21 +4264,44 @@ const getUserAttributes = (formValues) => {
4162
4264
  }
4163
4265
  return userAttributes;
4164
4266
  };
4165
- const getSignUpInput = (username, formValues, loginMechanism) => {
4267
+ const getSignUpInput = (username, formValues, loginMechanism, authMethod) => {
4166
4268
  const { password, ...values } = formValues;
4167
4269
  const attributes = getUserAttributes(values);
4270
+ const isPasswordless = authMethod && authMethod !== 'PASSWORD';
4271
+ // For SMS OTP, set the email channel to empty string if present to force account creation with phone number
4272
+ let userAttributes = attributes;
4273
+ if (authMethod === 'SMS_OTP' && attributes.email) {
4274
+ userAttributes = { ...attributes, email: '' };
4275
+ }
4168
4276
  const options = {
4169
- autoSignIn: DEFAULT_AUTO_SIGN_IN,
4277
+ autoSignIn: isPasswordless
4278
+ ? {
4279
+ enabled: true,
4280
+ authFlowType: 'USER_AUTH',
4281
+ preferredChallenge: authMethod,
4282
+ }
4283
+ : DEFAULT_AUTO_SIGN_IN,
4170
4284
  userAttributes: {
4171
4285
  // use `username` value for `phone_number`
4172
4286
  ...(loginMechanism === 'phone_number'
4173
- ? { ...attributes, phone_number: username }
4174
- : attributes),
4287
+ ? { ...userAttributes, phone_number: username }
4288
+ : userAttributes),
4175
4289
  },
4176
4290
  };
4177
- return { username, password, options };
4291
+ return { username, password: isPasswordless ? undefined : password, options };
4178
4292
  };
4179
4293
  const getUsernameSignUp = ({ formValues, loginMechanisms, }) => {
4294
+ // Check if a specific auth method was selected via form data
4295
+ const authMethod = formValues.__authMethod;
4296
+ // For SMS_OTP, always use phone_number as username
4297
+ if (authMethod === 'SMS_OTP') {
4298
+ const { country_code, phone_number } = formValues;
4299
+ return sanitizePhoneNumber(country_code, phone_number);
4300
+ }
4301
+ // For EMAIL_OTP, always use email as username
4302
+ if (authMethod === 'EMAIL_OTP') {
4303
+ return formValues.email;
4304
+ }
4180
4305
  // When 'username' is in loginMechanisms, always use the username field for the Username parameter.
4181
4306
  // This handles both username-only mode and alias mode (username + email/phone as sign-in options).
4182
4307
  // See: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-aliases
@@ -4191,13 +4316,47 @@ const getUsernameSignUp = ({ formValues, loginMechanisms, }) => {
4191
4316
  // Otherwise, use the primary login mechanism (email as username)
4192
4317
  return formValues[loginMechanism];
4193
4318
  };
4319
+ /**
4320
+ * Get available authentication methods based on backend capabilities and hidden methods
4321
+ */
4322
+ const getAvailableAuthMethods = (passwordlessCapabilities, hiddenAuthMethods) => {
4323
+ const allMethods = [];
4324
+ // If hiddenAuthMethods is explicitly provided (even as empty array),
4325
+ // assume all methods are available and let hiddenAuthMethods filter them
4326
+ const assumeAllAvailable = hiddenAuthMethods !== undefined;
4327
+ // PASSWORD is always available by default
4328
+ allMethods.push('PASSWORD');
4329
+ // Add passwordless methods if backend supports them OR if hiddenAuthMethods is explicitly set
4330
+ if (assumeAllAvailable || passwordlessCapabilities?.emailOtpEnabled) {
4331
+ allMethods.push('EMAIL_OTP');
4332
+ }
4333
+ if (assumeAllAvailable || passwordlessCapabilities?.smsOtpEnabled) {
4334
+ allMethods.push('SMS_OTP');
4335
+ }
4336
+ if (assumeAllAvailable || passwordlessCapabilities?.webAuthnEnabled) {
4337
+ allMethods.push('WEB_AUTHN');
4338
+ }
4339
+ // Filter out hidden methods
4340
+ const hidden = hiddenAuthMethods ?? [];
4341
+ const availableMethods = allMethods.filter((method) => !hidden.includes(method));
4342
+ // Validate that at least one method remains
4343
+ if (availableMethods.length === 0) {
4344
+ throw new Error('InvalidPasswordlessSettings: All authentication methods are hidden');
4345
+ }
4346
+ return availableMethods;
4347
+ };
4194
4348
 
4195
4349
  const { assign } = xstate.actions;
4196
4350
  const clearActorDoneData = assign({ actorDoneData: undefined });
4197
4351
  const clearChallengeName = assign({ challengeName: undefined });
4198
4352
  const clearMissingAttributes = assign({ missingAttributes: undefined });
4199
4353
  const clearError = assign({ remoteError: undefined });
4200
- const clearFormValues = assign({ formValues: {} });
4354
+ const clearFormValues = assign({
4355
+ formValues: (context) => ({
4356
+ // Preserve username for passwordless flows to avoid "username is required" errors
4357
+ username: context.formValues?.username,
4358
+ }),
4359
+ });
4201
4360
  const clearTouched = assign({ touched: {} });
4202
4361
  const clearUser = assign({ user: undefined });
4203
4362
  const clearValidationError = assign({ validationError: {} });
@@ -4298,7 +4457,28 @@ const setRemoteError = assign({
4298
4457
  if (data.name === 'NoUserPoolError') {
4299
4458
  return `Configuration error (see console) – please contact the administrator`;
4300
4459
  }
4301
- return data?.message || data;
4460
+ const message = data?.message || '';
4461
+ // Handle USER_AUTH flow not enabled error
4462
+ if (message.includes('USER_AUTH flow not enabled')) {
4463
+ return translate(DefaultTexts.PASSWORDLESS_NOT_ENABLED);
4464
+ }
4465
+ // Handle cannot send code error
4466
+ if (message.includes('Cannot send code to either')) {
4467
+ return translate(DefaultTexts.CODE_DELIVERY_FAILED);
4468
+ }
4469
+ // Handle invalid/wrong verification code
4470
+ if (message.includes('Invalid code or auth state')) {
4471
+ return translate(DefaultTexts.VERIFICATION_CODE_INVALID);
4472
+ }
4473
+ // Handle expired verification code
4474
+ if (message.includes('session is expired')) {
4475
+ return translate(DefaultTexts.VERIFICATION_CODE_EXPIRED);
4476
+ }
4477
+ // Handle passkey authentication canceled
4478
+ if (message.includes('ceremony has been canceled')) {
4479
+ return translate(DefaultTexts.PASSKEY_AUTHENTICATION_CANCELED);
4480
+ }
4481
+ return message || data;
4302
4482
  },
4303
4483
  });
4304
4484
  const setUser = assign({ user: (_, { data }) => data });
@@ -4335,8 +4515,12 @@ const handleBlur = assign({
4335
4515
  }),
4336
4516
  });
4337
4517
  const setUnverifiedUserAttributes = assign({
4338
- unverifiedUserAttributes: (_, { data }) => {
4339
- const { email, phone_number } = data;
4518
+ unverifiedUserAttributes: (context, { data }) => {
4519
+ // Use fetchedUserAttributes from context if data is not provided
4520
+ const attributes = data || context.fetchedUserAttributes;
4521
+ if (!attributes)
4522
+ return {};
4523
+ const { email, phone_number } = attributes;
4340
4524
  const unverifiedUserAttributes = {
4341
4525
  ...(email && { email }),
4342
4526
  ...(phone_number && { phone_number }),
@@ -4350,11 +4534,34 @@ const setSelectedUserAttribute = assign({
4350
4534
  });
4351
4535
  // Maps to unexposed `ConfirmSignUpSignUpStep`
4352
4536
  const setConfirmSignUpSignUpStep = assign({ step: 'CONFIRM_SIGN_UP' });
4537
+ // Passwordless actions
4538
+ const setSelectedAuthMethod = assign({
4539
+ selectedAuthMethod: (_, { data }) => data.method,
4540
+ });
4541
+ const setSelectedAuthMethodFromForm = assign({
4542
+ selectedAuthMethod: (_, { data }) => {
4543
+ // Extract method from form data if present, default to PASSWORD for form submissions
4544
+ return data?.__authMethod || 'PASSWORD';
4545
+ },
4546
+ });
4547
+ const setSelectAuthMethodStep = assign({
4548
+ step: 'SELECT_AUTH_METHOD',
4549
+ });
4550
+ const setFetchedUserAttributes = assign({
4551
+ fetchedUserAttributes: (_, event) => event.data,
4552
+ });
4553
+ const setHasExistingPasskeys = assign({
4554
+ hasExistingPasskeys: (_, event) => event.data,
4555
+ });
4556
+ const clearHasExistingPasskeys = assign({
4557
+ hasExistingPasskeys: false,
4558
+ });
4353
4559
  const ACTIONS = {
4354
4560
  clearActorDoneData,
4355
4561
  clearChallengeName,
4356
4562
  clearError,
4357
4563
  clearFormValues,
4564
+ clearHasExistingPasskeys,
4358
4565
  clearMissingAttributes,
4359
4566
  clearSelectedUserAttribute,
4360
4567
  clearTouched,
@@ -4366,7 +4573,9 @@ const ACTIONS = {
4366
4573
  setAllowedMfaTypes,
4367
4574
  setChallengeName,
4368
4575
  setCodeDeliveryDetails,
4576
+ setFetchedUserAttributes,
4369
4577
  setFieldErrors,
4578
+ setHasExistingPasskeys,
4370
4579
  setMissingAttributes,
4371
4580
  setNextResetPasswordStep,
4372
4581
  setNextSignInStep,
@@ -4374,6 +4583,9 @@ const ACTIONS = {
4374
4583
  setRemoteError,
4375
4584
  setConfirmAttributeCompleteStep,
4376
4585
  setConfirmSignUpSignUpStep,
4586
+ setSelectAuthMethodStep,
4587
+ setSelectedAuthMethod,
4588
+ setSelectedAuthMethodFromForm,
4377
4589
  setShouldVerifyUserAttributeStep,
4378
4590
  setSelectedUserAttribute,
4379
4591
  setSignInStep,
@@ -4417,7 +4629,11 @@ const shouldResetPassword = ({ step }) => step === 'RESET_PASSWORD';
4417
4629
  const shouldConfirmResetPassword = ({ step }) => step === 'CONFIRM_RESET_PASSWORD_WITH_CODE';
4418
4630
  const shouldConfirmSignUp = ({ step }) => step === 'CONFIRM_SIGN_UP';
4419
4631
  // miscellaneous guards
4420
- const shouldVerifyAttribute = (_, { data }) => {
4632
+ const shouldVerifyAttribute = (context, event) => {
4633
+ // Try to get data from event first (for backward compatibility), then from context
4634
+ const data = (event.data || context.fetchedUserAttributes);
4635
+ if (!data)
4636
+ return false;
4421
4637
  const { email, phone_number, phone_number_verified, email_verified } = data;
4422
4638
  // if neither email nor phone_number exist
4423
4639
  // there is nothing to verify
@@ -4442,11 +4658,51 @@ const shouldVerifyAttribute = (_, { data }) => {
4442
4658
  * https://github.com/aws-amplify/amplify-ui/issues/219
4443
4659
  */
4444
4660
  const isUserAlreadyConfirmed = (_, { data }) => data.message === 'User is already confirmed.';
4661
+ // passwordless guards
4662
+ const shouldSelectAuthMethod = ({ availableAuthMethods, preferredChallenge, selectedAuthMethod, }) => {
4663
+ // Show selection if:
4664
+ // 1. Multiple methods available
4665
+ // 2. AND either no preferredChallenge OR selectedAuthMethod is explicitly cleared (null)
4666
+ const hasMultipleMethods = availableAuthMethods && availableAuthMethods.length > 1;
4667
+ const shouldShowSelection = !preferredChallenge || selectedAuthMethod === null;
4668
+ return hasMultipleMethods && shouldShowSelection;
4669
+ };
4670
+ const shouldPromptPasskeyRegistration = ({ passwordless, hasExistingPasskeys, }) => {
4671
+ const { passkeyRegistrationPrompts } = passwordless || {};
4672
+ if (!passkeyRegistrationPrompts) {
4673
+ return false;
4674
+ }
4675
+ // Don't prompt if user already has passkeys
4676
+ if (hasExistingPasskeys) {
4677
+ return false;
4678
+ }
4679
+ if (typeof passkeyRegistrationPrompts === 'boolean') {
4680
+ return passkeyRegistrationPrompts;
4681
+ }
4682
+ return passkeyRegistrationPrompts.afterSignin === 'ALWAYS';
4683
+ };
4684
+ const shouldPromptPasskeyRegistrationAfterSignup = ({ passwordless, hasExistingPasskeys, }) => {
4685
+ const { passkeyRegistrationPrompts } = passwordless || {};
4686
+ if (!passkeyRegistrationPrompts) {
4687
+ return false;
4688
+ }
4689
+ // Don't prompt if user already has passkeys
4690
+ if (hasExistingPasskeys) {
4691
+ return false;
4692
+ }
4693
+ if (typeof passkeyRegistrationPrompts === 'boolean') {
4694
+ return passkeyRegistrationPrompts;
4695
+ }
4696
+ return passkeyRegistrationPrompts.afterSignup === 'ALWAYS';
4697
+ };
4698
+ const hasPasskeyRegistrationPrompts = ({ passwordless }) => passwordless?.passkeyRegistrationPrompts != null;
4699
+ const shouldReturnToSelectMethod = ({ selectedAuthMethod, step, }) => selectedAuthMethod != null && step === 'SELECT_AUTH_METHOD';
4445
4700
  const GUARDS = {
4446
4701
  hasCompletedAttributeConfirmation,
4447
4702
  hasCompletedResetPassword,
4448
4703
  hasCompletedSignIn,
4449
4704
  hasCompletedSignUp,
4705
+ hasPasskeyRegistrationPrompts,
4450
4706
  isConfirmSignUpStep,
4451
4707
  isConfirmUserAttributeStep,
4452
4708
  isResetPasswordStep,
@@ -4460,10 +4716,14 @@ const GUARDS = {
4460
4716
  shouldConfirmSignUpFromSignIn,
4461
4717
  shouldResetPassword,
4462
4718
  shouldResetPasswordFromSignIn,
4719
+ shouldReturnToSelectMethod,
4720
+ shouldSelectAuthMethod,
4463
4721
  shouldSetupTotp,
4464
4722
  shouldSetupEmail,
4465
4723
  shouldSelectMfaType,
4466
4724
  shouldVerifyAttribute,
4725
+ shouldPromptPasskeyRegistration,
4726
+ shouldPromptPasskeyRegistrationAfterSignup,
4467
4727
  };
4468
4728
 
4469
4729
  // Runs all validators given. Resolves if there are no error. Rejects otherwise.
@@ -4511,11 +4771,24 @@ const defaultServices = {
4511
4771
  const parsedSocialProviders = loginWith?.oauth?.providers
4512
4772
  ? loginWith.oauth.providers?.map((provider) => provider.toString().toLowerCase())
4513
4773
  : undefined;
4774
+ // Detect passwordless capabilities from amplify_outputs.json
4775
+ // Support both snake_case (legacy) and camelCase (current) formats
4776
+ const passwordlessConfig = result.Auth?.Cognito?.passwordless;
4777
+ const passwordlessCapabilities = {
4778
+ emailOtpEnabled: passwordlessConfig?.emailOtpEnabled ??
4779
+ passwordlessConfig?.email_otp_enabled === true,
4780
+ smsOtpEnabled: passwordlessConfig?.smsOtpEnabled ??
4781
+ passwordlessConfig?.sms_otp_enabled === true,
4782
+ webAuthnEnabled: !!(passwordlessConfig?.webAuthn ?? passwordlessConfig?.web_authn),
4783
+ preferredChallenge: passwordlessConfig?.preferredChallenge ??
4784
+ passwordlessConfig?.preferred_challenge,
4785
+ };
4514
4786
  return {
4515
4787
  ...cliConfig,
4516
4788
  loginMechanisms: parsedLoginMechanisms,
4517
4789
  signUpAttributes: parsedSignupAttributes,
4518
4790
  socialProviders: parsedSocialProviders,
4791
+ passwordlessCapabilities,
4519
4792
  };
4520
4793
  },
4521
4794
  getCurrentUser: auth.getCurrentUser,
@@ -4578,6 +4851,30 @@ const defaultServices = {
4578
4851
  }
4579
4852
  },
4580
4853
  async validatePreferredUsername(_, __) { },
4854
+ async validateRequiredFieldsForAuthMethod(formData) {
4855
+ const authMethod = formData.__authMethod;
4856
+ // If no auth method specified, skip validation (will use default required fields)
4857
+ if (!authMethod)
4858
+ return null;
4859
+ // Check required fields based on auth method
4860
+ if (authMethod === 'EMAIL_OTP' && !formData.email) {
4861
+ return { email: 'Email is required for Email OTP sign up' };
4862
+ }
4863
+ if (authMethod === 'SMS_OTP' && !formData.phone_number) {
4864
+ return { phone_number: 'Phone number is required for SMS OTP sign up' };
4865
+ }
4866
+ if (authMethod === 'PASSWORD') {
4867
+ const errors = {};
4868
+ if (!formData.password) {
4869
+ errors.password = 'Password is required';
4870
+ }
4871
+ if (!formData.confirm_password) {
4872
+ errors.confirm_password = 'Confirm Password is required';
4873
+ }
4874
+ return Object.keys(errors).length > 0 ? errors : null;
4875
+ }
4876
+ return null;
4877
+ },
4581
4878
  };
4582
4879
 
4583
4880
  function forgotPasswordActor({ services, }) {
@@ -4807,32 +5104,13 @@ const handleSignInResponse = {
4807
5104
  'setNextSignInStep',
4808
5105
  'setTotpSecretCode',
4809
5106
  'setAllowedMfaTypes',
5107
+ 'setCodeDeliveryDetails',
4810
5108
  ],
4811
5109
  target: '#signInActor.init',
4812
5110
  },
4813
5111
  ],
4814
5112
  onError: { actions: 'setRemoteError', target: 'edit' },
4815
5113
  };
4816
- const handleFetchUserAttributesResponse$1 = {
4817
- onDone: [
4818
- {
4819
- cond: 'shouldVerifyAttribute',
4820
- actions: [
4821
- 'setShouldVerifyUserAttributeStep',
4822
- 'setUnverifiedUserAttributes',
4823
- ],
4824
- target: '#signInActor.resolved',
4825
- },
4826
- {
4827
- actions: 'setConfirmAttributeCompleteStep',
4828
- target: '#signInActor.resolved',
4829
- },
4830
- ],
4831
- onError: {
4832
- actions: 'setConfirmAttributeCompleteStep',
4833
- target: '#signInActor.resolved',
4834
- },
4835
- };
4836
5114
  const getDefaultConfirmSignInState = (exit) => ({
4837
5115
  initial: 'edit',
4838
5116
  exit,
@@ -4843,6 +5121,7 @@ const getDefaultConfirmSignInState = (exit) => ({
4843
5121
  SUBMIT: { actions: 'handleSubmit', target: 'submit' },
4844
5122
  SIGN_IN: '#signInActor.signIn',
4845
5123
  CHANGE: { actions: 'handleInput' },
5124
+ RESEND: { target: 'resend' },
4846
5125
  },
4847
5126
  },
4848
5127
  submit: {
@@ -4850,6 +5129,21 @@ const getDefaultConfirmSignInState = (exit) => ({
4850
5129
  entry: ['sendUpdate', 'clearError'],
4851
5130
  invoke: { src: 'confirmSignIn', ...handleSignInResponse },
4852
5131
  },
5132
+ resend: {
5133
+ tags: 'pending',
5134
+ entry: ['sendUpdate', 'clearError'],
5135
+ invoke: {
5136
+ src: 'resendSignInCode',
5137
+ onDone: {
5138
+ actions: ['setCodeDeliveryDetails', 'sendUpdate'],
5139
+ target: 'edit',
5140
+ },
5141
+ onError: {
5142
+ actions: 'setRemoteError',
5143
+ target: 'edit',
5144
+ },
5145
+ },
5146
+ },
4853
5147
  },
4854
5148
  });
4855
5149
  function signInActor({ services }) {
@@ -4884,13 +5178,68 @@ function signInActor({ services }) {
4884
5178
  { target: 'signIn' },
4885
5179
  ],
4886
5180
  },
4887
- federatedSignIn: getFederatedSignInState('signIn'),
5181
+ federatedSignIn: { ...getFederatedSignInState('signIn') },
4888
5182
  fetchUserAttributes: {
4889
5183
  invoke: {
4890
5184
  src: 'fetchUserAttributes',
4891
- ...handleFetchUserAttributesResponse$1,
5185
+ onDone: [
5186
+ {
5187
+ cond: 'hasPasskeyRegistrationPrompts',
5188
+ actions: 'setFetchedUserAttributes',
5189
+ target: 'checkPasskeys',
5190
+ },
5191
+ {
5192
+ actions: 'setFetchedUserAttributes',
5193
+ target: 'evaluatePasskeyPrompt',
5194
+ },
5195
+ ],
5196
+ onError: {
5197
+ actions: 'setConfirmAttributeCompleteStep',
5198
+ target: '#signInActor.resolved',
5199
+ },
5200
+ },
5201
+ },
5202
+ checkPasskeys: {
5203
+ invoke: {
5204
+ src: async () => {
5205
+ try {
5206
+ const result = await auth.listWebAuthnCredentials();
5207
+ return result.credentials && result.credentials.length > 0;
5208
+ }
5209
+ catch {
5210
+ return false;
5211
+ }
5212
+ },
5213
+ onDone: {
5214
+ actions: 'setHasExistingPasskeys',
5215
+ target: 'evaluatePasskeyPrompt',
5216
+ },
5217
+ onError: {
5218
+ actions: 'clearHasExistingPasskeys',
5219
+ target: 'evaluatePasskeyPrompt',
5220
+ },
4892
5221
  },
4893
5222
  },
5223
+ evaluatePasskeyPrompt: {
5224
+ always: [
5225
+ {
5226
+ cond: 'shouldPromptPasskeyRegistration',
5227
+ target: '#signInActor.passkeyPrompt',
5228
+ },
5229
+ {
5230
+ cond: 'shouldVerifyAttribute',
5231
+ actions: [
5232
+ 'setShouldVerifyUserAttributeStep',
5233
+ 'setUnverifiedUserAttributes',
5234
+ ],
5235
+ target: '#signInActor.resolved',
5236
+ },
5237
+ {
5238
+ actions: 'setConfirmAttributeCompleteStep',
5239
+ target: '#signInActor.resolved',
5240
+ },
5241
+ ],
5242
+ },
4894
5243
  resendSignUpCode: {
4895
5244
  invoke: {
4896
5245
  src: 'handleResendSignUpCode',
@@ -4925,23 +5274,70 @@ function signInActor({ services }) {
4925
5274
  on: {
4926
5275
  CHANGE: { actions: 'handleInput' },
4927
5276
  FEDERATED_SIGN_IN: { target: '#signInActor.federatedSignIn' },
4928
- SUBMIT: { actions: 'handleSubmit', target: 'submit' },
5277
+ SHOW_AUTH_METHODS: {
5278
+ actions: 'setUsernameSignIn',
5279
+ target: 'selectMethod',
5280
+ },
5281
+ SUBMIT: [
5282
+ {
5283
+ cond: 'shouldSelectAuthMethod',
5284
+ actions: 'handleSubmit',
5285
+ target: 'selectMethod',
5286
+ },
5287
+ {
5288
+ actions: 'handleSubmit',
5289
+ target: 'submit',
5290
+ },
5291
+ ],
5292
+ },
5293
+ },
5294
+ selectMethod: {
5295
+ entry: [
5296
+ 'sendUpdate',
5297
+ 'setSelectAuthMethodStep',
5298
+ 'setUsernameSignIn',
5299
+ ],
5300
+ on: {
5301
+ SELECT_METHOD: {
5302
+ actions: 'setSelectedAuthMethod',
5303
+ target: 'submit',
5304
+ },
5305
+ SUBMIT: {
5306
+ actions: ['handleSubmit', 'setSelectedAuthMethodFromForm'],
5307
+ target: 'submit',
5308
+ },
5309
+ SIGN_IN: {
5310
+ target: 'edit',
5311
+ },
4929
5312
  },
4930
5313
  },
4931
5314
  submit: {
4932
5315
  tags: 'pending',
4933
5316
  entry: ['clearError', 'sendUpdate', 'setUsernameSignIn'],
4934
5317
  exit: 'clearFormValues',
4935
- invoke: { src: 'handleSignIn', ...handleSignInResponse },
5318
+ invoke: {
5319
+ src: 'handleSignIn',
5320
+ onDone: handleSignInResponse.onDone,
5321
+ onError: [
5322
+ {
5323
+ cond: 'shouldReturnToSelectMethod',
5324
+ actions: 'setRemoteError',
5325
+ target: 'selectMethod',
5326
+ },
5327
+ handleSignInResponse.onError,
5328
+ ],
5329
+ },
4936
5330
  },
4937
5331
  },
4938
5332
  },
4939
- confirmSignIn: getDefaultConfirmSignInState([
4940
- 'clearChallengeName',
4941
- 'clearFormValues',
4942
- 'clearError',
4943
- 'clearTouched',
4944
- ]),
5333
+ confirmSignIn: {
5334
+ ...getDefaultConfirmSignInState([
5335
+ 'clearChallengeName',
5336
+ 'clearFormValues',
5337
+ 'clearError',
5338
+ 'clearTouched',
5339
+ ]),
5340
+ },
4945
5341
  forceChangePassword: {
4946
5342
  entry: 'sendUpdate',
4947
5343
  type: 'parallel',
@@ -5014,21 +5410,40 @@ function signInActor({ services }) {
5014
5410
  },
5015
5411
  },
5016
5412
  },
5017
- setupTotp: getDefaultConfirmSignInState([
5018
- 'clearFormValues',
5019
- 'clearError',
5020
- 'clearTouched',
5021
- ]),
5022
- setupEmail: getDefaultConfirmSignInState([
5023
- 'clearFormValues',
5024
- 'clearError',
5025
- 'clearTouched',
5026
- ]),
5027
- selectMfaType: getDefaultConfirmSignInState([
5028
- 'clearFormValues',
5029
- 'clearError',
5030
- 'clearTouched',
5031
- ]),
5413
+ setupTotp: {
5414
+ ...getDefaultConfirmSignInState([
5415
+ 'clearFormValues',
5416
+ 'clearError',
5417
+ 'clearTouched',
5418
+ ]),
5419
+ },
5420
+ setupEmail: {
5421
+ ...getDefaultConfirmSignInState([
5422
+ 'clearFormValues',
5423
+ 'clearError',
5424
+ 'clearTouched',
5425
+ ]),
5426
+ },
5427
+ selectMfaType: {
5428
+ ...getDefaultConfirmSignInState([
5429
+ 'clearFormValues',
5430
+ 'clearError',
5431
+ 'clearTouched',
5432
+ ]),
5433
+ },
5434
+ passkeyPrompt: {
5435
+ entry: 'sendUpdate',
5436
+ on: {
5437
+ SUBMIT: {
5438
+ actions: 'setConfirmAttributeCompleteStep',
5439
+ target: 'resolved',
5440
+ },
5441
+ SKIP: {
5442
+ actions: 'setConfirmAttributeCompleteStep',
5443
+ target: 'resolved',
5444
+ },
5445
+ },
5446
+ },
5032
5447
  resolved: {
5033
5448
  type: 'final',
5034
5449
  data: (context) => ({
@@ -5054,9 +5469,41 @@ function signInActor({ services }) {
5054
5469
  handleResendSignUpCode({ username }) {
5055
5470
  return services.handleResendSignUpCode({ username });
5056
5471
  },
5057
- handleSignIn({ formValues, username }) {
5058
- const { password } = formValues;
5059
- return services.handleSignIn({ username, password });
5472
+ resendSignInCode({ username, selectedAuthMethod, availableAuthMethods, preferredChallenge, }) {
5473
+ // Resend code by calling signIn again with the same parameters
5474
+ const method = selectedAuthMethod ??
5475
+ preferredChallenge ??
5476
+ availableAuthMethods?.[0] ??
5477
+ 'PASSWORD';
5478
+ return services.handleSignIn({
5479
+ username,
5480
+ options: {
5481
+ authFlowType: 'USER_AUTH',
5482
+ preferredChallenge: method,
5483
+ },
5484
+ });
5485
+ },
5486
+ handleSignIn({ formValues, username, selectedAuthMethod, availableAuthMethods, preferredChallenge, }) {
5487
+ // Determine which method to use
5488
+ const method = selectedAuthMethod ??
5489
+ preferredChallenge ??
5490
+ availableAuthMethods?.[0] ??
5491
+ 'PASSWORD';
5492
+ if (method === 'PASSWORD') {
5493
+ // Traditional password flow
5494
+ const { password } = formValues;
5495
+ return services.handleSignIn({ username, password });
5496
+ }
5497
+ else {
5498
+ // Passwordless flow using USER_AUTH
5499
+ return services.handleSignIn({
5500
+ username,
5501
+ options: {
5502
+ authFlowType: 'USER_AUTH',
5503
+ preferredChallenge: method,
5504
+ },
5505
+ });
5506
+ }
5060
5507
  },
5061
5508
  confirmSignIn({ formValues, step }) {
5062
5509
  const formValuesKey = getConfirmSignInFormValuesKey(step);
@@ -5140,26 +5587,6 @@ const handleAutoSignInResponse = {
5140
5587
  target: '#signUpActor.resolved',
5141
5588
  },
5142
5589
  };
5143
- const handleFetchUserAttributesResponse = {
5144
- onDone: [
5145
- {
5146
- cond: 'shouldVerifyAttribute',
5147
- actions: [
5148
- 'setShouldVerifyUserAttributeStep',
5149
- 'setUnverifiedUserAttributes',
5150
- ],
5151
- target: '#signUpActor.resolved',
5152
- },
5153
- {
5154
- actions: 'setConfirmAttributeCompleteStep',
5155
- target: '#signUpActor.resolved',
5156
- },
5157
- ],
5158
- onError: {
5159
- actions: 'setConfirmAttributeCompleteStep',
5160
- target: '#signUpActor.resolved',
5161
- },
5162
- };
5163
5590
  function signUpActor({ services }) {
5164
5591
  return xstate.createMachine({
5165
5592
  id: 'signUpActor',
@@ -5179,10 +5606,65 @@ function signUpActor({ services }) {
5179
5606
  fetchUserAttributes: {
5180
5607
  invoke: {
5181
5608
  src: 'fetchUserAttributes',
5182
- ...handleFetchUserAttributesResponse,
5609
+ onDone: [
5610
+ {
5611
+ cond: 'hasPasskeyRegistrationPrompts',
5612
+ actions: 'setFetchedUserAttributes',
5613
+ target: 'checkPasskeys',
5614
+ },
5615
+ {
5616
+ actions: 'setFetchedUserAttributes',
5617
+ target: 'evaluatePasskeyPrompt',
5618
+ },
5619
+ ],
5620
+ onError: {
5621
+ actions: 'setConfirmAttributeCompleteStep',
5622
+ target: '#signUpActor.resolved',
5623
+ },
5624
+ },
5625
+ },
5626
+ checkPasskeys: {
5627
+ invoke: {
5628
+ src: async () => {
5629
+ try {
5630
+ const result = await auth.listWebAuthnCredentials();
5631
+ return result.credentials && result.credentials.length > 0;
5632
+ }
5633
+ catch {
5634
+ return false;
5635
+ }
5636
+ },
5637
+ onDone: {
5638
+ actions: 'setHasExistingPasskeys',
5639
+ target: 'evaluatePasskeyPrompt',
5640
+ },
5641
+ onError: {
5642
+ actions: 'clearHasExistingPasskeys',
5643
+ target: 'evaluatePasskeyPrompt',
5644
+ },
5183
5645
  },
5184
5646
  },
5185
- federatedSignIn: getFederatedSignInState('signUp'),
5647
+ evaluatePasskeyPrompt: {
5648
+ always: [
5649
+ {
5650
+ cond: 'shouldPromptPasskeyRegistrationAfterSignup',
5651
+ target: '#signUpActor.passkeyPrompt',
5652
+ },
5653
+ {
5654
+ cond: 'shouldVerifyAttribute',
5655
+ actions: [
5656
+ 'setShouldVerifyUserAttributeStep',
5657
+ 'setUnverifiedUserAttributes',
5658
+ ],
5659
+ target: '#signUpActor.resolved',
5660
+ },
5661
+ {
5662
+ actions: 'setConfirmAttributeCompleteStep',
5663
+ target: '#signUpActor.resolved',
5664
+ },
5665
+ ],
5666
+ },
5667
+ federatedSignIn: { ...getFederatedSignInState('signUp') },
5186
5668
  resetPassword: {
5187
5669
  invoke: { src: 'resetPassword', ...handleResetPasswordResponse },
5188
5670
  },
@@ -5201,7 +5683,10 @@ function signUpActor({ services }) {
5201
5683
  cond: 'isUserAlreadyConfirmed',
5202
5684
  target: '#signUpActor.resolved',
5203
5685
  },
5204
- { actions: ['setRemoteError', 'sendUpdate'] },
5686
+ {
5687
+ actions: ['setRemoteError', 'sendUpdate'],
5688
+ target: '#signUpActor.confirmSignUp',
5689
+ },
5205
5690
  ],
5206
5691
  },
5207
5692
  },
@@ -5239,7 +5724,13 @@ function signUpActor({ services }) {
5239
5724
  idle: {
5240
5725
  entry: ['sendUpdate'],
5241
5726
  on: {
5242
- SUBMIT: { actions: 'handleSubmit', target: 'validate' },
5727
+ SUBMIT: {
5728
+ actions: [
5729
+ 'setSelectedAuthMethodFromForm',
5730
+ 'handleSubmit',
5731
+ ],
5732
+ target: 'validate',
5733
+ },
5243
5734
  },
5244
5735
  },
5245
5736
  validate: {
@@ -5324,6 +5815,13 @@ function signUpActor({ services }) {
5324
5815
  },
5325
5816
  },
5326
5817
  },
5818
+ passkeyPrompt: {
5819
+ entry: 'sendUpdate',
5820
+ on: {
5821
+ SKIP: { target: 'resolved' },
5822
+ SUBMIT: { target: 'resolved' },
5823
+ },
5824
+ },
5327
5825
  resolved: {
5328
5826
  type: 'final',
5329
5827
  data: (context) => ({
@@ -5361,9 +5859,10 @@ function signUpActor({ services }) {
5361
5859
  return auth.signInWithRedirect(data);
5362
5860
  },
5363
5861
  handleSignUp(context) {
5364
- const { formValues, loginMechanisms, username } = context;
5862
+ const { formValues, loginMechanisms, username, selectedAuthMethod, preferredChallenge, } = context;
5365
5863
  const loginMechanism = loginMechanisms[0];
5366
- const input = getSignUpInput(username, formValues, loginMechanism);
5864
+ const authMethod = selectedAuthMethod ?? preferredChallenge;
5865
+ const input = getSignUpInput(username, formValues, loginMechanism, authMethod);
5367
5866
  return services.handleSignUp(input);
5368
5867
  },
5369
5868
  async validateSignUp(context) {
@@ -5374,6 +5873,8 @@ function signUpActor({ services }) {
5374
5873
  // Validation for default form fields
5375
5874
  services.validateConfirmPassword,
5376
5875
  services.validatePreferredUsername,
5876
+ // Validation for required fields based on auth method
5877
+ services.validateRequiredFieldsForAuthMethod,
5377
5878
  // Validation for any custom Sign Up fields
5378
5879
  services.validateCustomSignUp,
5379
5880
  ]);
@@ -5505,20 +6006,29 @@ function verifyUserAttributesActor() {
5505
6006
  });
5506
6007
  }
5507
6008
 
5508
- const getActorContext = (context, defaultStep) => ({
5509
- ...context.actorDoneData,
5510
- step: context?.actorDoneData?.step ?? defaultStep,
5511
- // initialize empty objects on actor start
5512
- formValues: {},
5513
- touched: {},
5514
- validationError: {},
5515
- // values included on `context.config` that should be available in actors
5516
- formFields: context.config?.formFields,
5517
- loginMechanisms: context.config?.loginMechanisms,
5518
- passwordSettings: context.config?.passwordSettings,
5519
- signUpAttributes: context.config?.signUpAttributes,
5520
- socialProviders: context.config?.socialProviders,
5521
- });
6009
+ const getActorContext = (context, defaultStep) => {
6010
+ const availableAuthMethods = getAvailableAuthMethods(context.passwordlessCapabilities, context.config?.passwordless?.hiddenAuthMethods);
6011
+ // Determine effective preferred challenge: component prop takes precedence over backend config
6012
+ const preferredChallenge = context.config?.passwordless?.preferredAuthMethod ??
6013
+ context.passwordlessCapabilities?.preferredChallenge;
6014
+ return {
6015
+ ...context.actorDoneData,
6016
+ step: context?.actorDoneData?.step ?? defaultStep,
6017
+ // initialize empty objects on actor start
6018
+ formValues: {},
6019
+ touched: {},
6020
+ validationError: {},
6021
+ // values included on `context.config` that should be available in actors
6022
+ formFields: context.config?.formFields,
6023
+ loginMechanisms: context.config?.loginMechanisms,
6024
+ passwordSettings: context.config?.passwordSettings,
6025
+ signUpAttributes: context.config?.signUpAttributes,
6026
+ socialProviders: context.config?.socialProviders,
6027
+ availableAuthMethods,
6028
+ preferredChallenge,
6029
+ passwordless: context.config?.passwordless,
6030
+ };
6031
+ };
5522
6032
  const { choose, stop } = xstate.actions;
5523
6033
  const stopActor = (machineId) => stop(machineId);
5524
6034
  // setup step waits for ui to emit INIT action to proceed to configure
@@ -5616,6 +6126,8 @@ function createAuthenticatorMachine(options) {
5616
6126
  },
5617
6127
  on: {
5618
6128
  FORGOT_PASSWORD: 'forgotPasswordActor',
6129
+ SELECT_METHOD: { actions: 'forwardToActor' },
6130
+ SHOW_AUTH_METHODS: { actions: 'forwardToActor' },
5619
6131
  SIGN_IN: 'signInActor',
5620
6132
  SIGN_UP: 'signUpActor',
5621
6133
  'done.invoke.signInActor': [
@@ -5782,10 +6294,11 @@ function createAuthenticatorMachine(options) {
5782
6294
  }),
5783
6295
  }),
5784
6296
  applyAmplifyConfig: xstate.assign({
6297
+ passwordlessCapabilities: (_, { data: cliConfig }) => cliConfig.passwordlessCapabilities,
5785
6298
  config(context, { data: cliConfig }) {
5786
6299
  // Prefer explicitly configured settings over default CLI values\
5787
6300
  const { loginMechanisms = cliConfig.loginMechanisms ?? [], signUpAttributes = cliConfig.signUpAttributes ?? [], socialProviders = cliConfig.socialProviders ?? [], initialState, formFields: _formFields, passwordSettings = cliConfig.passwordFormat ??
5788
- {}, } = context.config;
6301
+ {}, passwordless, } = context.config;
5789
6302
  // By default, Cognito assumes `username`, so there isn't a different username attribute like `email`.
5790
6303
  // We explicitly add it as a login mechanism to be consistent with Admin UI's language.
5791
6304
  if (loginMechanisms.length === 0) {
@@ -5797,6 +6310,7 @@ function createAuthenticatorMachine(options) {
5797
6310
  initialState,
5798
6311
  loginMechanisms,
5799
6312
  passwordSettings,
6313
+ passwordless,
5800
6314
  signUpAttributes,
5801
6315
  socialProviders,
5802
6316
  };