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
Add initial implementation for React Native (requires e2e testing)
  • Loading branch information
smithki committed Apr 15, 2020
commit cecf18fd07d4b5afc5371b06db0f1e4d2c194b5d
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"@typescript-eslint/await-thenable": 0,
"no-useless-constructor": 0,
"@typescript-eslint/no-useless-constructor": 0,
"@typescript-eslint/no-empty-function": 0
"@typescript-eslint/no-empty-function": 0,
"import/no-extraneous-dependencies": [1, {"devDependencies": true}]
},
"settings": {
"import/resolver": {
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
/.idea
.DS_Store
/dist
/RN
/coverage
/.nyc_output
/package-lock.json
4 changes: 4 additions & 0 deletions config/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"moduleResolution": "node",
"target": "es5",
"strict": true,
"jsx": "react",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
Expand All @@ -17,7 +18,10 @@
"declaration": true,
},
"include": [
"../react-native.js",
"../react-native.d.ts",
"../src/**/*.ts",
"../src/**/*.tsx",
"../test/**/*.ts",
"../webpack/**/*.ts"
],
Expand Down
5 changes: 4 additions & 1 deletion config/tsconfig.cdn.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"outDir": "../dist",
"declaration": false
},
"include": ["../src/**/*.ts"]
"include": [
"../src/**/*.ts",
"../src/**/*.tsx"
]
}
5 changes: 4 additions & 1 deletion config/tsconfig.cjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"rootDir": "../src",
"outDir": "../dist/cjs"
},
"include": ["../src/**/*.ts"]
"include": [
"../src/**/*.ts",
"../src/**/*.tsx"
]
}
7 changes: 5 additions & 2 deletions config/tsconfig.react-native.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"rootDir": "../src",
"outDir": "../RN"
"outDir": "../dist/react-native"
},
"include": ["../src/**/*.ts"]
"include": [
"../src/**/*.ts",
"../src/**/*.tsx"
]
}
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"url": "https://github.com/fortmatic/magic-js"
},
"homepage": "https://www.fortmatic.com",
"main": "dist/cjs/index.js",
"types": "dist/cjs/index.d.ts",
"main": "dist/cjs/magic.js",
"types": "dist/cjs/index.cjs.d.ts",
"scripts": {
"start": "yarn run clean:build && ./scripts/start.sh",
"build": "yarn run clean:build && ./scripts/build.sh",
Expand All @@ -28,6 +28,8 @@
"@ikscodes/prettier-config": "^0.1.0",
"@istanbuljs/nyc-config-typescript": "~0.1.3",
"@types/jsdom": "~12.2.4",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.2",
"@types/sinon": "~7.5.0",
"@types/webpack": "~4.41.0",
"@typescript-eslint/eslint-plugin": "~2.17.0",
Expand All @@ -45,6 +47,9 @@
"npm-run-all": "~4.1.5",
"nyc": "13.1.0",
"prettier": "~1.19.1",
"react": "^16.13.1",
"react-native": "^0.62.2",
"react-native-webview": "^9.1.4",
"rimraf": "~3.0.0",
"sinon": "7.1.1",
"ts-loader": "~6.2.1",
Expand Down
1 change: 1 addition & 0 deletions react-native.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/react-native/index.react-native';
1 change: 1 addition & 0 deletions react-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/react-native');
3 changes: 0 additions & 3 deletions scripts/build.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
#!/usr/bin/env bash

export NODE_ENV=production
export WEBPACK_ENV=production
export BABEL_ENV=production

export MAGIC_URL=https://auth.magic.link
export SDK_NAME=$(node -pe "require('./package.json')['name']")
export SDK_VERSION=$(node -pe "require('./package.json')['version']")
Expand Down
3 changes: 0 additions & 3 deletions scripts/start.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
#!/usr/bin/env bash

export NODE_ENV=development
export WEBPACK_ENV=development
export BABEL_ENV=development

export LOCAL_MAGIC_PORT=3014
export SDK_NAME=$(node -pe "require('./package.json')['name']")
export SDK_VERSION=$(node -pe "require('./package.json')['version']")
Expand Down
1 change: 1 addition & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ echo
echo "Running unit tests..."
echo

export NODE_ENV=test
export SDK_NAME=$(node -pe "require('./package.json')['name']")
export SDK_VERSION=$(node -pe "require('./package.json')['version']")

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(Number(process.env.IS_REACT_NATIVE));
export const IS_REACT_NATIVE = Boolean(process.env.IS_REACT_NATIVE);
export const SDK_NAME = process.env.SDK_NAME!;
export const SDK_VERSION = process.env.SDK_VERSION!;
73 changes: 55 additions & 18 deletions src/core/payload-transport.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import {
MagicIncomingWindowMessage,
MagicOutgoingWindowMessage,
JsonRpcRequestPayload,
MagicMessageEvent,
} from '../types';
import { IframeController } from './iframe-controller';
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 { IS_REACT_NATIVE } from '../constants/config';

interface RemoveEventListenerFunction {
(): void;
Expand Down Expand Up @@ -53,7 +57,7 @@ function standardizeResponse(
}

export class PayloadTransport {
private messageHandlers = new Set<(event: MessageEvent) => any>();
private messageHandlers = new Set<(event: MagicMessageEvent) => any>();

/**
* Create an instance of `PayloadTransport`
Expand All @@ -76,27 +80,40 @@ export class PayloadTransport {
* @param payload - The JSON RPC payload to emit via `window.postMessage`.
*/
public async post<ResultType = any>(
overlay: IframeController,
overlay: ViewController,
msgType: MagicOutgoingWindowMessage,
payload: JsonRpcRequestPayload[],
): Promise<JsonRpcResponse<ResultType>[]>;

public async post<ResultType = any>(
overlay: IframeController,
overlay: ViewController,
msgType: MagicOutgoingWindowMessage,
payload: JsonRpcRequestPayload,
): Promise<JsonRpcResponse<ResultType>>;

public async post<ResultType = any>(
overlay: IframeController,
overlay: ViewController,
msgType: MagicOutgoingWindowMessage,
payload: JsonRpcRequestPayload | JsonRpcRequestPayload[],
): Promise<JsonRpcResponse<ResultType> | JsonRpcResponse<ResultType>[]> {
await overlay.ready;
const iframe = await overlay.iframe;
const iframe = overlay instanceof IframeController ? await overlay.iframe : null;
const webView = overlay instanceof WebViewController ? overlay.webView : null;

return new Promise((resolve, reject) => {
if (iframe.contentWindow) {
const isViewReady = IS_REACT_NATIVE ? !!webView : iframe && iframe.contentWindow;
if (isViewReady) {
const batchData: JsonRpcResponse[] = [];
const batchIds = Array.isArray(payload) ? payload.map(p => p.id) : [];
iframe.contentWindow.postMessage({ msgType: `${msgType}-${this.encodedQueryParams}`, payload }, '*');

if (IS_REACT_NATIVE) {
(webView as any).postMessage(
JSON.stringify({ msgType: `${msgType}-${this.encodedQueryParams}`, payload }),
'*',
);
} else {
iframe!.contentWindow!.postMessage({ msgType: `${msgType}-${this.encodedQueryParams}`, payload }, '*');
}

/** Collect successful RPC responses and resolve. */
const acknowledgeResponse = (removeEventListener: RemoveEventListenerFunction) => (
Expand Down Expand Up @@ -146,29 +163,49 @@ export class PayloadTransport {
// by value. The functionality of this callback is tested within
// `initMessageListener`.
/* istanbul ignore next */
const listener = (event: MessageEvent) => {
const listener = (event: MagicMessageEvent) => {
if (event.data.msgType === `${msgType}-${this.encodedQueryParams}`) boundHandler(event);
};

this.messageHandlers.add(listener);
return () => this.messageHandlers.delete(listener);
}

public handleWebViewMessage(event: WebViewMessageEvent) {
if (
event.nativeEvent &&
event.nativeEvent.url === `${this.endpoint}/send/?params=${this.encodedQueryParams}` &&
typeof event.nativeEvent.data === 'string'
) {
const data: any = JSON.parse(event.nativeEvent.data);
if (data && data.msgType && this.messageHandlers.size) {
// If the response object is undefined, we ensure it's at least an
// empty object before passing to the event listener.
/* eslint-disable-next-line no-param-reassign */
data.response = data.response ?? {};

// Reconstruct event from RN event
const magicEvent: MagicMessageEvent = { data } as MagicMessageEvent;
for (const handler of this.messageHandlers.values()) {
handler(magicEvent);
}
}
}
}

/**
* Initialize the underlying `Window.onmessage` event listener.
*/
private initMessageListener() {
window.addEventListener('message', (event: MessageEvent) => {
if (event.origin === this.endpoint) {
if (event.data && event.data.msgType) {
if (this.messageHandlers.size) {
// If the response object is undefined, we ensure it's at least an
// empty object before passing to the event listener.
/* eslint-disable-next-line no-param-reassign */
event.data.response = event.data.response ?? {};
for (const handler of this.messageHandlers.values()) {
handler(event);
}
if (event.data && event.data.msgType && this.messageHandlers.size) {
// If the response object is undefined, we ensure it's at least an
// empty object before passing to the event listener.
/* eslint-disable-next-line no-param-reassign */
event.data.response = event.data.response ?? {};
for (const handler of this.messageHandlers.values()) {
handler(event);
}
}
}
Expand Down
24 changes: 16 additions & 8 deletions src/core/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import { encodeQueryParameters } from '../util/query-params';
import { createMissingApiKeyError } from './sdk-exceptions';
import { IframeController } from './iframe-controller';
import { IframeController } from './views/iframe-controller';
import { PayloadTransport } from './payload-transport';
import { AuthModule } from '../modules/auth';
import { UserModule } from '../modules/user';
import { MAGIC_URL, SDK_NAME, SDK_VERSION, IS_REACT_NATIVE } from '../constants/config';
import { MagicSDKAdditionalConfiguration } from '../types';
import { RPCProviderModule } from '../modules/rpc-provider';
import { ViewController } from '../types/core/view-types';
import { WebViewController } from './views/webview-controller';

export class MagicSDK {
private static readonly __transports__: Map<string, PayloadTransport> = new Map();
private static readonly __overlays__: Map<string, IframeController> = new Map();
private static readonly __overlays__: Map<string, ViewController> = new Map();

public readonly endpoint: string;
public readonly encodedQueryParams: string;
Expand Down Expand Up @@ -67,7 +69,7 @@ export class MagicSDK {
*
* @internal
*/
private get transport(): PayloadTransport {
protected get transport(): PayloadTransport {
if (!MagicSDK.__transports__.has(this.encodedQueryParams)) {
MagicSDK.__transports__.set(
this.encodedQueryParams,
Expand All @@ -83,12 +85,12 @@ export class MagicSDK {
*
* @internal
*/
private get overlay(): IframeController {
protected get overlay(): ViewController {
if (!MagicSDK.__overlays__.has(this.encodedQueryParams)) {
MagicSDK.__overlays__.set(
this.encodedQueryParams,
new IframeController(this.transport, this.endpoint, this.encodedQueryParams),
);
const controller = IS_REACT_NATIVE
? (undefined as any)
: new IframeController(this.transport, this.endpoint, this.encodedQueryParams);
MagicSDK.__overlays__.set(this.encodedQueryParams, controller);
}

return MagicSDK.__overlays__.get(this.encodedQueryParams)!;
Expand All @@ -103,3 +105,9 @@ export class MagicSDK {
await this.overlay.ready;
}
}

export class MagicSDKReactNative extends MagicSDK {
public get Render() {
return (this.overlay as WebViewController).init;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable no-underscore-dangle */

import { MagicIncomingWindowMessage } from '../types';
import { PayloadTransport } from './payload-transport';
import { createDuplicateIframeWarning } from './sdk-exceptions';
import { MagicIncomingWindowMessage } from '../../types';
import { PayloadTransport } from '../payload-transport';
import { createDuplicateIframeWarning } from '../sdk-exceptions';

/**
* Magic `<iframe>` overlay styles. These base styles enable `<iframe>` UI
Expand Down Expand Up @@ -47,8 +47,8 @@ function checkForSameSrcInstances(encodedQueryParams: string) {
* View controller for the Magic `<iframe>` overlay.
*/
export class IframeController {
public readonly iframe: Promise<HTMLIFrameElement>;
public readonly ready: Promise<void>;
public iframe: Promise<HTMLIFrameElement>;
public ready: Promise<void>;

constructor(
private readonly transport: PayloadTransport,
Expand Down
Loading