Skip to content

Commit 3862071

Browse files
authored
Add Cordova support to the compatibility layer (#4535)
* Compat popup/redirect * Get everything working; tests * License + Formatting * Add correct resolver to user methods * Formatting
1 parent d4ba8da commit 3862071

File tree

12 files changed

+390
-82
lines changed

12 files changed

+390
-82
lines changed

packages-exp/auth-compat-exp/src/auth.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { expect, use } from 'chai';
2121
import * as sinon from 'sinon';
2222
import * as sinonChai from 'sinon-chai';
2323
import { Auth } from './auth';
24+
import { CompatPopupRedirectResolver } from './popup_redirect';
2425

2526
use(sinonChai);
2627

@@ -69,7 +70,7 @@ describe('auth compat', () => {
6970
exp._getInstance(exp.inMemoryPersistence),
7071
exp._getInstance(exp.indexedDBLocalPersistence)
7172
],
72-
exp.browserPopupRedirectResolver
73+
CompatPopupRedirectResolver
7374
);
7475
}
7576
});

packages-exp/auth-compat-exp/src/auth.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727

2828
import { _validatePersistenceArgument, Persistence } from './persistence';
2929
import { _isPopupRedirectSupported } from './platform';
30+
import { CompatPopupRedirectResolver } from './popup_redirect';
3031
import { User } from './user';
3132
import {
3233
convertConfirmationResult,
@@ -71,7 +72,7 @@ export class Auth
7172
// eslint-disable-next-line @typescript-eslint/no-floating-promises
7273
this.auth._initializeWithPersistence(
7374
hierarchy,
74-
exp.browserPopupRedirectResolver
75+
CompatPopupRedirectResolver
7576
);
7677
}
7778

@@ -142,7 +143,7 @@ export class Auth
142143
);
143144
const credential = await exp.getRedirectResult(
144145
this.auth,
145-
exp.browserPopupRedirectResolver
146+
CompatPopupRedirectResolver
146147
);
147148
if (!credential) {
148149
return {
@@ -283,7 +284,7 @@ export class Auth
283284
exp.signInWithPopup(
284285
this.auth,
285286
provider as exp.AuthProvider,
286-
exp.browserPopupRedirectResolver
287+
CompatPopupRedirectResolver
287288
)
288289
);
289290
}
@@ -297,7 +298,7 @@ export class Auth
297298
return exp.signInWithRedirect(
298299
this.auth,
299300
provider as exp.AuthProvider,
300-
exp.browserPopupRedirectResolver
301+
CompatPopupRedirectResolver
301302
);
302303
}
303304
updateCurrentUser(user: compat.User | null): Promise<void> {

packages-exp/auth-compat-exp/src/platform.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ declare global {
3131
}
3232
}
3333

34+
const CORDOVA_ONDEVICEREADY_TIMEOUT_MS = 1000;
35+
3436
function _getCurrentScheme(): string | null {
3537
return self?.location?.protocol || null;
3638
}
@@ -47,7 +49,7 @@ function _isHttpOrHttps(): boolean {
4749
* @return {boolean} Whether the app is rendered in a mobile iOS or Android
4850
* Cordova environment.
4951
*/
50-
function _isAndroidOrIosCordovaScheme(ua: string = getUA()): boolean {
52+
export function _isAndroidOrIosCordovaScheme(ua: string = getUA()): boolean {
5153
return !!(
5254
(_getCurrentScheme() === 'file:' || _getCurrentScheme() === 'ionic:') &&
5355
ua.toLowerCase().match(/iphone|ipad|ipod|android/)
@@ -159,3 +161,21 @@ export function _getClientPlatform(): impl.ClientPlatform {
159161
}
160162
return impl.ClientPlatform.BROWSER;
161163
}
164+
165+
export async function _isCordova(): Promise<boolean> {
166+
if (!_isAndroidOrIosCordovaScheme() || typeof document === 'undefined') {
167+
return false;
168+
}
169+
170+
return new Promise(resolve => {
171+
const timeoutId = setTimeout(() => {
172+
// We've waited long enough; the telltale Cordova event didn't happen
173+
resolve(false);
174+
}, CORDOVA_ONDEVICEREADY_TIMEOUT_MS);
175+
176+
document.addEventListener('deviceready', () => {
177+
clearTimeout(timeoutId);
178+
resolve(true);
179+
});
180+
});
181+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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 sinonChai from 'sinon-chai';
20+
import * as sinon from 'sinon';
21+
import * as exp from '@firebase/auth-exp/internal';
22+
import * as platform from './platform';
23+
import { CompatPopupRedirectResolver } from './popup_redirect';
24+
import { FirebaseApp } from '@firebase/app-compat';
25+
26+
use(sinonChai);
27+
28+
describe('popup_redirect/CompatPopupRedirectResolver', () => {
29+
let compatResolver: CompatPopupRedirectResolver;
30+
let auth: exp.AuthImpl;
31+
32+
beforeEach(() => {
33+
compatResolver = new CompatPopupRedirectResolver();
34+
const app = { options: { apiKey: 'api-key' } } as FirebaseApp;
35+
auth = new exp.AuthImpl(app, {
36+
apiKey: 'api-key'
37+
} as exp.Config);
38+
});
39+
40+
afterEach(() => {
41+
sinon.restore();
42+
});
43+
44+
context('initialization and resolver selection', () => {
45+
const browserResolver = exp._getInstance<exp.PopupRedirectResolverInternal>(
46+
exp.browserPopupRedirectResolver
47+
);
48+
const cordovaResolver = exp._getInstance<exp.PopupRedirectResolverInternal>(
49+
exp.cordovaPopupRedirectResolver
50+
);
51+
52+
beforeEach(() => {
53+
sinon.stub(browserResolver, '_initialize');
54+
sinon.stub(cordovaResolver, '_initialize');
55+
});
56+
57+
it('selects the Cordova resolver if in Cordova', async () => {
58+
sinon.stub(platform, '_isCordova').returns(Promise.resolve(true));
59+
await compatResolver._initialize(auth);
60+
expect(cordovaResolver._initialize).to.have.been.calledWith(auth);
61+
expect(browserResolver._initialize).not.to.have.been.called;
62+
});
63+
64+
it('selects the Browser resolver if in Browser', async () => {
65+
sinon.stub(platform, '_isCordova').returns(Promise.resolve(false));
66+
await compatResolver._initialize(auth);
67+
expect(cordovaResolver._initialize).not.to.have.been.called;
68+
expect(browserResolver._initialize).to.have.been.calledWith(auth);
69+
});
70+
});
71+
72+
context('callthrough methods', () => {
73+
let underlyingResolver: sinon.SinonStubbedInstance<exp.PopupRedirectResolverInternal>;
74+
let provider: exp.AuthProvider;
75+
76+
beforeEach(() => {
77+
underlyingResolver = sinon.createStubInstance(FakeResolver);
78+
((compatResolver as unknown) as {
79+
underlyingResolver: exp.PopupRedirectResolverInternal;
80+
}).underlyingResolver = underlyingResolver;
81+
provider = new exp.GoogleAuthProvider();
82+
});
83+
84+
it('_openPopup', async () => {
85+
await compatResolver._openPopup(
86+
auth,
87+
provider,
88+
exp.AuthEventType.LINK_VIA_POPUP,
89+
'eventId'
90+
);
91+
expect(underlyingResolver._openPopup).to.have.been.calledWith(
92+
auth,
93+
provider,
94+
exp.AuthEventType.LINK_VIA_POPUP,
95+
'eventId'
96+
);
97+
});
98+
99+
it('_openRedirect', async () => {
100+
await compatResolver._openRedirect(
101+
auth,
102+
provider,
103+
exp.AuthEventType.LINK_VIA_REDIRECT,
104+
'eventId'
105+
);
106+
expect(underlyingResolver._openRedirect).to.have.been.calledWith(
107+
auth,
108+
provider,
109+
exp.AuthEventType.LINK_VIA_REDIRECT,
110+
'eventId'
111+
);
112+
});
113+
114+
it('_isIframeWebStorageSupported', () => {
115+
const cb = (): void => {};
116+
compatResolver._isIframeWebStorageSupported(auth, cb);
117+
expect(
118+
underlyingResolver._isIframeWebStorageSupported
119+
).to.have.been.calledWith(auth, cb);
120+
});
121+
122+
it('_originValidation', async () => {
123+
await compatResolver._originValidation(auth);
124+
expect(underlyingResolver._originValidation).to.have.been.calledWith(
125+
auth
126+
);
127+
});
128+
});
129+
});
130+
131+
class FakeResolver implements exp.PopupRedirectResolverInternal {
132+
_completeRedirectFn = async (): Promise<null> => null;
133+
_redirectPersistence = exp.inMemoryPersistence;
134+
135+
_initialize(): Promise<exp.EventManager> {
136+
throw new Error('Method not implemented.');
137+
}
138+
_openPopup(): Promise<exp.AuthPopup> {
139+
throw new Error('Method not implemented.');
140+
}
141+
_openRedirect(): Promise<void> {
142+
throw new Error('Method not implemented.');
143+
}
144+
_isIframeWebStorageSupported(): void {
145+
throw new Error('Method not implemented.');
146+
}
147+
148+
_originValidation(): Promise<void> {
149+
throw new Error('Method not implemented.');
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 exp from '@firebase/auth-exp/internal';
19+
import { _isCordova } from './platform';
20+
21+
const _assert: typeof exp._assert = exp._assert;
22+
23+
/** Platform-agnostic popup-redirect resolver */
24+
export class CompatPopupRedirectResolver
25+
implements exp.PopupRedirectResolverInternal {
26+
private underlyingResolver: exp.PopupRedirectResolverInternal | null = null;
27+
_redirectPersistence = exp.browserSessionPersistence;
28+
29+
_completeRedirectFn: (
30+
auth: exp.Auth,
31+
resolver: exp.PopupRedirectResolver,
32+
bypassAuthState: boolean
33+
) => Promise<exp.UserCredential | null> = exp._getRedirectResult;
34+
35+
async _initialize(auth: exp.AuthImpl): Promise<exp.EventManager> {
36+
if (this.underlyingResolver) {
37+
return this.underlyingResolver._initialize(auth);
38+
}
39+
40+
// We haven't yet determined whether or not we're in Cordova; go ahead
41+
// and determine that state now.
42+
const isCordova = await _isCordova();
43+
this.underlyingResolver = exp._getInstance(
44+
isCordova
45+
? exp.cordovaPopupRedirectResolver
46+
: exp.browserPopupRedirectResolver
47+
);
48+
return this.assertedUnderlyingResolver._initialize(auth);
49+
}
50+
51+
_openPopup(
52+
auth: exp.AuthImpl,
53+
provider: exp.AuthProvider,
54+
authType: exp.AuthEventType,
55+
eventId?: string
56+
): Promise<exp.AuthPopup> {
57+
return this.assertedUnderlyingResolver._openPopup(
58+
auth,
59+
provider,
60+
authType,
61+
eventId
62+
);
63+
}
64+
65+
_openRedirect(
66+
auth: exp.AuthImpl,
67+
provider: exp.AuthProvider,
68+
authType: exp.AuthEventType,
69+
eventId?: string
70+
): Promise<void> {
71+
return this.assertedUnderlyingResolver._openRedirect(
72+
auth,
73+
provider,
74+
authType,
75+
eventId
76+
);
77+
}
78+
79+
_isIframeWebStorageSupported(
80+
auth: exp.AuthImpl,
81+
cb: (support: boolean) => unknown
82+
): void {
83+
this.assertedUnderlyingResolver._isIframeWebStorageSupported(auth, cb);
84+
}
85+
86+
_originValidation(auth: exp.Auth): Promise<void> {
87+
return this.assertedUnderlyingResolver._originValidation(auth);
88+
}
89+
90+
private get assertedUnderlyingResolver(): exp.PopupRedirectResolverInternal {
91+
_assert(this.underlyingResolver, exp.AuthErrorCode.INTERNAL_ERROR);
92+
return this.underlyingResolver;
93+
}
94+
}

packages-exp/auth-compat-exp/src/user.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import * as exp from '@firebase/auth-exp/internal';
1919
import * as compat from '@firebase/auth-types';
20+
import { CompatPopupRedirectResolver } from './popup_redirect';
2021
import {
2122
convertConfirmationResult,
2223
convertCredential
@@ -91,15 +92,15 @@ export class User implements compat.User, Wrapper<exp.User> {
9192
exp.linkWithPopup(
9293
this.user,
9394
provider as exp.AuthProvider,
94-
exp.browserPopupRedirectResolver
95+
CompatPopupRedirectResolver
9596
)
9697
);
9798
}
9899
linkWithRedirect(provider: compat.AuthProvider): Promise<void> {
99100
return exp.linkWithRedirect(
100101
this.user,
101102
provider as exp.AuthProvider,
102-
exp.browserPopupRedirectResolver
103+
CompatPopupRedirectResolver
103104
);
104105
}
105106
reauthenticateAndRetrieveDataWithCredential(
@@ -139,15 +140,15 @@ export class User implements compat.User, Wrapper<exp.User> {
139140
exp.reauthenticateWithPopup(
140141
this.user,
141142
provider as exp.AuthProvider,
142-
exp.browserPopupRedirectResolver
143+
CompatPopupRedirectResolver
143144
)
144145
);
145146
}
146147
reauthenticateWithRedirect(provider: compat.AuthProvider): Promise<void> {
147148
return exp.reauthenticateWithRedirect(
148149
this.user,
149150
provider as exp.AuthProvider,
150-
exp.browserPopupRedirectResolver
151+
CompatPopupRedirectResolver
151152
);
152153
}
153154
sendEmailVerification(

0 commit comments

Comments
 (0)