Skip to content

Add gapi and iframe loading libraries #3334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages-exp/auth-exp/src/platform_browser/auth_window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export type AuthWindow = {
} & {
// Any known / named properties we want to add
grecaptcha?: Recaptcha;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
___jsl?: Record<string, any>;
gapi: typeof gapi;
} & {
// A final catch-all for callbacks (which will have random names) that
// we will stick on the window.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright 2020 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

declare namespace gapi {
type LoadCallback = () => void;
interface LoadConfig {}
interface LoadOptions {
callback?: LoadCallback;
timeout?: number;
ontimeout?: LoadCallback;
}
function load(
features: 'gapi.iframes',
options?: LoadOptions | LoadCallback
): void;
}

declare namespace gapi.iframes {
interface Message {
type: string;
}

type IframesFilter = (iframe: Iframe) => boolean;
type MessageHandler<T extends Message> = (
message: T
) => unknown | Promise<void>;
type SendCallback = () => void;
type Callback = (iframe: Iframe) => void;

class Context {
open(
options: Record<string, unknown>,
callback?: Callback
): Promise<Iframe>;
}

class Iframe {
register<T extends Message>(
message: string,
handler: MessageHandler<T>,
filter?: IframesFilter
): void;
ping(callback: SendCallback, data?: unknown): Promise<unknown[]>;
restyle(
style: Record<string, string | boolean>,
callback?: SendCallback
): Promise<unknown[]>;
}

const CROSS_ORIGIN_IFRAMES_FILTER: IframesFilter;

function getContext(): Context;
}
138 changes: 138 additions & 0 deletions packages-exp/auth-exp/src/platform_browser/iframe/gapi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2020 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';

import { FirebaseError } from '@firebase/util';

import { testAuth } from '../../../test/mock_auth';
import { Auth } from '../../model/auth';
import { AUTH_WINDOW } from '../auth_window';
import * as js from '../load_js';
import { _loadGapi, _resetLoader } from './gapi';

use(sinonChai);
use(chaiAsPromised);

describe('src/platform_browser/iframe/gapi', () => {
let library: typeof gapi;
let auth: Auth;
function onJsLoad(globalLoadFnName: string): void {
AUTH_WINDOW.gapi = library as typeof gapi;
AUTH_WINDOW[globalLoadFnName]();
}

beforeEach(async () => {
sinon.stub(js, '_loadJS').callsFake(url => {
onJsLoad(url.split('onload=')[1]);
return Promise.resolve(new Event('load'));
});

auth = await testAuth();
});

function makeGapi(result: unknown, timesout = false): typeof gapi {
const callbackFn = timesout === false ? 'callback' : 'ontimeout';
return ({
load: sinon
.stub()
.callsFake((_name: string, params: Record<string, Function>) =>
params[callbackFn]()
),
iframes: {
getContext: () => result as gapi.iframes.Context
}
} as unknown) as typeof gapi;
}

afterEach(() => {
sinon.restore();
delete AUTH_WINDOW.gapi;
_resetLoader();
});

it('calls gapi.load once it is ready', async () => {
const gapi = makeGapi('context!');

library = gapi;
expect(await _loadGapi(auth)).to.eq('context!');
expect(gapi.load).to.have.been.called;
});

it('resets the gapi.load state', async () => {
AUTH_WINDOW.___jsl = {
H: {
something: {
r: ['requested'],
L: ['loaded', 'test']
}
},
CP: [1, 2, 3, 4]
};

library = makeGapi('iframes');

await _loadGapi(auth);

// Expect deep equality, but *not* pointer equality
expect(AUTH_WINDOW.___jsl.H.something.r).to.eql(
AUTH_WINDOW.___jsl.H.something.L
);
expect(AUTH_WINDOW.___jsl.H.something.r).not.to.eq(
AUTH_WINDOW.___jsl.H.something.L
);
expect(AUTH_WINDOW.___jsl.CP).to.eql([null, null, null, null]);
});

it('returns the cached object without reloading', async () => {
library = makeGapi('test');

expect(await _loadGapi(auth)).to.eq('test');
expect(await _loadGapi(auth)).to.eq('test');

expect(js._loadJS).to.have.been.calledOnce;
});

it('rejects with a network error if load fails', async () => {
library = {} as typeof gapi;
await expect(_loadGapi(auth)).to.be.rejectedWith(
FirebaseError,
'auth/network-request-failed'
);
});

it('rejects with a network error if ontimeout called', async () => {
library = makeGapi(undefined, /* timesout */ true);
await expect(_loadGapi(auth)).to.be.rejectedWith(
FirebaseError,
'auth/network-request-failed'
);
});

it('resets the load promise if the load errors', async () => {
library = {} as typeof gapi;
const firstAttempt = _loadGapi(auth);
await expect(firstAttempt).to.be.rejectedWith(
FirebaseError,
'auth/network-request-failed'
);
expect(_loadGapi(auth)).not.to.eq(firstAttempt);
});
});
131 changes: 131 additions & 0 deletions packages-exp/auth-exp/src/platform_browser/iframe/gapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* @license
* Copyright 2020 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../../core/errors';
import { Delay } from '../../core/util/delay';
import { Auth } from '../../model/auth';
import { AUTH_WINDOW } from '../auth_window';
import * as js from '../load_js';

const NETWORK_TIMEOUT = new Delay(30000, 60000);
const LOADJS_CALLBACK_PREFIX = 'iframefcb';

/**
* Reset unlaoded GApi modules. If gapi.load fails due to a network error,
* it will stop working after a retrial. This is a hack to fix this issue.
*/
function resetUnloadedGapiModules(): void {
// Clear last failed gapi.load state to force next gapi.load to first
// load the failed gapi.iframes module.
// Get gapix.beacon context.
const beacon = AUTH_WINDOW.___jsl;
// Get current hint.
if (beacon?.H) {
// Get gapi hint.
for (const hint of Object.keys(beacon.H)) {
// Requested modules.
beacon.H[hint].r = beacon.H[hint].r || [];
// Loaded modules.
beacon.H[hint].L = beacon.H[hint].L || [];
// Set requested modules to a copy of the loaded modules.
beacon.H[hint].r = [...beacon.H[hint].L];
// Clear pending callbacks.
if (beacon.CP) {
for (let i = 0; i < beacon.CP.length; i++) {
// Remove all failed pending callbacks.
beacon.CP[i] = null;
}
}
}
}
}

function loadGapi(auth: Auth): Promise<gapi.iframes.Context> {
return new Promise<gapi.iframes.Context>((resolve, reject) => {
// Function to run when gapi.load is ready.
function loadGapiIframe(): void {
// The developer may have tried to previously run gapi.load and failed.
// Run this to fix that.
resetUnloadedGapiModules();
gapi.load('gapi.iframes', {
callback: () => {
resolve(gapi.iframes.getContext());
},
ontimeout: () => {
// The above reset may be sufficient, but having this reset after
// failure ensures that if the developer calls gapi.load after the
// connection is re-established and before another attempt to embed
// the iframe, it would work and would not be broken because of our
// failed attempt.
// Timeout when gapi.iframes.Iframe not loaded.
resetUnloadedGapiModules();
reject(
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
appName: auth.name
})
);
},
timeout: NETWORK_TIMEOUT.get()
});
}

if (AUTH_WINDOW.gapi?.iframes?.Iframe) {
// If gapi.iframes.Iframe available, resolve.
resolve(gapi.iframes.getContext());
} else if (!!AUTH_WINDOW.gapi?.load) {
// Gapi loader ready, load gapi.iframes.
loadGapiIframe();
} else {
// Create a new iframe callback when this is called so as not to overwrite
// any previous defined callback. This happens if this method is called
// multiple times in parallel and could result in the later callback
// overwriting the previous one. This would end up with a iframe
// timeout.
const cbName = js._generateCallbackName(LOADJS_CALLBACK_PREFIX);
// GApi loader not available, dynamically load platform.js.
AUTH_WINDOW[cbName] = () => {
// GApi loader should be ready.
if (!!gapi.load) {
loadGapiIframe();
} else {
// Gapi loader failed, throw error.
reject(
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
appName: auth.name
})
);
}
};
// Load GApi loader.
return js._loadJS(`https://apis.google.com/js/api.js?onload=${cbName}`);
}
}).catch(error => {
// Reset cached promise to allow for retrial.
cachedGApiLoader = null;
throw error;
});
}

let cachedGApiLoader: Promise<gapi.iframes.Context> | null = null;
export function _loadGapi(auth: Auth): Promise<gapi.iframes.Context> {
cachedGApiLoader = cachedGApiLoader || loadGapi(auth);
return cachedGApiLoader;
}

export function _resetLoader(): void {
cachedGApiLoader = null;
}
Loading