Skip to content

Commit 8452cf7

Browse files
authored
Add signInWithPhoneNumber implementation (#3191)
* Add signInWithPhoneNumber flow * Formatting * PR feedback * Formatting
1 parent c21e817 commit 8452cf7

File tree

11 files changed

+714
-5
lines changed

11 files changed

+714
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
import * as sinon from 'sinon';
20+
21+
import { mockEndpoint } from '../../../test/api/helper';
22+
import { testAuth } from '../../../test/mock_auth';
23+
import * as fetch from '../../../test/mock_fetch';
24+
import { Endpoint } from '../../api';
25+
import { Auth } from '../../model/auth';
26+
import { RecaptchaVerifier } from '../../platform_browser/recaptcha/recaptcha_verifier';
27+
import { PhoneAuthProvider } from './phone';
28+
29+
describe('core/providers/phone', () => {
30+
let auth: Auth;
31+
32+
beforeEach(async () => {
33+
fetch.setUp();
34+
auth = await testAuth();
35+
});
36+
37+
afterEach(() => {
38+
fetch.tearDown();
39+
sinon.restore();
40+
});
41+
42+
context('#verifyPhoneNumber', () => {
43+
it('calls verify on the appVerifier and then calls the server', async () => {
44+
const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
45+
sessionInfo: 'verification-id'
46+
});
47+
48+
const verifier = new RecaptchaVerifier(
49+
document.createElement('div'),
50+
{},
51+
auth
52+
);
53+
sinon
54+
.stub(verifier, 'verify')
55+
.returns(Promise.resolve('verification-code'));
56+
57+
const provider = new PhoneAuthProvider(auth);
58+
const result = await provider.verifyPhoneNumber('+15105550000', verifier);
59+
expect(result).to.eq('verification-id');
60+
expect(route.calls[0].request).to.eql({
61+
phoneNumber: '+15105550000',
62+
recaptchaToken: 'verification-code'
63+
});
64+
});
65+
});
66+
67+
context('.credential', () => {
68+
it('creates a phone auth credential', () => {
69+
const credential = PhoneAuthProvider.credential('id', 'code');
70+
71+
// Allows us to inspect the object
72+
const blob = credential.toJSON() as Record<string, string>;
73+
74+
expect(blob.verificationId).to.eq('id');
75+
expect(blob.verificationCode).to.eq('code');
76+
});
77+
});
78+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as externs from '@firebase/auth-types-exp';
19+
import { FirebaseError } from '@firebase/util';
20+
21+
import { Auth } from '../../model/auth';
22+
import { initializeAuth } from '../auth/auth_impl';
23+
import { _verifyPhoneNumber } from '../strategies/phone';
24+
import { PhoneAuthCredential } from '../strategies/phone_credential';
25+
import { debugFail } from '../util/assert';
26+
27+
export class PhoneAuthProvider implements externs.AuthProvider {
28+
static readonly PROVIDER_ID = externs.ProviderId.PHONE;
29+
static readonly PHONE_SIGN_IN_METHOD = externs.SignInMethod.PHONE;
30+
31+
private readonly auth: Auth;
32+
readonly providerId = PhoneAuthProvider.PROVIDER_ID;
33+
34+
constructor(auth?: externs.Auth | null) {
35+
this.auth = (auth || initializeAuth()) as Auth;
36+
}
37+
38+
verifyPhoneNumber(
39+
phoneNumber: string,
40+
applicationVerifier: externs.ApplicationVerifier
41+
/* multiFactorSession?: MultiFactorSession, */
42+
): Promise<string> {
43+
return _verifyPhoneNumber(this.auth, phoneNumber, applicationVerifier);
44+
}
45+
46+
static credential(
47+
verificationId: string,
48+
verificationCode: string
49+
): PhoneAuthCredential {
50+
return new PhoneAuthCredential({ verificationId, verificationCode });
51+
}
52+
53+
static credentialFromResult(
54+
userCredential: externs.UserCredential
55+
): externs.AuthCredential | null {
56+
void userCredential;
57+
return debugFail('not implemented');
58+
}
59+
60+
static credentialFromError(
61+
error: FirebaseError
62+
): externs.AuthCredential | null {
63+
void error;
64+
return debugFail('not implemented');
65+
}
66+
67+
static credentialFromJSON(json: string | object): externs.AuthCredential {
68+
void json;
69+
return debugFail('not implemented');
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
import * as sinonChai from 'sinon-chai';
22+
23+
import { ApplicationVerifier, OperationType } from '@firebase/auth-types-exp';
24+
import { FirebaseError } from '@firebase/util';
25+
26+
import { mockEndpoint } from '../../../test/api/helper';
27+
import { testAuth } from '../../../test/mock_auth';
28+
import * as fetch from '../../../test/mock_fetch';
29+
import { Endpoint } from '../../api';
30+
import { Auth } from '../../model/auth';
31+
import { IdTokenResponse } from '../../model/id_token';
32+
import { RecaptchaVerifier } from '../../platform_browser/recaptcha/recaptcha_verifier';
33+
import { _verifyPhoneNumber, signInWithPhoneNumber } from './phone';
34+
35+
use(chaiAsPromised);
36+
use(sinonChai);
37+
38+
describe('core/strategies/phone', () => {
39+
let auth: Auth;
40+
let verifier: ApplicationVerifier;
41+
let sendCodeEndpoint: fetch.Route;
42+
43+
beforeEach(async () => {
44+
auth = await testAuth();
45+
fetch.setUp();
46+
47+
sendCodeEndpoint = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
48+
sessionInfo: 'session-info'
49+
});
50+
51+
verifier = new RecaptchaVerifier(document.createElement('div'), {}, auth);
52+
sinon.stub(verifier, 'verify').returns(Promise.resolve('recaptcha-token'));
53+
});
54+
55+
afterEach(() => {
56+
fetch.tearDown();
57+
sinon.restore();
58+
});
59+
60+
describe('signInWithPhoneNumber', () => {
61+
it('calls verify phone number', async () => {
62+
await signInWithPhoneNumber(auth, '+15105550000', verifier);
63+
64+
expect(sendCodeEndpoint.calls[0].request).to.eql({
65+
recaptchaToken: 'recaptcha-token',
66+
phoneNumber: '+15105550000'
67+
});
68+
});
69+
70+
context('ConfirmationResult', () => {
71+
it('result contains verification id baked in', async () => {
72+
const result = await signInWithPhoneNumber(auth, 'number', verifier);
73+
expect(result.verificationId).to.eq('session-info');
74+
});
75+
76+
it('calling #confirm finishes the sign in flow', async () => {
77+
const idTokenResponse: IdTokenResponse = {
78+
idToken: 'my-id-token',
79+
refreshToken: 'my-refresh-token',
80+
expiresIn: '1234',
81+
localId: 'uid',
82+
kind: 'my-kind'
83+
};
84+
85+
// This endpoint is called from within the callback, in
86+
// signInWithCredential
87+
const signInEndpoint = mockEndpoint(
88+
Endpoint.SIGN_IN_WITH_PHONE_NUMBER,
89+
idTokenResponse
90+
);
91+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
92+
users: [{ localId: 'uid' }]
93+
});
94+
95+
const result = await signInWithPhoneNumber(auth, 'number', verifier);
96+
const userCred = await result.confirm('6789');
97+
expect(userCred.user.uid).to.eq('uid');
98+
expect(userCred.operationType).to.eq(OperationType.SIGN_IN);
99+
expect(signInEndpoint.calls[0].request).to.eql({
100+
sessionInfo: 'session-info',
101+
code: '6789'
102+
});
103+
});
104+
});
105+
});
106+
107+
describe('_verifyPhoneNumber', () => {
108+
it('works with a string phone number', async () => {
109+
await _verifyPhoneNumber(auth, 'number', verifier);
110+
expect(sendCodeEndpoint.calls[0].request).to.eql({
111+
recaptchaToken: 'recaptcha-token',
112+
phoneNumber: 'number'
113+
});
114+
});
115+
116+
it('works with an options object', async () => {
117+
await _verifyPhoneNumber(
118+
auth,
119+
{
120+
phoneNumber: 'number'
121+
},
122+
verifier
123+
);
124+
expect(sendCodeEndpoint.calls[0].request).to.eql({
125+
recaptchaToken: 'recaptcha-token',
126+
phoneNumber: 'number'
127+
});
128+
});
129+
130+
it('throws if the verifier does not return a string', async () => {
131+
(verifier.verify as sinon.SinonStub).returns(Promise.resolve(123));
132+
await expect(
133+
_verifyPhoneNumber(auth, 'number', verifier)
134+
).to.be.rejectedWith(
135+
FirebaseError,
136+
'Firebase: Error (auth/argument-error)'
137+
);
138+
});
139+
140+
it('throws if the verifier type is not recaptcha', async () => {
141+
const mutVerifier: {
142+
-readonly [K in keyof ApplicationVerifier]: ApplicationVerifier[K];
143+
} = verifier;
144+
mutVerifier.type = 'not-recaptcha-thats-for-sure';
145+
await expect(
146+
_verifyPhoneNumber(auth, 'number', mutVerifier)
147+
).to.be.rejectedWith(
148+
FirebaseError,
149+
'Firebase: Error (auth/argument-error)'
150+
);
151+
});
152+
153+
it('resets the verifer after successful verification', async () => {
154+
sinon.spy(verifier, 'reset');
155+
expect(await _verifyPhoneNumber(auth, 'number', verifier)).to.eq(
156+
'session-info'
157+
);
158+
expect(verifier.reset).to.have.been.called;
159+
});
160+
161+
it('resets the verifer after a failed verification', async () => {
162+
sinon.spy(verifier, 'reset');
163+
(verifier.verify as sinon.SinonStub).returns(Promise.resolve(123));
164+
165+
await expect(_verifyPhoneNumber(auth, 'number', verifier)).to.be.rejected;
166+
expect(verifier.reset).to.have.been.called;
167+
});
168+
});
169+
});

0 commit comments

Comments
 (0)