Skip to content

Commit 518fb2c

Browse files
committed
gapi iframe code
1 parent 8948954 commit 518fb2c

File tree

6 files changed

+398
-7
lines changed

6 files changed

+398
-7
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
config?: LoadConfig;
24+
timeout?: number;
25+
dotimeout?: LoadCallback;
26+
sync?: boolean;
27+
ontimeout?: LoadCallback;
28+
}
29+
function load(
30+
features: 'gapi.iframes',
31+
options?: LoadOptions | LoadCallback
32+
): void;
33+
}
34+
35+
declare namespace gapi.iframes {
36+
interface OptionsBag {
37+
[key: string]: unknown;
38+
}
39+
interface Message {
40+
type: string;
41+
}
42+
43+
type IframesFilter = (iframe: Iframe) => boolean;
44+
type MessageHandler<T extends Message> = (message: T) => unknown | Promise<void>;
45+
type SendCallback = () => void;
46+
type StyleHandler = (options: OptionsBag) => void;
47+
type RpcFilter = (iframe: Iframe) => boolean | Promise<boolean>;
48+
type Callback = (iframe: Iframe) => void;
49+
type ContextCallback = (iframe: Iframe, done: boolean) => void;
50+
interface Params {
51+
[key: string]: unknown;
52+
}
53+
interface StyleData {
54+
[key: string]: unknown;
55+
}
56+
57+
interface IframeOptions {
58+
iframe: Iframe;
59+
role?: string;
60+
data?: unknown;
61+
isReady: boolean;
62+
}
63+
64+
class Context {
65+
constructor(options?: OptionsBag);
66+
isDisposed(): boolean;
67+
getFrameName(): string;
68+
getWindow(): Window;
69+
getGlobalParam(key: string): unknown;
70+
setGlobalParam(key: string, value: unknown): void;
71+
openChild(options: OptionsBag): Iframe;
72+
open(options: OptionsBag, callback?: Callback): Promise<Iframe>;
73+
getParentIframe(): Iframe;
74+
closeSelf(params?: Params, callback?: ContextCallback): Promise<boolean>;
75+
restyleSelf(
76+
style?: StyleData,
77+
callback?: ContextCallback
78+
): Promise<boolean>;
79+
ready<T extends Message>(
80+
params?: Params,
81+
methods?: { [key: string]: MessageHandler<T> },
82+
callback?: SendCallback,
83+
filter?: IframesFilter
84+
): void;
85+
setCloseSelfFilter(filter: RpcFilter): void;
86+
setRestyleSelfFilter(filteR: RpcFilter): void;
87+
connectIframes(
88+
iframe1Data: IframeOptions,
89+
iframe2Data?: IframeOptions
90+
): void;
91+
addOnConnectHandler(
92+
optionsOrRole: string | OptionsBag,
93+
handler?: (iframe: Iframe, obj: object) => void,
94+
apis?: string[],
95+
filter?: IframesFilter
96+
): void;
97+
removeOnConnectHander(role: string): void;
98+
addOnOpenerHandler(
99+
handler: (iframe: Iframe) => void,
100+
apis?: string[],
101+
filter?: IframesFilter
102+
): void;
103+
}
104+
105+
class Iframe {
106+
constructor(
107+
context: Context,
108+
rpcAddr: string,
109+
frameName: string,
110+
options: OptionsBag
111+
);
112+
isDisposed(): boolean;
113+
getContext(): Context;
114+
getFrameName(): string;
115+
getId(): string;
116+
getParam(key: string): unknown;
117+
setParam(key: string, value: unknown): void;
118+
register<T extends Message>(
119+
message: string,
120+
handler: MessageHandler<T>,
121+
filter?: IframesFilter
122+
): void;
123+
unregister<T extends Message>(
124+
message: string,
125+
hander: MessageHandler<T>
126+
): void;
127+
send(
128+
message: string,
129+
data?: unknown,
130+
callback?: SendCallback,
131+
filter?: IframesFilter
132+
): Promise<unknown[]>;
133+
ping(callback: SendCallback, data?: unknown): Promise<unknown[]>;
134+
applyIframesApi(api: string): void;
135+
getIframeEl(): HTMLIFrameElement;
136+
getSiteEl(): HTMLElement;
137+
setSiteEl(el: HTMLElement): void;
138+
getWindow(): Window;
139+
getOrigin(): string;
140+
close(params?: Params, callback?: SendCallback): Promise<unknown[]>;
141+
restyle(style: StyleData, callback?: SendCallback): Promise<unknown[]>;
142+
registerWasRestyled<T extends Message>(
143+
handler: MessageHandler<T>,
144+
filter?: IframesFilter
145+
): void;
146+
registerWasClosed<T extends Message>(
147+
handler: MessageHandler<T>,
148+
filter?: IframesFilter
149+
): void;
150+
}
151+
152+
const SAME_ORIGIN_IFRAMES_FILTER: IframesFilter;
153+
const CROSS_ORIGIN_IFRAMES_FILTER: IframesFilter;
154+
155+
let selfContext: Context;
156+
157+
function create(
158+
url: string,
159+
where: HTMLElement,
160+
options?: OptionsBag
161+
): HTMLIFrameElement;
162+
function getContext(): Context;
163+
function makeWhiteListIframesFilter(origins: string[]): IframesFilter;
164+
function registerStyle(style: string, handler: StyleHandler): void;
165+
function registerBeforeOpenStyle(style: string, handler: StyleHandler): void;
166+
function getStyle(style: string): StyleHandler;
167+
function getBeforeOpenStyle(style: string): StyleHandler;
168+
function registerIframesApi<T extends Message>(
169+
api: string,
170+
registry: { [key: string]: MessageHandler<T> },
171+
filter?: IframesFilter
172+
): void;
173+
function registerIframesApiHandler<T extends Message>(
174+
api: string,
175+
message: string,
176+
handler: MessageHandler<T>
177+
): void;
178+
}

packages-exp/auth-exp/src/platform_browser/iframe/gapi.test.ts

Whitespace-only changes.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 { _generateCallbackName, _loadJS } 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 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 (gapi?.iframes?.Iframe) {
87+
// If gapi.iframes.Iframe available, resolve.
88+
resolve(gapi.iframes.getContext());
89+
} else if (!!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 = _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 _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+
}

packages-exp/auth-exp/src/platform_browser/iframe/iframe.test.ts

Whitespace-only changes.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 { SDK_VERSION } from '@firebase/app-exp';
19+
import { querystring } from '@firebase/util';
20+
21+
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../../core/errors';
22+
import { Delay } from '../../core/util/delay';
23+
import { Auth } from '../../model/auth';
24+
import { _loadGapi } from './gapi';
25+
26+
const PING_TIMEOUT = new Delay(5000, 15000);
27+
const IFRAME_ATTRIBUTES = {
28+
style: {
29+
position: 'absolute',
30+
top: '-100px',
31+
width: '1px',
32+
height: '1px'
33+
}
34+
};
35+
36+
function getIframeUrl(auth: Auth): string {
37+
const url = `https://${auth.config.authDomain!}/__/auth/iframe`;
38+
39+
const params = {
40+
apiKey: auth.config.apiKey,
41+
appName: auth.name,
42+
v: SDK_VERSION
43+
}
44+
// Can pass 'eid' as one of 'p' (production), 's' (staging), or 't' (test)
45+
// TODO: do we care about frameworks? pass them as fw=
46+
47+
return `${url}?${querystring(params)}`;
48+
}
49+
50+
export async function _openIframe(auth: Auth): Promise<gapi.iframes.Iframe> {
51+
const context = await _loadGapi(auth);
52+
return context.open(
53+
{
54+
where: document.body,
55+
url: getIframeUrl(auth),
56+
messageHandlersFilter: gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER,
57+
attributes: IFRAME_ATTRIBUTES,
58+
dontclear: true
59+
},
60+
(iframe: gapi.iframes.Iframe) => new Promise(async (resolve, reject) => {
61+
await iframe.restyle({
62+
// Prevent iframe from closing on mouse out.
63+
setHideOnLeave: false
64+
});
65+
// Confirm iframe is correctly loaded.
66+
// To fallback on failure, set a timeout.
67+
const networkErrorTimer = setTimeout(() => {
68+
reject(
69+
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
70+
appName: auth.name
71+
})
72+
);
73+
}, PING_TIMEOUT.get());
74+
// Clear timer and resolve pending iframe ready promise.
75+
function clearTimerAndResolve(): void {
76+
clearTimeout(networkErrorTimer);
77+
resolve(iframe);
78+
};
79+
// This returns an IThenable. However the reject part does not call
80+
// when the iframe is not loaded.
81+
iframe.ping(clearTimerAndResolve).then(clearTimerAndResolve, () => {
82+
reject(
83+
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
84+
appName: auth.name
85+
})
86+
);
87+
});
88+
}));
89+
}

0 commit comments

Comments
 (0)