diff --git a/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx b/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx index d4b38eda8c7..1ac25e15493 100644 --- a/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx +++ b/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx @@ -8,7 +8,7 @@ import { TouchableOpacity, View, } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; // External dependencies. import ButtonPrimary from '../../Buttons/Button/variants/ButtonPrimary'; diff --git a/app/components/Base/AnimatedFox/__snapshots__/index.test.tsx.snap b/app/components/Base/AnimatedFox/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..cba50627e80 --- /dev/null +++ b/app/components/Base/AnimatedFox/__snapshots__/index.test.tsx.snap @@ -0,0 +1,1510 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnimatedFox renders correctly and matches snapshot 1`] = ` + + + + + --- + + + +
+ + + + ", + } + } + style={ + { + "flex": 1, + } + } +/> +`; diff --git a/app/components/Base/AnimatedFox/index.test.tsx b/app/components/Base/AnimatedFox/index.test.tsx new file mode 100644 index 00000000000..31d3fde17b3 --- /dev/null +++ b/app/components/Base/AnimatedFox/index.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import AnimatedFox from './'; +import { getTotalMemorySync } from 'react-native-device-info'; + +jest.mock('react-native-device-info', () => ({ + getTotalMemorySync: jest.fn(), +})); + +jest.mock('react-native-sensors', () => ({ + gyroscope: { + subscribe: jest.fn(({ next }) => { + next({ x: 1, y: 2 }); + + return { unsubscribe: jest.fn() }; + }), + }, + setUpdateIntervalForType: jest.fn(), + SensorTypes: { + gyroscope: 'gyroscope', + }, +})); + +describe('AnimatedFox', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders correctly and matches snapshot', () => { + // Mock device memory to ensure consistent environment for snapshot + (getTotalMemorySync as jest.Mock).mockReturnValueOnce( + 3 * 1024 * 1024 * 1024, + ); // Mock 3GB device + + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Base/AnimatedFox/index.tsx b/app/components/Base/AnimatedFox/index.tsx new file mode 100644 index 00000000000..51eb1afc844 --- /dev/null +++ b/app/components/Base/AnimatedFox/index.tsx @@ -0,0 +1,1580 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect, useRef } from 'react'; +import { WebView } from '@metamask/react-native-webview'; +import { + gyroscope, + SensorTypes, + setUpdateIntervalForType, +} from 'react-native-sensors'; +import { getTotalMemorySync } from 'react-native-device-info'; + +interface AnimatedFoxProps { + bgColor: string; +} +const round = (value: number, decimals: number): number => + Number(Math.round(Number(value + 'e' + decimals)) + 'e-' + decimals); + +const styles = { flex: 1 }; + +const AnimatedFox: React.FC = ({ bgColor }) => { + const webviewRef = useRef(null); + const position = useRef({ beta: 0, gamma: 0 }); + + /** + * If a device have lower than 2GB Ram we consider a low end device + * @returns boolean + */ + const isLowEndDevice = () => { + // Total device memory in bytes. + const totalMemory = getTotalMemorySync(); + const oneGigaByte = 1024 * 1024 * 1024; + return totalMemory <= 2 * oneGigaByte; + }; + + useEffect(() => { + const updateInterval = isLowEndDevice() ? 1000 / 30 : 1000 / 60; // 30Hz for low-end, 60Hz for others. + setUpdateIntervalForType(SensorTypes.gyroscope, updateInterval); + + const subscription = gyroscope.subscribe({ + next: ({ x, y }) => { + position.current = { + beta: position.current.beta - round(x * -10, 4), + gamma: position.current.gamma - round(y * -10, 4), + }; + + requestAnimationFrame(() => { + const JS = ` + (function () { + const event = new CustomEvent('nativedeviceorientation', { + detail: { + beta:${position.current.beta}, + gamma:${position.current.gamma} + } + }); + + window.dispatchEvent(event); + })(); + `; + webviewRef.current?.injectJavaScript(JS); + }); + }, + error: () => { + // gyroscope is not available + }, + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + return ( + + + + + --- + + + +
+ + + + `, + }} + javaScriptEnabled + bounces={false} + scrollEnabled={false} + injectedJavaScript={`document.body.style.background="${bgColor}"`} + /> + ); +}; + +AnimatedFox.defaultProps = { + bgColor: 'white', +}; + +export default AnimatedFox; diff --git a/app/components/UI/Fox/index.js b/app/components/UI/Fox/index.js index 8e18da54c6f..8767c05f41f 100644 --- a/app/components/UI/Fox/index.js +++ b/app/components/UI/Fox/index.js @@ -1,7 +1,7 @@ import React, { forwardRef } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import { useTheme } from '../../../util/theme'; import Animated, { useAnimatedStyle, diff --git a/app/components/UI/Ramp/Views/Checkout.tsx b/app/components/UI/Ramp/Views/Checkout.tsx index f532aee5b94..85eb8b52baf 100644 --- a/app/components/UI/Ramp/Views/Checkout.tsx +++ b/app/components/UI/Ramp/Views/Checkout.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { View } from 'react-native'; import { useDispatch } from 'react-redux'; import { parseUrl } from 'query-string'; -import { WebView, WebViewNavigation } from 'react-native-webview'; +import { WebView, WebViewNavigation } from '@metamask/react-native-webview'; import { useNavigation } from '@react-navigation/native'; import { Provider } from '@consensys/on-ramp-sdk'; import { OrderOrderTypeEnum } from '@consensys/on-ramp-sdk/dist/API'; diff --git a/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap b/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap index 8efadd056c1..442f0245b8a 100644 --- a/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap +++ b/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap @@ -1273,59 +1273,1591 @@ exports[`Quotes renders animation on first fetching 1`] = ` } > + + + + --- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ", + } + } style={ [ { + "backgroundColor": "#ffffff", "flex": 1, - "overflow": "hidden", }, undefined, ] } - > - - + />
diff --git a/app/components/UI/Ramp/index.tsx b/app/components/UI/Ramp/index.tsx index 91a26fec925..799fcf3810d 100644 --- a/app/components/UI/Ramp/index.tsx +++ b/app/components/UI/Ramp/index.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; import { Order } from '@consensys/on-ramp-sdk'; import { OrderOrderTypeEnum } from '@consensys/on-ramp-sdk/dist/API'; -import WebView from 'react-native-webview'; +import WebView from '@metamask/react-native-webview'; import AppConstants from '../../../core/AppConstants'; import NotificationManager from '../../../core/NotificationManager'; import { FIAT_ORDER_STATES } from '../../../constants/on-ramp'; diff --git a/app/components/UI/WebviewError/index.js b/app/components/UI/WebviewError/index.js index 0ea91894b55..5665243ee35 100644 --- a/app/components/UI/WebviewError/index.js +++ b/app/components/UI/WebviewError/index.js @@ -4,7 +4,7 @@ import { Image, StyleSheet, View, Text, Platform } from 'react-native'; import StyledButton from '../StyledButton'; import { strings } from '../../../../locales/i18n'; import { fontStyles } from '../../../styles/common'; -import AnimatedFox from '@metamask/react-native-animated-fox'; +import AnimatedFox from '../../Base/AnimatedFox'; import { ThemeContext, mockTheme } from '../../../util/theme'; import Device from '../../../util/device'; import generateTestId from '../../../../wdio/utils/generateTestId'; diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 49b55be4097..0198d6fdf62 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -11,7 +11,7 @@ import { } from 'react-native'; import { isEqual } from 'lodash'; import { withNavigation } from '@react-navigation/compat'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import Icon from 'react-native-vector-icons/FontAwesome'; import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; import BrowserBottomBar from '../../UI/BrowserBottomBar'; diff --git a/app/components/Views/ChoosePassword/index.js b/app/components/Views/ChoosePassword/index.js index 4057e52d9aa..cfa89ab4c41 100644 --- a/app/components/Views/ChoosePassword/index.js +++ b/app/components/Views/ChoosePassword/index.js @@ -57,7 +57,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics'; import { Authentication } from '../../../core'; import AUTHENTICATION_TYPE from '../../../constants/userProperties'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import AnimatedFox from '@metamask/react-native-animated-fox'; +import AnimatedFox from '../../Base/AnimatedFox'; import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse'; diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 2a449c674d4..a7558291ec5 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -47,7 +47,7 @@ import DefaultPreference from 'react-native-default-preference'; import { Authentication } from '../../../core'; import AUTHENTICATION_TYPE from '../../../constants/userProperties'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import AnimatedFox from '@metamask/react-native-animated-fox'; +import AnimatedFox from '../../Base/AnimatedFox'; import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; import { createRestoreWalletNavDetailsNested } from '../RestoreWallet/RestoreWallet'; import { parseVaultValue } from '../../../util/validators'; diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index a94b4179995..7c7599904c6 100644 --- a/app/components/Views/Onboarding/index.js +++ b/app/components/Views/Onboarding/index.js @@ -43,7 +43,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics'; import { withMetricsAwareness } from '../../hooks/useMetrics'; import { Authentication } from '../../../core'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import AnimatedFox from '@metamask/react-native-animated-fox'; +import AnimatedFox from '../../Base/AnimatedFox'; import { OnboardingSelectorIDs } from '../../../../e2e/selectors/Onboarding/Onboarding.selectors'; import Routes from '../../../constants/navigation/Routes'; diff --git a/app/components/Views/ResetPassword/index.js b/app/components/Views/ResetPassword/index.js index d917a16806a..8afcb7af43b 100644 --- a/app/components/Views/ResetPassword/index.js +++ b/app/components/Views/ResetPassword/index.js @@ -49,7 +49,7 @@ import { import { Authentication } from '../../../core'; import AUTHENTICATION_TYPE from '../../../constants/userProperties'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import AnimatedFox from '@metamask/react-native-animated-fox'; +import AnimatedFox from '../../Base/AnimatedFox'; import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; import { recreateVaultWithNewPassword } from '../../../core/Vault'; import Logger from '../../../util/Logger'; diff --git a/app/components/Views/SimpleWebview/__snapshots__/index.test.tsx.snap b/app/components/Views/SimpleWebview/__snapshots__/index.test.tsx.snap index e4b861e5208..8db2c8f4330 100644 --- a/app/components/Views/SimpleWebview/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/SimpleWebview/__snapshots__/index.test.tsx.snap @@ -9,20 +9,11 @@ exports[`SimpleWebview should render correctly 1`] = ` } > `; diff --git a/app/components/Views/SimpleWebview/index.js b/app/components/Views/SimpleWebview/index.js index c9b9dd933b3..5ff7318ca50 100644 --- a/app/components/Views/SimpleWebview/index.js +++ b/app/components/Views/SimpleWebview/index.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { View } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import { getWebviewNavbar } from '../../UI/Navbar'; import Share from 'react-native-share'; // eslint-disable-line import/default import Logger from '../../../util/Logger'; diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx b/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx index 601b18837cd..1b9d97bb4f5 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { SafeAreaView, StyleSheet, View } from 'react-native'; import AntDesignIcon from 'react-native-vector-icons/AntDesign'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import type { NetworkState } from '@metamask/network-controller'; import Text, { diff --git a/app/core/BackgroundBridge/Port.ts b/app/core/BackgroundBridge/Port.ts index dd841df9516..83249bc413b 100644 --- a/app/core/BackgroundBridge/Port.ts +++ b/app/core/BackgroundBridge/Port.ts @@ -23,9 +23,7 @@ class Port extends EventEmitter { const js = this._isMainFrame ? JS_POST_MESSAGE_TO_PROVIDER(msg, origin) : JS_IFRAME_POST_MESSAGE_TO_PROVIDER(msg, origin); - if (this._window.webViewRef?.current) { - this._window?.injectJavaScript(js); - } + this._window?.injectJavaScript(js); }; } diff --git a/app/lib/ppom/PPOMView.tsx b/app/lib/ppom/PPOMView.tsx index 718d2a24438..f061caad6e0 100644 --- a/app/lib/ppom/PPOMView.tsx +++ b/app/lib/ppom/PPOMView.tsx @@ -1,6 +1,6 @@ import React, { Component, RefObject } from 'react'; import { StyleSheet, View } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import createInvoke from 'react-native-webview-invoke/native'; import { fromByteArray } from 'react-native-quick-base64'; diff --git a/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap b/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap index 66d7efb75a2..d3de6215cac 100644 --- a/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap +++ b/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap @@ -11,49 +11,21 @@ exports[`PPOMView should render correctly deeply 1`] = ` } > - Webpack App", } - useSharedProcessPool={true} - /> - + } + /> `; diff --git a/app/lib/snaps/SnapsExecutionWebView.tsx b/app/lib/snaps/SnapsExecutionWebView.tsx index 4959a6cce2a..5cee96bc182 100644 --- a/app/lib/snaps/SnapsExecutionWebView.tsx +++ b/app/lib/snaps/SnapsExecutionWebView.tsx @@ -4,10 +4,10 @@ ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import React, { Component, RefObject } from 'react'; import { View, ScrollView, NativeSyntheticEvent } from 'react-native'; -import WebView, { WebViewMessageEvent } from 'react-native-webview'; +import WebView, { WebViewMessageEvent } from '@metamask/react-native-webview'; import { createStyles } from './styles'; import { WebViewInterface } from '@metamask/snaps-controllers/dist/types/services/webview/WebViewMessageStream'; -import { WebViewError } from 'react-native-webview/lib/WebViewTypes'; +import { WebViewError } from '@metamask/react-native-webview/lib/WebViewTypes'; import { PostMessageEvent } from '@metamask/post-message-stream'; const styles = createStyles(); diff --git a/app/util/test/testSetup.js b/app/util/test/testSetup.js index 78c88376375..3b8b49ba068 100644 --- a/app/util/test/testSetup.js +++ b/app/util/test/testSetup.js @@ -20,6 +20,20 @@ jest.mock('react-native', () => { return originalModule; }); +/* + * NOTE: react-native-webview requires a jest mock starting on v12. + * More info on https://github.com/react-native-webview/react-native-webview/issues/2934 + */ +jest.mock('@metamask/react-native-webview', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const { View } = require('react-native'); + const WebView = (props) => ; + + return { + WebView, + }; +}); + jest.mock('../../lib/snaps/preinstalled-snaps'); const mockFs = { diff --git a/e2e/utils/Matchers.js b/e2e/utils/Matchers.js index 413a67c3433..e9522b6d2a1 100644 --- a/e2e/utils/Matchers.js +++ b/e2e/utils/Matchers.js @@ -73,15 +73,29 @@ class Matchers { return element(by.id(childElement).withAncestor(by.id(parentElement))); } + /** + * Get Native WebView instance by elementId + * + * Because Android Webview might have more that one WebView instance present on the main activity, the correct element + * is select based on its parent element id. + * @param {string} elementId The web ID of the browser webview + * @returns {Detox.WebViewElement} WebView element + */ + static getWebViewByID(elementId) { + return device.getPlatform() === 'ios' + ? web(by.id(elementId)) + : web(by.type('android.webkit.WebView').withAncestor(by.id(elementId))); + } + /** * Get element by web ID. * - * * @param {string} webviewID - The web ID of the inner element to locate within the webview - * @param {string} innerID - The web ID of the browser webview + * @param {string} webviewID - The web ID of the inner element to locate within the webview + * @param {string} innerID - The web ID of the browser webview * @return {Promise} Resolves to the located element */ static async getElementByWebID(webviewID, innerID) { - const myWebView = web(by.id(webviewID)); + const myWebView = this.getWebViewByID(webviewID); return myWebView.element(by.web.id(innerID)); } @@ -101,15 +115,11 @@ class Matchers { * Get element by XPath. * @param {string} webviewID - The web ID of the browser webview * @param {string} xpath - XPath expression to locate the element - * @param {number} index - index to locate the webview (iOS only) * @return {Promise} - Resolves to the located element */ - static async getElementByXPath(webviewID, xpath, index = 0) { - const myWebView = - device.getPlatform() === 'ios' - ? web(by.id(webviewID)).atIndex(index) - : web(by.id(webviewID)); - return myWebView.element(by.web.xpath(xpath)).atIndex(0); + static async getElementByXPath(webviewID, xpath) { + const myWebView = this.getWebViewByID(webviewID); + return myWebView.element(by.web.xpath(xpath)); } /** * Get element by href. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f3c1ddfaecb..b0636a31545 100755 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -500,7 +500,7 @@ PODS: - React-Core - react-native-view-shot (3.1.2): - React - - react-native-webview (11.13.0): + - react-native-webview-mm (14.0.1): - React-Core - React-perflogger (0.71.15) - React-RCTActionSheet (0.71.15): @@ -762,7 +762,7 @@ DEPENDENCIES: - "react-native-splash-screen (from `../node_modules/@metamask/react-native-splash-screen`)" - react-native-video (from `../node_modules/react-native-video`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) - - react-native-webview (from `../node_modules/react-native-webview`) + - "react-native-webview-mm (from `../node_modules/@metamask/react-native-webview`)" - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -939,8 +939,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-video" react-native-view-shot: :path: "../node_modules/react-native-view-shot" - react-native-webview: - :path: "../node_modules/react-native-webview" + react-native-webview-mm: + :path: "../node_modules/@metamask/react-native-webview" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -1028,7 +1028,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Base64: cecfb41a004124895a7bcee567a89bae5a89d49b - boost: 7dcd2de282d72e344012f7d6564d024930a6a440 + boost: 57d2868c099736d80fcd648bf211b4431e51a558 Branch: 4ac024cb3c29b0ef628048694db3c4cfa679beb0 BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 @@ -1104,7 +1104,7 @@ SPEC CHECKSUMS: react-native-splash-screen: 49a7160705f32169d27ab8dff9dda53331592412 react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c - react-native-webview: 133a6a5149f963259646e710b4545c67ef35d7c9 + react-native-webview-mm: 067bec145f395c6c46c598c8721f557dc3deafcd React-perflogger: 0cc42978a483a47f3696171dac2e7033936fc82d React-RCTActionSheet: ea922b476d24f6d40b8e02ac3228412bd3637468 React-RCTAnimation: 7be2c148398eaa5beac950b2b5ec7102389ec3ad diff --git a/jest.config.js b/jest.config.js index 2cbdf35c327..8f6df8f69fb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -31,6 +31,7 @@ const config = { '/app/util/testUtils/', '/app/lib/ppom/ppom.html.js', '/app/lib/ppom/blockaid-version.js', + '/app/core/InpageBridgeWeb3.js', ], coverageReporters: ['text-summary', 'lcov'], coverageDirectory: '/tests/coverage', diff --git a/package.json b/package.json index 552e94579b0..4fe7634522e 100644 --- a/package.json +++ b/package.json @@ -168,11 +168,11 @@ "@metamask/ppom-validator": "0.31.0", "@metamask/preferences-controller": "^11.0.0", "@metamask/react-native-actionsheet": "2.4.2", - "@metamask/react-native-animated-fox": "^2.1.0", "@metamask/react-native-button": "^3.0.0", "@metamask/react-native-payments": "^2.0.0", "@metamask/react-native-search-api": "1.0.1", "@metamask/react-native-splash-screen": "^3.2.0", + "@metamask/react-native-webview": "^14.0.1", "@metamask/rpc-errors": "^6.2.1", "@metamask/scure-bip39": "^2.1.0", "@metamask/sdk-communication-layer": "^0.20.2", @@ -326,7 +326,6 @@ "react-native-vector-icons": "6.4.2", "react-native-video": "5.2.1", "react-native-view-shot": "^3.1.2", - "react-native-webview": "11.13.0", "react-native-webview-invoke": "^0.6.2", "react-redux": "^8.1.3", "readable-stream": "2.3.7", diff --git a/patches/react-native-webview+11.13.0.patch b/patches/react-native-webview+11.13.0.patch deleted file mode 100644 index d613485d404..00000000000 --- a/patches/react-native-webview+11.13.0.patch +++ /dev/null @@ -1,1401 +0,0 @@ -diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/CustomCookieJar.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/CustomCookieJar.java -new file mode 100644 -index 0000000..f4a6af9 ---- /dev/null -+++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/CustomCookieJar.java -@@ -0,0 +1,78 @@ -+package com.reactnativecommunity.webview; -+import android.util.Log; -+import android.webkit.CookieManager; -+import android.webkit.ValueCallback; -+import java.net.HttpURLConnection; -+import java.net.URL; -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import okhttp3.Cookie; -+import okhttp3.CookieJar; -+import okhttp3.HttpUrl; -+ -+class CustomCookieJar implements CookieJar { -+ private Worker worker; -+ private CookieManager cookieManager; -+ -+ public CustomCookieJar() { -+ worker = new Worker(); -+ cookieManager = this.getCookieManager(); -+ } -+ -+ private CookieManager getCookieManager() { -+ CookieManager cookieManager = CookieManager.getInstance(); -+ cookieManager.setAcceptCookie(true); -+ return cookieManager; -+ } -+ -+ @Override -+ public void saveFromResponse(HttpUrl url, List cookies) { -+ worker.execute(() -> { -+ try { -+ -+ for (Cookie cookie : cookies) { -+ String _url = url.toString(); -+ String _cookie = cookie.toString(); -+ cookieManager.setCookie(_url, _cookie, new ValueCallback() { -+ @Override -+ public void onReceiveValue(Boolean value) {} -+ }); -+ cookieManager.flush(); -+ } -+ } catch (Exception e) { -+ e.printStackTrace(); -+ } -+ }); -+ } -+ -+ @Override -+ public List loadForRequest(HttpUrl httpUrl) { -+ List cookieList = new ArrayList(); -+ try { -+ -+ if (cookieManager.hasCookies()) { -+ String response = cookieManager.getCookie(httpUrl.toString()); -+ -+ if (response != null) { -+ String[] browserCookies = response.split(";"); -+ -+ for (String cookieStr : browserCookies) { -+ Cookie cookie = Cookie.parse(httpUrl, cookieStr); -+ if (cookie == null) { -+ continue; -+ } -+ cookieList.add(cookie); -+ } -+ } -+ -+ } -+ return cookieList; -+ } catch (Exception e) { -+ e.printStackTrace(); -+ return cookieList; -+ } -+ } -+} -diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -index f743bbc..b520532 100644 ---- a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -+++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -@@ -5,6 +5,7 @@ import android.annotation.TargetApi; - import android.app.Activity; - import android.app.DownloadManager; - import android.content.Context; -+import android.content.Intent; - import android.content.pm.ActivityInfo; - import android.content.pm.PackageManager; - import android.graphics.Bitmap; -@@ -14,6 +15,7 @@ import android.net.http.SslError; - import android.net.Uri; - import android.os.Build; - import android.os.Environment; -+import android.os.Handler; - import android.os.Message; - import android.os.SystemClock; - import android.text.TextUtils; -@@ -24,12 +26,17 @@ import android.view.View; - import android.view.ViewGroup; - import android.view.ViewGroup.LayoutParams; - import android.view.WindowManager; -+import android.view.inputmethod.InputMethodManager; - import android.webkit.ConsoleMessage; - import android.webkit.CookieManager; - import android.webkit.DownloadListener; - import android.webkit.GeolocationPermissions; - import android.webkit.JavascriptInterface; -+import android.webkit.JsPromptResult; -+import android.webkit.JsResult; - import android.webkit.RenderProcessGoneDetail; -+import android.webkit.ServiceWorkerClient; -+import android.webkit.ServiceWorkerController; - import android.webkit.SslErrorHandler; - import android.webkit.PermissionRequest; - import android.webkit.URLUtil; -@@ -40,6 +47,7 @@ import android.webkit.WebResourceResponse; - import android.webkit.WebSettings; - import android.webkit.WebView; - import android.webkit.WebViewClient; -+import android.widget.Button; - import android.widget.FrameLayout; - - import androidx.annotation.Nullable; -@@ -88,18 +96,54 @@ import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent; - import org.json.JSONException; - import org.json.JSONObject; - -+import java.io.ByteArrayInputStream; -+import java.io.IOException; -+import java.io.InputStream; - import java.io.UnsupportedEncodingException; -+import java.lang.reflect.Field; -+import java.net.CookieStore; -+import java.net.HttpCookie; -+import java.net.HttpURLConnection; - import java.net.MalformedURLException; -+import java.net.URI; -+import java.net.URISyntaxException; - import java.net.URL; - import java.net.URLEncoder; -+import java.nio.charset.Charset; -+import java.nio.charset.StandardCharsets; -+import java.nio.charset.UnsupportedCharsetException; -+import java.text.Bidi; - import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collection; - import java.util.Collections; - import java.util.HashMap; -+import java.util.HashSet; - import java.util.List; - import java.util.Locale; - import java.util.Map; -+import java.util.Objects; -+import java.util.Set; - import java.util.concurrent.atomic.AtomicReference; - -+import okhttp3.MediaType; -+import okhttp3.OkHttpClient; -+import okhttp3.Request; -+import okhttp3.RequestBody; -+import okhttp3.Response; -+ -+import android.view.inputmethod.BaseInputConnection; -+import android.view.inputmethod.EditorInfo; -+import android.view.inputmethod.InputConnection; -+ -+ -+import android.content.DialogInterface; -+import android.os.Bundle; -+import android.widget.Toast; -+ -+import androidx.appcompat.app.AlertDialog; -+import androidx.appcompat.app.AppCompatActivity; -+ - /** - * Manages instances of {@link WebView} - *

-@@ -137,13 +181,19 @@ public class RNCWebViewManager extends SimpleViewManager { - public static final int COMMAND_LOAD_URL = 7; - public static final int COMMAND_FOCUS = 8; - -+ protected static final String MIME_UNKNOWN = "application/octet-stream"; -+ protected static final String HTML_ENCODING = "UTF-8"; -+ protected static final long BYTES_IN_MEGABYTE = 1000000; -+ - // android commands - public static final int COMMAND_CLEAR_FORM_DATA = 1000; - public static final int COMMAND_CLEAR_CACHE = 1001; - public static final int COMMAND_CLEAR_HISTORY = 1002; - - protected static final String REACT_CLASS = "RNCWebView"; -- protected static final String HTML_ENCODING = "UTF-8"; -+ -+ protected static final String HEADER_CONTENT_TYPE = "content-type"; -+ - protected static final String HTML_MIME_TYPE = "text/html"; - protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView"; - protected static final String HTTP_METHOD_POST = "POST"; -@@ -155,13 +205,19 @@ public class RNCWebViewManager extends SimpleViewManager { - - protected RNCWebChromeClient mWebChromeClient = null; - protected boolean mAllowsFullscreenVideo = false; -- protected @Nullable String mUserAgent = null; -- protected @Nullable String mUserAgentWithApplicationName = null; -+ protected @Nullable String RNUserAgent = null; -+ protected @Nullable String RNUserAgentWithApplicationName = null; -+ protected static String deviceUserAgent; -+ -+ protected static OkHttpClient httpClient; - - public RNCWebViewManager() { -- mWebViewConfig = new WebViewConfig() { -- public void configWebView(WebView webView) { -- } -+ mWebViewConfig = webView -> { -+ httpClient = new OkHttpClient.Builder() -+ .cookieJar(new CustomCookieJar()) -+ .followRedirects(false) -+ .followSslRedirects(false) -+ .build(); - }; - } - -@@ -182,6 +238,7 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - protected WebView createViewInstance(ThemedReactContext reactContext) { - RNCWebView webView = createRNCWebViewInstance(reactContext); -+ deviceUserAgent = webView.getSettings().getUserAgentString(); - setupWebChromeClient(reactContext, webView); - reactContext.addLifecycleEventListener(webView); - mWebViewConfig.configWebView(webView); -@@ -209,47 +266,161 @@ public class RNCWebViewManager extends SimpleViewManager { - } - - webView.setDownloadListener(new DownloadListener() { -+ protected ReactContext mReactContext; -+ - public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - webView.setIgnoreErrFailedForThisURL(url); -+ this.mReactContext = reactContext; - - RNCWebViewModule module = getModule(reactContext); -- - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); -- - String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); -- String downloadMessage = "Downloading " + fileName; -+ //Filename validation checking for files that use RTL characters and do not allow those types -+ if(fileName == null || (fileName != null && (new Bidi(fileName, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isMixed()))) { -+ Toast.makeText(mReactContext, "Invalid filename or type", Toast.LENGTH_SHORT).show(); -+ } else { -+ AlertDialog.Builder builder = new AlertDialog.Builder(mReactContext); -+ builder.setMessage("Do you want to download \n" + fileName + "?"); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Download", new DialogInterface.OnClickListener() { -+ public void onClick(DialogInterface dialog, int which) { -+ String downloadMessage = "Downloading " + fileName; -+ -+ //Attempt to add cookie, if it exists -+ URL urlObj = null; -+ try { -+ urlObj = new URL(url); -+ String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost(); -+ String cookie = CookieManager.getInstance().getCookie(baseUrl); -+ request.addRequestHeader("Cookie", cookie); -+ } catch (MalformedURLException e) { -+ System.out.println("Error getting cookie for DownloadManager: " + e.toString()); -+ e.printStackTrace(); -+ } - -- //Attempt to add cookie, if it exists -- URL urlObj = null; -- try { -- urlObj = new URL(url); -- String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost(); -- String cookie = CookieManager.getInstance().getCookie(baseUrl); -- request.addRequestHeader("Cookie", cookie); -- } catch (MalformedURLException e) { -- System.out.println("Error getting cookie for DownloadManager: " + e.toString()); -- e.printStackTrace(); -+ //Finish setting up request -+ request.addRequestHeader("User-Agent", userAgent); -+ request.setTitle(fileName); -+ request.setDescription(downloadMessage); -+ request.allowScanningByMediaScanner(); -+ request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); -+ request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); -+ module.setDownloadRequest(request); -+ if (module.grantFileDownloaderPermissions()) { -+ module.downloadFile(); -+ } -+ } -+ }); -+ builder.setNegativeButton("Cancel", (DialogInterface.OnClickListener) (dialog, which) -> { -+ return; -+ }); -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); - } -+ } -+ }); - -- //Finish setting up request -- request.addRequestHeader("User-Agent", userAgent); -- request.setTitle(fileName); -- request.setDescription(downloadMessage); -- request.allowScanningByMediaScanner(); -- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); -- request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -+ ServiceWorkerController swController = ServiceWorkerController.getInstance(); -+ swController.setServiceWorkerClient(new ServiceWorkerClient() { -+ @Override -+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { -+ String method = request.getMethod(); - -- module.setDownloadRequest(request); -+ if (method.equals("GET")) { -+ WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, false, webView); -+ if (response != null) { -+ return response; -+ } -+ } - -- if (module.grantFileDownloaderPermissions()) { -- module.downloadFile(); -+ return super.shouldInterceptRequest(request); - } -- } -- }); -+ }); -+ } - - return webView; - } - -+ private Boolean urlStringLooksInvalid(String urlString) { -+ return urlString == null || -+ urlString.trim().equals("") || -+ !(urlString.startsWith("http") && !urlString.startsWith("www")) || -+ urlString.contains("|"); -+ } -+ -+ public static Boolean responseRequiresJSInjection(Response response) { -+ if (response.isRedirect()) { -+ return false; -+ } -+ final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, MIME_UNKNOWN); -+ final int responseCode = response.code(); -+ -+ boolean contentTypeIsHtml = contentTypeAndCharset.startsWith(HTML_MIME_TYPE); -+ boolean responseCodeIsInjectible = responseCode == 200; -+ String responseBody = ""; -+ -+ if (contentTypeIsHtml && responseCodeIsInjectible) { -+ try { -+ assert response.body() != null; -+ responseBody = response.peekBody(BYTES_IN_MEGABYTE).string(); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ return false; -+ } -+ -+ -+ boolean responseBodyContainsHTMLLikeString = responseBody.matches("[\\S\\s]*<[a-z]+[\\S\\s]*>[\\S\\s]*"); -+ return responseBodyContainsHTMLLikeString; -+ } else { -+ return false; -+ } -+ } -+ -+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, RNCWebView webView) { -+ Uri url = request.getUrl(); -+ String urlStr = url.toString(); -+ -+ if (onlyMainFrame && !request.isForMainFrame() || -+ urlStringLooksInvalid(urlStr)) { -+ return null; -+ } -+ -+ String _userAgent; -+ -+ if (RNUserAgent != null) { -+ _userAgent = RNUserAgent; -+ } else { -+ _userAgent = deviceUserAgent; -+ } -+ -+ try { -+ Request req = new Request.Builder() -+ .url(urlStr) -+ .header("User-Agent", _userAgent) -+ .build(); -+ -+ Response response = httpClient.newCall(req).execute(); -+ -+ if (!responseRequiresJSInjection(response)) { -+ return null; -+ } -+ -+ InputStream is = response.body().byteStream(); -+ MediaType contentType = response.body().contentType(); -+ Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; -+ -+ RNCWebView reactWebView = (RNCWebView) webView; -+ if (response.code() == HttpURLConnection.HTTP_OK) { -+ is = new InputStreamWithInjectedJS(is, reactWebView.injectedJSBeforeContentLoaded, charset); -+ } -+ -+ return new WebResourceResponse("text/html", charset.name(), is); -+ } catch (IOException e) { -+ return null; -+ } -+ } -+ - @ReactProp(name = "javaScriptEnabled") - public void setJavaScriptEnabled(WebView view, boolean enabled) { - view.getSettings().setJavaScriptEnabled(enabled); -@@ -285,13 +456,10 @@ public class RNCWebViewManager extends SimpleViewManager { - if (enabled) { - Context ctx = view.getContext(); - if (ctx != null) { -- view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath()); - view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); -- view.getSettings().setAppCacheEnabled(true); - } - } else { - view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); -- view.getSettings().setAppCacheEnabled(false); - } - } - -@@ -327,12 +495,12 @@ public class RNCWebViewManager extends SimpleViewManager { - public void setLayerType(WebView view, String layerTypeString) { - int layerType = View.LAYER_TYPE_NONE; - switch (layerTypeString) { -- case "hardware": -- layerType = View.LAYER_TYPE_HARDWARE; -- break; -- case "software": -- layerType = View.LAYER_TYPE_SOFTWARE; -- break; -+ case "hardware": -+ layerType = View.LAYER_TYPE_HARDWARE; -+ break; -+ case "software": -+ layerType = View.LAYER_TYPE_SOFTWARE; -+ break; - } - view.setLayerType(layerType, null); - } -@@ -387,9 +555,9 @@ public class RNCWebViewManager extends SimpleViewManager { - @ReactProp(name = "userAgent") - public void setUserAgent(WebView view, @Nullable String userAgent) { - if (userAgent != null) { -- mUserAgent = userAgent; -+ RNUserAgent = userAgent; - } else { -- mUserAgent = null; -+ RNUserAgent = null; - } - this.setUserAgentString(view); - } -@@ -399,19 +567,19 @@ public class RNCWebViewManager extends SimpleViewManager { - if(applicationName != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - String defaultUserAgent = WebSettings.getDefaultUserAgent(view.getContext()); -- mUserAgentWithApplicationName = defaultUserAgent + " " + applicationName; -+ RNUserAgentWithApplicationName = defaultUserAgent + " " + applicationName; - } - } else { -- mUserAgentWithApplicationName = null; -+ RNUserAgentWithApplicationName = null; - } - this.setUserAgentString(view); - } - - protected void setUserAgentString(WebView view) { -- if(mUserAgent != null) { -- view.getSettings().setUserAgentString(mUserAgent); -- } else if(mUserAgentWithApplicationName != null) { -- view.getSettings().setUserAgentString(mUserAgentWithApplicationName); -+ if(RNUserAgent != null) { -+ view.getSettings().setUserAgentString(RNUserAgent); -+ } else if(RNUserAgentWithApplicationName != null) { -+ view.getSettings().setUserAgentString(RNUserAgentWithApplicationName); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - // handle unsets of `userAgent` prop as long as device is >= API 17 - view.getSettings().setUserAgentString(WebSettings.getDefaultUserAgent(view.getContext())); -@@ -490,7 +658,6 @@ public class RNCWebViewManager extends SimpleViewManager { - - // Disable caching - view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); -- view.getSettings().setAppCacheEnabled(false); - view.clearHistory(); - view.clearCache(true); - -@@ -842,13 +1009,116 @@ public class RNCWebViewManager extends SimpleViewManager { - } - } - -- protected static class RNCWebViewClient extends WebViewClient { -+ public static class InputStreamWithInjectedJS extends InputStream { -+ private InputStream pageIS; -+ private InputStream scriptIS; -+ private Charset charset; -+ private static final String REACT_CLASS = "InputStreamWithInjectedJS"; -+ private static Map script = new HashMap<>(); -+ -+ private boolean hasJS = false; -+ private boolean headWasFound = false; -+ private boolean scriptWasInjected = false; -+ -+ private int lowercaseD = 100; -+ private int closingTag = 62; -+ private boolean hasClosingHead = false; -+ -+ private StringBuffer contentBuffer = new StringBuffer(); -+ -+ @SuppressLint("LongLogTag") -+ private static Charset getCharset(String charsetName) { -+ Charset cs = StandardCharsets.UTF_8; -+ try { -+ if (charsetName != null) { -+ cs = Charset.forName(charsetName); -+ } -+ } catch (UnsupportedCharsetException e) { -+ Log.d(REACT_CLASS, "wrong charset: " + charsetName); -+ } -+ -+ return cs; -+ } -+ -+ private static InputStream getScript(Charset charset) { -+ String js = script.get(charset); -+ if (js == null) { -+ String defaultJs = script.get(StandardCharsets.UTF_8); -+ js = new String(defaultJs.getBytes(StandardCharsets.UTF_8), charset); -+ script.put(charset, js); -+ } -+ -+ return new ByteArrayInputStream(js.getBytes(charset)); -+ } -+ -+ InputStreamWithInjectedJS(InputStream is, String js, Charset charset) { -+ if (js == null) { -+ this.pageIS = is; -+ } else { -+ this.hasJS = true; -+ this.charset = charset; -+ Charset cs = StandardCharsets.UTF_8; -+ String jsScript = ""; -+ script.put(cs, jsScript); -+ this.pageIS = is; -+ } -+ } -+ -+ @Override -+ public int read() throws IOException { -+ if (scriptWasInjected || !hasJS) { -+ return pageIS.read(); -+ } -+ -+ if (!scriptWasInjected && headWasFound) { -+ int nextByte; -+ if (!hasClosingHead) { -+ nextByte = pageIS.read(); -+ if (nextByte != closingTag) { -+ return nextByte; -+ } -+ hasClosingHead = true; -+ return nextByte; -+ } -+ nextByte = scriptIS.read(); -+ if (nextByte == -1) { -+ scriptIS.close(); -+ scriptWasInjected = true; -+ return pageIS.read(); -+ } else { -+ return nextByte; -+ } -+ } -+ -+ if (!headWasFound) { -+ int nextByte = pageIS.read(); -+ contentBuffer.append((char) nextByte); -+ int bufferLength = contentBuffer.length(); -+ if (nextByte == lowercaseD && bufferLength >= 5) { -+ if (contentBuffer.substring(bufferLength - 5).equals(" { - @Override - public void onPageFinished(WebView webView, String url) { - super.onPageFinished(webView, url); -+ // Only return the URL that the web view is currently showing. -+ String visibleUrl = webView.getUrl(); -+ Boolean isFinishedLoading = url.equals(visibleUrl); - -- if (!mLastLoadFailed) { -+ if (!mLastLoadFailed && isFinishedLoading) { -+ if(Objects.nonNull(mWebChromeClient)) mWebChromeClient.blockJsDuringLoading = false; - RNCWebView reactWebView = (RNCWebView) webView; - - reactWebView.callInjectedJavaScript(); - -- emitFinishEvent(webView, url); -+ emitFinishEvent(webView, visibleUrl); - } - } - - @Override - public void onPageStarted(WebView webView, String url, Bitmap favicon) { - super.onPageStarted(webView, url, favicon); -+ if(Objects.nonNull(mWebChromeClient)) mWebChromeClient.blockJsDuringLoading = true; - mLastLoadFailed = false; - -- RNCWebView reactWebView = (RNCWebView) webView; -- reactWebView.callInjectedJavaScriptBeforeContentLoaded(); -- - ((RNCWebView) webView).dispatchEvent( - webView, - new TopLoadingStartEvent( -@@ -882,6 +1154,20 @@ public class RNCWebViewManager extends SimpleViewManager { - createWebViewEvent(webView, url))); - } - -+ @Override -+ public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) { -+ String method = request.getMethod(); -+ -+ if (method.equals("GET")) { -+ WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, true, (RNCWebView)webView); -+ if (response != null) { -+ return response; -+ } -+ } -+ -+ return super.shouldInterceptRequest(webView, request); -+ } -+ - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - final RNCWebView rncWebView = (RNCWebView) view; -@@ -891,7 +1177,6 @@ public class RNCWebViewManager extends SimpleViewManager { - final Pair> lock = RNCWebViewModule.shouldOverrideUrlLoadingLock.getNewLock(); - final int lockIdentifier = lock.first; - final AtomicReference lockObject = lock.second; -- - final WritableMap event = createWebViewEvent(view, url); - event.putInt("lockIdentifier", lockIdentifier); - rncWebView.sendDirectMessage("onShouldStartLoadWithRequest", event); -@@ -919,6 +1204,17 @@ public class RNCWebViewManager extends SimpleViewManager { - RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); - - return shouldOverride; -+ } else if (url != null && Arrays.asList(DEEPLINK_ALLOW_LIST).contains(url)) { -+ // This case is used to support deeplinking within the webview. We are limiting this but -+ // if more links are to be supported we should consider a more scaleable solution. That is -+ // secure and scaleable. -+ Intent intent = new Intent(Intent.ACTION_VIEW); -+ intent.setData(Uri.parse(url)); -+ if(intent.resolveActivity(view.getContext().getPackageManager()) != null) { -+ view.getContext().startActivity(intent); -+ return true; -+ } else -+ return false; - } else { - FLog.w(TAG, "Couldn't use blocking synchronous call for onShouldStartLoadWithRequest due to debugging or missing Catalyst instance, falling back to old event-and-load."); - progressChangedFilter.setWaitingForCommandLoadUrl(true); -@@ -934,67 +1230,86 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { -+ if(Objects.nonNull(mWebChromeClient)) mWebChromeClient.blockJsDuringLoading = true; -+ -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -+ -+ /* -+ * In order to follow redirects properly, we return null in interceptRequest(). -+ * Doing this breaks the web3 injection on the resulting page, so we have to reload to -+ * make sure web3 is available. -+ * */ -+ -+ if (request.isForMainFrame() && request.isRedirect()) { -+ view.loadUrl(request.getUrl().toString()); -+ return true; -+ } -+ } -+ - final String url = request.getUrl().toString(); -+ - return this.shouldOverrideUrlLoading(view, url); - } - -+ -+ - @Override - public void onReceivedSslError(final WebView webView, final SslErrorHandler handler, final SslError error) { -- // onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError) -- // WebView.getUrl() will return the top-level window URL. -- // If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page). -- // This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request. -- String topWindowUrl = webView.getUrl(); -- String failingUrl = error.getUrl(); -- -- // Cancel request after obtaining top-level URL. -- // If request is cancelled before obtaining top-level URL, undesired behavior may occur. -- // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. -- handler.cancel(); -- -- if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { -- // If error is not due to top-level navigation, then do not call onReceivedError() -- Log.w("RNCWebViewManager", "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); -- return; -- } -+ // onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError) -+ // WebView.getUrl() will return the top-level window URL. -+ // If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page). -+ // This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request. -+ String topWindowUrl = webView.getUrl(); -+ String failingUrl = error.getUrl(); -+ -+ // Cancel request after obtaining top-level URL. -+ // If request is cancelled before obtaining top-level URL, undesired behavior may occur. -+ // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. -+ handler.cancel(); -+ -+ if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { -+ // If error is not due to top-level navigation, then do not call onReceivedError() -+ Log.w("RNCWebViewManager", "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); -+ return; -+ } - -- int code = error.getPrimaryError(); -- String description = ""; -- String descriptionPrefix = "SSL error: "; -- -- // https://developer.android.com/reference/android/net/http/SslError.html -- switch (code) { -- case SslError.SSL_DATE_INVALID: -- description = "The date of the certificate is invalid"; -- break; -- case SslError.SSL_EXPIRED: -- description = "The certificate has expired"; -- break; -- case SslError.SSL_IDMISMATCH: -- description = "Hostname mismatch"; -- break; -- case SslError.SSL_INVALID: -- description = "A generic error occurred"; -- break; -- case SslError.SSL_NOTYETVALID: -- description = "The certificate is not yet valid"; -- break; -- case SslError.SSL_UNTRUSTED: -- description = "The certificate authority is not trusted"; -- break; -- default: -- description = "Unknown SSL Error"; -- break; -- } -+ int code = error.getPrimaryError(); -+ String description = ""; -+ String descriptionPrefix = "SSL error: "; - -- description = descriptionPrefix + description; -+ // https://developer.android.com/reference/android/net/http/SslError.html -+ switch (code) { -+ case SslError.SSL_DATE_INVALID: -+ description = "The date of the certificate is invalid"; -+ break; -+ case SslError.SSL_EXPIRED: -+ description = "The certificate has expired"; -+ break; -+ case SslError.SSL_IDMISMATCH: -+ description = "Hostname mismatch"; -+ break; -+ case SslError.SSL_INVALID: -+ description = "A generic error occurred"; -+ break; -+ case SslError.SSL_NOTYETVALID: -+ description = "The certificate is not yet valid"; -+ break; -+ case SslError.SSL_UNTRUSTED: -+ description = "The certificate authority is not trusted"; -+ break; -+ default: -+ description = "Unknown SSL Error"; -+ break; -+ } - -- this.onReceivedError( -- webView, -- code, -- description, -- failingUrl -- ); -+ description = descriptionPrefix + description; -+ -+ this.onReceivedError( -+ webView, -+ code, -+ description, -+ failingUrl -+ ); - } - - @Override -@@ -1005,9 +1320,9 @@ public class RNCWebViewManager extends SimpleViewManager { - String failingUrl) { - - if (ignoreErrFailedForThisURL != null -- && failingUrl.equals(ignoreErrFailedForThisURL) -- && errorCode == -1 -- && description.equals("net::ERR_FAILED")) { -+ && failingUrl.equals(ignoreErrFailedForThisURL) -+ && errorCode == -1 -+ && description.equals("net::ERR_FAILED")) { - - // This is a workaround for a bug in the WebView. - // See these chromium issues for more context: -@@ -1056,36 +1371,36 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.O) - @Override - public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) { -- // WebViewClient.onRenderProcessGone was added in O. -- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { -- return false; -- } -- super.onRenderProcessGone(webView, detail); -+ // WebViewClient.onRenderProcessGone was added in O. -+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { -+ return false; -+ } -+ super.onRenderProcessGone(webView, detail); - -- if(detail.didCrash()){ -- Log.e("RNCWebViewManager", "The WebView rendering process crashed."); -- } -- else{ -- Log.w("RNCWebViewManager", "The WebView rendering process was killed by the system."); -- } -+ if(detail.didCrash()){ -+ Log.e("RNCWebViewManager", "The WebView rendering process crashed."); -+ } -+ else{ -+ Log.w("RNCWebViewManager", "The WebView rendering process was killed by the system."); -+ } - -- // if webView is null, we cannot return any event -- // since the view is already dead/disposed -- // still prevent the app crash by returning true. -- if(webView == null){ -- return true; -- } -+ // if webView is null, we cannot return any event -+ // since the view is already dead/disposed -+ // still prevent the app crash by returning true. -+ if(webView == null){ -+ return true; -+ } - -- WritableMap event = createWebViewEvent(webView, webView.getUrl()); -- event.putBoolean("didCrash", detail.didCrash()); -+ WritableMap event = createWebViewEvent(webView, webView.getUrl()); -+ event.putBoolean("didCrash", detail.didCrash()); - - ((RNCWebView) webView).dispatchEvent( -- webView, -- new TopRenderProcessGoneEvent(webView.getId(), event) -- ); -+ webView, -+ new TopRenderProcessGoneEvent(webView.getId(), event) -+ ); - -- // returning false would crash the app. -- return true; -+ // returning false would crash the app. -+ return true; - } - - protected void emitFinishEvent(WebView webView, String url) { -@@ -1138,6 +1453,7 @@ public class RNCWebViewManager extends SimpleViewManager { - - protected View mVideoView; - protected WebChromeClient.CustomViewCallback mCustomViewCallback; -+ protected boolean blockJsDuringLoading = true; //This boolean block JS prompts and alerts from displaying during loading - - /* - * - Permissions - -@@ -1211,8 +1527,8 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void onPermissionRequest(final PermissionRequest request) { -- - grantedPermissions = new ArrayList<>(); -+ ArrayList requestPermissionIdentifiers = new ArrayList<>(); - - ArrayList requestedAndroidPermissions = new ArrayList<>(); - for (String requestedResource : request.getResources()) { -@@ -1220,36 +1536,74 @@ public class RNCWebViewManager extends SimpleViewManager { - - if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { - androidPermission = Manifest.permission.RECORD_AUDIO; -+ requestPermissionIdentifiers.add("microphone"); - } else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { - androidPermission = Manifest.permission.CAMERA; -+ requestPermissionIdentifiers.add("camera"); - } else if(requestedResource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { - androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID; - } - // TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID. -- - if (androidPermission != null) { -- if (ContextCompat.checkSelfPermission(mReactContext, androidPermission) == PackageManager.PERMISSION_GRANTED) { -- grantedPermissions.add(requestedResource); -- } else { -- requestedAndroidPermissions.add(androidPermission); -- } -+ if (ContextCompat.checkSelfPermission(mReactContext, androidPermission) == PackageManager.PERMISSION_GRANTED) { -+ grantedPermissions.add(requestedResource); -+ } else { -+ requestedAndroidPermissions.add(androidPermission); -+ } - } - } - -- // If all the permissions are already granted, send the response to the WebView synchronously -- if (requestedAndroidPermissions.isEmpty()) { -- request.grant(grantedPermissions.toArray(new String[0])); -- grantedPermissions = null; -- return; -- } -- -- // Otherwise, ask to Android System for native permissions asynchronously -+ if (!requestedAndroidPermissions.isEmpty()) { -+ // Show the dialog and request the permissions -+ AlertDialog.Builder builder = new AlertDialog.Builder(mReactContext); -+ String permissionsIdentifiers = TextUtils.join(" and ", requestPermissionIdentifiers); -+ builder.setMessage("The app needs access to your " + permissionsIdentifiers + ". Allow?"); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Allow", new DialogInterface.OnClickListener() { -+ public void onClick(DialogInterface dialog, int which) { -+ permissionRequest = request; -+ requestPermissions(requestedAndroidPermissions); -+ } -+ }); -+ builder.setNegativeButton("Don't allow", (DialogInterface.OnClickListener) (dialog, which) -> { -+ request.deny(); -+ }); - -- this.permissionRequest = request; -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); -+ // Delay making `allow` clickable for 500ms -+ Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); -+ posButton.setEnabled(false); -+ this.runDelayed(() -> posButton.setEnabled(true), 500); -+ } else if (!grantedPermissions.isEmpty()) { -+ // You need to show to the user that the website is requesting permissions -+ // If that happens and the permissions are already granted you need to ask again -+ AlertDialog.Builder builder = new AlertDialog.Builder(mReactContext); -+ String permissionsIdentifiers = TextUtils.join(" and ", requestPermissionIdentifiers); -+ builder.setMessage("The app needs access to your " + permissionsIdentifiers + ". Allow?"); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Allow", new DialogInterface.OnClickListener() { -+ public void onClick(DialogInterface dialog, int which) { -+ request.grant(grantedPermissions.toArray(new String[0])); -+ } -+ }); -+ builder.setNegativeButton("Don't allow", (DialogInterface.OnClickListener) (dialog, which) -> { -+ request.deny(); -+ }); - -- requestPermissions(requestedAndroidPermissions); -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); -+ // Delay making `allow` clickable for 500ms -+ Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); -+ posButton.setEnabled(false); -+ this.runDelayed(() -> posButton.setEnabled(true), 500); -+ } - } - -+ private void runDelayed(Runnable function, long delayMillis) { -+ Handler handler = new Handler(); -+ handler.postDelayed(function, delayMillis); -+ } - - @Override - public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { -@@ -1266,7 +1620,22 @@ public class RNCWebViewManager extends SimpleViewManager { - requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION)); - - } else { -- callback.invoke(origin, true, false); -+ String alertMessage = String.format("Allow this app to use your location?"); -+ AlertDialog.Builder builder = new AlertDialog.Builder(this.mWebView.getContext()); -+ builder.setMessage(alertMessage); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Allow", (dialog, which) -> { -+ callback.invoke(origin, true, false); -+ }); -+ builder.setNegativeButton("Don't allow", (dialog, which) -> { -+ callback.invoke(origin, false, false); -+ }); -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); -+ //Delay making `allow` clickable for 500ms to avoid unwanted presses. -+ Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); -+ posButton.setEnabled(false); -+ this.runDelayed(() -> posButton.setEnabled(true), 500); - } - } - -@@ -1402,6 +1771,15 @@ public class RNCWebViewManager extends SimpleViewManager { - } - } - -+ @Override -+ public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { -+ if(blockJsDuringLoading) { -+ result.cancel(); -+ return true; -+ } else -+ return super.onJsPrompt(view, url, message, defaultValue, result); -+ } -+ - @Override - public void onHostPause() { } - -@@ -1447,6 +1825,13 @@ public class RNCWebViewManager extends SimpleViewManager { - protected boolean nestedScrollEnabled = false; - protected ProgressChangedFilter progressChangedFilter; - -+ /** -+ * Taken from EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING We can't use that -+ * value directly as it was only added on Oreo, but we can apply the value -+ * anyway. -+ */ -+ private static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000; -+ - /** - * WebView must be created with an context of the current activity - *

-@@ -1475,6 +1860,42 @@ public class RNCWebViewManager extends SimpleViewManager { - this.nestedScrollEnabled = nestedScrollEnabled; - } - -+ @Override -+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) { -+ InputConnection inputConnection; -+ if (!usingGoogleKeyboard()) { -+ inputConnection = super.onCreateInputConnection(outAttrs); -+ } else { -+ inputConnection = new BaseInputConnection(this, false); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -+ outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; -+ } else { -+ // Cover OS versions below Oreo -+ outAttrs.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING; -+ } -+ } -+ -+ return inputConnection; -+ } -+ -+ public boolean usingGoogleKeyboard() { -+ final InputMethodManager richImm = -+ (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); -+ -+ boolean isKeyboard = false; -+ -+ final Field field; -+ try { -+ field = richImm.getClass().getDeclaredField("mCurId"); -+ field.setAccessible(true); -+ Object value = field.get(richImm); -+ isKeyboard = Objects.equals(value, "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME"); -+ } catch (IllegalAccessException | NoSuchFieldException e) { -+ return false; -+ } -+ return isKeyboard; -+ } -+ - @Override - public void onHostResume() { - // do nothing -@@ -1533,6 +1954,8 @@ public class RNCWebViewManager extends SimpleViewManager { - } - } - -+ -+ - public @Nullable - RNCWebViewClient getRNCWebViewClient() { - return mRNCWebViewClient; -@@ -1609,8 +2032,8 @@ public class RNCWebViewManager extends SimpleViewManager { - - public void callInjectedJavaScriptBeforeContentLoaded() { - if (getSettings().getJavaScriptEnabled() && -- injectedJSBeforeContentLoaded != null && -- !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { -+ injectedJSBeforeContentLoaded != null && -+ !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { - evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();"); - } - } -@@ -1672,16 +2095,16 @@ public class RNCWebViewManager extends SimpleViewManager { - - if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { - ScrollEvent event = ScrollEvent.obtain( -- this.getId(), -- ScrollEventType.SCROLL, -- x, -- y, -- mOnScrollDispatchHelper.getXFlingVelocity(), -- mOnScrollDispatchHelper.getYFlingVelocity(), -- this.computeHorizontalScrollRange(), -- this.computeVerticalScrollRange(), -- this.getWidth(), -- this.getHeight()); -+ this.getId(), -+ ScrollEventType.SCROLL, -+ x, -+ y, -+ mOnScrollDispatchHelper.getXFlingVelocity(), -+ mOnScrollDispatchHelper.getYFlingVelocity(), -+ this.computeHorizontalScrollRange(), -+ this.computeVerticalScrollRange(), -+ this.getWidth(), -+ this.getHeight()); - - dispatchEvent(this, event); - } -diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/Worker.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/Worker.java -new file mode 100644 -index 0000000..b9581ac ---- /dev/null -+++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/Worker.java -@@ -0,0 +1,21 @@ -+package com.reactnativecommunity.webview; -+ -+import android.os.Handler; -+import android.os.HandlerThread; -+ -+class Worker extends HandlerThread { -+ private Handler handler; -+ -+ private static final String TAG = "WORKER"; -+ -+ public Worker() { -+ super(TAG); -+ start(); -+ handler = new Handler(getLooper()); -+ } -+ -+ public Worker execute(Runnable task) { -+ handler.post(task); -+ return this; -+ } -+} -\ No newline at end of file -diff --git a/node_modules/react-native-webview/apple/RNCWebView.m b/node_modules/react-native-webview/apple/RNCWebView.m -index 28c078a..9bb5368 100644 ---- a/node_modules/react-native-webview/apple/RNCWebView.m -+++ b/node_modules/react-native-webview/apple/RNCWebView.m -@@ -105,6 +105,7 @@ static NSDictionary* customCertificatesForHost; - UIStatusBarStyle _savedStatusBarStyle; - #endif // !TARGET_OS_OSX - BOOL _savedStatusBarHidden; -+ BOOL _disablePromptDuringLoading; //Disables the display of prompts during site navigation/loading - - #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - UIScrollViewContentInsetAdjustmentBehavior _savedContentInsetAdjustmentBehavior; -@@ -139,6 +140,7 @@ static NSDictionary* customCertificatesForHost; - _injectedJavaScriptForMainFrameOnly = YES; - _injectedJavaScriptBeforeContentLoaded = nil; - _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES; -+ _disablePromptDuringLoading = YES; - - #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - _savedContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; -@@ -417,6 +419,7 @@ static NSDictionary* customCertificatesForHost; - - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ - if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) { - if(_onLoadingProgress){ -+ _disablePromptDuringLoading = YES; - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary:@{@"progress":[NSNumber numberWithDouble:self.webView.estimatedProgress]}]; - _onLoadingProgress(event); -@@ -492,6 +495,7 @@ static NSDictionary* customCertificatesForHost; - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{@"navigationType": message.body}]; - _onLoadingFinish(event); -+ _disablePromptDuringLoading = NO; - } - } else if ([message.name isEqualToString:MessageHandlerName]) { - if (_onMessage) { -@@ -851,11 +855,13 @@ static NSDictionary* customCertificatesForHost; - - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler - { - #if !TARGET_OS_OSX -- UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; -- [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -- completionHandler(); -- }]]; -- [[self topViewController] presentViewController:alert animated:YES completion:NULL]; -+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ -+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; -+ [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -+ completionHandler(); -+ }]]; -+ [[self topViewController] presentViewController:alert animated:YES completion:NULL]; -+ }); - #else - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText:message]; -@@ -868,6 +874,51 @@ static NSDictionary* customCertificatesForHost; - /** - * confirm - */ -+// This patch made to overridde the restrictions that webView is imposing to the native Alert, by restricting its size. -+- (void)webView:(WKWebView *)webView requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin initiatedByFrame:(WKFrameInfo *)frame type:(WKMediaCaptureType)type decisionHandler:(void (^)(WKPermissionDecision decision))decisionHandler API_AVAILABLE(ios(15.0)){ -+ -+ NSString *deviceType; -+ -+ switch (type) { -+ case WKMediaCaptureTypeCamera: -+ deviceType = @"camera"; -+ break; -+ case WKMediaCaptureTypeMicrophone: -+ deviceType = @"microphone"; -+ break; -+ case WKMediaCaptureTypeCameraAndMicrophone: -+ deviceType = @"camera and microphone"; -+ break; -+ default: -+ deviceType = @"unknown device"; -+ } -+ -+ NSString *message = [NSString stringWithFormat:@"The webpage %@ is requesting access to your %@. Do you want to allow this?", origin.host, deviceType]; -+ -+ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Permission Request" -+ message:message -+ preferredStyle:UIAlertControllerStyleAlert]; -+ -+ UIAlertAction *allowAction = [UIAlertAction actionWithTitle:@"Allow" -+ style:UIAlertActionStyleDefault -+ handler:^(UIAlertAction * _Nonnull action) { -+ decisionHandler(WKPermissionDecisionGrant); -+ } -+ ]; -+ -+ UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"Deny" -+ style:UIAlertActionStyleCancel -+ handler:^(UIAlertAction * _Nonnull action) { -+ decisionHandler(WKPermissionDecisionDeny); -+ } -+ ]; -+ -+ [alertController addAction:allowAction]; -+ [alertController addAction:denyAction]; -+ -+ [[self topViewController] presentViewController:alertController animated:YES completion:NULL]; -+} -+ - - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{ - #if !TARGET_OS_OSX - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; -@@ -894,44 +945,49 @@ static NSDictionary* customCertificatesForHost; - * prompt - */ - - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{ --#if !TARGET_OS_OSX -- UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; -- [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { -- textField.text = defaultText; -- }]; -- UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -- completionHandler([[alert.textFields lastObject] text]); -- }]; -- [alert addAction:okAction]; -- UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -- completionHandler(nil); -- }]; -- [alert addAction:cancelAction]; -- alert.preferredAction = okAction; -- [[self topViewController] presentViewController:alert animated:YES completion:NULL]; --#else -- NSAlert *alert = [[NSAlert alloc] init]; -- [alert setMessageText:prompt]; -- -- const NSRect RCTSingleTextFieldFrame = NSMakeRect(0.0, 0.0, 275.0, 22.0); -- NSTextField *textField = [[NSTextField alloc] initWithFrame:RCTSingleTextFieldFrame]; -- textField.cell.scrollable = YES; -- if (@available(macOS 10.11, *)) { -- textField.maximumNumberOfLines = 1; -- } -- textField.stringValue = defaultText; -- [alert setAccessoryView:textField]; - -- [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; -- [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel button")]; -- [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSModalResponse response) { -- if (response == NSAlertFirstButtonReturn) { -- completionHandler([textField stringValue]); -+ if(!_disablePromptDuringLoading) { -+ #if !TARGET_OS_OSX -+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; -+ [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { -+ textField.text = defaultText; -+ }]; -+ UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -+ completionHandler([[alert.textFields lastObject] text]); -+ }]; -+ [alert addAction:okAction]; -+ UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -+ completionHandler(nil); -+ }]; -+ [alert addAction:cancelAction]; -+ alert.preferredAction = okAction; -+ [[self topViewController] presentViewController:alert animated:YES completion:NULL]; -+ #else -+ NSAlert *alert = [[NSAlert alloc] init]; -+ [alert setMessageText:prompt]; -+ -+ const NSRect RCTSingleTextFieldFrame = NSMakeRect(0.0, 0.0, 275.0, 22.0); -+ NSTextField *textField = [[NSTextField alloc] initWithFrame:RCTSingleTextFieldFrame]; -+ textField.cell.scrollable = YES; -+ if (@available(macOS 10.11, *)) { -+ textField.maximumNumberOfLines = 1; -+ } -+ textField.stringValue = defaultText; -+ [alert setAccessoryView:textField]; -+ -+ [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; -+ [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel button")]; -+ [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSModalResponse response) { -+ if (response == NSAlertFirstButtonReturn) { -+ completionHandler([textField stringValue]); -+ } else { -+ completionHandler(nil); -+ } -+ }]; -+ #endif // !TARGET_OS_OSX - } else { -- completionHandler(nil); -+ completionHandler(nil); - } -- }]; --#endif // !TARGET_OS_OSX - } - - #if !TARGET_OS_OSX -@@ -1157,6 +1213,7 @@ static NSDictionary* customCertificatesForHost; - } - - if (_onLoadingFinish) { -+ _disablePromptDuringLoading = NO; - _onLoadingFinish([self baseEvent]); - } - } -@@ -1446,3 +1503,4 @@ static NSDictionary* customCertificatesForHost; - } - - @end -+ diff --git a/scripts/build-inpage-bridge.sh b/scripts/build-inpage-bridge.sh index 7823e49dd80..52753a1589a 100755 --- a/scripts/build-inpage-bridge.sh +++ b/scripts/build-inpage-bridge.sh @@ -3,11 +3,7 @@ set -euo pipefail rm -f app/core/InpageBridgeWeb3.js mkdir -p scripts/inpage-bridge/dist && rm -rf scripts/inpage-bridge/dist/* -cd scripts/inpage-bridge/inpage -../../../node_modules/.bin/webpack --config webpack.config.js -cd .. -node content-script/build.js -cat dist/inpage-bundle.js content-script/index.js > dist/index-raw.js +cd scripts/inpage-bridge/ ../../node_modules/.bin/webpack --config webpack.config.js cd ../.. cp scripts/inpage-bridge/dist/index.js app/core/InpageBridgeWeb3.js diff --git a/scripts/inpage-bridge/content-script/build.js b/scripts/inpage-bridge/content-script/build.js deleted file mode 100644 index cd0ca95605c..00000000000 --- a/scripts/inpage-bridge/content-script/build.js +++ /dev/null @@ -1,14 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const distPath = path.join(__dirname, '..', '/', 'dist'); - -const inpageContent = fs - .readFileSync(path.join(distPath, 'inpage-content.js')) - .toString(); - -// wrap the inpage content in a variable declaration -const code = `const inpageBundle = ${JSON.stringify(inpageContent)}`; - -fs.writeFileSync(path.join(distPath, 'inpage-bundle.js'), code, 'ascii'); -console.log('content-script.js generated succesfully'); diff --git a/scripts/inpage-bridge/inpage/webpack.config.js b/scripts/inpage-bridge/inpage/webpack.config.js deleted file mode 100644 index f1d28be96f1..00000000000 --- a/scripts/inpage-bridge/inpage/webpack.config.js +++ /dev/null @@ -1,63 +0,0 @@ -const webpack = require('webpack'); -const path = require('path'); -const { readFileSync } = require('fs'); - -const SVG_LOGO_PATH = - '../../../app/images/fox.svg'; -function getBuildIcon() { - const svg = readFileSync(SVG_LOGO_PATH, 'utf8'); - return `data:image/svg+xml,${encodeURIComponent(svg)}`; -} - -const config = { - entry: './index.js', - - output: { - path: path.resolve(__dirname, '..', 'dist'), - filename: 'inpage-content.js', - }, - - mode: 'production', - module: { - rules: [ - { - test: /\.(js|jsx|mjs)$/u, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'], - }, - }, - }, - ], - }, - resolve: { - fallback: { - buffer: require.resolve('buffer'), - stream: require.resolve('stream-browserify'), - _stream_transform: require.resolve('readable-stream/transform'), - _stream_readable: require.resolve('readable-stream/readable'), - _stream_writable: require.resolve('readable-stream/writable'), - _stream_duplex: require.resolve('readable-stream/duplex'), - _stream_passthrough: require.resolve('readable-stream/passthrough'), - }, - }, - plugins: [ - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - process: 'process/browser', - }), - new webpack.DefinePlugin({ - 'process.env.METAMASK_BUILD_NAME': JSON.stringify('MetaMask'), - 'process.env.METAMASK_BUILD_ICON': JSON.stringify(getBuildIcon()), - 'process.env.METAMASK_BUILD_APP_ID': JSON.stringify('io.metamask.mobile'), - }), - ], -}; - -module.exports = (_env, argv) => { - if (argv.mode === 'development') { - config.mode = 'development'; - } - return config; -}; diff --git a/scripts/inpage-bridge/inpage/MobilePortStream.js b/scripts/inpage-bridge/src/MobilePortStream.js similarity index 100% rename from scripts/inpage-bridge/inpage/MobilePortStream.js rename to scripts/inpage-bridge/src/MobilePortStream.js diff --git a/scripts/inpage-bridge/inpage/ReactNativePostMessageStream.js b/scripts/inpage-bridge/src/ReactNativePostMessageStream.js similarity index 100% rename from scripts/inpage-bridge/inpage/ReactNativePostMessageStream.js rename to scripts/inpage-bridge/src/ReactNativePostMessageStream.js diff --git a/scripts/inpage-bridge/content-script/index.js b/scripts/inpage-bridge/src/index.js similarity index 73% rename from scripts/inpage-bridge/content-script/index.js rename to scripts/inpage-bridge/src/index.js index 16052682c1f..2a4787822fc 100644 --- a/scripts/inpage-bridge/content-script/index.js +++ b/scripts/inpage-bridge/src/index.js @@ -1,7 +1,8 @@ -/* global inpageBundle */ + +import injectInpageProvider from './provider'; if (shouldInject()) { - injectScript(inpageBundle); + injectInpageProvider(); start(); } @@ -16,28 +17,6 @@ async function start() { window._metamaskSetupProvider(); } -/** - * Injects a script tag into the current document - * - * @param {string} content - Code to be executed in the current document - */ -function injectScript(content) { - try { - const container = document.head || document.documentElement; - - // synchronously execute script in page context - const scriptTag = document.createElement('script'); - scriptTag.setAttribute('async', false); - scriptTag.textContent = content; - container.insertBefore(scriptTag, container.children[0]); - - // script executed; remove script element from DOM - container.removeChild(scriptTag); - } catch (err) { - console.error('MetaMask script injection failed', err); - } -} - /** * Determines if the provider should be injected. * @@ -47,7 +26,6 @@ function shouldInject() { return ( doctypeCheck() && suffixCheck() && - documentElementCheck() && !blockedDomainCheck() ); } @@ -86,19 +64,6 @@ function suffixCheck() { return true; } -/** - * Checks the documentElement of the current document - * - * @returns {boolean} {@code true} if the documentElement is an html node or if none exists - */ -function documentElementCheck() { - const documentElement = document.documentElement.nodeName; - if (documentElement) { - return documentElement.toLowerCase() === 'html'; - } - return true; -} - /** * Checks if the current domain is blocked * diff --git a/scripts/inpage-bridge/inpage/index.js b/scripts/inpage-bridge/src/provider.js similarity index 83% rename from scripts/inpage-bridge/inpage/index.js rename to scripts/inpage-bridge/src/provider.js index b76b2ae006a..f3cdca54008 100644 --- a/scripts/inpage-bridge/inpage/index.js +++ b/scripts/inpage-bridge/src/provider.js @@ -15,28 +15,31 @@ const metamaskStream = new ReactNativePostMessageStream({ target: CONTENT_SCRIPT, }); -// Initialize provider object (window.ethereum) -initializeProvider({ - connectionStream: metamaskStream, - shouldSendMetadata: false, - providerInfo: { - uuid: uuid(), - name: process.env.METAMASK_BUILD_NAME, - icon: process.env.METAMASK_BUILD_ICON, - rdns: process.env.METAMASK_BUILD_APP_ID, - }, -}); +const init = () => { + // Initialize provider object (window.ethereum) + initializeProvider({ + connectionStream: metamaskStream, + shouldSendMetadata: false, + providerInfo: { + uuid: uuid(), + name: process.env.METAMASK_BUILD_NAME, + icon: process.env.METAMASK_BUILD_ICON, + rdns: process.env.METAMASK_BUILD_APP_ID, + }, + }); -// Set content script post-setup function -Object.defineProperty(window, '_metamaskSetupProvider', { - value: () => { - setupProviderStreams(); - delete window._metamaskSetupProvider; - }, - configurable: true, - enumerable: false, - writable: false, -}); + // Set content script post-setup function + Object.defineProperty(window, '_metamaskSetupProvider', { + value: () => { + setupProviderStreams(); + delete window._metamaskSetupProvider; + }, + configurable: true, + enumerable: false, + writable: false, + }); + +} // Functions @@ -130,3 +133,5 @@ function notifyProviderOfStreamFailure() { window.location.origin, ); } + +export default init; diff --git a/scripts/inpage-bridge/webpack.config.js b/scripts/inpage-bridge/webpack.config.js index 8c0dff54856..b8fa14e20d1 100644 --- a/scripts/inpage-bridge/webpack.config.js +++ b/scripts/inpage-bridge/webpack.config.js @@ -1,7 +1,16 @@ +const webpack = require('webpack'); const path = require('path'); +const { readFileSync } = require('fs'); + +const SVG_LOGO_PATH = + '../../app/images/fox.svg'; +function getBuildIcon() { + const svg = readFileSync(SVG_LOGO_PATH, 'utf8'); + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} const config = { - entry: './dist/index-raw.js', + entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), @@ -9,6 +18,41 @@ const config = { }, mode: 'production', + module: { + rules: [ + { + test: /\.(js|jsx|mjs)$/u, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'], + }, + }, + }, + ], + }, + resolve: { + fallback: { + buffer: require.resolve('buffer'), + stream: require.resolve('stream-browserify'), + _stream_transform: require.resolve('readable-stream/transform'), + _stream_readable: require.resolve('readable-stream/readable'), + _stream_writable: require.resolve('readable-stream/writable'), + _stream_duplex: require.resolve('readable-stream/duplex'), + _stream_passthrough: require.resolve('readable-stream/passthrough'), + }, + }, + plugins: [ + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser', + }), + new webpack.DefinePlugin({ + 'process.env.METAMASK_BUILD_NAME': JSON.stringify('MetaMask'), + 'process.env.METAMASK_BUILD_ICON': JSON.stringify(getBuildIcon()), + 'process.env.METAMASK_BUILD_APP_ID': JSON.stringify('io.metamask.mobile'), + }), + ], }; module.exports = (_env, argv) => { diff --git a/yarn.lock b/yarn.lock index 3e03fa82aef..30a497d0534 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4569,13 +4569,6 @@ resolved "https://registry.yarnpkg.com/@metamask/react-native-actionsheet/-/react-native-actionsheet-2.4.2.tgz#9f956fe9e784d92c8e33656877fcfaabe4a482f1" integrity sha512-oibRXUzF+7DB0Nyzp2cMGN7ztB9Sl21W1NFq1IMa00mB4/X43JY+u+LCkx625WvQUeq0GO2ZQ6hG1L5XjMumSA== -"@metamask/react-native-animated-fox@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@metamask/react-native-animated-fox/-/react-native-animated-fox-2.1.0.tgz#504e1f68e13ad273fb193c6f6a3832f3a5242518" - integrity sha512-Hc+DyaEIXYa7NjzqXfgh01bsoP9WbE/ENNKZ4A65YwSBmJk5ZDvhMgTMFz+qybkUllx4kn4ENkmr0SXERZ2wmg== - dependencies: - prop-types "^15.5.10" - "@metamask/react-native-button@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@metamask/react-native-button/-/react-native-button-3.0.0.tgz#4af8affd11e2b285cfc1b1752280797e1b33e62b" @@ -4602,6 +4595,14 @@ resolved "https://registry.yarnpkg.com/@metamask/react-native-splash-screen/-/react-native-splash-screen-3.2.0.tgz#06a6547c143b088e47af40eacea9ac6657ac937f" integrity sha512-V8Cn0MXe9jdaUli/DK3PoJ71tx7k3IW2v2slqflvNstvHiO3MpCtdylsYIyu+tiPwI2JiyLRzLK8s02/3jxk6g== +"@metamask/react-native-webview@^14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@metamask/react-native-webview/-/react-native-webview-14.0.1.tgz#db1a1f4abea077b17fcc163538db4fcd7b2890bd" + integrity sha512-euctbS170XwxCCOIP36pr3N2bW8hQdZIKzpoEpWFK+5aumQ7UrTmwswcD3OCqnlk5JvpsRvlixIje8AA+6Gmtg== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + "@metamask/rpc-errors@^6.0.0", "@metamask/rpc-errors@^6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.2.1.tgz#f5daf429ededa7cb83069dc621bd5738fe2a1d80" @@ -24850,14 +24851,6 @@ react-native-webview-invoke@^0.6.2: resolved "https://registry.yarnpkg.com/react-native-webview-invoke/-/react-native-webview-invoke-0.6.2.tgz#75cc27ef98ea1cbc9386269347d3aafe90d33aa3" integrity sha512-PCzP7Zl3XwHU10JYS8nR0gwuR8XiOO0MhC8y9ZuPPI+HeISn95GvNYhOXxeLgfbdbUcpNWh1HqxPDySlfCIqxg== -react-native-webview@11.13.0: - version "11.13.0" - resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.13.0.tgz#a2eca0f87b2ae9bba0dd8144594aeff9947cc5d6" - integrity sha512-jjQAKWv8JzRmcn76fMe4lXD84AAeR7kn43kAmUe1GX312BMLaP+RbKlgpYAlNuOBXL0YirItGKDrpaD0bNROOA== - dependencies: - escape-string-regexp "2.0.0" - invariant "2.2.4" - react-native@0.71.15: version "0.71.15" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.15.tgz#7d99f478238c559b8b3fdaad2514f11d53ef135a"