Skip to content

Commit 2592c8e

Browse files
committed
Add custom email flow methods to auth-next (#3223)
1 parent fdd958d commit 2592c8e

File tree

10 files changed

+193
-43
lines changed

10 files changed

+193
-43
lines changed

packages-exp/auth-exp/src/api/account_management/email_and_password.test.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import { testAuth } from '../../../test/mock_auth';
2626
import * as mockFetch from '../../../test/mock_fetch';
2727
import { Auth } from '../../model/auth';
2828
import { ServerError } from '../errors';
29-
import { resetPassword, updateEmailPassword } from './email_and_password';
29+
import {
30+
applyActionCode,
31+
resetPassword,
32+
updateEmailPassword
33+
} from './email_and_password';
3034

3135
use(chaiAsPromised);
3236

@@ -141,3 +145,55 @@ describe('api/account_management/updateEmailPassword', () => {
141145
expect(mock.calls[0].request).to.eql(request);
142146
});
143147
});
148+
149+
describe('api/account_management/applyActionCode', () => {
150+
const request = {
151+
oobCode: 'oob-code'
152+
};
153+
154+
let auth: Auth;
155+
156+
beforeEach(async () => {
157+
auth = await testAuth();
158+
mockFetch.setUp();
159+
});
160+
161+
afterEach(mockFetch.tearDown);
162+
163+
it('should POST to the correct endpoint', async () => {
164+
const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
165+
166+
const response = await applyActionCode(auth, request);
167+
expect(response).to.be.empty;
168+
expect(mock.calls[0].request).to.eql(request);
169+
expect(mock.calls[0].method).to.eq('POST');
170+
expect(mock.calls[0].headers).to.eql({
171+
'Content-Type': 'application/json',
172+
'X-Client-Version': 'testSDK/0.0.0'
173+
});
174+
});
175+
176+
it('should handle errors', async () => {
177+
const mock = mockEndpoint(
178+
Endpoint.SET_ACCOUNT_INFO,
179+
{
180+
error: {
181+
code: 400,
182+
message: ServerError.INVALID_OOB_CODE,
183+
errors: [
184+
{
185+
message: ServerError.INVALID_OOB_CODE
186+
}
187+
]
188+
}
189+
},
190+
400
191+
);
192+
193+
await expect(applyActionCode(auth, request)).to.be.rejectedWith(
194+
FirebaseError,
195+
'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
196+
);
197+
expect(mock.calls[0].request).to.eql(request);
198+
});
199+
});

packages-exp/auth-exp/src/api/account_management/email_and_password.ts

+18
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,21 @@ export async function updateEmailPassword(
6161
UpdateEmailPasswordResponse
6262
>(auth, HttpMethod.POST, Endpoint.SET_ACCOUNT_INFO, request);
6363
}
64+
65+
export interface ApplyActionCodeRequest {
66+
oobCode: string;
67+
}
68+
69+
export interface ApplyActionCodeResponse {}
70+
71+
export async function applyActionCode(
72+
auth: Auth,
73+
request: ApplyActionCodeRequest
74+
): Promise<ApplyActionCodeResponse> {
75+
return _performApiRequest<ApplyActionCodeRequest, ApplyActionCodeResponse>(
76+
auth,
77+
HttpMethod.POST,
78+
Endpoint.SET_ACCOUNT_INFO,
79+
request
80+
);
81+
}

packages-exp/auth-exp/src/core/action_code_url.test.ts

+14-14
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('core/action_code_url', () => {
3838
'continueUrl=' +
3939
encodeURIComponent(continueUrl) +
4040
'&languageCode=en&tenantId=TENANT_ID&state=bla';
41-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
41+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
4242
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
4343
expect(actionCodeUrl!.code).to.eq('CODE');
4444
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
@@ -54,7 +54,7 @@ describe('core/action_code_url', () => {
5454
'https://www.example.com/finishSignIn?' +
5555
'oobCode=CODE&mode=signIn&apiKey=API_KEY&' +
5656
'languageCode=en';
57-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
57+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
5858
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
5959
});
6060

@@ -63,7 +63,7 @@ describe('core/action_code_url', () => {
6363
'https://www.example.com/finishSignIn?' +
6464
'oobCode=CODE&mode=verifyAndChangeEmail&apiKey=API_KEY&' +
6565
'languageCode=en';
66-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
66+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
6767
expect(actionCodeUrl!.operation).to.eq(
6868
Operation.VERIFY_AND_CHANGE_EMAIL
6969
);
@@ -74,7 +74,7 @@ describe('core/action_code_url', () => {
7474
'https://www.example.com/finishSignIn?' +
7575
'oobCode=CODE&mode=verifyEmail&apiKey=API_KEY&' +
7676
'languageCode=en';
77-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
77+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
7878
expect(actionCodeUrl!.operation).to.eq(Operation.VERIFY_EMAIL);
7979
});
8080

@@ -83,7 +83,7 @@ describe('core/action_code_url', () => {
8383
'https://www.example.com/finishSignIn?' +
8484
'oobCode=CODE&mode=recoverEmail&apiKey=API_KEY&' +
8585
'languageCode=en';
86-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
86+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
8787
expect(actionCodeUrl!.operation).to.eq(Operation.RECOVER_EMAIL);
8888
});
8989

@@ -92,7 +92,7 @@ describe('core/action_code_url', () => {
9292
'https://www.example.com/finishSignIn?' +
9393
'oobCode=CODE&mode=resetPassword&apiKey=API_KEY&' +
9494
'languageCode=en';
95-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
95+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
9696
expect(actionCodeUrl!.operation).to.eq(Operation.PASSWORD_RESET);
9797
});
9898

@@ -101,7 +101,7 @@ describe('core/action_code_url', () => {
101101
'https://www.example.com/finishSignIn?' +
102102
'oobCode=CODE&mode=revertSecondFactorAddition&apiKey=API_KEY&' +
103103
'languageCode=en';
104-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
104+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
105105
expect(actionCodeUrl!.operation).to.eq(
106106
Operation.REVERT_SECOND_FACTOR_ADDITION
107107
);
@@ -112,7 +112,7 @@ describe('core/action_code_url', () => {
112112
const actionLink =
113113
'https://www.example.com:8080/finishSignIn?' +
114114
'oobCode=CODE&mode=signIn&apiKey=API_KEY&state=bla';
115-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
115+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
116116
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
117117
expect(actionCodeUrl!.code).to.eq('CODE');
118118
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
@@ -126,7 +126,7 @@ describe('core/action_code_url', () => {
126126
'https://www.example.com/finishSignIn?' +
127127
'oobCode=CODE1&mode=signIn&apiKey=API_KEY1&state=bla' +
128128
'#oobCode=CODE2&mode=signIn&apiKey=API_KEY2&state=bla';
129-
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
129+
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
130130
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
131131
expect(actionCodeUrl!.code).to.eq('CODE1');
132132
expect(actionCodeUrl!.apiKey).to.eq('API_KEY1');
@@ -138,31 +138,31 @@ describe('core/action_code_url', () => {
138138
context('invalid links', () => {
139139
it('should handle missing API key, code & mode', () => {
140140
const actionLink = 'https://www.example.com/finishSignIn';
141-
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
141+
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
142142
});
143143

144144
it('should handle invalid mode', () => {
145145
const actionLink =
146146
'https://www.example.com/finishSignIn?oobCode=CODE&mode=INVALID_MODE&apiKey=API_KEY';
147-
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
147+
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
148148
});
149149

150150
it('should handle missing code', () => {
151151
const actionLink =
152152
'https://www.example.com/finishSignIn?mode=signIn&apiKey=API_KEY';
153-
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
153+
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
154154
});
155155

156156
it('should handle missing API key', () => {
157157
const actionLink =
158158
'https://www.example.com/finishSignIn?oobCode=CODE&mode=signIn';
159-
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
159+
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
160160
});
161161

162162
it('should handle missing mode', () => {
163163
const actionLink =
164164
'https://www.example.com/finishSignIn?oobCode=CODE&apiKey=API_KEY';
165-
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
165+
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
166166
});
167167
});
168168
});

packages-exp/auth-exp/src/core/action_code_url.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
*/
1717

1818
import * as externs from '@firebase/auth-types-exp';
19-
20-
import { Auth } from '../model/auth';
21-
import { AUTH_ERROR_FACTORY, AuthErrorCode } from './errors';
19+
import { AuthErrorCode, AUTH_ERROR_FACTORY } from './errors';
2220

2321
/**
2422
* Enums for fields in URL query string.
@@ -73,7 +71,7 @@ export class ActionCodeURL implements externs.ActionCodeURL {
7371
readonly operation: externs.Operation;
7472
readonly tenantId: string | null;
7573

76-
constructor(auth: Auth, actionLink: string) {
74+
constructor(auth: externs.Auth, actionLink: string) {
7775
const uri = new URL(actionLink);
7876
const apiKey = uri.searchParams.get(QueryField.API_KEY);
7977
const code = uri.searchParams.get(QueryField.CODE);
@@ -92,7 +90,10 @@ export class ActionCodeURL implements externs.ActionCodeURL {
9290
this.tenantId = uri.searchParams.get(QueryField.TENANT_ID);
9391
}
9492

95-
static _fromLink(auth: Auth, link: string): ActionCodeURL | null {
93+
static parseLink(
94+
auth: externs.Auth,
95+
link: string
96+
): externs.ActionCodeURL | null {
9697
const actionLink = parseDeepLink(link);
9798
try {
9899
return new ActionCodeURL(auth, actionLink);
@@ -101,3 +102,10 @@ export class ActionCodeURL implements externs.ActionCodeURL {
101102
}
102103
}
103104
}
105+
106+
export function parseActionCodeURL(
107+
auth: externs.Auth,
108+
link: string
109+
): externs.ActionCodeURL | null {
110+
return ActionCodeURL.parseLink(auth, link);
111+
}

packages-exp/auth-exp/src/core/providers/email.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class EmailAuthProvider implements externs.EmailAuthProvider {
4747
email: string,
4848
emailLink: string
4949
): EmailAuthCredential {
50-
const actionCodeUrl = ActionCodeURL._fromLink(auth, emailLink);
50+
const actionCodeUrl = ActionCodeURL.parseLink(auth, emailLink);
5151
assert(actionCodeUrl, auth.name, AuthErrorCode.ARGUMENT_ERROR);
5252

5353
// Check if the tenant ID in the email link matches the tenant ID on Auth

packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts

+45-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ import {
3838
createUserWithEmailAndPassword,
3939
sendPasswordResetEmail,
4040
signInWithEmailAndPassword,
41-
verifyPasswordResetCode
41+
verifyPasswordResetCode,
42+
applyActionCode
4243
} from './email_and_password';
4344

4445
use(chaiAsPromised);
@@ -187,6 +188,45 @@ describe('core/strategies/confirmPasswordReset', () => {
187188
});
188189
});
189190

191+
describe('core/strategies/applyActionCode', () => {
192+
const oobCode = 'oob-code';
193+
194+
let auth: Auth;
195+
196+
beforeEach(async () => {
197+
auth = await testAuth();
198+
mockFetch.setUp();
199+
});
200+
201+
afterEach(mockFetch.tearDown);
202+
203+
it('should apply the oob code', async () => {
204+
const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
205+
await applyActionCode(auth, oobCode);
206+
expect(mock.calls[0].request).to.eql({
207+
oobCode
208+
});
209+
});
210+
211+
it('should surface errors', async () => {
212+
const mock = mockEndpoint(
213+
Endpoint.SET_ACCOUNT_INFO,
214+
{
215+
error: {
216+
code: 400,
217+
message: ServerError.INVALID_OOB_CODE
218+
}
219+
},
220+
400
221+
);
222+
await expect(applyActionCode(auth, oobCode)).to.be.rejectedWith(
223+
FirebaseError,
224+
'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
225+
);
226+
expect(mock.calls.length).to.eq(1);
227+
});
228+
});
229+
190230
describe('core/strategies/checkActionCode', () => {
191231
const oobCode = 'oob-code';
192232
const email = '[email protected]';
@@ -210,7 +250,7 @@ describe('core/strategies/checkActionCode', () => {
210250
expect(response).to.eql({
211251
data: {
212252
email,
213-
fromEmail: null
253+
previousEmail: null
214254
},
215255
operation: Operation.PASSWORD_RESET
216256
});
@@ -229,7 +269,7 @@ describe('core/strategies/checkActionCode', () => {
229269
expect(response).to.eql({
230270
data: {
231271
email,
232-
fromEmail: newEmail
272+
previousEmail: newEmail
233273
},
234274
operation: Operation.PASSWORD_RESET
235275
});
@@ -284,7 +324,8 @@ describe('core/strategies/verifyPasswordResetCode', () => {
284324
it('should verify the oob code', async () => {
285325
const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
286326
requestType: Operation.PASSWORD_RESET,
287-
327+
328+
previousEmail: null
288329
});
289330
const response = await verifyPasswordResetCode(auth, oobCode);
290331
expect(response).to.eq(email);

0 commit comments

Comments
 (0)