Skip to content
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

Support React Native #37

Merged
merged 21 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
544e411
Update build system to support a React Native bundle
smithki Apr 10, 2020
cecf18f
Add initial implementation for React Native (requires e2e testing)
smithki Apr 11, 2020
40d5e2c
Fix CJS builds and rename 'WebViewController' to 'ReactNativeWebViewC…
smithki Apr 11, 2020
60c926f
Remove unnecessary return statement
smithki Apr 11, 2020
063b820
Fix React dependency regex in Webpack config
smithki Apr 11, 2020
9e7e68d
Update CircleCI config
smithki Apr 11, 2020
ac50c28
Re-organize '/core' tests to reflect additional view controllers
smithki Apr 11, 2020
93e2657
Organizational comments
smithki Apr 11, 2020
d2e2021
Remove superfluous comment
smithki Apr 11, 2020
73e6c53
Add first draft CHANGELOG entry for React Native feature
smithki Apr 11, 2020
e6a1957
Update 'isViewReady' condition
smithki Apr 11, 2020
768dc7b
Package 'react-native-webview' and 'whatwg-url' in the RN bundle
smithki Apr 11, 2020
1abad60
Add .vscode to git ignores
smithki Apr 11, 2020
337e6c3
Add /dist to eslint ignores
smithki Apr 11, 2020
c62a6a6
Clean up Webpack config
smithki Apr 11, 2020
520e510
Externalize 'react-native-webview' & fix event listener bug in 'Paylo…
smithki Apr 14, 2020
857e503
Merge branch 'master' into react_native
smithki Apr 17, 2020
a690a39
Merge branch 'master' into react_native
smithki Apr 21, 2020
5385d97
Merge branch 'master' into react_native
smithki Apr 21, 2020
849d1b0
React native unit tests (#49)
smithki Apr 22, 2020
62cedc0
Add some testing cleanups
smithki Apr 22, 2020
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
Prev Previous commit
Next Next commit
Fix CJS builds and rename 'WebViewController' to 'ReactNativeWebViewC…
…ontroller'
  • Loading branch information
smithki committed Apr 15, 2020
commit 40d5e2c359eca4a11bc991bc8e92293ffe4856ea
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules
react-native.js
react-native.d.ts
2 changes: 0 additions & 2 deletions config/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
"declaration": true,
},
"include": [
"../react-native.js",
"../react-native.d.ts",
"../src/**/*.ts",
"../src/**/*.tsx",
"../test/**/*.ts",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"url": "https://github.com/fortmatic/magic-js"
},
"homepage": "https://www.fortmatic.com",
"main": "dist/cjs/magic.js",
"main": "dist/cjs/index.js",
"types": "dist/cjs/index.cjs.d.ts",
"scripts": {
"start": "yarn run clean:build && ./scripts/start.sh",
Expand Down
2 changes: 1 addition & 1 deletion src/constants/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const MAGIC_URL = process.env.MAGIC_URL || 'https://auth.magic.link/';
export const IS_REACT_NATIVE = Boolean(process.env.IS_REACT_NATIVE);
export const MAGIC_URL = IS_REACT_NATIVE ? 'https://mgbox.io/' : process.env.MAGIC_URL || 'https://auth.magic.link/';
export const SDK_NAME = process.env.SDK_NAME!;
export const SDK_VERSION = process.env.SDK_VERSION!;
11 changes: 7 additions & 4 deletions src/core/payload-transport.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { WebViewMessageEvent } from 'react-native-webview';
import {
MagicIncomingWindowMessage,
MagicOutgoingWindowMessage,
Expand All @@ -9,7 +9,7 @@ import { IframeController } from './views/iframe-controller';
import { JsonRpcResponse } from './json-rpc';
import { createModalNotReadyError } from './sdk-exceptions';
import { ViewController } from '../types/core/view-types';
import { WebViewController } from './views/webview-controller';
import { ReactNativeWebViewController } from './views/react-native-webview-controller';
import { IS_REACT_NATIVE } from '../constants/config';

interface RemoveEventListenerFunction {
Expand Down Expand Up @@ -98,7 +98,7 @@ export class PayloadTransport {
): Promise<JsonRpcResponse<ResultType> | JsonRpcResponse<ResultType>[]> {
await overlay.ready;
const iframe = overlay instanceof IframeController ? await overlay.iframe : null;
const webView = overlay instanceof WebViewController ? overlay.webView : null;
const webView = overlay instanceof ReactNativeWebViewController ? overlay.webView : null;

return new Promise((resolve, reject) => {
const isViewReady = IS_REACT_NATIVE ? !!webView : iframe && iframe.contentWindow;
Expand Down Expand Up @@ -171,7 +171,10 @@ export class PayloadTransport {
return () => this.messageHandlers.delete(listener);
}

public handleWebViewMessage(event: WebViewMessageEvent) {
/**
* Route incoming messages from a React Native `<WebView>`.
*/
public handleReactNativeWebViewMessage(event: WebViewMessageEvent) {
if (
event.nativeEvent &&
event.nativeEvent.url === `${this.endpoint}/send/?params=${this.encodedQueryParams}` &&
Expand Down
8 changes: 4 additions & 4 deletions src/core/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { MAGIC_URL, SDK_NAME, SDK_VERSION, IS_REACT_NATIVE } from '../constants/
import { MagicSDKAdditionalConfiguration } from '../types';
import { RPCProviderModule } from '../modules/rpc-provider';
import { ViewController } from '../types/core/view-types';
import { WebViewController } from './views/webview-controller';
import { ReactNativeWebViewController } from './views/react-native-webview-controller';

export class MagicSDK {
private static readonly __transports__: Map<string, PayloadTransport> = new Map();
Expand Down Expand Up @@ -42,7 +42,7 @@ export class MagicSDK {
constructor(public readonly apiKey: string, options?: MagicSDKAdditionalConfiguration) {
if (!apiKey) throw createMissingApiKeyError();

this.endpoint = new URL(options?.endpoint ?? MAGIC_URL).origin;
this.endpoint = IS_REACT_NATIVE ? 'https://mgbox.io/' : new URL(options?.endpoint ?? MAGIC_URL).origin;
this.encodedQueryParams = encodeQueryParameters({
API_KEY: this.apiKey,
DOMAIN_ORIGIN: window.location ? window.location.origin : '',
Expand Down Expand Up @@ -107,7 +107,7 @@ export class MagicSDK {
}

export class MagicSDKReactNative extends MagicSDK {
public get Render() {
return (this.overlay as WebViewController).init;
public get Modal() {
return (this.overlay as ReactNativeWebViewController).Modal;
}
}
4 changes: 2 additions & 2 deletions src/core/views/iframe-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class IframeController {
}

/**
* Initialize the Magic `<iframe>` and pre-load overlay content when DOM
* Initialize the Magic `<iframe>` and pre-load overlay content when the DOM
* is ready.
*/
private init(): Promise<HTMLIFrameElement> {
Expand All @@ -80,7 +80,7 @@ export class IframeController {
}
};

// Check Dom state and load...
// Check DOM state and load...
if (['loaded', 'interactive', 'complete'].includes(document.readyState)) {
onload();
} else {
Expand Down
163 changes: 163 additions & 0 deletions src/core/views/react-native-webview-controller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useState, useCallback, useMemo } from 'react';
import { StyleSheet, View } from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { MagicIncomingWindowMessage } from '../../types';
import { PayloadTransport } from '../payload-transport';

/*

IMPORTANT NOTE:
~~~~~~~~~~~~~~

Do not reference any React dependencies at the top-level of this file! Only
use these dependencies within a closure (such as the body of a class method or
function). We completely remove React dependencies from CJS and CDN bundles,
so referencing these imports will raise a `TypeError`.

*/

/**
* Builds the Magic `<WebView>` overlay styles. These base styles enable
* `<WebView>` UI to render above all other DOM content.
*/
function createWebViewStyles() {
return StyleSheet.create({
'magic-webview': {
flex: 1,
backgroundColor: 'transparent',
},

'webview-container': {
flex: 1,
width: '100%',
backgroundColor: 'transparent',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},

show: {
zIndex: 10000,
},

hide: {
zIndex: -10000,
},
});
}

/**
* Overloads React Native's `<View>` with helper methods we require to hide/show
* the rendered `<WebView>`.
*/
interface ViewWrapper extends Partial<View> {
showOverlay: () => void;
hideOverlay: () => void;
}

/**
* View controller for the Magic `<WebView>` overlay.
*/
export class ReactNativeWebViewController {
public webView: WebView | null;
private container: ViewWrapper | null;
public ready: Promise<void>;
private styles: ReturnType<typeof StyleSheet.create>;

constructor(
private readonly transport: PayloadTransport,
private readonly endpoint: string,
private readonly encodedQueryParams: string,
) {
this.webView = null;
this.container = null;
this.ready = this.waitForReady();
this.styles = createWebViewStyles();
this.listen();
}

/**
* Renders a React Native `<WebView>` with built-in message handling to and
* from the Magic `<iframe>` context.
*/
public Modal: React.FC = () => {
const [show, setShow] = useState(false);

/**
* Saves a reference to the underlying `<WebView>` node so we can interact
* with incoming messages.
*/
const webViewRef = useCallback((webView: WebView): void => {
this.webView = webView;
}, []);

/**
* Saves a reference to the underlying `<View>` node so we can interact with
* display styles.
*/
const containerRef = useCallback((view: View): void => {
this.container = {
...view,
showOverlay,
hideOverlay,
};
}, []);

/**
* Show the Magic `<WebView>` overlay.
*/
const showOverlay = useCallback(() => {
setShow(true);
}, []);

/**
* Hide the Magic `<WebView>` overlay.
*/
const hideOverlay = useCallback(() => {
setShow(false);
}, []);

const containerStyles = useMemo(() => {
return [this.styles['webview-container'], show ? this.styles.show : this.styles.hide];
}, [show]);

const handleWebViewMessage = useCallback((event: WebViewMessageEvent) => {
this.transport.handleReactNativeWebViewMessage(event);
}, []);

return (
<View ref={containerRef} style={containerStyles}>
<WebView
ref={webViewRef}
source={{ uri: `${this.endpoint}/send/?params=${this.encodedQueryParams}` }}
onMessage={handleWebViewMessage}
style={this.styles['magic-webview']}
/>
</View>
);
};

/**
* Set the controller as "ready" for JSON RPC events.
*/
private waitForReady() {
return new Promise<void>(resolve => {
this.transport.on(MagicIncomingWindowMessage.MAGIC_OVERLAY_READY, () => resolve());
});
}

/**
* Listen for messages sent from the underlying Magic `<WebView>`.
*/
private listen() {
this.transport.on(MagicIncomingWindowMessage.MAGIC_HIDE_OVERLAY, () => {
return this.container?.hideOverlay();
});

this.transport.on(MagicIncomingWindowMessage.MAGIC_SHOW_OVERLAY, () => {
return this.container?.showOverlay();
});
}
}
125 changes: 0 additions & 125 deletions src/core/views/webview-controller.tsx

This file was deleted.

Loading