Skip to content

Commit 22e3ec4

Browse files
authored
Merge 7a04e8a into 8948954
2 parents 8948954 + 7a04e8a commit 22e3ec4

File tree

7 files changed

+559
-0
lines changed

7 files changed

+559
-0
lines changed

packages-exp/auth-exp/src/platform_browser/auth_window.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export type AuthWindow = {
2828
} & {
2929
// Any known / named properties we want to add
3030
grecaptcha?: Recaptcha;
31+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
32+
___jsl?: Record<string, any>;
33+
gapi: typeof gapi;
3134
} & {
3235
// A final catch-all for callbacks (which will have random names) that
3336
// we will stick on the window.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
declare namespace gapi {
19+
type LoadCallback = () => void;
20+
interface LoadConfig {}
21+
interface LoadOptions {
22+
callback?: LoadCallback;
23+
timeout?: number;
24+
ontimeout?: LoadCallback;
25+
}
26+
function load(
27+
features: 'gapi.iframes',
28+
options?: LoadOptions | LoadCallback
29+
): void;
30+
}
31+
32+
declare namespace gapi.iframes {
33+
interface Message {
34+
type: string;
35+
}
36+
37+
type IframesFilter = (iframe: Iframe) => boolean;
38+
type MessageHandler<T extends Message> = (
39+
message: T
40+
) => unknown | Promise<void>;
41+
type SendCallback = () => void;
42+
type Callback = (iframe: Iframe) => void;
43+
44+
class Context {
45+
open(
46+
options: Record<string, unknown>,
47+
callback?: Callback
48+
): Promise<Iframe>;
49+
}
50+
51+
class Iframe {
52+
register<T extends Message>(
53+
message: string,
54+
handler: MessageHandler<T>,
55+
filter?: IframesFilter
56+
): void;
57+
ping(callback: SendCallback, data?: unknown): Promise<unknown[]>;
58+
restyle(
59+
style: Record<string, string | boolean>,
60+
callback?: SendCallback
61+
): Promise<unknown[]>;
62+
}
63+
64+
const CROSS_ORIGIN_IFRAMES_FILTER: IframesFilter;
65+
66+
function getContext(): Context;
67+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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 { FirebaseError } from '@firebase/util';
24+
25+
import { testAuth } from '../../../test/mock_auth';
26+
import { Auth } from '../../model/auth';
27+
import { AUTH_WINDOW } from '../auth_window';
28+
import * as js from '../load_js';
29+
import { _loadGapi, _resetLoader } from './gapi';
30+
31+
use(sinonChai);
32+
use(chaiAsPromised);
33+
34+
describe('src/platform_browser/iframe/gapi', () => {
35+
let library: typeof gapi;
36+
let auth: Auth;
37+
function onJsLoad(globalLoadFnName: string): void {
38+
AUTH_WINDOW.gapi = library as typeof gapi;
39+
AUTH_WINDOW[globalLoadFnName]();
40+
}
41+
42+
beforeEach(async () => {
43+
sinon.stub(js, '_loadJS').callsFake(url => {
44+
onJsLoad(url.split('onload=')[1]);
45+
return Promise.resolve(new Event('load'));
46+
});
47+
48+
auth = await testAuth();
49+
});
50+
51+
function makeGapi(result: unknown, timesout = false): typeof gapi {
52+
const callbackFn = timesout === false ? 'callback' : 'ontimeout';
53+
return ({
54+
load: sinon
55+
.stub()
56+
.callsFake((_name: string, params: Record<string, Function>) =>
57+
params[callbackFn]()
58+
),
59+
iframes: {
60+
getContext: () => result as gapi.iframes.Context
61+
}
62+
} as unknown) as typeof gapi;
63+
}
64+
65+
afterEach(() => {
66+
sinon.restore();
67+
delete AUTH_WINDOW.gapi;
68+
_resetLoader();
69+
});
70+
71+
it('calls gapi.load once it is ready', async () => {
72+
const gapi = makeGapi('context!');
73+
74+
library = gapi;
75+
expect(await _loadGapi(auth)).to.eq('context!');
76+
expect(gapi.load).to.have.been.called;
77+
});
78+
79+
it('resets the gapi.load state', async () => {
80+
AUTH_WINDOW.___jsl = {
81+
H: {
82+
something: {
83+
r: ['requested'],
84+
L: ['loaded', 'test']
85+
}
86+
},
87+
CP: [1, 2, 3, 4]
88+
};
89+
90+
library = makeGapi('iframes');
91+
92+
await _loadGapi(auth);
93+
94+
// Expect deep equality, but *not* pointer equality
95+
expect(AUTH_WINDOW.___jsl.H.something.r).to.eql(
96+
AUTH_WINDOW.___jsl.H.something.L
97+
);
98+
expect(AUTH_WINDOW.___jsl.H.something.r).not.to.eq(
99+
AUTH_WINDOW.___jsl.H.something.L
100+
);
101+
expect(AUTH_WINDOW.___jsl.CP).to.eql([null, null, null, null]);
102+
});
103+
104+
it('returns the cached object without reloading', async () => {
105+
library = makeGapi('test');
106+
107+
expect(await _loadGapi(auth)).to.eq('test');
108+
expect(await _loadGapi(auth)).to.eq('test');
109+
110+
expect(js._loadJS).to.have.been.calledOnce;
111+
});
112+
113+
it('rejects with a network error if load fails', async () => {
114+
library = {} as typeof gapi;
115+
await expect(_loadGapi(auth)).to.be.rejectedWith(
116+
FirebaseError,
117+
'auth/network-request-failed'
118+
);
119+
});
120+
121+
it('rejects with a network error if ontimeout called', async () => {
122+
library = makeGapi(undefined, /* timesout */ true);
123+
await expect(_loadGapi(auth)).to.be.rejectedWith(
124+
FirebaseError,
125+
'auth/network-request-failed'
126+
);
127+
});
128+
129+
it('resets the load promise if the load errors', async () => {
130+
library = {} as typeof gapi;
131+
const firstAttempt = _loadGapi(auth);
132+
await expect(firstAttempt).to.be.rejectedWith(
133+
FirebaseError,
134+
'auth/network-request-failed'
135+
);
136+
expect(_loadGapi(auth)).not.to.eq(firstAttempt);
137+
});
138+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 { AUTH_ERROR_FACTORY, AuthErrorCode } from '../../core/errors';
19+
import { Delay } from '../../core/util/delay';
20+
import { Auth } from '../../model/auth';
21+
import { AUTH_WINDOW } from '../auth_window';
22+
import * as js from '../load_js';
23+
24+
const NETWORK_TIMEOUT = new Delay(30000, 60000);
25+
const LOADJS_CALLBACK_PREFIX = 'iframefcb';
26+
27+
/**
28+
* Reset unlaoded GApi modules. If gapi.load fails due to a network error,
29+
* it will stop working after a retrial. This is a hack to fix this issue.
30+
*/
31+
function resetUnloadedGapiModules(): void {
32+
// Clear last failed gapi.load state to force next gapi.load to first
33+
// load the failed gapi.iframes module.
34+
// Get gapix.beacon context.
35+
const beacon = AUTH_WINDOW.___jsl;
36+
// Get current hint.
37+
if (beacon?.H) {
38+
// Get gapi hint.
39+
for (const hint of Object.keys(beacon.H)) {
40+
// Requested modules.
41+
beacon.H[hint].r = beacon.H[hint].r || [];
42+
// Loaded modules.
43+
beacon.H[hint].L = beacon.H[hint].L || [];
44+
// Set requested modules to a copy of the loaded modules.
45+
beacon.H[hint].r = [...beacon.H[hint].L];
46+
// Clear pending callbacks.
47+
if (beacon.CP) {
48+
for (let i = 0; i < beacon.CP.length; i++) {
49+
// Remove all failed pending callbacks.
50+
beacon.CP[i] = null;
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
function loadGapi(auth: Auth): Promise<gapi.iframes.Context> {
58+
return new Promise<gapi.iframes.Context>((resolve, reject) => {
59+
// Function to run when gapi.load is ready.
60+
function loadGapiIframe(): void {
61+
// The developer may have tried to previously run gapi.load and failed.
62+
// Run this to fix that.
63+
resetUnloadedGapiModules();
64+
gapi.load('gapi.iframes', {
65+
callback: () => {
66+
resolve(gapi.iframes.getContext());
67+
},
68+
ontimeout: () => {
69+
// The above reset may be sufficient, but having this reset after
70+
// failure ensures that if the developer calls gapi.load after the
71+
// connection is re-established and before another attempt to embed
72+
// the iframe, it would work and would not be broken because of our
73+
// failed attempt.
74+
// Timeout when gapi.iframes.Iframe not loaded.
75+
resetUnloadedGapiModules();
76+
reject(
77+
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
78+
appName: auth.name
79+
})
80+
);
81+
},
82+
timeout: NETWORK_TIMEOUT.get()
83+
});
84+
}
85+
86+
if (AUTH_WINDOW.gapi?.iframes?.Iframe) {
87+
// If gapi.iframes.Iframe available, resolve.
88+
resolve(gapi.iframes.getContext());
89+
} else if (!!AUTH_WINDOW.gapi?.load) {
90+
// Gapi loader ready, load gapi.iframes.
91+
loadGapiIframe();
92+
} else {
93+
// Create a new iframe callback when this is called so as not to overwrite
94+
// any previous defined callback. This happens if this method is called
95+
// multiple times in parallel and could result in the later callback
96+
// overwriting the previous one. This would end up with a iframe
97+
// timeout.
98+
const cbName = js._generateCallbackName(LOADJS_CALLBACK_PREFIX);
99+
// GApi loader not available, dynamically load platform.js.
100+
AUTH_WINDOW[cbName] = () => {
101+
// GApi loader should be ready.
102+
if (!!gapi.load) {
103+
loadGapiIframe();
104+
} else {
105+
// Gapi loader failed, throw error.
106+
reject(
107+
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
108+
appName: auth.name
109+
})
110+
);
111+
}
112+
};
113+
// Load GApi loader.
114+
return js._loadJS(`https://apis.google.com/js/api.js?onload=${cbName}`);
115+
}
116+
}).catch(error => {
117+
// Reset cached promise to allow for retrial.
118+
cachedGApiLoader = null;
119+
throw error;
120+
});
121+
}
122+
123+
let cachedGApiLoader: Promise<gapi.iframes.Context> | null = null;
124+
export function _loadGapi(auth: Auth): Promise<gapi.iframes.Context> {
125+
cachedGApiLoader = cachedGApiLoader || loadGapi(auth);
126+
return cachedGApiLoader;
127+
}
128+
129+
export function _resetLoader(): void {
130+
cachedGApiLoader = null;
131+
}

0 commit comments

Comments
 (0)