Skip to content

Commit a6470c1

Browse files
osama-rizkbobbor
andauthored
fix(authenticator): redirect to sign-in after account confirmation (#6789)
* fix(authenticator): handle auto sign-in for refresh scenario in signup confirmation Co-authored-by: Philipp Andreas Paul <phandpau@amazon.de>
1 parent 3b0710f commit a6470c1

5 files changed

Lines changed: 192 additions & 1 deletion

File tree

.changeset/quiet-numbers-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/ui': patch
3+
---
4+
5+
fix(authenticator): redirect to sign-in after account confirmation

packages/e2e/features/ui/components/authenticator/sign-in-with-email.feature

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ Feature: Sign In with Email
4848
Then I click the "Confirm" button
4949
Then I confirm request '{"headers": { "X-Amz-Target": "AWSCognitoIdentityProviderService.ConfirmSignUp" } }'
5050

51+
@angular @react @vue @svelte @react-native
52+
Scenario: Sign in with unconfirmed credentials redirects to sign-in after confirmation
53+
54+
Tests that after confirming an unconfirmed account, user is redirected to sign-in screen (not sign-up screen).
55+
56+
When I type my "email" with status "UNCONFIRMED"
57+
Then I type my password
58+
Then I spy request '{ "headers": { "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth" } }'
59+
Then I click the "Sign in" button
60+
Then I confirm request '{"headers": { "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth" } }'
61+
Then I see "Confirmation Code"
62+
Then I type a valid confirmation code
63+
Then I intercept '{ "headers": { "X-Amz-Target": "AWSCognitoIdentityProviderService.ConfirmSignUp" } }' with fixture "confirm-sign-up-with-email"
64+
Then I click the "Confirm" button
65+
Then I don't see "Confirmation Code"
66+
Then I see "Sign in"
67+
Then I don't see "Create Account"
68+
5169
@angular @react @vue @svelte @react-native
5270
Scenario: Sign in with confirmed credentials
5371
When I type my "email" with status "CONFIRMED"

packages/ui/src/machines/authenticator/actors/__tests__/signUp.test.ts

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as AuthModule from 'aws-amplify/auth';
55

66
import { SignUpMachineOptions, signUpActor } from '../signUp';
77
import { SignUpContext } from '../../types';
8+
import guards from '../../guards';
89

910
jest.mock('aws-amplify');
1011

@@ -32,7 +33,164 @@ describe('signUpActor', () => {
3233
afterEach(() => {
3334
jest.clearAllMocks();
3435
jest.clearAllTimers();
35-
service.stop();
36+
service?.stop();
37+
});
38+
39+
// New tests for shouldManualSignIn guard
40+
describe('shouldManualSignIn guard', () => {
41+
const createMockContext = (step: string) =>
42+
({
43+
step,
44+
loginMechanisms: ['username'],
45+
socialProviders: [],
46+
formValues: {},
47+
touched: {},
48+
validationError: {},
49+
}) as any;
50+
51+
const createMockEvent = (signUpStep: string) => ({
52+
type: 'done.invoke.confirmSignUp' as any,
53+
data: {
54+
nextStep: { signUpStep },
55+
},
56+
});
57+
58+
it('should return true when nextStep is DONE and contextStep is CONFIRM_SIGN_UP', () => {
59+
const context = createMockContext('CONFIRM_SIGN_UP');
60+
const event = createMockEvent('DONE');
61+
const meta = {} as any;
62+
63+
const result = guards.shouldManualSignIn(context, event, meta);
64+
expect(result).toBe(true);
65+
});
66+
67+
it('should return false when nextStep is COMPLETE_AUTO_SIGN_IN', () => {
68+
const context = createMockContext('CONFIRM_SIGN_UP');
69+
const event = createMockEvent('COMPLETE_AUTO_SIGN_IN');
70+
const meta = {} as any;
71+
72+
const result = guards.shouldManualSignIn(context, event, meta);
73+
expect(result).toBe(false);
74+
});
75+
76+
it('should return false when contextStep is not CONFIRM_SIGN_UP', () => {
77+
const context = createMockContext('SIGN_UP');
78+
const event = createMockEvent('DONE');
79+
const meta = {} as any;
80+
81+
const result = guards.shouldManualSignIn(context, event, meta);
82+
expect(result).toBe(false);
83+
});
84+
85+
it('should return false when nextStep is not DONE', () => {
86+
const context = createMockContext('CONFIRM_SIGN_UP');
87+
const event = createMockEvent('CONFIRM_SIGN_UP');
88+
const meta = {} as any;
89+
90+
const result = guards.shouldManualSignIn(context, event, meta);
91+
expect(result).toBe(false);
92+
});
93+
});
94+
95+
describe('confirmSignUp with shouldManualSignIn', () => {
96+
it('should transition to resolved with SIGN_IN step when shouldManualSignIn is true', async () => {
97+
const mockConfirmSignUp = jest.fn().mockResolvedValue({
98+
nextStep: { signUpStep: 'DONE' },
99+
});
100+
101+
service = interpret(
102+
signUpActor(signUpMachineProps)
103+
.withContext({
104+
step: 'CONFIRM_SIGN_UP',
105+
username: mockUsername,
106+
formValues: {
107+
confirmation_code: mockConfirmationCode,
108+
},
109+
loginMechanisms: ['username'],
110+
} as unknown as SignUpContext)
111+
.withConfig({
112+
actions: {
113+
clearFormValues: jest.fn(),
114+
clearError: jest.fn(),
115+
clearTouched: jest.fn(),
116+
sendUpdate: jest.fn(),
117+
setNextSignUpStep: jest.fn(),
118+
setSignInStep: (context) => {
119+
context.step = 'SIGN_IN';
120+
},
121+
},
122+
services: {
123+
confirmSignUp: mockConfirmSignUp,
124+
validateSignUp: jest.fn().mockResolvedValue(null),
125+
},
126+
guards: {
127+
shouldAutoSignIn: jest.fn(() => false),
128+
shouldManualSignIn: jest.fn(() => true),
129+
},
130+
})
131+
);
132+
133+
service.start();
134+
135+
// Submit confirmation code
136+
service.send({ type: 'SUBMIT' });
137+
await flushPromises();
138+
139+
// Should reach resolved state
140+
expect(service.getSnapshot().value).toBe('resolved');
141+
142+
// Should have called setSignInStep action and set step to 'SIGN_IN'
143+
expect(service.getSnapshot().context.step).toBe('SIGN_IN');
144+
});
145+
});
146+
147+
describe('confirmSignUp flow with refresh scenario', () => {
148+
it('should route directly to resolved when coming from sign-in flow', async () => {
149+
const mockConfirmSignUp = jest.fn().mockResolvedValue({
150+
nextStep: { signUpStep: 'DONE' },
151+
});
152+
153+
service = interpret(
154+
signUpActor(signUpMachineProps)
155+
.withContext({
156+
step: 'CONFIRM_SIGN_UP', // This indicates refresh scenario
157+
username: mockUsername,
158+
formValues: {
159+
confirmation_code: mockConfirmationCode,
160+
},
161+
loginMechanisms: ['username'],
162+
} as unknown as SignUpContext)
163+
.withConfig({
164+
actions: {
165+
clearFormValues: jest.fn(),
166+
clearError: jest.fn(),
167+
sendUpdate: jest.fn(),
168+
setNextSignUpStep: jest.fn(),
169+
setSignInStep: jest.fn(),
170+
},
171+
services: {
172+
confirmSignUp: mockConfirmSignUp,
173+
},
174+
})
175+
);
176+
177+
service.start();
178+
179+
// Submit confirmation
180+
service.send({ type: 'SUBMIT' });
181+
await flushPromises();
182+
183+
// Verify confirmSignUp was called (with full context as first parameter)
184+
expect(mockConfirmSignUp).toHaveBeenCalled();
185+
186+
// Verify the service was called with context containing the right data
187+
const callArgs = mockConfirmSignUp.mock.calls[0][0];
188+
expect(callArgs.username).toBe(mockUsername);
189+
expect(callArgs.formValues.confirmation_code).toBe(mockConfirmationCode);
190+
191+
// Should reach resolved state directly
192+
expect(service.getSnapshot().value).toBe('resolved');
193+
});
36194
});
37195

38196
// @todo-migration

packages/ui/src/machines/authenticator/actors/signUp.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ export function signUpActor({ services }: SignUpMachineOptions) {
285285
actions: ['setNextSignUpStep', 'clearFormValues'],
286286
target: '#signUpActor.autoSignIn',
287287
},
288+
{
289+
cond: 'shouldManualSignIn',
290+
actions: ['clearFormValues', 'setSignInStep'],
291+
target: '#signUpActor.resolved',
292+
},
288293
{
289294
actions: 'setNextSignUpStep',
290295
target: '#signUpActor.init',

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const shouldConfirmSignUpFromSignIn = (
3636
const shouldAutoSignIn = (_: AuthActorContext, { data }: AuthEvent) =>
3737
(data as SignUpOutput)?.nextStep.signUpStep === 'COMPLETE_AUTO_SIGN_IN';
3838

39+
const shouldManualSignIn = (context: AuthActorContext, { data }: AuthEvent) =>
40+
(data as SignUpOutput)?.nextStep.signUpStep === 'DONE' &&
41+
context.step === 'CONFIRM_SIGN_UP';
42+
3943
const hasCompletedSignIn = (_: AuthActorContext, { data }: AuthEvent) =>
4044
(data as SignInOutput)?.nextStep.signInStep === 'DONE';
4145

@@ -215,6 +219,7 @@ const GUARDS: MachineOptions<AuthActorContext, AuthEvent>['guards'] = {
215219
isShouldConfirmUserAttributeStep,
216220
isUserAlreadyConfirmed,
217221
shouldAutoSignIn,
222+
shouldManualSignIn,
218223
shouldConfirmResetPassword,
219224
shouldConfirmSignIn,
220225
shouldConfirmSignInWithNewPassword,

0 commit comments

Comments
 (0)