Skip to content

Commit 733fd63

Browse files
vishwaktbobbor
andauthored
fix(authenticator): apply UI config when signing out before init (#7083)
* fix(authenticator): apply UI config when signing out before init The machine parks in `setup.initConfig` until the UI sends `INIT`, which carries the `config` and `services` passed to the `Authenticator`. `initConfig` also handles `SIGN_OUT`, and the sign out actor completing targeted `setup.getConfig` unconditionally, skipping `initConfig` and the `configure` action. The machine then settled on `signIn`, so `useAuthenticatorInitMachine` never sent `INIT` and every prop passed to the `Authenticator` was dropped. Return to `setup` when setup has not completed, so the machine waits for `INIT` again. `hasSetup` is only set after `getConfig` resolves, so the normal sign out path is unchanged. Fixes #6967 * fix(authenticator): accept INIT after setup instead of rerouting sign out Returning to `setup.initConfig` after signing out before init broke headless usage. `examples/svelte` .../useAuthenticator/home navigates off `route === 'signIn'`, which the machine no longer reached, so the e2e `Headless Usage` scenario timed out waiting for the sign in form. Leave the sign out transition alone and fix the initialization path instead. `configure` now records that the UI has configured the machine, and `INIT` is accepted at the root until it has, so an `Authenticator` rendered after the machine has already moved past setup can still apply its `config` and `services`. `useAuthenticatorInitMachine` sends `INIT` on any route other than `idle`, which has to resolve `handleGetCurrentUser` first. --------- Co-authored-by: Philipp Andreas Paul <phandpau@amazon.de>
1 parent 146ba6d commit 733fd63

6 files changed

Lines changed: 165 additions & 27 deletions

File tree

.changeset/soft-pugs-invite.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@aws-amplify/ui-react-core': patch
3+
'@aws-amplify/ui': patch
4+
---
5+
6+
fix(authenticator): apply UI provided `config` and `services` when signing out before the Authenticator is rendered
7+
8+
`setup.initConfig` handles `SIGN_OUT`, so signing out before the UI sends `INIT` moved the machine past setup configured with defaults, and it settled on `signIn`. `useAuthenticatorInitMachine` only sent `INIT` on the `setup` route, so every prop passed to the `Authenticator` was ignored. The machine now accepts `INIT` until the UI has configured it, and the UI sends `INIT` on any route other than `idle`.

packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/__tests__/useAuthenticatorInitMachine.spec.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('useAuthenticatorInitMachine', () => {
3838
expect(initializeMachine).toHaveBeenCalledTimes(1);
3939
});
4040

41-
it('does not call initializeMachine when the route !== "setup"', () => {
41+
it('does not call initializeMachine when the route === "idle"', () => {
4242
const route = 'idle';
4343
const data = {};
4444

@@ -51,6 +51,22 @@ describe('useAuthenticatorInitMachine', () => {
5151

5252
expect(initializeMachine).toHaveBeenCalledTimes(0);
5353
});
54+
55+
// the machine moves past `setup` on its own when signed out before the
56+
// `Authenticator` is rendered, it accepts `INIT` until it has been configured
57+
it('calls initializeMachine when the route is past "setup"', () => {
58+
const route = 'signIn';
59+
const data = {};
60+
61+
(useAuthenticator as jest.Mock).mockReturnValue({
62+
initializeMachine,
63+
route,
64+
} as unknown as UseAuthenticator);
65+
66+
renderHook(() => useAuthenticatorInitMachine(data));
67+
68+
expect(initializeMachine).toHaveBeenCalledTimes(1);
69+
});
5470
});
5571

5672
describe('routeSelector', () => {

packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/useAuthenticatorInitMachine.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ export default function useAuthenticatorInitMachine(
1414

1515
const hasInitialized = React.useRef(false);
1616
React.useEffect(() => {
17-
if (!hasInitialized.current && route === 'setup') {
17+
// `setup` is the usual route on first render, but signing out before the
18+
// `Authenticator` is rendered moves the machine past setup on its own. Send
19+
// `INIT` on any route other than `idle`, which resolves the current user
20+
// before the machine can accept it.
21+
if (!hasInitialized.current && route !== 'idle') {
1822
initializeMachine(data);
1923

2024
hasInitialized.current = true;

packages/ui/src/machines/authenticator/__tests__/index.test.ts

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ const flushPromises = () => new Promise(setImmediate);
1111

1212
let service;
1313

14+
// `configure` replaces `services` with the values sent on `INIT`, so overrides
15+
// have to be provided on the event as well as the initial context
16+
const mockServices = {
17+
getCurrentUser: () => Promise.reject(),
18+
getAmplifyConfig: () =>
19+
Promise.resolve({}) as ReturnType<
20+
(typeof defaultServices)['getAmplifyConfig']
21+
>,
22+
};
23+
1424
describe('authenticator', () => {
1525
afterEach(() => {
1626
jest.clearAllMocks();
@@ -486,51 +496,136 @@ describe('authenticator', () => {
486496
});
487497
});
488498

489-
// @todo-migration
490-
// - Expected - 1
491-
// + Received + 1
492-
493-
// Object {
494-
// - "signOut": "runActor",
495-
// + "setup": "initConfig",
496-
// }
497-
it.skip('should spawn the signOutActor', async () => {
499+
it('should spawn the signOutActor and return to getConfig after setup', async () => {
498500
service = interpret(
499-
createAuthenticatorMachine().withConfig({
500-
actions: {
501-
setUser: jest.fn(() => Promise.resolve),
502-
configure: jest.fn(() => Promise.resolve),
503-
setHasSetup: jest.fn(() => Promise.resolve),
504-
},
505-
services: {
506-
getCurrentUser: jest.fn(async () => Promise.resolve),
507-
},
508-
guards: {},
509-
})
501+
createAuthenticatorMachine()
502+
.withContext({
503+
config: {},
504+
services: {
505+
getAmplifyConfig: () =>
506+
Promise.resolve({}) as ReturnType<
507+
(typeof defaultServices)['getAmplifyConfig']
508+
>,
509+
},
510+
})
511+
.withConfig({
512+
actions: {
513+
clearUser: jest.fn(() => Promise.resolve),
514+
clearActorDoneData: jest.fn(() => Promise.resolve),
515+
applyAmplifyConfig: jest.fn(() => Promise.resolve),
516+
setUser: jest.fn(() => Promise.resolve),
517+
spawnSignOutActor: jest.fn(() => Promise.resolve),
518+
stopSignOutActor: jest.fn(() => Promise.resolve),
519+
configure: jest.fn(() => Promise.resolve),
520+
},
521+
services: {
522+
handleGetCurrentUser: jest.fn(async () => Promise.resolve),
523+
},
524+
guards: { hasUser: () => true },
525+
})
510526
);
511527

512528
service.start();
513529

514530
expect(service.getSnapshot().value).toStrictEqual('idle');
515531

532+
await flushPromises();
533+
expect(service.getSnapshot().value).toStrictEqual({ setup: 'initConfig' });
534+
535+
service.send({ type: 'INIT' });
516536
await flushPromises();
517537
expect(service.getSnapshot().value).toStrictEqual({
518538
authenticated: 'idle',
519539
});
520540

541+
service.send({ type: 'SIGN_OUT' });
542+
await flushPromises();
543+
expect(service.getSnapshot().value).toStrictEqual({ signOut: 'runActor' });
544+
545+
// `setHasSetup` is not mocked, so `INIT` has already been handled and there
546+
// is no need to wait for the UI to send it a second time
547+
service.send({ type: 'done.invoke.signOutActor' });
548+
expect(service.getSnapshot().value).toStrictEqual({ setup: 'getConfig' });
549+
});
550+
551+
it('should accept INIT after signing out before the UI has initialized the machine', async () => {
552+
service = interpret(
553+
createAuthenticatorMachine()
554+
.withContext({ config: {}, services: mockServices })
555+
.withConfig({
556+
actions: {
557+
clearUser: jest.fn(() => Promise.resolve),
558+
clearActorDoneData: jest.fn(() => Promise.resolve),
559+
setUser: jest.fn(() => Promise.resolve),
560+
spawnSignUpActor: jest.fn(() => Promise.resolve),
561+
spawnSignInActor: jest.fn(() => Promise.resolve),
562+
spawnSignOutActor: jest.fn(() => Promise.resolve),
563+
stopSignOutActor: jest.fn(() => Promise.resolve),
564+
},
565+
})
566+
);
567+
568+
service.start();
569+
570+
await flushPromises();
571+
expect(service.getSnapshot().value).toStrictEqual({ setup: 'initConfig' });
572+
573+
// external `signOut` calls are forwarded to the machine as `SIGN_OUT` by the
574+
// Hub listener, which can happen before the UI has sent `INIT`
575+
service.send({ type: 'SIGN_OUT' });
576+
await flushPromises();
577+
expect(service.getSnapshot().value).toStrictEqual({ signOut: 'runActor' });
578+
579+
// headless usage relies on reaching `signIn` without the UI sending `INIT`
580+
service.send({ type: 'done.invoke.signOutActor' });
581+
await flushPromises();
582+
expect(service.getSnapshot().value).toStrictEqual({
583+
signInActor: 'runActor',
584+
});
585+
expect(service.getSnapshot().context.hasInitialized).toBeUndefined();
586+
587+
// a UI rendered after the sign out can still apply its `config`
521588
service.send({
522-
type: 'SIGN_OUT',
589+
type: 'INIT',
590+
data: { initialState: 'signUp', services: mockServices },
523591
});
524592
await flushPromises();
525593
expect(service.getSnapshot().value).toStrictEqual({
526-
signOut: 'runActor',
594+
signUpActor: 'runActor',
527595
});
596+
expect(service.getSnapshot().context.config.initialState).toBe('signUp');
597+
});
528598

529-
service.send({
530-
type: 'done.invoke.signOutActor',
599+
it('should ignore INIT once the UI has initialized the machine', async () => {
600+
service = interpret(
601+
createAuthenticatorMachine()
602+
.withContext({ config: {}, services: mockServices })
603+
.withConfig({
604+
actions: {
605+
clearActorDoneData: jest.fn(() => Promise.resolve),
606+
setUser: jest.fn(() => Promise.resolve),
607+
spawnSignUpActor: jest.fn(() => Promise.resolve),
608+
spawnSignInActor: jest.fn(() => Promise.resolve),
609+
},
610+
})
611+
);
612+
613+
service.start();
614+
615+
await flushPromises();
616+
service.send({ type: 'INIT', data: { services: mockServices } });
617+
await flushPromises();
618+
expect(service.getSnapshot().value).toStrictEqual({
619+
signInActor: 'runActor',
531620
});
532621

622+
service.send({
623+
type: 'INIT',
624+
data: { initialState: 'signUp', services: mockServices },
625+
});
533626
await flushPromises();
534-
expect(service.getSnapshot().value).toStrictEqual({ setup: 'initConfig' });
627+
expect(service.getSnapshot().value).toStrictEqual({
628+
signInActor: 'runActor',
629+
});
535630
});
536631
});

packages/ui/src/machines/authenticator/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,14 @@ export function createAuthenticatorMachine(
309309
},
310310
},
311311
on: {
312+
// `SIGN_OUT` handled by `setup.initConfig` moves the machine past setup without
313+
// the UI ever sending `INIT`, leaving it configured with defaults. Accept `INIT`
314+
// late so a subsequently rendered UI can still apply its `config` and `services`.
315+
INIT: {
316+
cond: 'shouldInitialize',
317+
actions: 'configure',
318+
target: '#authenticator.setup.getConfig',
319+
},
312320
SIGN_IN_WITH_REDIRECT: { target: '#authenticator.getCurrentUser' },
313321
CHANGE: { actions: 'forwardToActor' },
314322
BLUR: { actions: 'forwardToActor' },
@@ -424,6 +432,7 @@ export function createAuthenticatorMachine(
424432
return {
425433
services: { ...defaultServices, ...customServices },
426434
config,
435+
hasInitialized: true,
427436
};
428437
}),
429438
setHasSetup: assign({ hasSetup: true }),
@@ -435,6 +444,10 @@ export function createAuthenticatorMachine(
435444
isInitialStateResetPassword: ({ config }) =>
436445
config.initialState === 'forgotPassword',
437446
shouldSetup: ({ hasSetup }) => !hasSetup,
447+
// `hasSetup` prevents the late `INIT` from interrupting `idle`, which must
448+
// resolve `handleGetCurrentUser` before the machine leaves it
449+
shouldInitialize: ({ hasSetup, hasInitialized }) =>
450+
hasSetup && !hasInitialized,
438451
hasUser: ({ user }) => {
439452
return !!user;
440453
},

packages/ui/src/machines/authenticator/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ export interface AuthContext {
165165
// data returned from actors when they finish and reach their final state
166166
actorDoneData?: ActorDoneData;
167167
hasSetup?: boolean;
168+
// whether the UI has sent `INIT`, applying its `config` and `services`
169+
hasInitialized?: boolean;
168170
passwordlessCapabilities?: PasswordlessCapabilities;
169171
}
170172

0 commit comments

Comments
 (0)