From 31a11b3f121f68ec41d8c50a2e4e9b274524d051 Mon Sep 17 00:00:00 2001 From: William Lee <43682783+wlee221@users.noreply.github.com> Date: Tue, 17 Aug 2021 14:43:08 -0700 Subject: [PATCH] refactor(ui-core): auth machine refactor with actors (#180) * setup local dev helper * Set tab size * initial directory setup * sign in actor skeleton implementation * sign in staging * Temp workaround for sign in to render * signIn initial refactor * signUp initial refactor * removed unused import * Remove unused actors * Remove unused exports * spawn signIn and signUp with context * Send CHANGE event instead of INPUT * signIn with context values * fix typo * Rename INPUT to CHANGE * Set user on success * Enable uncofirmed signIn -> confirmSignUp use case * get context from actor whenever needed * Be explicit about what each actor passes * implement signOut actor * use actor context * formalize logic for passing contexts between actors * remove TODO * fix reference * fix function signature * Remove unneeded context * Removed unused export * Use intent instead of an explicit error * enable auto sign in * send autoSignIn intent from signUp * Move context definitions to /types * Update actor changes to forceNewPassword * get challengeName from actor * provide initial context and don't use sync * Reflect actor changes to React! * sendUpdate on each validation * vue refactors! * Don't finish on federatedSignIn * fix typo * implement resetPassword * use formValues from context * pass login_mechanism * display error on signIn * persist through authAttributes * Remove unused var * Remove unused actions * Remove unnecessary check * clear formValues on transition * Remove context from actor def * Remove unused transition * Update packages/vue/src/components/confirm-sign-up.vue Co-authored-by: Erik Hanchett * Use first available username * Pass username from signIn with `authAttributes` * Use username from authAttributes.username * Remove unused export * Separaete out invoke event types * Use Record * call federatedSignIn from confirm-sign-up * Rename state to _state * Remove unnecessary state export * check both username sources * Strictly type helper functions * Strongly type helpers in Vue * React: use tryped result from helpers * Add missed assertions * More vue strong typings * Fianl strongly typed actorState! * Update packages/core/src/types/authMachine.ts Thanks @eddiekeller! Co-authored-by: Eddie Keller <116ekg@gmail.com> * Update packages/react/src/components/Authenticator/SignIn/SignIn.tsx * Forward SIGN_IN event * Remove unused imports * Fix angular build * Type ErrorText context * Provide default context * get user from context * Remove autoSignIn state and go directly to signIn.submit * Remove unused import/var * Update packages/core/src/machines/authMachine.ts Co-authored-by: Erik Hanchett Co-authored-by: Eddie Keller <116ekg@gmail.com> --- .vscode/settings.json | 1 + .../amplify-authenticator.component.html | 16 +- .../amplify-authenticator.component.ts | 7 +- .../amplify-confirm-sign-in.component.ts | 20 +- .../amplify-confirm-sign-up.component.html | 2 +- .../amplify-confirm-sign-up.component.ts | 34 +- .../amplify-force-new-password.component.ts | 20 +- .../amplify-setup-totp.component.ts | 16 +- .../amplify-sign-in.component.ts | 15 +- .../amplify-sign-up.component.ts | 14 +- .../amplify-input/amplify-input.component.ts | 11 +- .../src/lib/services/state-machine.service.ts | 6 +- packages/core/src/authMachine.ts | 723 ------------------ packages/core/src/authService.ts | 2 +- packages/core/src/helpers/auth.ts | 26 +- packages/core/src/index.ts | 2 +- packages/core/src/machines/actions/auth.ts | 7 + packages/core/src/machines/actions/index.ts | 1 + .../src/machines/actors/auth/resetPassword.ts | 115 +++ .../core/src/machines/actors/auth/signIn.ts | 275 +++++++ .../core/src/machines/actors/auth/signOut.ts | 27 + .../core/src/machines/actors/auth/signUp.ts | 228 ++++++ packages/core/src/machines/actors/index.ts | 4 + packages/core/src/machines/authMachine.ts | 177 +++++ packages/core/src/machines/index.ts | 1 + packages/core/src/types/authMachine.ts | 68 +- .../ConfirmSignIn/ConfirmSignIn.tsx | 14 +- .../ConfirmSignUp/ConfirmSignUp.tsx | 6 +- .../ForceNewPassword/ForceNewPassword.tsx | 8 +- .../ResetPassword/ConfirmResetPassword.tsx | 18 +- .../ResetPassword/ResetPassword.tsx | 13 +- .../Authenticator/SetupTOTP/SetupTOTP.tsx | 8 +- .../Authenticator/SignIn/SignIn.tsx | 22 +- .../Authenticator/SignUp/SignUp.tsx | 28 +- .../src/components/Authenticator/index.tsx | 21 +- .../Authenticator/shared/ErrorText.tsx | 7 +- packages/vue/src/components/authenticator.vue | 21 +- .../vue/src/components/confirm-sign-in.vue | 21 +- .../vue/src/components/confirm-sign-up.vue | 37 +- .../vue/src/components/force-new-password.vue | 18 +- packages/vue/src/components/setup-totp.vue | 31 +- packages/vue/src/components/sign-in.vue | 18 +- packages/vue/src/components/sign-up.vue | 22 +- .../vue/src/components/user-name-alias.vue | 9 +- packages/vue/src/composables/useAuth.ts | 6 +- packages/vue/src/types/index.ts | 22 +- 46 files changed, 1242 insertions(+), 926 deletions(-) delete mode 100644 packages/core/src/authMachine.ts create mode 100644 packages/core/src/machines/actions/auth.ts create mode 100644 packages/core/src/machines/actions/index.ts create mode 100644 packages/core/src/machines/actors/auth/resetPassword.ts create mode 100644 packages/core/src/machines/actors/auth/signIn.ts create mode 100644 packages/core/src/machines/actors/auth/signOut.ts create mode 100644 packages/core/src/machines/actors/auth/signUp.ts create mode 100644 packages/core/src/machines/actors/index.ts create mode 100644 packages/core/src/machines/authMachine.ts create mode 100644 packages/core/src/machines/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 29518b8a8cb..aee31c24ce0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.insertSpaces": true, + "editor.tabSize": 2, "files.insertFinalNewline": true, "prettier.requireConfig": true, "typescript.tsdk": "node_modules/typescript/lib", diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.html b/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.html index 2cf1e501910..c1a66cf46c6 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.html +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.html @@ -33,9 +33,7 @@ @@ -43,7 +41,7 @@ @@ -51,7 +49,7 @@ @@ -59,7 +57,7 @@ @@ -67,7 +65,7 @@ @@ -75,7 +73,7 @@ @@ -83,6 +81,6 @@ diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.ts b/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.ts index a0b619bf16f..6e865009721 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-authenticator/amplify-authenticator.component.ts @@ -12,7 +12,7 @@ import { AuthState } from '../../common/types'; import { AmplifyOverrideDirective } from '../../directives/amplify-override.directive'; import { StateMachineService, AuthPropService } from '../../services'; import { CustomComponents } from '../../common'; -import { State } from 'xstate'; +import { getActorState } from '@aws-amplify/ui-core'; @Component({ selector: 'amplify-authenticator', @@ -53,8 +53,11 @@ export class AmplifyAuthenticatorComponent implements AfterContentInit { /** * Class Functions */ + public get actorState() { + return getActorState(this.stateMachine.authState); + } - public getAuthState(): State { + public get authenticatorState() { return this.stateMachine.authState; } diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-in/amplify-confirm-sign-in.component.ts b/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-in/amplify-confirm-sign-in.component.ts index 905177409b9..82a5e7da082 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-in/amplify-confirm-sign-in.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-in/amplify-confirm-sign-in.component.ts @@ -9,7 +9,14 @@ import { import { Logger } from '@aws-amplify/core'; import { AuthPropService, StateMachineService } from '../../services'; import { Subscription } from 'xstate'; -import { AuthChallengeNames, AuthMachineState } from '@aws-amplify/ui-core'; +import { + AuthChallengeNames, + AuthMachineState, + getActorContext, + getActorState, + SignInContext, + SignInState, +} from '@aws-amplify/ui-core'; const logger = new Logger('ConfirmSignIn'); @@ -51,7 +58,9 @@ export class AmplifyConfirmSignInComponent } setHeaderText(): void { - const { challengeName } = this.stateMachine.context; + const state = this.stateMachine.authState; + const actorContext: SignInContext = getActorContext(state); + const { challengeName } = actorContext; switch (challengeName) { case AuthChallengeNames.SOFTWARE_TOKEN_MFA: // TODO: this string should be centralized and translated from ui-core. @@ -66,15 +75,16 @@ export class AmplifyConfirmSignInComponent } onStateUpdate(state: AuthMachineState): void { - this.remoteError = state.context.remoteError; - this.isPending = !state.matches('confirmSignIn.edit'); + const actorState: SignInState = getActorState(state); + this.remoteError = actorState.context.remoteError; + this.isPending = !actorState.matches('confirmSignIn.edit'); } onInput(event: Event) { event.preventDefault(); const { name, value } = event.target; this.stateMachine.send({ - type: 'INPUT', + type: 'CHANGE', data: { name, value }, }); } diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-up/amplify-confirm-sign-up.component.html b/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-up/amplify-confirm-sign-up.component.html index 0153f76c8ac..76eb09b9ec5 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-up/amplify-confirm-sign-up.component.html +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-confirm-sign-up/amplify-confirm-sign-up.component.html @@ -9,7 +9,7 @@
{ + onSubmit(event: Event) { event.preventDefault(); - const formValues = this.stateMachine.context.formValues; - // get form data + const state = this.stateMachine.authState; + const actorContext: SignUpContext = getActorContext(state); + const { formValues } = actorContext; const { username, confirmation_code } = formValues; this.stateMachine.send({ diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-force-new-password/amplify-force-new-password.component.ts b/packages/angular/projects/ui-angular/src/lib/components/amplify-force-new-password/amplify-force-new-password.component.ts index 2f953401c37..5ac06f7c7f6 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-force-new-password/amplify-force-new-password.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-force-new-password/amplify-force-new-password.component.ts @@ -9,7 +9,13 @@ import { } from '@angular/core'; import { Subscription } from 'xstate'; import { Logger } from '@aws-amplify/core'; -import { AuthMachineState } from '@aws-amplify/ui-core'; +import { + AuthMachineState, + getActorContext, + getActorState, + SignInContext, + SignInState, +} from '@aws-amplify/ui-core'; import { AuthPropService, StateMachineService } from '../../services'; const logger = new Logger('ForceNewPassword'); @@ -53,8 +59,9 @@ export class AmplifyForceNewPasswordComponent } onStateUpdate(state: AuthMachineState): void { - this.remoteError = state.context.remoteError; - this.isPending = !state.matches('forceNewPassword.edit'); + const actorState: SignInState = getActorState(state); + this.remoteError = actorState.context.remoteError; + this.isPending = !actorState.matches('forceNewPassword.edit'); } toSignIn(): void { @@ -65,14 +72,17 @@ export class AmplifyForceNewPasswordComponent event.preventDefault(); const { name, value } = event.target; this.stateMachine.send({ - type: 'INPUT', + type: 'CHANGE', data: { name, value }, }); } onSubmit(event: Event) { event.preventDefault(); - const formValues = this.stateMachine.context.formValues; + // consider stateMachine directly providing actorState / actorContext + const state = this.stateMachine.authState; + const actorState: SignInContext = getActorContext(state); + const { formValues } = actorState; this.stateMachine.send({ type: 'SUBMIT', diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-setup-totp/amplify-setup-totp.component.ts b/packages/angular/projects/ui-angular/src/lib/components/amplify-setup-totp/amplify-setup-totp.component.ts index a6f74ac7f07..63d554163e1 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-setup-totp/amplify-setup-totp.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-setup-totp/amplify-setup-totp.component.ts @@ -8,9 +8,12 @@ import { } from '@angular/core'; import { Subscription } from 'xstate'; import QRCode from 'qrcode'; -import { Logger } from '@aws-amplify/core'; -import { AuthMachineState } from '@aws-amplify/ui-core'; -import Auth from '@aws-amplify/auth'; +import { Auth, Logger } from 'aws-amplify'; +import { + AuthMachineState, + getActorState, + SignInState, +} from '@aws-amplify/ui-core'; import { AuthPropService, StateMachineService } from '../../services'; const logger = new Logger('SetupTotp'); @@ -53,8 +56,9 @@ export class AmplifySetupTotpComponent } onStateUpdate(state: AuthMachineState): void { - this.remoteError = state.context.remoteError; - this.isPending = !state.matches('setupTOTP.edit'); + const actorState: SignInState = getActorState(state); + this.remoteError = actorState.context.remoteError; + this.isPending = !actorState.matches('setupTOTP.edit'); } async generateQRCode() { @@ -77,7 +81,7 @@ export class AmplifySetupTotpComponent event.preventDefault(); const { name, value } = event.target; this.stateMachine.send({ - type: 'INPUT', + type: 'CHANGE', data: { name, value }, }); } diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-in/amplify-sign-in.component.ts b/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-in/amplify-sign-in.component.ts index 04d1c5cdc0d..f4464b50303 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-in/amplify-sign-in.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-in/amplify-sign-in.component.ts @@ -11,7 +11,11 @@ import { } from '@angular/core'; import { AuthPropService, StateMachineService } from '../../services'; import { Subscription } from 'xstate'; -import { AuthMachineState } from '@aws-amplify/ui-core'; +import { + AuthMachineState, + getActorState, + SignInState, +} from '@aws-amplify/ui-core'; const logger = new Logger('SignIn'); @@ -54,8 +58,9 @@ export class AmplifySignInComponent } onStateUpdate(state: AuthMachineState): void { - this.remoteError = state.context.remoteError; - this.isPending = !state.matches('signIn.edit'); + const actorState: SignInState = getActorState(state); + this.remoteError = actorState.context.remoteError; + this.isPending = !actorState.matches('signIn.edit'); } toSignUp(): void { @@ -66,18 +71,16 @@ export class AmplifySignInComponent event.preventDefault(); const { name, value } = event.target; this.stateMachine.send({ - type: 'INPUT', + type: 'CHANGE', data: { name, value }, }); } async onSubmit(event: Event): Promise { event.preventDefault(); - const formValues = this.stateMachine.context.formValues; this.stateMachine.send({ type: 'SUBMIT', - data: formValues, }); } } diff --git a/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-up/amplify-sign-up.component.ts b/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-up/amplify-sign-up.component.ts index 0eec2008d76..abb8217bf4b 100644 --- a/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-up/amplify-sign-up.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/components/amplify-sign-up/amplify-sign-up.component.ts @@ -10,7 +10,12 @@ import { } from '@angular/core'; import { AuthPropService, StateMachineService } from '../../services'; import { Subscription } from 'xstate'; -import { AuthMachineState, getConfiguredAliases } from '@aws-amplify/ui-core'; +import { + AuthMachineState, + getActorState, + getConfiguredAliases, + SignUpState, +} from '@aws-amplify/ui-core'; const logger = new Logger('SignUp'); @Component({ @@ -57,10 +62,11 @@ export class AmplifySignUpComponent } private onStateUpdate(state: AuthMachineState): void { - this.remoteError = state.context.remoteError; - this.isPending = state.matches({ + const actorState: SignUpState = getActorState(state); + this.remoteError = actorState.context.remoteError; + this.isPending = !actorState.matches({ signUp: { - submission: 'valid', + submission: 'idle', }, }); } diff --git a/packages/angular/projects/ui-angular/src/lib/primitives/amplify-input/amplify-input.component.ts b/packages/angular/projects/ui-angular/src/lib/primitives/amplify-input/amplify-input.component.ts index a16c6129caa..d3a5c66d37f 100644 --- a/packages/angular/projects/ui-angular/src/lib/primitives/amplify-input/amplify-input.component.ts +++ b/packages/angular/projects/ui-angular/src/lib/primitives/amplify-input/amplify-input.component.ts @@ -1,5 +1,9 @@ import { Component, Input } from '@angular/core'; -import { AuthInputAttributes } from '@aws-amplify/ui-core'; +import { + ActorContextWithForms, + AuthInputAttributes, + getActorContext, +} from '@aws-amplify/ui-core'; import { getAttributeMap } from '../../common'; import { StateMachineService } from '../../services'; @@ -29,7 +33,10 @@ export class AmplifyInputComponent { } get error(): string { - const { validationError } = this.stateMachine.context; + const formContext: ActorContextWithForms = getActorContext( + this.stateMachine.authState + ); + const { validationError } = formContext; return validationError[this.name]; } diff --git a/packages/angular/projects/ui-angular/src/lib/services/state-machine.service.ts b/packages/angular/projects/ui-angular/src/lib/services/state-machine.service.ts index 8a9c7414949..6dbfcfc13ac 100644 --- a/packages/angular/projects/ui-angular/src/lib/services/state-machine.service.ts +++ b/packages/angular/projects/ui-angular/src/lib/services/state-machine.service.ts @@ -6,10 +6,8 @@ import { authMachine, AuthMachineState, } from '@aws-amplify/ui-core'; -import { Logger } from '@aws-amplify/core'; import { interpret, Event } from 'xstate'; -const logger = new Logger('state-machine-service'); /** * AmplifyContextService contains access to the xstate machine * and custom components passed by the user. @@ -55,9 +53,7 @@ export class StateMachineService { constructor() { this._authService = interpret(authMachine, { devTools: true }) .onTransition((state) => { - logger.log('transitioned to', state, this._authService); - const user = state.context?.user; - if (user) this._user = user; + this._user = state.context.user; this._authState = state; }) .start(); diff --git a/packages/core/src/authMachine.ts b/packages/core/src/authMachine.ts deleted file mode 100644 index 9dce75c371e..00000000000 --- a/packages/core/src/authMachine.ts +++ /dev/null @@ -1,723 +0,0 @@ -import { get } from 'lodash'; -import { Auth, Amplify } from 'aws-amplify'; -import { Machine, assign } from 'xstate'; -import { AuthChallengeNames, AuthContext, AuthEvent } from './types'; -import { passwordMatches, runValidators } from './validators'; - -export const authMachine = Machine( - { - id: 'auth', - initial: 'idle', - context: { - remoteError: '', - formValues: {}, - validationError: {}, - user: undefined, - session: undefined, - }, - states: { - // See: https://xstate.js.org/docs/guides/communication.html#invoking-promises - idle: { - invoke: [ - { - // TODO Wait for Auth to be configured - src: 'getCurrentUser', - onDone: { - actions: 'setUser', - target: 'authenticated', - }, - onError: 'signIn', - }, - { - src: 'getAmplifyConfig', - onDone: { - actions: 'setAuthConfig', - }, - }, - ], - }, - federatedSignIn: { - initial: 'federatedSignIn', - entry: 'clearError', - - states: { - federatedSignIn: { - invoke: { - src: 'federatedSignIn', - onDone: [ - { - actions: 'setUser', - target: 'confirmFederatedSignIn', - }, - ], - onError: [ - { - actions: 'setRemoteError', - target: 'rejected', - }, - ], - }, - }, - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - }, - confirmFederatedSignIn: { - entry: 'clearError', - invoke: { - src: 'confirmFederatedSignIn', - onDone: [ - { - actions: 'setUser', - target: 'resolved', - }, - ], - onError: [ - { - actions: 'setRemoteError', - target: 'rejected', - }, - ], - }, - }, - rejected: { - // TODO Set errors and go back ? - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - - authenticated: { - on: { - SIGN_OUT: 'signOut', - }, - }, - - signIn: { - initial: 'edit', - exit: ['clearError'], - onDone: 'authenticated', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - INPUT: { actions: 'handleInput' }, - SIGN_UP: '#auth.signUp', - FEDERATED_SIGN_IN: '#auth.federatedSignIn', - RESET_PASSWORD: '#auth.resetPassword', - }, - }, - submit: { - entry: 'clearError', - invoke: { - src: 'signIn', - onDone: [ - { - cond: 'shouldSetupTOTP', - actions: ['setUser', 'setChallengeName'], - target: '#auth.setupTOTP', - }, - { - cond: 'shouldConfirmSignIn', - actions: ['setUser', 'setChallengeName'], - target: '#auth.confirmSignIn', - }, - { - cond: 'shouldForceChangePassword', - actions: ['setUser', 'setChallengeName'], - target: '#auth.forceNewPassword', - }, - { - actions: 'setUser', - target: 'resolved', - }, - ], - onError: [ - { - cond: 'shouldRedirectToConfirmSignUp', - actions: ['setUser'], - target: '#auth.confirmSignUp', - }, - { - actions: 'setRemoteError', - target: 'rejected', - }, - ], - }, - }, - - resolved: { - exit: ['clearFormValues'], - type: 'final', - }, - rejected: { - // TODO Set errors and go back to `idle`? - always: 'edit.error', - }, - }, - }, - forceNewPassword: { - initial: 'edit', - exit: ['clearFormValues', 'clearError'], - onDone: 'idle', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - SIGN_IN: '#auth.signIn', - INPUT: { actions: 'handleInput' }, - }, - }, - submit: { - entry: 'clearError', - invoke: { - src: 'forceNewPassword', - onDone: { - actions: ['setUser', 'clearChallengeName'], - target: 'resolved', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - rejected: { - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - resetPassword: { - initial: 'edit', - exit: ['clearFormValues', 'clearError'], - onDone: 'confirmResetPassword', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - SIGN_IN: '#auth.signIn', - INPUT: { actions: 'handleInput' }, - }, - }, - submit: { - entry: 'clearError', - invoke: { - src: 'resetPassword', - onDone: { - target: 'resolved', - }, - onError: { - actions: ['setRemoteError', 'clearUsername'], - target: 'rejected', - }, - }, - }, - rejected: { - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - confirmResetPassword: { - initial: 'edit', - exit: ['clearFormValues', 'clearError', 'clearUsername'], - onDone: 'signIn', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - SIGN_IN: '#auth.signIn', - RESEND: 'resendCode', - INPUT: { actions: 'handleInput' }, - }, - }, - resendCode: { - entry: 'clearError', - invoke: { - src: 'resetPassword', - onDone: { - target: 'edit', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - submit: { - entry: 'clearError', - invoke: { - src: 'confirmResetPassword', - onDone: { - actions: ['clearUsername'], - target: 'resolved', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - rejected: { - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - confirmSignIn: { - initial: 'edit', - exit: ['clearFormValues, clearError'], - onDone: 'idle', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - SIGN_IN: '#auth.signIn', - INPUT: { actions: 'handleInput' }, - }, - }, - submit: { - entry: 'clearError', - invoke: { - src: 'confirmSignIn', - onDone: { - actions: ['setUser', 'clearChallengeName'], - target: 'resolved', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - rejected: { - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - setupTOTP: { - initial: 'edit', - exit: ['clearFormValues, clearError'], - onDone: 'idle', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - SIGN_IN: '#auth.signIn', - INPUT: { actions: 'handleInput' }, - }, - }, - submit: { - invoke: { - src: 'verifyTotpToken', - onDone: { - actions: ['setUser', 'clearChallengeName'], - target: 'resolved', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - rejected: { - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - signUp: { - type: 'parallel', - exit: ['clearError'], - states: { - validation: { - initial: 'pending', - states: { - pending: { - invoke: { - src: 'validateFields', - onDone: { - target: 'valid', - actions: 'clearValidationError', - }, - onError: { - target: 'invalid', - actions: 'setFieldErrors', - }, - }, - }, - valid: {}, - invalid: {}, - }, - on: { - CHANGE: { - actions: 'handleInput', - target: '.pending', - }, - }, - }, - submission: { - initial: 'idle', - onDone: '#auth.confirmSignUp', - states: { - idle: { - on: { - SUBMIT: 'validate', - }, - }, - validate: { - invoke: { - src: 'validateFields', - onDone: { - target: 'pending', - actions: 'clearValidationError', - }, - onError: { - target: 'idle', - actions: 'setFieldErrors', - }, - }, - }, - pending: { - invoke: { - src: 'signUp', - onDone: { - target: 'done', - actions: ['setUser'], - }, - onError: { - target: 'idle', - actions: 'setRemoteError', - }, - }, - }, - done: { type: 'final' }, - }, - }, - }, - on: { - SIGN_IN: '#auth.signIn', - FEDERATED_SIGN_IN: '#auth.federatedSignIn', - }, - }, - confirmSignUp: { - initial: 'edit', - exit: ['clearFormValues', 'clearError'], - onDone: 'authenticated', - states: { - edit: { - initial: 'clean', - states: { - clean: {}, - error: {}, - }, - on: { - SUBMIT: 'submit', - RESEND: 'resend', - SIGN_IN: '#auth.signIn', - INPUT: { actions: 'handleInput' }, - }, - }, - submit: { - invoke: { - src: 'confirmSignUp', - onDone: { - target: 'confirmedSignIn', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - confirmedSignIn: { - invoke: { - src: 'signIn', - onDone: { - target: 'resolved', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - resend: { - invoke: { - src: 'resendConfirmationCode', - onDone: { - target: 'edit', - }, - onError: { - actions: 'setRemoteError', - target: 'rejected', - }, - }, - }, - rejected: { - always: 'edit.error', - }, - resolved: { - type: 'final', - }, - }, - }, - signOut: { - initial: 'pending', - onDone: 'idle', - states: { - pending: { - invoke: { - src: 'signOut', - onDone: { - actions: 'setUser', - target: 'resolved', - }, - // See: https://xstate.js.org/docs/guides/communication.html#the-invoke-property - onError: 'rejected', - }, - }, - rejected: { - // TODO Why would signOut be rejected? - type: 'final', - }, - resolved: { - type: 'final', - }, - }, - }, - }, - }, - { - actions: { - setUser: assign({ - user(_, event) { - return event.data?.user || event.data; - }, - }), - clearUsername: assign({ username: undefined }), - setAuthConfig: assign({ - config(_, event) { - return event.data.auth; - }, - }), - setRemoteError: assign({ - remoteError(_, event) { - return event.data?.message || event.data; - }, - }), - clearFormValues: assign({ formValues: {} }), - clearValidationError: assign({ validationError: {} }), - clearError: assign({ remoteError: '' }), - handleInput: assign({ - formValues(context, event) { - const { name, value } = event.data; - return { ...context.formValues, [name]: value }; - }, - }), - setFieldErrors: assign({ - validationError(_, event) { - return event.data; - }, - }), - setChallengeName: assign({ - challengeName(_, event) { - return event.data?.challengeName; - }, - }), - clearChallengeName: assign({ challengeName: undefined }), - }, - // See: https://xstate.js.org/docs/guides/guards.html#guards-condition-functions - guards: { - shouldConfirmSignIn: (context, event): boolean => { - const challengeName = get(event, 'data.challengeName'); - const validChallengeNames = [ - AuthChallengeNames.SMS_MFA, - AuthChallengeNames.SOFTWARE_TOKEN_MFA, - ]; - - return validChallengeNames.includes(challengeName); - }, - shouldSetupTOTP: (context, event): boolean => { - const challengeName = get(event, 'data.challengeName'); - - return challengeName === AuthChallengeNames.MFA_SETUP; - }, - shouldRedirectToConfirmSignUp: (context, event): boolean => { - return event.data.code === 'UserNotConfirmedException'; - }, - shouldForceChangePassword: (context, event): boolean => { - const challengeName = get(event, 'data.challengeName'); - - return challengeName === AuthChallengeNames.NEW_PASSWORD_REQUIRED; - }, - }, - services: { - async validateFields(context, _event) { - const { formValues } = context; - const validators = [passwordMatches]; // this can contain custom validators too - return runValidators(formValues, validators); - }, - async getCurrentUser() { - return Auth.currentAuthenticatedUser(); - }, - async getAmplifyConfig() { - return Amplify.configure(); - }, - async signIn(_context, event) { - /** - * SignIn could be called from both the SignIn page and the ConfirmSignUp page. - * Depending on where it is called from, username and password might live in - * different places - either in the event payload, or the `user/formValues` in Xstate's - * context. - */ - const { - username = _context.user.username, - password = _context.formValues.password, - } = event.data; - - return Auth.signIn(username, password); - }, - async confirmSignIn(context, event) { - const { challengeName, user } = context; - const { confirmation_code: code } = event.data; - - let mfaType; - if ( - challengeName === AuthChallengeNames.SMS_MFA || - challengeName === AuthChallengeNames.SOFTWARE_TOKEN_MFA - ) { - mfaType = challengeName; - } - - return Auth.confirmSignIn(user, code, mfaType); - }, - async federatedSignIn(context, event) { - const { provider } = event.data; - const result = await Auth.federatedSignIn({ provider }); - - return result; - }, - async confirmFederatedSignIn(context, event) { - const result = await Auth.currentAuthenticatedUser(); - - return result; - }, - async verifyTotpToken(context, event) { - const { user } = context; - const { confirmation_code } = event.data; - - return Auth.verifyTotpToken(user, confirmation_code); - }, - async confirmSignUp(context, event) { - const { username, confirmation_code: code } = event.data; - - return Auth.confirmSignUp(username, code); - }, - async resendConfirmationCode(context, event) { - const { username } = event.data; - - return Auth.resendSignUp(username); - }, - async signUp(context, _event) { - const { - formValues: { password, ...formValues }, - config, - } = context; - - const [primaryAlias] = config?.login_mechanisms ?? ['username']; - - if (formValues.phone_number) { - formValues.phone_number = formValues.phone_number.replace( - /[^A-Z0-9+]/gi, - '' - ); - } - - const username = formValues[primaryAlias]; - delete formValues[primaryAlias]; - delete formValues.confirm_password; // confirm_password field should not be sent to Cognito - - const result = await Auth.signUp({ - username, - password, - attributes: formValues, - }); - - // TODO `cond`itionally transition to `signUp.confirm` or `resolved` based on result - return result; - }, - async signOut() { - await Auth.signOut(/* global? */); - }, - async forceNewPassword(context, event) { - const { user } = context; - const password = get(event, 'data.password'); - - const result = await Auth.completeNewPassword(user, password); - - return result; - }, - async resetPassword(context, event) { - const { username } = event.data; - context.username = username; - - return Auth.forgotPassword(username); - }, - async confirmResetPassword(context, event) { - const { username } = context; - const { confirmation_code: code, password } = event.data; - - return Auth.forgotPasswordSubmit(username, code, password); - }, - }, - } -); diff --git a/packages/core/src/authService.ts b/packages/core/src/authService.ts index 30909d98cbb..c7f5951aa88 100644 --- a/packages/core/src/authService.ts +++ b/packages/core/src/authService.ts @@ -1,5 +1,5 @@ import { interpret } from 'xstate'; -import { authMachine } from './authMachine'; +import { authMachine } from './machines'; // TODO: Share machines https://github.com/davidkpiano/xstate/discussions/1754 // NOTE! This may not be desirable on the server! diff --git a/packages/core/src/helpers/auth.ts b/packages/core/src/helpers/auth.ts index 49d50e6a60f..0016801ae05 100644 --- a/packages/core/src/helpers/auth.ts +++ b/packages/core/src/helpers/auth.ts @@ -1,6 +1,12 @@ import { includes } from 'lodash'; import { AuthContext } from '..'; -import { AuthInputAttributes, userNameAliasArray } from '../types'; +import { + AuthActorContext, + AuthActorState, + AuthInputAttributes, + AuthMachineState, + userNameAliasArray, +} from '../types'; export const authInputAttributes: AuthInputAttributes = { username: { @@ -42,7 +48,7 @@ export enum FederatedIdentityProviders { */ export const getAliasInfoFromContext = (context: AuthContext) => { const loginMechanisms = context.config?.login_mechanisms ?? ['username']; - const error = context.validationError['username']; + const error = context.actorRef?.context?.validationError['username']; let type = 'text'; const label = loginMechanisms @@ -76,3 +82,19 @@ export const getConfiguredAliases = (context: AuthContext) => { const [primaryAlias, ...secondaryAliases] = aliases; return { primaryAlias, secondaryAliases }; }; + +/** + * Get the state of current actor. This is useful for checking which screen + * to render: e.g. `getActorState(state).matches('confirmSignUp.edit'). + */ +export const getActorState = (state: AuthMachineState): AuthActorState => { + return state.context.actorRef?.getSnapshot(); +}; + +/** + * Get the context of current actor. Useful for getting any nested context + * like remoteError. + */ +export const getActorContext = (state: AuthMachineState): AuthActorContext => { + return getActorState(state)?.context; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0d971e5f646..5a17989bc8e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export * from './authMachine'; +export * from './machines'; export * from './authService'; export * from './types'; export * from './helpers'; diff --git a/packages/core/src/machines/actions/auth.ts b/packages/core/src/machines/actions/auth.ts new file mode 100644 index 00000000000..4373b2ab79d --- /dev/null +++ b/packages/core/src/machines/actions/auth.ts @@ -0,0 +1,7 @@ +import { stop } from 'xstate/lib/actions'; + +// TODO: Add more shared actions here + +export const stopActor = (machineId: string) => { + return stop(machineId); +}; diff --git a/packages/core/src/machines/actions/index.ts b/packages/core/src/machines/actions/index.ts new file mode 100644 index 00000000000..269586ee8b8 --- /dev/null +++ b/packages/core/src/machines/actions/index.ts @@ -0,0 +1 @@ +export * from './auth'; diff --git a/packages/core/src/machines/actors/auth/resetPassword.ts b/packages/core/src/machines/actors/auth/resetPassword.ts new file mode 100644 index 00000000000..8cb4fbb9553 --- /dev/null +++ b/packages/core/src/machines/actors/auth/resetPassword.ts @@ -0,0 +1,115 @@ +import { createMachine, assign, sendUpdate } from 'xstate'; + +import { AuthEvent, ResetPasswordContext } from '../../../types'; +import { Auth } from 'aws-amplify'; + +export const resetPasswordActor = createMachine< + ResetPasswordContext, + AuthEvent +>( + { + id: 'resetPasswordActor', + initial: 'resetPassword', + states: { + resetPassword: { + initial: 'edit', + exit: ['clearFormValues', 'clearError'], + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + entry: [sendUpdate(), 'setUsername', 'clearError'], + invoke: { + src: 'resetPassword', + onDone: { + target: '#resetPasswordActor.confirmResetPassword', + }, + onError: { + actions: ['setRemoteError'], + target: 'edit', + }, + }, + }, + }, + }, + confirmResetPassword: { + initial: 'edit', + exit: ['clearFormValues', 'clearError', 'clearUsername'], + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + RESEND: 'resendCode', + CHANGE: { actions: 'handleInput' }, + }, + }, + resendCode: { + entry: ['clearError', sendUpdate()], + invoke: { + src: 'resetPassword', + onDone: { target: 'edit' }, + onError: { + actions: 'setRemoteError', + target: 'edit', + }, + }, + }, + submit: { + entry: ['clearError', sendUpdate()], + invoke: { + src: 'confirmResetPassword', + onDone: { + actions: 'clearUsername', + target: '#resetPasswordActor.resolved', + }, + onError: { + actions: 'setRemoteError', + target: 'edit', + }, + }, + }, + }, + }, + resolved: { type: 'final' }, + }, + }, + { + actions: { + setRemoteError: assign({ + remoteError: (_, event) => event.data?.message || event.data, + }), + setUsername: assign({ + username: (context) => context.formValues.username, + }), + handleInput: assign({ + formValues: (context, event) => { + const { name, value } = event.data; + return { ...context.formValues, [name]: value }; + }, + }), + clearFormValues: assign({ formValues: {} }), + clearError: assign({ remoteError: '' }), + clearUsername: assign({ username: undefined }), + }, + services: { + async resetPassword(context) { + const username = context.formValues?.username ?? context.username; + context.username = username; + + return Auth.forgotPassword(username); + }, + async confirmResetPassword(context) { + const { username } = context; + const { confirmation_code: code, password } = context.formValues; + + return Auth.forgotPasswordSubmit(username, code, password); + }, + }, + } +); diff --git a/packages/core/src/machines/actors/auth/signIn.ts b/packages/core/src/machines/actors/auth/signIn.ts new file mode 100644 index 00000000000..d8767419ffe --- /dev/null +++ b/packages/core/src/machines/actors/auth/signIn.ts @@ -0,0 +1,275 @@ +import { createMachine, assign, sendUpdate } from 'xstate'; +import { get } from 'lodash'; + +import { AuthEvent, AuthChallengeNames, SignInContext } from '../../../types'; +import { Auth } from 'aws-amplify'; + +export const signInActor = createMachine( + { + initial: 'init', + id: 'signInActor', + states: { + init: { + always: [ + { target: 'signIn.submit', cond: 'shouldAutoSignIn' }, + { target: 'signIn' }, + ], + }, + signIn: { + initial: 'edit', + exit: 'clearFormValues', + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + CHANGE: { actions: 'handleInput' }, + FEDERATED_SIGN_IN: 'federatedSignIn', + }, + }, + federatedSignIn: { + entry: [sendUpdate(), 'clearError'], + invoke: { + src: 'federatedSignIn', + // getting navigated out anyway, only track errors. + // onDone: '#signInActor.resolved', + onError: { actions: 'setRemoteError' }, + }, + }, + submit: { + entry: ['clearError', sendUpdate()], + invoke: { + src: 'signIn', + onDone: [ + { + cond: 'shouldSetupTOTP', + actions: ['setUser', 'setChallengeName'], + target: '#signInActor.setupTOTP', + }, + { + cond: 'shouldConfirmSignIn', + actions: ['setUser', 'setChallengeName'], + target: '#signInActor.confirmSignIn', + }, + { + cond: 'shouldForceChangePassword', + actions: ['setUser', 'setChallengeName'], + target: '#signInActor.forceNewPassword', + }, + { + actions: 'setUser', + target: 'resolved', + }, + ], + onError: [ + { + cond: 'shouldRedirectToConfirmSignUp', + actions: 'setUsername', + target: 'rejected', + }, + { + actions: 'setRemoteError', + target: 'edit', + }, + ], + }, + }, + resolved: { always: '#signInActor.resolved' }, + rejected: { always: '#signInActor.rejected' }, + }, + }, + confirmSignIn: { + initial: 'edit', + exit: ['clearFormValues', 'clearError'], + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + SIGN_IN: '#signInActor.signIn', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + entry: ['clearError', sendUpdate()], + invoke: { + src: 'confirmSignIn', + onDone: { + target: '#signInActor.resolved', + actions: ['setUser', 'clearChallengeName'], + }, + onError: { + target: 'edit', + actions: 'setRemoteError', + }, + }, + }, + }, + }, + forceNewPassword: { + initial: 'edit', + exit: ['clearFormValues', 'clearError'], + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + SIGN_IN: '#signInActor.signIn', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + entry: 'clearError', + invoke: { + src: 'forceNewPassword', + onDone: { + actions: ['setUser', 'clearChallengeName'], + target: '#signInActor.resolved', + }, + onError: { + actions: 'setRemoteError', + target: 'edit', + }, + }, + }, + }, + }, + setupTOTP: { + initial: 'edit', + exit: ['clearFormValues', 'clearError'], + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + SIGN_IN: '#signInActor.signIn', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + entry: [sendUpdate(), 'clearError'], + invoke: { + src: 'verifyTotpToken', + onDone: { + actions: ['setUser', 'clearChallengeName'], + target: '#signInActor.resolved', + }, + onError: { + actions: 'setRemoteError', + target: 'edit', + }, + }, + }, + }, + }, + resolved: { + type: 'final', + data: (context) => ({ + user: context.user, + }), + }, + rejected: { + type: 'final', + data: (context) => ({ + intent: 'confirmSignUp', + authAttributes: context.authAttributes, + }), + }, + }, + }, + { + actions: { + handleInput: assign({ + formValues(context, event) { + const { name, value } = event.data; + return { ...context.formValues, [name]: value }; + }, + }), + setUser: assign({ + user: (_, event) => event.data.user || event.data, + }), + setUsername: assign({ + authAttributes: (context) => ({ + username: context.formValues.username, + }), + }), + setRemoteError: assign({ + remoteError: (_, event) => event.data?.message || event.data, + }), + setChallengeName: assign({ + challengeName: (_, event) => event.data?.challengeName, + }), + clearChallengeName: assign({ challengeName: undefined }), + clearError: assign({ remoteError: '' }), + clearFormValues: assign({ formValues: {} }), + }, + guards: { + shouldConfirmSignIn: (_, event): boolean => { + const challengeName = get(event, 'data.challengeName'); + const validChallengeNames = [ + AuthChallengeNames.SMS_MFA, + AuthChallengeNames.SOFTWARE_TOKEN_MFA, + ]; + + return validChallengeNames.includes(challengeName); + }, + shouldRedirectToConfirmSignUp: (_, event): boolean => { + return event.data.code === 'UserNotConfirmedException'; + }, + shouldSetupTOTP: (_, event): boolean => { + const challengeName = get(event, 'data.challengeName'); + + return challengeName === AuthChallengeNames.MFA_SETUP; + }, + shouldForceChangePassword: (_, event): boolean => { + const challengeName = get(event, 'data.challengeName'); + + return challengeName === AuthChallengeNames.NEW_PASSWORD_REQUIRED; + }, + shouldAutoSignIn: (context) => { + return !!(context.intent && context.intent === 'autoSignIn'); + }, + }, + services: { + async signIn(context) { + const source = !!(context.intent && context.intent === 'autoSignIn') + ? context.authAttributes + : context.formValues; + const { username, password } = source; + return Auth.signIn(username, password); + }, + async confirmSignIn(context, event) { + const { challengeName, user } = context; + const { confirmation_code: code } = event.data; + + let mfaType; + if ( + challengeName === AuthChallengeNames.SMS_MFA || + challengeName === AuthChallengeNames.SOFTWARE_TOKEN_MFA + ) { + mfaType = challengeName; + } + + return Auth.confirmSignIn(user, code, mfaType); + }, + async forceNewPassword(context, event) { + const { user } = context; + const password = get(event, 'data.password'); + + return Auth.completeNewPassword(user, password); + }, + async verifyTotpToken(context, event) { + const { user } = context; + const { confirmation_code } = event.data; + + return Auth.verifyTotpToken(user, confirmation_code); + }, + async federatedSignIn(_, event) { + const { provider } = event.data; + const result = await Auth.federatedSignIn({ provider }); + + return result; + }, + }, + } +); diff --git a/packages/core/src/machines/actors/auth/signOut.ts b/packages/core/src/machines/actors/auth/signOut.ts new file mode 100644 index 00000000000..893d6fb776c --- /dev/null +++ b/packages/core/src/machines/actors/auth/signOut.ts @@ -0,0 +1,27 @@ +import { createMachine } from 'xstate'; + +import { AuthEvent, SignOutContext } from '../../../types'; +import { Auth } from 'aws-amplify'; + +export const signOutActor = createMachine( + { + initial: 'pending', + id: 'signOutActor', + states: { + pending: { + invoke: { + src: 'signOut', + onDone: 'resolved', + onError: 'rejected', + }, + }, + resolved: { type: 'final' }, + rejected: { type: 'final' }, + }, + }, + { + services: { + signOut: () => Auth.signOut(/* global? */), + }, + } +); diff --git a/packages/core/src/machines/actors/auth/signUp.ts b/packages/core/src/machines/actors/auth/signUp.ts new file mode 100644 index 00000000000..b14b7794455 --- /dev/null +++ b/packages/core/src/machines/actors/auth/signUp.ts @@ -0,0 +1,228 @@ +import { createMachine, assign, sendUpdate } from 'xstate'; +import { passwordMatches, runValidators } from '../../../validators'; + +import { AuthEvent, SignUpContext } from '../../../types'; +import { Auth } from 'aws-amplify'; + +export const signUpActor = createMachine( + { + id: 'signUpActor', + initial: 'init', + states: { + init: { + always: [ + { target: 'confirmSignUp', cond: 'shouldInitConfirmSignUp' }, + { target: 'signUp' }, + ], + }, + signUp: { + type: 'parallel', + exit: ['clearError', 'clearFormValues'], + 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: { + CHANGE: { + actions: 'handleInput', + target: '.pending', + }, + }, + }, + submission: { + initial: 'idle', + states: { + idle: { + entry: sendUpdate(), + on: { + SUBMIT: 'validate', + FEDERATED_SIGN_IN: 'federatedSignIn', + }, + }, + federatedSignIn: { + entry: [sendUpdate(), 'clearError'], + invoke: { + src: 'federatedSignIn', + onDone: '#signUpActor.resolved', + onError: { actions: 'setRemoteError' }, + }, + }, + validate: { + entry: sendUpdate(), + invoke: { + src: 'validateFields', + onDone: { + target: 'pending', + actions: 'clearValidationError', + }, + onError: { + target: 'idle', + actions: 'setFieldErrors', + }, + }, + }, + pending: { + entry: [sendUpdate(), 'clearError'], + invoke: { + src: 'signUp', + onDone: { + target: 'resolved', + actions: ['setUser', 'setCredentials'], + }, + onError: { + target: 'idle', + actions: 'setRemoteError', + }, + }, + }, + resolved: { type: 'final', always: '#signUpActor.confirmSignUp' }, + }, + }, + }, + }, + confirmSignUp: { + initial: 'edit', + states: { + edit: { + entry: sendUpdate(), + on: { + SUBMIT: 'submit', + CHANGE: { actions: 'handleInput' }, + RESEND: 'resend', + }, + }, + resend: { + entry: sendUpdate(), + invoke: { + src: 'resendConfirmationCode', + onDone: { target: 'edit' }, + onError: { target: 'edit', actions: 'setRemoteError' }, + }, + }, + submit: { + entry: [sendUpdate(), 'clearError'], + invoke: { + src: 'confirmSignUp', + onDone: { target: '#signUpActor.resolved' }, + onError: { target: 'edit', actions: 'setRemoteError' }, + }, + }, + }, + }, + resolved: { + type: 'final', + data: (context) => { + const { username, password } = context.authAttributes; + const canAutoSignIn = !!(username && password); + return { + user: context.user, + intent: canAutoSignIn ? 'autoSignIn' : null, + authAttributes: { username, password }, + }; + }, + }, + }, + }, + { + guards: { + shouldInitConfirmSignUp: (context) => { + return context.intent && context.intent === 'confirmSignUp'; + }, + }, + actions: { + setUser: assign({ + user: (_, event) => event.data.user ?? event.data, + }), + setRemoteError: assign({ + remoteError: (_, event) => event.data?.message || event.data, + }), + setFieldErrors: assign({ + validationError: (_, event) => event.data, + }), + // stores username and password from signUp + setCredentials: assign({ + authAttributes: (context) => { + const [primaryAlias] = context.login_mechanisms ?? ['username']; + const username = context.formValues[primaryAlias]; + const password = context.formValues?.password; + return { username, password }; + }, + }), + handleInput: assign({ + formValues: (context, event) => { + const { name, value } = event.data; + return { ...context.formValues, [name]: value }; + }, + }), + clearError: assign({ remoteError: '' }), + clearFormValues: assign({ formValues: {} }), + clearValidationError: assign({ validationError: {} }), + }, + services: { + async confirmSignUp(_, event) { + const { username, confirmation_code: code } = event.data; + + return Auth.confirmSignUp(username, code); + }, + async resendConfirmationCode(context, event) { + const { username } = event.data; + + return Auth.resendSignUp(username); + }, + async federatedSignIn(_, event) { + const { provider } = event.data; + const result = await Auth.federatedSignIn({ provider }); + return result; + }, + async signUp(context, _event) { + const { + formValues: { password, ...formValues }, + login_mechanisms, + } = context; + + const [primaryAlias] = login_mechanisms ?? ['username']; + + if (formValues.phone_number) { + formValues.phone_number = formValues.phone_number.replace( + /[^A-Z0-9+]/gi, + '' + ); + } + + const username = formValues[primaryAlias]; + delete formValues[primaryAlias]; + delete formValues.confirm_password; // confirm_password field should not be sent to Cognito + + const result = await Auth.signUp({ + username, + password, + attributes: formValues, + }); + + // TODO `cond`itionally transition to `signUp.confirm` or `resolved` based on result + return result; + }, + async validateFields(context, _event) { + const { formValues } = context; + const validators = [passwordMatches]; // this can contain custom validators too + return runValidators(formValues, validators); + }, + }, + } +); diff --git a/packages/core/src/machines/actors/index.ts b/packages/core/src/machines/actors/index.ts new file mode 100644 index 00000000000..ab7c3b7b3cd --- /dev/null +++ b/packages/core/src/machines/actors/index.ts @@ -0,0 +1,4 @@ +export * from './auth/signIn'; +export * from './auth/signUp'; +export * from './auth/signOut'; +export * from './auth/resetPassword'; diff --git a/packages/core/src/machines/authMachine.ts b/packages/core/src/machines/authMachine.ts new file mode 100644 index 00000000000..10e591235c2 --- /dev/null +++ b/packages/core/src/machines/authMachine.ts @@ -0,0 +1,177 @@ +import { assign, createMachine, forwardTo, spawn } from 'xstate'; +import { Auth, Amplify } from 'aws-amplify'; +import { AuthContext, AuthEvent } from '../types'; +import { inspect } from '@xstate/inspect'; +import { + signInActor, + signUpActor, + signOutActor, + resetPasswordActor, +} from './actors'; +import { stopActor } from './actions'; + +// TODO: Remove this before it's merged. +if (typeof window !== 'undefined') { + inspect({ + // options + // url: 'https://statecharts.io/inspect', // (default) + iframe: false, // open in new window + }); +} + +export const authMachine = createMachine( + { + id: 'auth', + initial: 'idle', + context: { + user: undefined, + config: undefined, + actorRef: undefined, + }, + states: { + // See: https://xstate.js.org/docs/guides/communication.html#invoking-promises + idle: { + invoke: [ + { + // TODO Wait for Auth to be configured + src: 'getCurrentUser', + onDone: { + actions: 'setUser', + target: 'authenticated', + }, + onError: 'signIn', + }, + { + src: 'getAmplifyConfig', + onDone: { + actions: 'setAuthConfig', + }, + }, + ], + }, + signIn: { + entry: 'spawnSignInActor', + exit: stopActor('signInActor'), + on: { + SIGN_UP: 'signUp', + RESET_PASSWORD: 'resetPassword', + 'done.invoke.signInActor': [ + { + target: 'signUp', + cond: 'shouldRedirectToSignUp', + }, + { + target: 'authenticated', + actions: 'setUser', + }, + ], + }, + }, + signUp: { + entry: 'spawnSignUpActor', + exit: stopActor('signUpActor'), + on: { + SIGN_IN: 'signIn', + 'done.invoke.signUpActor': { + target: 'signIn', + actions: 'setUser', + }, + }, + }, + resetPassword: { + entry: 'spawnResetPasswordActor', + exit: stopActor('resetPasswordActor'), + on: { + SIGN_IN: 'signIn', + 'done.invoke.resetPasswordActor': 'signIn', + }, + }, + signOut: { + entry: 'spawnSignOutActor', + exit: [stopActor('signOutActor'), 'clearUser'], + on: { 'done.invoke.signOutActor': 'idle' }, + }, + authenticated: { + on: { SIGN_OUT: 'signOut' }, + }, + }, + on: { + CHANGE: { actions: 'forwardToActor' }, + SUBMIT: { actions: 'forwardToActor' }, + FEDERATED_SIGN_IN: { actions: 'forwardToActor' }, + RESEND: { actions: 'forwardToActor' }, + SIGN_OUT: { actions: 'forwardToActor' }, + SIGN_IN: { actions: 'forwardToActor' }, + }, + }, + { + actions: { + forwardToActor: forwardTo((context) => context.actorRef), + setUser: assign({ + user: (_, event) => event.data.user || event.data, + }), + clearUser: assign({ + user: undefined, + }), + setAuthConfig: assign({ + config(_, event) { + return event.data.auth; + }, + }), + spawnSignInActor: assign({ + actorRef: (_, event) => { + const actor = signInActor.withContext({ + authAttributes: event.data?.authAttributes, + user: event.data?.user, + intent: event.data?.intent, + formValues: {}, + validationError: {}, + }); + return spawn(actor, { name: 'signInActor' }); + }, + }), + spawnSignUpActor: assign({ + actorRef: (context, event) => { + const actor = signUpActor.withContext({ + authAttributes: event.data?.authAttributes ?? {}, + intent: event.data?.intent, + formValues: {}, + validationError: {}, + login_mechanisms: context.config?.login_mechanisms, + }); + return spawn(actor, { name: 'signUpActor' }); + }, + }), + spawnResetPasswordActor: assign({ + actorRef: (context, event) => { + const actor = resetPasswordActor.withContext({ + formValues: {}, + }); + return spawn(actor, { name: 'resetPasswordActor' }); + }, + }), + spawnSignOutActor: assign({ + actorRef: (context) => { + const actor = signOutActor.withContext({ + user: context.user, + }); + return spawn(actor, { name: 'signOutActor' }); + }, + }), + }, + guards: { + shouldRedirectToSignUp: (_, event): boolean => { + if (!event.data?.intent) return false; + return event.data.intent === 'confirmSignUp'; + }, + }, + services: { + async getCurrentUser() { + return Auth.currentAuthenticatedUser(); + }, + async getAmplifyConfig() { + return Amplify.configure(); + }, + }, + } +); diff --git a/packages/core/src/machines/index.ts b/packages/core/src/machines/index.ts new file mode 100644 index 00000000000..c6075c8425f --- /dev/null +++ b/packages/core/src/machines/index.ts @@ -0,0 +1 @@ +export * from './authMachine'; diff --git a/packages/core/src/types/authMachine.ts b/packages/core/src/types/authMachine.ts index da3f2bb74c5..bd46faf06e7 100644 --- a/packages/core/src/types/authMachine.ts +++ b/packages/core/src/types/authMachine.ts @@ -1,24 +1,68 @@ -import { CognitoUser, CognitoUserSession } from 'amazon-cognito-identity-js'; +import { CognitoUser } from 'amazon-cognito-identity-js'; import { Interpreter, State } from 'xstate'; import { ValidationError } from './validator'; export type AuthFormData = Record; + export interface AuthContext { - remoteError?: string; // contains Amplify or Cognito error - validationError?: ValidationError; // contains validation error for each input user?: CognitoUserAmplify; - username?: string; - session?: CognitoUserSession; - formValues?: AuthFormData; config?: { login_mechanisms: string[]; }; - challengeName?: AuthChallengeNames; + actorRef?: any; +} + +export interface SignInContext { + remoteError?: string; + validationError?: ValidationError; + formValues?: AuthFormData; + user?: CognitoUserAmplify; + challengeName?: string; + authAttributes?: Record; + intent?: string; +} + +export interface SignUpContext { + remoteError?: string; + validationError?: ValidationError; + formValues?: AuthFormData; + user?: CognitoUserAmplify; + login_mechanisms?: string[]; + intent?: string; + authAttributes?: Record; } -interface CognitoUserAmplify extends CognitoUser { +export interface ResetPasswordContext { + validationError?: ValidationError; + remoteError?: string; + formValues?: ValidationError; username?: string; } +export interface SignOutContext { + user?: CognitoUserAmplify; +} + +// actors that have forms. Has `formValues, remoteErrror, and validationError in common. +export type ActorContextWithForms = + | SignInContext + | SignUpContext + | ResetPasswordContext; + +export type SignInState = State; +export type SignUpState = State; +export type SignOutState = State; +export type ResetPasswordState = State; +export type AuthActorContext = ActorContextWithForms | SignOutContext; +export type AuthActorState = State; +export interface CognitoUserAmplify extends CognitoUser { + username?: string; +} + +export type InvokeActorEventTypes = + | 'done.invoke.signInActor' + | 'done.invoke.signUpActor' + | 'done.invoke.signOutActor' + | 'done.invoke.resetPasswordActor'; export type AuthEventTypes = | 'SIGN_IN' @@ -26,12 +70,10 @@ export type AuthEventTypes = | 'SIGN_OUT' | 'SUBMIT' | 'RESEND' - | 'CONFIRM_SIGN_UP' - | 'CONFIRM_SIGN_IN' - | 'INPUT' | 'CHANGE' | 'FEDERATED_SIGN_IN' - | 'RESET_PASSWORD'; + | 'RESET_PASSWORD' + | InvokeActorEventTypes; export enum AuthChallengeNames { SMS_MFA = 'SMS_MFA', @@ -63,7 +105,7 @@ export type AuthInputAttributes = Record; export interface AuthEvent { type: AuthEventTypes; - data?: any; // TODO: strongly type data for each AuthEventType + data?: Record; } export type AuthMachineState = State; diff --git a/packages/react/src/components/Authenticator/ConfirmSignIn/ConfirmSignIn.tsx b/packages/react/src/components/Authenticator/ConfirmSignIn/ConfirmSignIn.tsx index 83c6afb60ae..a4496b22d9f 100644 --- a/packages/react/src/components/Authenticator/ConfirmSignIn/ConfirmSignIn.tsx +++ b/packages/react/src/components/Authenticator/ConfirmSignIn/ConfirmSignIn.tsx @@ -1,4 +1,9 @@ -import { AuthChallengeNames } from '@aws-amplify/ui-core'; +import { + AuthChallengeNames, + getActorState, + SignInContext, + SignInState, +} from '@aws-amplify/ui-core'; import { useAmplify, useAuth } from '../../../hooks'; import { @@ -16,8 +21,9 @@ export const ConfirmSignIn = (): JSX.Element => { components: { Fieldset, Form, Heading, Label }, } = useAmplify(amplifyNamespace); - const [state, send] = useAuth(); - const isPending = state.matches('confirmSignIn.pending'); + const [_state, send] = useAuth(); + const actorState: SignInState = getActorState(_state); + const isPending = actorState.matches('confirmSignIn.pending'); const footerProps: ConfirmSignInFooterProps = { amplifyNamespace, @@ -25,7 +31,7 @@ export const ConfirmSignIn = (): JSX.Element => { send, }; - const { challengeName, remoteError } = state.context; + const { challengeName, remoteError } = actorState.context as SignInContext; let mfaType: string = 'SMS'; if (challengeName === AuthChallengeNames.SOFTWARE_TOKEN_MFA) { mfaType = 'TOTP'; diff --git a/packages/react/src/components/Authenticator/ConfirmSignUp/ConfirmSignUp.tsx b/packages/react/src/components/Authenticator/ConfirmSignUp/ConfirmSignUp.tsx index 0fb09ec5a6c..a7b26bf8999 100644 --- a/packages/react/src/components/Authenticator/ConfirmSignUp/ConfirmSignUp.tsx +++ b/packages/react/src/components/Authenticator/ConfirmSignUp/ConfirmSignUp.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; +import { getActorState, SignUpState } from '@aws-amplify/ui-core'; import { useAmplify, useAuth } from '../../../hooks'; import { @@ -17,8 +18,9 @@ export function ConfirmSignUp() { components: { Box, Button, Fieldset, Form, Heading, Label, Text }, } = useAmplify(amplifyNamespace); - const [state, send] = useAuth(); - const isPending = state.matches('confirmSignUp.pending'); + const [_state, send] = useAuth(); + const actorState: SignUpState = getActorState(_state); + const isPending = actorState.matches('confirmSignUp.pending'); const footerProps: ConfirmSignInFooterProps = { amplifyNamespace, diff --git a/packages/react/src/components/Authenticator/ForceNewPassword/ForceNewPassword.tsx b/packages/react/src/components/Authenticator/ForceNewPassword/ForceNewPassword.tsx index c6894a80c25..17a64759c06 100644 --- a/packages/react/src/components/Authenticator/ForceNewPassword/ForceNewPassword.tsx +++ b/packages/react/src/components/Authenticator/ForceNewPassword/ForceNewPassword.tsx @@ -1,3 +1,4 @@ +import { getActorState, SignInState } from '@aws-amplify/ui-core'; import { useAmplify, useAuth } from '../../../hooks'; export const ForceNewPassword = (): JSX.Element => { @@ -16,9 +17,10 @@ export const ForceNewPassword = (): JSX.Element => { }, } = useAmplify(amplifyNamespace); - const [state, send] = useAuth(); - const { remoteError } = state.context; - const isPending = state.matches('forceNewPassword.pending'); + const [_state, send] = useAuth(); + const actorState: SignInState = getActorState(_state); + const { remoteError } = actorState.context; + const isPending = actorState.matches('forceNewPassword.pending'); const headerText = 'Change Password'; diff --git a/packages/react/src/components/Authenticator/ResetPassword/ConfirmResetPassword.tsx b/packages/react/src/components/Authenticator/ResetPassword/ConfirmResetPassword.tsx index 0e986d4b0a6..7fa19dd6bb2 100644 --- a/packages/react/src/components/Authenticator/ResetPassword/ConfirmResetPassword.tsx +++ b/packages/react/src/components/Authenticator/ResetPassword/ConfirmResetPassword.tsx @@ -1,3 +1,4 @@ +import { getActorState, ResetPasswordState } from '@aws-amplify/ui-core'; import { useAmplify, useAuth } from '../../../hooks'; import { ConfirmationCodeInput, @@ -12,11 +13,20 @@ export const ConfirmResetPassword = (): JSX.Element => { components: { Box, Button, Fieldset, Form, Heading, Input, Label, Text }, } = useAmplify(amplifyNamespace); - const [state, send] = useAuth(); - const isPending = state.matches('confirmResetPassword.pending'); + const [_state, send] = useAuth(); + const actorState = getActorState(_state) as ResetPasswordState; + const isPending = actorState.matches('confirmResetPassword.pending'); const headerText = 'Reset your Password'; + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + send({ + type: 'CHANGE', + data: { name, value }, + }); + }; + return ( { data: Object.fromEntries(formData), }); }} + onChange={handleChange} > {headerText} @@ -53,9 +64,6 @@ export const ConfirmResetPassword = (): JSX.Element => { onClick={() => { send({ type: 'RESEND', - data: { - username: state.context.username, - }, }); }} type="button" diff --git a/packages/react/src/components/Authenticator/ResetPassword/ResetPassword.tsx b/packages/react/src/components/Authenticator/ResetPassword/ResetPassword.tsx index 1532d782f0d..30cd16c4b8f 100644 --- a/packages/react/src/components/Authenticator/ResetPassword/ResetPassword.tsx +++ b/packages/react/src/components/Authenticator/ResetPassword/ResetPassword.tsx @@ -1,3 +1,4 @@ +import { getActorState, ResetPasswordState } from '@aws-amplify/ui-core'; import { useAmplify, useAuth } from '../../../hooks'; import { ErrorText, SignInOrSubmitFooter } from '../shared'; @@ -8,15 +9,25 @@ export const ResetPassword = (): JSX.Element => { } = useAmplify(amplifyNamespace); const [state, send] = useAuth(); - const isPending = state.matches('resetPassword.pending'); + const actorState = getActorState(state) as ResetPasswordState; + const isPending = actorState.matches('resetPassword.submit'); const headerText = 'Reset your Password'; const submitText = isPending ? <>Sending… : <>Send code; + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + send({ + type: 'CHANGE', + data: { name, value }, + }); + }; + return ( { event.preventDefault(); diff --git a/packages/react/src/components/Authenticator/SetupTOTP/SetupTOTP.tsx b/packages/react/src/components/Authenticator/SetupTOTP/SetupTOTP.tsx index b809e336917..e90a23f5851 100644 --- a/packages/react/src/components/Authenticator/SetupTOTP/SetupTOTP.tsx +++ b/packages/react/src/components/Authenticator/SetupTOTP/SetupTOTP.tsx @@ -9,6 +9,7 @@ import { ConfirmSignInFooter, ConfirmSignInFooterProps, } from '../shared'; +import { getActorState, SignInState } from '@aws-amplify/ui-core'; const logger = new Logger('SetupTOTP-logger'); @@ -21,8 +22,9 @@ export const SetupTOTP = (): JSX.Element => { components: { Fieldset, Form, Heading, Image, Label }, } = useAmplify(amplifyNamespace); - const [state, send] = useAuth(); - const isPending = state.matches('confirmSignIn.pending'); + const [_state, send] = useAuth(); + const actorState = getActorState(_state) as SignInState; + const isPending = actorState.matches('confirmSignIn.pending'); const generateQRCode = async (user): Promise => { try { @@ -40,7 +42,7 @@ export const SetupTOTP = (): JSX.Element => { }; useEffect(() => { - const { user } = state.context; + const { user } = actorState.context; if (!user) { return; } diff --git a/packages/react/src/components/Authenticator/SignIn/SignIn.tsx b/packages/react/src/components/Authenticator/SignIn/SignIn.tsx index d902a497e54..52464a35293 100644 --- a/packages/react/src/components/Authenticator/SignIn/SignIn.tsx +++ b/packages/react/src/components/Authenticator/SignIn/SignIn.tsx @@ -1,9 +1,11 @@ +import { getActorState, SignInState } from '@aws-amplify/ui-core'; import { useAmplify, useAuth } from '../../../hooks'; import { FederatedSignIn } from '../FederatedSignIn'; -import { UserNameAlias } from '../shared'; +import { ErrorText, UserNameAlias } from '../shared'; export function SignIn() { + const amplifyNamespace = 'Authenticator.SignIn'; const { components: { Box, @@ -17,10 +19,19 @@ export function SignIn() { Spacer, Text, }, - } = useAmplify('Authenticator.SignIn'); + } = useAmplify(amplifyNamespace); - const [state, send] = useAuth(); - const isPending = state.matches('signIn.pending'); + const [_state, send] = useAuth(); + const actorState: SignInState = getActorState(_state); + const isPending = actorState.matches('signIn.pending'); + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + send({ + type: 'CHANGE', + data: { name, value }, + }); + }; return ( // TODO Automatically add these namespaces again from `useAmplify` @@ -38,6 +49,7 @@ export function SignIn() { data: Object.fromEntries(formData), }); }} + onChange={handleChange} > Sign in to your account @@ -71,7 +83,7 @@ export function SignIn() { {isPending ? <>Signing in… : <>Sign In} - {state.event.data?.message} + ); } diff --git a/packages/react/src/components/Authenticator/SignUp/SignUp.tsx b/packages/react/src/components/Authenticator/SignUp/SignUp.tsx index cf2ee838010..91e28f3a426 100644 --- a/packages/react/src/components/Authenticator/SignUp/SignUp.tsx +++ b/packages/react/src/components/Authenticator/SignUp/SignUp.tsx @@ -4,6 +4,10 @@ import { useAmplify, useAuth } from '../../../hooks'; import { authInputAttributes, + getActorContext, + getActorState, + SignUpContext, + SignUpState, socialProviderLoginMechanisms, } from '@aws-amplify/ui-core'; import { FederatedSignIn } from '../FederatedSignIn'; @@ -22,11 +26,12 @@ export function SignUp() { }, } = useAmplify('Authenticator.SignUp'); - const [state, send] = useAuth(); - const isPending = state.matches('signUp.pending'); - const { remoteError } = state.context; + const [_state, send] = useAuth(); + const actorState: SignUpState = getActorState(_state); + const isPending = actorState.matches('signUp.pending'); + const { remoteError } = actorState.context; - const [primaryAlias, ...secondaryAliases] = state.context.config + const [primaryAlias, ...secondaryAliases] = _state.context.config ?.login_mechanisms ?? ['username', 'email', 'phone_number']; const handleChange = (event: React.ChangeEvent) => { @@ -100,8 +105,9 @@ SignUp.AliasControl = ({ const { components: { Input, Label, Text, ErrorText }, } = useAmplify('Authenticator.SignUp.Password'); - const [{ context }] = useAuth(); - const error = context.validationError[name]; + const [_state] = useAuth(); + const { validationError } = getActorContext(_state) as SignUpContext; + const error = validationError[name]; return ( <> @@ -127,8 +133,9 @@ SignUp.PasswordControl = ({ const { components: { Input, Label, Text, ErrorText }, } = useAmplify('Authenticator.SignUp.Password'); - const [{ context }] = useAuth(); - const error = context.validationError[name]; + const [_state] = useAuth(); + const { validationError } = getActorContext(_state) as SignUpContext; + const error = validationError[name]; return ( <> @@ -148,8 +155,9 @@ SignUp.ConfirmPasswordControl = ({ const { components: { Input, Label, Text, ErrorText }, } = useAmplify('Authenticator.SignUp.Password'); - const [{ context }] = useAuth(); - const error = context.validationError[name]; + const [state] = useAuth(); + const { validationError } = getActorContext(state) as SignUpContext; + const error = validationError[name]; return ( <> diff --git a/packages/react/src/components/Authenticator/index.tsx b/packages/react/src/components/Authenticator/index.tsx index 3e19816f379..8281faa4c07 100644 --- a/packages/react/src/components/Authenticator/index.tsx +++ b/packages/react/src/components/Authenticator/index.tsx @@ -1,4 +1,4 @@ -import { authMachine } from '@aws-amplify/ui-core'; +import { authMachine, getActorState } from '@aws-amplify/ui-core'; import { useAmplify } from '../../hooks'; import { useActor, useInterpret } from '@xstate/react'; @@ -36,6 +36,7 @@ export function Authenticator({ if (state.matches('authenticated')) { return children({ state, send }); } + const actorState = getActorState(state); return ( @@ -44,24 +45,24 @@ export function Authenticator({ switch (true) { case state.matches('idle'): return null; - case state.matches('confirmSignUp'): + case actorState?.matches('confirmSignUp'): return ; - case state.matches('confirmSignIn'): + case actorState?.matches('confirmSignIn'): return ; - case state.matches('setupTOTP'): + case actorState?.matches('setupTOTP'): return ; - case state.matches('signIn'): + case actorState?.matches('signIn'): return ; - case state.matches('signUp'): + case actorState?.matches('signUp'): return ; - case state.matches('forceNewPassword'): + case actorState?.matches('forceNewPassword'): return ; - case state.matches('resetPassword'): + case actorState?.matches('resetPassword'): return ; - case state.matches('confirmResetPassword'): + case actorState?.matches('confirmResetPassword'): return ; default: - console.warn('Unhandled Auth state', state); + console.warn('Unhandled Auth state', state.value); return null; } })()} diff --git a/packages/react/src/components/Authenticator/shared/ErrorText.tsx b/packages/react/src/components/Authenticator/shared/ErrorText.tsx index 1d6e234f12e..b923f138fb2 100644 --- a/packages/react/src/components/Authenticator/shared/ErrorText.tsx +++ b/packages/react/src/components/Authenticator/shared/ErrorText.tsx @@ -1,3 +1,5 @@ +import { getActorContext } from '@aws-amplify/ui-core'; +import { ActorContextWithForms } from '@aws-amplify/ui-core/src/types/authMachine'; import { useAmplify, useAuth } from '../../../hooks'; export interface ErrorTextProps { @@ -10,8 +12,9 @@ export const ErrorText = (props: ErrorTextProps): JSX.Element => { components: { Text }, } = useAmplify(amplifyNamespace); - const [state] = useAuth(); - const { remoteError } = state.context; + const [_state] = useAuth(); + const actorContext: ActorContextWithForms = getActorContext(_state); + const { remoteError } = actorContext; return ( diff --git a/packages/vue/src/components/authenticator.vue b/packages/vue/src/components/authenticator.vue index 25599bfa0b6..47c892095e7 100644 --- a/packages/vue/src/components/authenticator.vue +++ b/packages/vue/src/components/authenticator.vue @@ -1,7 +1,7 @@ @@ -100,9 +100,11 @@ -
Error! Can't sign in!
+
+ Error! Can't sign in! +
@@ -145,7 +147,7 @@ @@ -164,7 +166,7 @@ @@ -192,7 +194,8 @@