@portal-hq/webview 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/lib/commonjs/index.js +153 -0
- package/lib/commonjs/loading/index.js +25 -0
- package/lib/commonjs/loading/loader.js +11 -0
- package/lib/esm/index.js +125 -0
- package/lib/esm/loading/index.js +20 -0
- package/lib/esm/loading/loader.js +6 -0
- package/package.json +40 -0
- package/src/index.tsx +196 -0
- package/src/loading/index.tsx +25 -0
- package/src/loading/loader.tsx +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Portal React Native WebView
|
|
2
|
+
|
|
3
|
+
The `@portal-hq/webview` package exports a single `WebView` component for loading dApps within your app with built-in Portal Provider injection.
|
|
4
|
+
|
|
5
|
+
## Dependency linking
|
|
6
|
+
|
|
7
|
+
Because this package uses the `react-native-webview` package (which contain native modules) there is some additional linking required to make it work with your React Native project.
|
|
8
|
+
|
|
9
|
+
Explicitly install the `react-native-webview` package in your project.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn add react-native-webview
|
|
13
|
+
|
|
14
|
+
# OR #
|
|
15
|
+
|
|
16
|
+
npm install --save react-native-webview
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Using the WebView Component
|
|
20
|
+
|
|
21
|
+
To use the WebView component, import it from the `@portal-hq/webview` package and use it as you would the `react-native-webview` package and render the component within your app.
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
import WebView from '@portal-hq/webview'
|
|
25
|
+
|
|
26
|
+
const App = () => {
|
|
27
|
+
return (
|
|
28
|
+
<WebView
|
|
29
|
+
url={'https://app.uniswap.com/'}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Optional Props
|
|
36
|
+
|
|
37
|
+
The `WebView` component two optional props that can be used to customize the behavior of the WebView.
|
|
38
|
+
|
|
39
|
+
#### `onNavigationStateChange`
|
|
40
|
+
|
|
41
|
+
This allows you to provide a callback for when the navigation state changes. This allows you to manage loading states and other navigation events.
|
|
42
|
+
|
|
43
|
+
#### `onSigningRequested`
|
|
44
|
+
|
|
45
|
+
This allows you to provide a callback for when signing is requested. For the most part, this prop is not needed if you have already set up the `PortalProvider` in your app.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
// Node modules.
|
|
39
|
+
const react_1 = __importStar(require("react"));
|
|
40
|
+
const react_native_webview_1 = __importDefault(require("react-native-webview"));
|
|
41
|
+
const react_native_1 = require("react-native");
|
|
42
|
+
const injected_provider_1 = require("@portal-hq/injected-provider");
|
|
43
|
+
const core_1 = require("@portal-hq/core");
|
|
44
|
+
// Relative modules.
|
|
45
|
+
const loading_1 = __importDefault(require("./loading"));
|
|
46
|
+
const Webview = (0, react_1.memo)((0, react_1.forwardRef)(({ onNavigationStateChange, onSigningRequested, url }, webViewRef) => {
|
|
47
|
+
const Webview = () => {
|
|
48
|
+
// Derive context.
|
|
49
|
+
const portal = (0, core_1.usePortal)();
|
|
50
|
+
// Derive refs.
|
|
51
|
+
const canGoBack = (0, react_1.useRef)(false);
|
|
52
|
+
const initialized = (0, react_1.useRef)(false);
|
|
53
|
+
const webView = (0, react_1.useRef)();
|
|
54
|
+
// Derive state.
|
|
55
|
+
const [currentUrl, setCurrentUrl] = (0, react_1.useState)(url);
|
|
56
|
+
const [walletAddress, setWalletAddress] = (0, react_1.useState)();
|
|
57
|
+
const source = (0, react_1.useMemo)(() => ({
|
|
58
|
+
uri: currentUrl,
|
|
59
|
+
}), [currentUrl]);
|
|
60
|
+
const scriptToInject = (0, injected_provider_1.injectionScript)({
|
|
61
|
+
address: walletAddress,
|
|
62
|
+
apiKey: portal.apiKey,
|
|
63
|
+
chainId: portal.chainId,
|
|
64
|
+
gatewayConfig: portal.gatewayConfig,
|
|
65
|
+
});
|
|
66
|
+
const handleBackPress = () => {
|
|
67
|
+
var _a;
|
|
68
|
+
if (react_native_1.Platform.OS === 'android' && canGoBack) {
|
|
69
|
+
(_a = webView.current) === null || _a === void 0 ? void 0 : _a.goBack();
|
|
70
|
+
}
|
|
71
|
+
else if (react_native_1.Platform.OS === 'ios') {
|
|
72
|
+
// Don't need to handle this on IOS I don't think
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
const handleNavigationStateChange = (event) => {
|
|
77
|
+
const { canGoBack: _canGoBack, loading, url } = event;
|
|
78
|
+
if (loading) {
|
|
79
|
+
if (onNavigationStateChange) {
|
|
80
|
+
onNavigationStateChange(event);
|
|
81
|
+
}
|
|
82
|
+
// Set the source
|
|
83
|
+
setCurrentUrl(url);
|
|
84
|
+
canGoBack.current = _canGoBack;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const handlePostMessage = (event) => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const { type, data } = JSON.parse(event.nativeEvent.data);
|
|
89
|
+
const { method, params } = data;
|
|
90
|
+
switch (type) {
|
|
91
|
+
case 'portal_sign':
|
|
92
|
+
yield portal.request(method, params);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Initialize Portal Bindings and set the wallet address
|
|
97
|
+
(0, react_1.useEffect)(() => {
|
|
98
|
+
if (portal && !initialized.current) {
|
|
99
|
+
// Prevent this from running again
|
|
100
|
+
initialized.current = true;
|
|
101
|
+
// Bind to signature messages after signing
|
|
102
|
+
portal.on('portal_signatureReceived', (data) => {
|
|
103
|
+
var _a;
|
|
104
|
+
(_a = webView.current) === null || _a === void 0 ? void 0 : _a.postMessage(JSON.stringify({
|
|
105
|
+
type: 'portal_signatureReceived',
|
|
106
|
+
data,
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
// Bind to signing requests
|
|
110
|
+
if (onSigningRequested) {
|
|
111
|
+
portal.on('portal_signingRequested', onSigningRequested);
|
|
112
|
+
}
|
|
113
|
+
// Store the wallet address in the state
|
|
114
|
+
const storeWalletAddress = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const address = yield portal.address;
|
|
116
|
+
setWalletAddress(address);
|
|
117
|
+
});
|
|
118
|
+
void storeWalletAddress();
|
|
119
|
+
}
|
|
120
|
+
}, [portal]);
|
|
121
|
+
// Clean up Portal Provider bindings
|
|
122
|
+
(0, react_1.useEffect)(() => {
|
|
123
|
+
return () => {
|
|
124
|
+
portal.provider.removeEventListener('portal_signatureReceived');
|
|
125
|
+
portal.provider.removeEventListener('portal_signingRequested', onSigningRequested);
|
|
126
|
+
};
|
|
127
|
+
}, []);
|
|
128
|
+
// Handle Android stuff
|
|
129
|
+
(0, react_1.useEffect)(() => {
|
|
130
|
+
react_native_1.BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
|
131
|
+
return () => {
|
|
132
|
+
react_native_1.BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
// Ensure that the webView ref gets forwarded to the parent component
|
|
136
|
+
(0, react_1.useImperativeHandle)(webViewRef, () => webView.current);
|
|
137
|
+
return (<react_native_1.View style={styles.container}>
|
|
138
|
+
{walletAddress && source && (<react_native_webview_1.default allowsBackForwardNavigationGestures allowsFullscreenVideo={false} allowsInlineMediaPlayback={true} incognito={true} injectedJavaScriptBeforeContentLoaded={scriptToInject} injectedJavaScriptBeforeContentLoadedForMainFrameOnly={true} mediaPlaybackRequiresUserAction={true} onMessage={handlePostMessage} onNavigationStateChange={handleNavigationStateChange} ref={webView} renderLoading={() => <loading_1.default />} source={source} startInLoadingState={true} webviewDebuggingEnabled={true}/>)}
|
|
139
|
+
</react_native_1.View>);
|
|
140
|
+
};
|
|
141
|
+
return <Webview />;
|
|
142
|
+
}), (prevProps, nextProps) => {
|
|
143
|
+
console.log(`prevProps:`, prevProps);
|
|
144
|
+
console.log(`nextProps:`, nextProps);
|
|
145
|
+
// return true
|
|
146
|
+
return prevProps.url === nextProps.url;
|
|
147
|
+
});
|
|
148
|
+
const styles = react_native_1.StyleSheet.create({
|
|
149
|
+
container: {
|
|
150
|
+
flex: 1,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
exports.default = Webview;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// Node modules.
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
// Relative modules.
|
|
10
|
+
const loader_1 = __importDefault(require("./loader"));
|
|
11
|
+
const Loading = () => (<react_native_1.View style={styles.container}>
|
|
12
|
+
<loader_1.default />
|
|
13
|
+
</react_native_1.View>);
|
|
14
|
+
const styles = react_native_1.StyleSheet.create({
|
|
15
|
+
container: {
|
|
16
|
+
alignContent: 'center',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
19
|
+
height: '100%',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
justifyItems: 'center',
|
|
22
|
+
width: '100%',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
exports.default = Loading;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const react_1 = __importDefault(require("react"));
|
|
7
|
+
const react_native_1 = require("react-native");
|
|
8
|
+
const Loader = ({ color = '#3E71F8', size = 'small' }) => {
|
|
9
|
+
return <react_native_1.ActivityIndicator color={color} size={size}/>;
|
|
10
|
+
};
|
|
11
|
+
exports.default = Loader;
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
// Node modules.
|
|
11
|
+
import React, { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
12
|
+
import WebView from 'react-native-webview';
|
|
13
|
+
import { BackHandler, Platform, StyleSheet, View } from 'react-native';
|
|
14
|
+
import { injectionScript, } from '@portal-hq/injected-provider';
|
|
15
|
+
import { usePortal } from '@portal-hq/core';
|
|
16
|
+
// Relative modules.
|
|
17
|
+
import Loading from './loading';
|
|
18
|
+
const Webview = memo(forwardRef(({ onNavigationStateChange, onSigningRequested, url }, webViewRef) => {
|
|
19
|
+
const Webview = () => {
|
|
20
|
+
// Derive context.
|
|
21
|
+
const portal = usePortal();
|
|
22
|
+
// Derive refs.
|
|
23
|
+
const canGoBack = useRef(false);
|
|
24
|
+
const initialized = useRef(false);
|
|
25
|
+
const webView = useRef();
|
|
26
|
+
// Derive state.
|
|
27
|
+
const [currentUrl, setCurrentUrl] = useState(url);
|
|
28
|
+
const [walletAddress, setWalletAddress] = useState();
|
|
29
|
+
const source = useMemo(() => ({
|
|
30
|
+
uri: currentUrl,
|
|
31
|
+
}), [currentUrl]);
|
|
32
|
+
const scriptToInject = injectionScript({
|
|
33
|
+
address: walletAddress,
|
|
34
|
+
apiKey: portal.apiKey,
|
|
35
|
+
chainId: portal.chainId,
|
|
36
|
+
gatewayConfig: portal.gatewayConfig,
|
|
37
|
+
});
|
|
38
|
+
const handleBackPress = () => {
|
|
39
|
+
var _a;
|
|
40
|
+
if (Platform.OS === 'android' && canGoBack) {
|
|
41
|
+
(_a = webView.current) === null || _a === void 0 ? void 0 : _a.goBack();
|
|
42
|
+
}
|
|
43
|
+
else if (Platform.OS === 'ios') {
|
|
44
|
+
// Don't need to handle this on IOS I don't think
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
const handleNavigationStateChange = (event) => {
|
|
49
|
+
const { canGoBack: _canGoBack, loading, url } = event;
|
|
50
|
+
if (loading) {
|
|
51
|
+
if (onNavigationStateChange) {
|
|
52
|
+
onNavigationStateChange(event);
|
|
53
|
+
}
|
|
54
|
+
// Set the source
|
|
55
|
+
setCurrentUrl(url);
|
|
56
|
+
canGoBack.current = _canGoBack;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const handlePostMessage = (event) => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
const { type, data } = JSON.parse(event.nativeEvent.data);
|
|
61
|
+
const { method, params } = data;
|
|
62
|
+
switch (type) {
|
|
63
|
+
case 'portal_sign':
|
|
64
|
+
yield portal.request(method, params);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Initialize Portal Bindings and set the wallet address
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (portal && !initialized.current) {
|
|
71
|
+
// Prevent this from running again
|
|
72
|
+
initialized.current = true;
|
|
73
|
+
// Bind to signature messages after signing
|
|
74
|
+
portal.on('portal_signatureReceived', (data) => {
|
|
75
|
+
var _a;
|
|
76
|
+
(_a = webView.current) === null || _a === void 0 ? void 0 : _a.postMessage(JSON.stringify({
|
|
77
|
+
type: 'portal_signatureReceived',
|
|
78
|
+
data,
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
// Bind to signing requests
|
|
82
|
+
if (onSigningRequested) {
|
|
83
|
+
portal.on('portal_signingRequested', onSigningRequested);
|
|
84
|
+
}
|
|
85
|
+
// Store the wallet address in the state
|
|
86
|
+
const storeWalletAddress = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
87
|
+
const address = yield portal.address;
|
|
88
|
+
setWalletAddress(address);
|
|
89
|
+
});
|
|
90
|
+
void storeWalletAddress();
|
|
91
|
+
}
|
|
92
|
+
}, [portal]);
|
|
93
|
+
// Clean up Portal Provider bindings
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
return () => {
|
|
96
|
+
portal.provider.removeEventListener('portal_signatureReceived');
|
|
97
|
+
portal.provider.removeEventListener('portal_signingRequested', onSigningRequested);
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
// Handle Android stuff
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
|
103
|
+
return () => {
|
|
104
|
+
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
107
|
+
// Ensure that the webView ref gets forwarded to the parent component
|
|
108
|
+
useImperativeHandle(webViewRef, () => webView.current);
|
|
109
|
+
return (<View style={styles.container}>
|
|
110
|
+
{walletAddress && source && (<WebView allowsBackForwardNavigationGestures allowsFullscreenVideo={false} allowsInlineMediaPlayback={true} incognito={true} injectedJavaScriptBeforeContentLoaded={scriptToInject} injectedJavaScriptBeforeContentLoadedForMainFrameOnly={true} mediaPlaybackRequiresUserAction={true} onMessage={handlePostMessage} onNavigationStateChange={handleNavigationStateChange} ref={webView} renderLoading={() => <Loading />} source={source} startInLoadingState={true} webviewDebuggingEnabled={true}/>)}
|
|
111
|
+
</View>);
|
|
112
|
+
};
|
|
113
|
+
return <Webview />;
|
|
114
|
+
}), (prevProps, nextProps) => {
|
|
115
|
+
console.log(`prevProps:`, prevProps);
|
|
116
|
+
console.log(`nextProps:`, nextProps);
|
|
117
|
+
// return true
|
|
118
|
+
return prevProps.url === nextProps.url;
|
|
119
|
+
});
|
|
120
|
+
const styles = StyleSheet.create({
|
|
121
|
+
container: {
|
|
122
|
+
flex: 1,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
export default Webview;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Node modules.
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { StyleSheet, View } from 'react-native';
|
|
4
|
+
// Relative modules.
|
|
5
|
+
import Loader from './loader';
|
|
6
|
+
const Loading = () => (<View style={styles.container}>
|
|
7
|
+
<Loader />
|
|
8
|
+
</View>);
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
container: {
|
|
11
|
+
alignContent: 'center',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
14
|
+
height: '100%',
|
|
15
|
+
justifyContent: 'center',
|
|
16
|
+
justifyItems: 'center',
|
|
17
|
+
width: '100%',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
export default Loading;
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@portal-hq/webview",
|
|
3
|
+
"version": "2.0.13",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "lib/commonjs/index",
|
|
6
|
+
"module": "lib/esm/index",
|
|
7
|
+
"source": "src/index",
|
|
8
|
+
"types": "src/index",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"coverage": "jest --passWithNoTests --coverage",
|
|
15
|
+
"prepare": "yarn prepare:cjs && yarn prepare:esm",
|
|
16
|
+
"prepare:cjs": "tsc --outDir lib/commonjs --module commonjs",
|
|
17
|
+
"prepare:esm": "tsc --outDir lib/esm --module es2015 --target es2015",
|
|
18
|
+
"test": "jest --passWithNoTests"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@portal-hq/core": "^2.0.13",
|
|
22
|
+
"@portal-hq/injected-provider": "^2.0.13",
|
|
23
|
+
"@portal-hq/utils": "^2.0.13"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/jest": "^29.2.0",
|
|
27
|
+
"@types/react": "*",
|
|
28
|
+
"@types/react-native": "*",
|
|
29
|
+
"jest": "^29.2.1",
|
|
30
|
+
"jest-environment-jsdom": "^29.2.2",
|
|
31
|
+
"ts-jest": "^29.0.3",
|
|
32
|
+
"typescript": "*"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "*",
|
|
36
|
+
"react-native": "*",
|
|
37
|
+
"react-native-webview": "*"
|
|
38
|
+
},
|
|
39
|
+
"gitHead": "bd4ff2f92e936f51b1611083d42804980e96bf27"
|
|
40
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Node modules.
|
|
2
|
+
import React, {
|
|
3
|
+
FC,
|
|
4
|
+
MutableRefObject,
|
|
5
|
+
forwardRef,
|
|
6
|
+
memo,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react'
|
|
13
|
+
import WebView, {
|
|
14
|
+
WebViewMessageEvent,
|
|
15
|
+
WebViewNavigation,
|
|
16
|
+
} from 'react-native-webview'
|
|
17
|
+
import { BackHandler, Platform, StyleSheet, View } from 'react-native'
|
|
18
|
+
import {
|
|
19
|
+
injectionScript,
|
|
20
|
+
type RequestArguments,
|
|
21
|
+
} from '@portal-hq/injected-provider'
|
|
22
|
+
import { type WebViewSourceUri } from 'react-native-webview/lib/WebViewTypes'
|
|
23
|
+
import { usePortal } from '@portal-hq/core'
|
|
24
|
+
// Relative modules.
|
|
25
|
+
import Loading from './loading'
|
|
26
|
+
|
|
27
|
+
interface WebviewProps {
|
|
28
|
+
onNavigationStateChange?: (event: WebViewNavigation) => void
|
|
29
|
+
onSigningRequested?: (approvalRequest: any) => void
|
|
30
|
+
url: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const Webview = memo(
|
|
34
|
+
forwardRef<WebView, WebviewProps>(
|
|
35
|
+
({ onNavigationStateChange, onSigningRequested, url }, webViewRef) => {
|
|
36
|
+
const Webview: FC = () => {
|
|
37
|
+
// Derive context.
|
|
38
|
+
const portal = usePortal()
|
|
39
|
+
|
|
40
|
+
// Derive refs.
|
|
41
|
+
const canGoBack = useRef<boolean>(false)
|
|
42
|
+
const initialized = useRef<boolean>(false)
|
|
43
|
+
const webView = useRef<WebView>()
|
|
44
|
+
|
|
45
|
+
// Derive state.
|
|
46
|
+
const [currentUrl, setCurrentUrl] = useState<string>(url)
|
|
47
|
+
const [walletAddress, setWalletAddress] = useState<string>()
|
|
48
|
+
|
|
49
|
+
const source = useMemo(
|
|
50
|
+
(): WebViewSourceUri => ({
|
|
51
|
+
uri: currentUrl,
|
|
52
|
+
}),
|
|
53
|
+
[currentUrl],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const scriptToInject = injectionScript({
|
|
57
|
+
address: walletAddress as string,
|
|
58
|
+
apiKey: portal.apiKey,
|
|
59
|
+
chainId: portal.chainId,
|
|
60
|
+
gatewayConfig: portal.gatewayConfig,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const handleBackPress = (): boolean => {
|
|
64
|
+
if (Platform.OS === 'android' && canGoBack) {
|
|
65
|
+
webView.current?.goBack()
|
|
66
|
+
} else if (Platform.OS === 'ios') {
|
|
67
|
+
// Don't need to handle this on IOS I don't think
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleNavigationStateChange = (event: WebViewNavigation) => {
|
|
73
|
+
const { canGoBack: _canGoBack, loading, url } = event
|
|
74
|
+
|
|
75
|
+
if (loading) {
|
|
76
|
+
if (onNavigationStateChange) {
|
|
77
|
+
onNavigationStateChange(event)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Set the source
|
|
81
|
+
setCurrentUrl(url)
|
|
82
|
+
|
|
83
|
+
canGoBack.current = _canGoBack
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handlePostMessage = async (event: WebViewMessageEvent) => {
|
|
88
|
+
const { type, data } = JSON.parse(event.nativeEvent.data)
|
|
89
|
+
const { method, params } = data as RequestArguments
|
|
90
|
+
|
|
91
|
+
switch (type) {
|
|
92
|
+
case 'portal_sign':
|
|
93
|
+
await portal.request(method, params as unknown[])
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Initialize Portal Bindings and set the wallet address
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (portal && !initialized.current) {
|
|
101
|
+
// Prevent this from running again
|
|
102
|
+
initialized.current = true
|
|
103
|
+
|
|
104
|
+
// Bind to signature messages after signing
|
|
105
|
+
portal.on('portal_signatureReceived', (data: any) => {
|
|
106
|
+
webView.current?.postMessage(
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
type: 'portal_signatureReceived',
|
|
109
|
+
data,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Bind to signing requests
|
|
115
|
+
if (onSigningRequested) {
|
|
116
|
+
portal.on('portal_signingRequested', onSigningRequested)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Store the wallet address in the state
|
|
120
|
+
const storeWalletAddress = async () => {
|
|
121
|
+
const address = await portal.address
|
|
122
|
+
setWalletAddress(address)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
void storeWalletAddress()
|
|
126
|
+
}
|
|
127
|
+
}, [portal])
|
|
128
|
+
|
|
129
|
+
// Clean up Portal Provider bindings
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
return () => {
|
|
132
|
+
portal.provider.removeEventListener('portal_signatureReceived')
|
|
133
|
+
portal.provider.removeEventListener(
|
|
134
|
+
'portal_signingRequested',
|
|
135
|
+
onSigningRequested,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}, [])
|
|
139
|
+
|
|
140
|
+
// Handle Android stuff
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
BackHandler.addEventListener('hardwareBackPress', handleBackPress)
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
BackHandler.removeEventListener(
|
|
146
|
+
'hardwareBackPress',
|
|
147
|
+
handleBackPress,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}, [])
|
|
151
|
+
|
|
152
|
+
// Ensure that the webView ref gets forwarded to the parent component
|
|
153
|
+
useImperativeHandle(webViewRef, () => webView.current as WebView)
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={styles.container}>
|
|
157
|
+
{walletAddress && source && (
|
|
158
|
+
<WebView
|
|
159
|
+
allowsBackForwardNavigationGestures
|
|
160
|
+
allowsFullscreenVideo={false}
|
|
161
|
+
allowsInlineMediaPlayback={true}
|
|
162
|
+
incognito={true}
|
|
163
|
+
injectedJavaScriptBeforeContentLoaded={scriptToInject}
|
|
164
|
+
injectedJavaScriptBeforeContentLoadedForMainFrameOnly={true}
|
|
165
|
+
mediaPlaybackRequiresUserAction={true}
|
|
166
|
+
onMessage={handlePostMessage}
|
|
167
|
+
onNavigationStateChange={handleNavigationStateChange}
|
|
168
|
+
ref={webView as MutableRefObject<WebView>}
|
|
169
|
+
renderLoading={() => <Loading />}
|
|
170
|
+
source={source}
|
|
171
|
+
startInLoadingState={true}
|
|
172
|
+
webviewDebuggingEnabled={true}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
</View>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return <Webview />
|
|
180
|
+
},
|
|
181
|
+
),
|
|
182
|
+
(prevProps, nextProps) => {
|
|
183
|
+
console.log(`prevProps:`, prevProps)
|
|
184
|
+
console.log(`nextProps:`, nextProps)
|
|
185
|
+
// return true
|
|
186
|
+
return prevProps.url === nextProps.url
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const styles = StyleSheet.create({
|
|
191
|
+
container: {
|
|
192
|
+
flex: 1,
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
export default Webview
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Node modules.
|
|
2
|
+
import React, { FC } from 'react'
|
|
3
|
+
import { StyleSheet, View } from 'react-native'
|
|
4
|
+
// Relative modules.
|
|
5
|
+
import Loader from './loader'
|
|
6
|
+
|
|
7
|
+
const Loading: FC = () => (
|
|
8
|
+
<View style={styles.container}>
|
|
9
|
+
<Loader />
|
|
10
|
+
</View>
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
container: {
|
|
15
|
+
alignContent: 'center',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
18
|
+
height: '100%',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
justifyItems: 'center',
|
|
21
|
+
width: '100%',
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export default Loading
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React, { FC } from 'react'
|
|
2
|
+
import { ActivityIndicator } from 'react-native'
|
|
3
|
+
|
|
4
|
+
interface LoaderProps {
|
|
5
|
+
color?: string
|
|
6
|
+
size?: 'large' | 'small'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Loader: FC<LoaderProps> = ({ color = '#3E71F8', size = 'small' }) => {
|
|
10
|
+
return <ActivityIndicator color={color} size={size} />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default Loader
|