diff --git a/.all-contributorsrc b/.all-contributorsrc index 4fc330fbd..a5f00ab73 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,6 +1,6 @@ { "projectName": "react-native-webview", - "projectOwner": "react-native-community", + "projectOwner": "react-native-webview", "repoType": "github", "repoHost": "https://github.com", "files": [ diff --git a/.circleci/config.yml b/.circleci/config.yml index 962b022b1..d4448cc2b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,7 +34,7 @@ jobs: - node_modules-{{ arch }}-{{ checksum "yarn.lock" }} - run: - name: Run Tests + name: Lint checks command: yarn ci publish: diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d274719bf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto + +*.bat text eol=crlf +*.def text eol=crlf +*.filters text eol=crlf +*.idl text eol=crlf +*.props text eol=crlf +*.ps1 text eol=crlf +*.sln text eol=crlf +*.vcxproj text eol=crlf +*.xaml text eol=crlf + diff --git a/.github/workflows/detox.yml b/.github/workflows/detox.yml index e817c4447..d42abe666 100644 --- a/.github/workflows/detox.yml +++ b/.github/workflows/detox.yml @@ -1,5 +1,5 @@ name: 'Detox CI Tests' -on: [push] +on: [pull_request] jobs: tests: diff --git a/.github/workflows/scripts/install-vs-features.ps1 b/.github/workflows/scripts/install-vs-features.ps1 new file mode 100644 index 000000000..6a836bc1c --- /dev/null +++ b/.github/workflows/scripts/install-vs-features.ps1 @@ -0,0 +1,108 @@ +param ( + [Parameter(Mandatory=$true)] + [string[]] $Components, + + [uri] $InstallerUri = "https://download.visualstudio.microsoft.com/download/pr/c4fef23e-cc45-4836-9544-70e213134bc8/1ee5717e9a1e05015756dff77eb27d554a79a6db91f2716d836df368381af9a1/vs_Enterprise.exe", + + [string] $VsInstaller = "${env:System_DefaultWorkingDirectory}\vs_Enterprise.exe", + + [string] $VsInstallOutputDir = "${env:System_DefaultWorkingDirectory}\vs", + + [System.IO.FileInfo] $VsInstallPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Enterprise", + + [System.IO.FileInfo] $VsInstallerPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer", + + [switch] $Collect = $false, + + [switch] $Cleanup = $false, + + [switch] $UseWebInstaller = $false +) + +$Components | ForEach-Object { + $componentList += '--add', $_ +} + +$LocalVsInstaller = "$VsInstallerPath\vs_installershell.exe" + +$UseWebInstaller = $UseWebInstaller -or -not (Test-Path -Path "$LocalVsInstaller") + +if ($UseWebInstaller) { + Write-Host "Downloading web installer..." + + Invoke-WebRequest -Method Get ` + -Uri $InstallerUri ` + -OutFile $VsInstaller + + New-Item -ItemType directory -Path $VsInstallOutputDir + + Write-Host "Running web installer to download requested components..." + + Start-Process ` + -FilePath "$VsInstaller" ` + -ArgumentList ( ` + '--layout', "$VsInstallOutputDir", + '--wait', + '--norestart', + '--quiet' + ` + $componentList + ) ` + -Wait ` + -PassThru + + Write-Host "Running downloaded VS installer to add requested components..." + + Start-Process ` + -FilePath "$VsInstallOutputDir\vs_Enterprise.exe" ` + -ArgumentList ( + 'modify', + '--installPath', "`"$VsInstallPath`"" , + '--wait', + '--norestart', + '--quiet' + ` + $componentList + ) ` + -Wait ` + -PassThru ` + -OutVariable returnCode + + if ($Cleanup) { + Write-Host "Cleaning up..." + + Remove-Item -Path $VsInstaller + Remove-Item -Path $VsInstallOutputDir -Recurse + } + +} else { + Write-Host "Running local installer to add requested components..." + + Start-Process ` + -FilePath "$LocalVsInstaller" ` + -ArgumentList ( + 'modify', + '--installPath', "`"$VsInstallPath`"" , + '--norestart', + '--quiet' + ` + $componentList + ) ` + -Wait ` + -OutVariable returnCode +} + +if ($Collect) { + Invoke-WebRequest -Method Get ` + -Uri 'https://download.microsoft.com/download/8/3/4/834E83F6-C377-4DCE-A757-69A418B6C6DF/Collect.exe' ` + -OutFile ${env:System_DefaultWorkingDirectory}\Collect.exe + + # Should generate ${env:Temp}\vslogs.zip + Start-Process ` + -FilePath "${env:System_DefaultWorkingDirectory}\Collect.exe" ` + -Wait ` + -PassThru + + New-Item -ItemType Directory -Force ${env:System_DefaultWorkingDirectory}\vslogs + Expand-Archive -Path ${env:TEMP}\vslogs.zip -DestinationPath ${env:System_DefaultWorkingDirectory}\vslogs\ + + Write-Host "VC versions after installation:" + Get-ChildItem -Name "$VsInstallPath\VC\Tools\MSVC\" +} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 11ac44411..ebee3eb17 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,10 +7,11 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v1 + - uses: actions/stale@v3.0.14 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Hello 👋, this issue has been opened for more than 2 months with no activity on it. If the issue is still here, please keep in mind that we need community support and help to fix it! Just comment something like _still searching for solutions_ and if you found one, please open a pull request! You have 7 days until this gets closed automatically' stale-pr-message: 'Hello 👋, this PR has been opened for more than 2 months with no activity on it. If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing! You have 7 days until this gets closed automatically' exempt-issue-label: 'Keep opened' exempt-pr-label: 'Keep opened' + remove-stale-when-updated: true diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml new file mode 100644 index 000000000..4437b7f4c --- /dev/null +++ b/.github/workflows/windows-ci.yml @@ -0,0 +1,59 @@ +name: Windows CI +on: [pull_request] + +jobs: + run-windows-tests: + name: Build & run tests + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v2 + name: Checkout Code + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: '12.9.1' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.0.2 + + - name: Check node modules cache + uses: actions/cache@v1 + id: yarn-cache + with: + path: ./node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install node modules + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn --pure-lockfile + + - name: yarn build + if: steps.yarn-cache.outputs.cache-hit == 'true' + run: | + yarn build + yarn tsc + + - name: Build x64 release + shell: powershell + run: npx react-native run-windows --root example --arch x64 --release --no-packager --no-deploy --logging + + # Workaround for a bug in package searching during deploy. + # The deploy script only searches windows/{*/bin/x64/Release,Release/*}, but the build step above placed the pakcages at windows/x64/Release. + # Copy the packages to Windows/Release before deploying. + - name: Deploy + shell: powershell + run: | + cd example + Copy-Item -Path windows\x64\Release -Recurse -Destination windows\ + npx react-native run-windows --arch x64 --release --no-build --no-packager + + - name: Start Appium server + shell: powershell + run: Start-Process PowerShell -ArgumentList "yarn appium" + + - name: Run tests + run: yarn test:windows diff --git a/.gitignore b/.gitignore index e08691c94..109d94a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,9 @@ android/gradle android/gradlew android/gradlew.bat -lib/ \ No newline at end of file +lib/ +.classpath +.project +.settings/ +msbuild.binlog +example/msbuild.binlog diff --git a/README.md b/README.md index 70cf2ddc6..d38985197 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # React Native WebView - a Modern, Cross-Platform WebView for React Native -[![star this repo](http://githubbadges.com/star.svg?user=react-native-community&repo=react-native-webview&style=flat)](https://github.com/react-native-community/react-native-webview) +[![star this repo](http://githubbadges.com/star.svg?user=react-native-webview&repo=react-native-webview&style=flat)](https://github.com/react-native-webview/react-native-webview) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors) -[![Known Vulnerabilities](https://snyk.io/test/github/react-native-community/react-native-webview/badge.svg?style=flat-square)](https://snyk.io/test/github/react-native-community/react-native-webview) - +[![Known Vulnerabilities](https://snyk.io/test/github/react-native-webview/react-native-webview/badge.svg?style=flat-square)](https://snyk.io/test/github/react-native-webview/react-native-webview) +[![NPM Version](https://img.shields.io/npm/v/react-native-webview.svg?style=flat-square)](https://www.npmjs.com/package/react-native-webview) +[![Lean Core Extracted](https://img.shields.io/badge/Lean%20Core-Extracted-brightgreen.svg?style=flat-square)][lean-core-issue] **React Native WebView** is a modern, well-supported, and cross-platform WebView for React Native. It is intended to be a replacement for the built-in WebView (which will be [removed from core](https://github.com/react-native-community/discussions-and-proposals/pull/3)). @@ -19,6 +20,8 @@ _This project is maintained for free by these people using both their free time - [x] iOS - [x] Android +- [x] macOS +- [x] Windows _Note: Expo support for React Native WebView started with [Expo SDK v33.0.0](https://blog.expo.io/expo-sdk-v33-0-0-is-now-available-52d1c99dfe4c)._ @@ -34,18 +37,20 @@ This project follows [semantic versioning](https://semver.org/). We do not hesit Current Version: ![version](https://img.shields.io/npm/v/react-native-webview.svg) -- [7.0.1](https://github.com/react-native-community/react-native-webview/releases/tag/v7.0.1) - Removed UIWebView - -- [6.0.**2**](https://github.com/react-native-community/react-native-webview/releases/tag/v6.0.2) - Update to AndroidX. Make sure to enable it in your project's `android/gradle.properties`. See [Getting Started Guide](docs/Getting-Started.md). - -- [5.0.**1**](https://github.com/react-native-community/react-native-webview/releases/tag/v5.0.0) - Refactored the old postMessage implementation for communication from webview to native. -- [4.0.0](https://github.com/react-native-community/react-native-webview/releases/tag/v4.0.0) - Added cache (enabled by default). -- [3.0.0](https://github.com/react-native-community/react-native-webview/releases/tag/v3.0.0) - WKWebview: Add shared process pool so cookies and localStorage are shared across webviews in iOS (enabled by default). -- [2.0.0](https://github.com/react-native-community/react-native-webview/releases/tag/v2.0.0) - First release this is a replica of the core webview component +- [11.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0) - Android setSupportMultipleWindows. +- [10.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v10.0.0) - Android Gradle plugin is only required when opening the project stand-alone +- [9.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v9.0.0) - props updates to injectedJavaScript are no longer immutable. +- [8.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v8.0.0) - onNavigationStateChange now triggers with hash url changes +- [7.0.1](https://github.com/react-native-webview/react-native-webview/releases/tag/v7.0.1) - Removed UIWebView +- [6.0.**2**](https://github.com/react-native-webview/react-native-webview/releases/tag/v6.0.2) - Update to AndroidX. Make sure to enable it in your project's `android/gradle.properties`. See [Getting Started Guide](docs/Getting-Started.md). +- [5.0.**1**](https://github.com/react-native-webview/react-native-webview/releases/tag/v5.0.0) - Refactored the old postMessage implementation for communication from webview to native. +- [4.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v4.0.0) - Added cache (enabled by default). +- [3.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v3.0.0) - WKWebview: Add shared process pool so cookies and localStorage are shared across webviews in iOS (enabled by default). +- [2.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v2.0.0) - First release this is a replica of the core webview component **Upcoming:** -- this.webView.postMessage() removal (never documented and less flexible than injectJavascript) -> [how to migrate](https://github.com/react-native-community/react-native-webview/issues/809) +- this.webView.postMessage() removal (never documented and less flexible than injectJavascript) -> [how to migrate](https://github.com/react-native-webview/react-native-webview/issues/809) - Kotlin rewrite - Maybe Swift rewrite @@ -61,9 +66,7 @@ import { WebView } from 'react-native-webview'; // ... class MyWebComponent extends Component { render() { - return ( - - ); + return ; } } ``` @@ -76,7 +79,7 @@ For more, read the [API Reference](./docs/Reference.md) and [Guide](./docs/Guide ## Contributing -See [Contributing.md](https://github.com/react-native-community/react-native-webview/blob/master/docs/Contributing.md) +See [Contributing.md](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Contributing.md) ## Contributors @@ -94,8 +97,10 @@ This project follows the [all-contributors](https://github.com/all-contributors/ MIT -## Traduções +## Translations This readme is available in: - [Brazilian portuguese](docs/README.portuguese.md) + +[lean-core-issue]: https://github.com/facebook/react-native/issues/23313 diff --git a/__tests__/Alert.test.js b/__tests__/Alert.test.js new file mode 100644 index 000000000..f72769600 --- /dev/null +++ b/__tests__/Alert.test.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { driver, By2 } from 'selenium-appium' +import { until } from 'selenium-webdriver'; + +const setup = require('../jest-setups/jest.setup'); +jest.setTimeout(50000); + +beforeAll(() => { + return driver.startWithCapabilities(setup.capabilites); +}); + +afterAll(() => { + return driver.quit(); +}); + +describe('Alert Tests', () => { + + test('Show Alert', async () => { + const showAlertButton = await driver.wait(until.elementLocated(By2.nativeName('Show alert'))); + await showAlertButton.click(); + await driver.wait(until.elementLocated(By2.nativeName('Hello! I am an alert box!'))); + await By2.nativeName('OK').click(); + const dismissMessage = await driver.wait(until.elementLocated(By2.nativeName('Alert dismissed!'))); + expect(dismissMessage).not.toBeNull(); + }); + +}); \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index c467ee987..e6d05084b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,21 +1,30 @@ buildscript { - //Buildscript is evaluated before everything else so we can't use getExtOrDefault - def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['ReactNativeWebView_kotlinVersion'] - - repositories { - google() - jcenter() + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeWebView_' + name] } - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - //noinspection DifferentKotlinGradleVersion - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. + if (project == rootProject) { + repositories { + google() + jcenter() + } + + dependencies { + classpath("com.android.tools.build:gradle:3.6.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}") + } + } else { + repositories { + jcenter() + } -def getExtOrDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeWebView_' + name] + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}") + } + } } def getExtOrIntegerDefault(name) { @@ -123,6 +132,6 @@ def kotlin_version = getExtOrDefault('kotlinVersion') dependencies { //noinspection GradleDynamicVersion - api 'com.facebook.react:react-native:+' + implementation 'com.facebook.react:react-native:+' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } diff --git a/android/gradle.properties b/android/gradle.properties index 4d2041751..b45b8c82d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -ReactNativeWebView_kotlinVersion=1.3.11 -ReactNativeWebView_compileSdkVersion=28 -ReactNativeWebView_buildToolsVersion=28.0.3 +ReactNativeWebView_kotlinVersion=1.3.50 +ReactNativeWebView_compileSdkVersion=29 +ReactNativeWebView_buildToolsVersion=29.0.3 ReactNativeWebView_targetSdkVersion=28 \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index 5dfbc3e2c..9477d5e33 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -7,12 +7,12 @@ import android.app.AlertDialog; 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; import android.graphics.Color; import android.Manifest; +import android.net.http.SslError; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -22,6 +22,8 @@ import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; +import android.os.Message; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.Gravity; @@ -35,6 +37,8 @@ import android.webkit.DownloadListener; import android.webkit.GeolocationPermissions; import android.webkit.JavascriptInterface; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.SslErrorHandler; import android.webkit.PermissionRequest; import android.webkit.URLUtil; import android.webkit.ValueCallback; @@ -47,16 +51,25 @@ import android.widget.FrameLayout; import com.facebook.react.uimanager.events.Event; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.core.util.Pair; + +import com.facebook.common.logging.FLog; import com.facebook.react.views.scroll.ScrollEvent; import com.facebook.react.views.scroll.ScrollEventType; import com.facebook.react.views.scroll.OnScrollDispatchHelper; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.module.annotations.ReactModule; @@ -66,6 +79,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.ContentSizeChangeEvent; import com.facebook.react.uimanager.events.EventDispatcher; +import com.reactnativecommunity.webview.RNCWebViewModule.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; @@ -73,6 +87,7 @@ import com.reactnativecommunity.webview.events.TopLoadingStartEvent; import com.reactnativecommunity.webview.events.TopMessageEvent; import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent; +import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent; import org.json.JSONException; import org.json.JSONObject; @@ -88,7 +103,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicReference; import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; @@ -118,8 +133,8 @@ */ @ReactModule(name = RNCWebViewManager.REACT_CLASS) public class RNCWebViewManager extends SimpleViewManager { + private static final String TAG = "RNCWebViewManager"; - public static String activeUrl = null; public static final int COMMAND_GO_BACK = 1; public static final int COMMAND_GO_FORWARD = 2; public static final int COMMAND_RELOAD = 3; @@ -145,6 +160,7 @@ public class RNCWebViewManager extends SimpleViewManager { // Use `webView.loadUrl("about:blank")` to reliably reset the view // state and release page resources (including any running JavaScript). protected static final String BLANK_URL = "about:blank"; + protected static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250; protected WebViewConfig mWebViewConfig; @@ -214,53 +230,54 @@ protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) { @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) protected WebView createViewInstance(ThemedReactContext reactContext) { - RNCWebView webView = createRNCWebViewInstance(reactContext); - setupWebChromeClient(reactContext, webView); - reactContext.addLifecycleEventListener(webView); - mWebViewConfig.configWebView(webView); - WebSettings settings = webView.getSettings(); - settings.setBuiltInZoomControls(true); - settings.setDisplayZoomControls(false); - settings.setDomStorageEnabled(true); - - settings.setAllowFileAccess(false); - settings.setAllowContentAccess(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - settings.setAllowFileAccessFromFileURLs(false); - setAllowUniversalAccessFromFileURLs(webView, false); - } - setMixedContentMode(webView, "never"); + RNCWebView webView = createRNCWebViewInstance(reactContext); + setupWebChromeClient(reactContext, webView); + reactContext.addLifecycleEventListener(webView); + mWebViewConfig.configWebView(webView); + WebSettings settings = webView.getSettings(); + settings.setBuiltInZoomControls(true); + settings.setDisplayZoomControls(false); + settings.setDomStorageEnabled(true); + settings.setSupportMultipleWindows(true); + + settings.setAllowFileAccess(false); + settings.setAllowContentAccess(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + settings.setAllowFileAccessFromFileURLs(false); + setAllowUniversalAccessFromFileURLs(webView, false); + } + setMixedContentMode(webView, "never"); - // Fixes broken full-screen modals/galleries due to body height being 0. - webView.setLayoutParams( - new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); + // Fixes broken full-screen modals/galleries due to body height being 0. + webView.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); - setGeolocationEnabled(webView, false); - if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - WebView.setWebContentsDebuggingEnabled(true); - } + if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true); + } - webView.setDownloadListener(new DownloadListener() { - public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - RNCWebViewModule module = getModule(reactContext); - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); - - String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); - 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); - System.out.println("Got cookie for DownloadManager: " + cookie); - } catch (MalformedURLException e) { - System.out.println("Error getting cookie for DownloadManager: " + e.toString()); - e.printStackTrace(); - } + webView.setDownloadListener(new DownloadListener() { + public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { + webView.setIgnoreErrFailedForThisURL(url); + + RNCWebViewModule module = getModule(reactContext); + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); + + String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); + 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(); + } //Finish setting up request request.addRequestHeader("User-Agent", userAgent); @@ -286,6 +303,11 @@ public void setJavaScriptEnabled(WebView view, boolean enabled) { view.getSettings().setJavaScriptEnabled(enabled); } + @ReactProp(name = "setSupportMultipleWindows") + public void setSupportMultipleWindows(WebView view, boolean enabled){ + view.getSettings().setSupportMultipleWindows(enabled); + } + @ReactProp(name = "showsHorizontalScrollIndicator") public void setShowsHorizontalScrollIndicator(WebView view, boolean enabled) { view.setHorizontalScrollBarEnabled(enabled); @@ -301,13 +323,10 @@ public void setCacheEnabled(WebView view, boolean enabled) { 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); } } @@ -339,6 +358,21 @@ public void setHardwareAccelerationDisabled(WebView view, boolean disabled) { } } + @ReactProp(name = "androidLayerType") + 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; + } + view.setLayerType(layerType, null); + } + + @ReactProp(name = "overScrollMode") public void setOverScrollMode(WebView view, String overScrollModeString) { Integer overScrollMode; @@ -420,6 +454,11 @@ public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) { view.getSettings().setMediaPlaybackRequiresUserGesture(requires); } + @ReactProp(name = "javaScriptCanOpenWindowsAutomatically") + public void setJavaScriptCanOpenWindowsAutomatically(WebView view, boolean enabled) { + view.getSettings().setJavaScriptCanOpenWindowsAutomatically(enabled); + } + @ReactProp(name = "allowFileAccessFromFileURLs") public void setAllowFileAccessFromFileURLs(WebView view, boolean allow) { view.getSettings().setAllowFileAccessFromFileURLs(allow); @@ -440,13 +479,38 @@ public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScr ((RNCWebView) view).setInjectedJavaScript(injectedJavaScript); } + @ReactProp(name = "injectedJavaScriptBeforeContentLoaded") + public void setInjectedJavaScriptBeforeContentLoaded(WebView view, @Nullable String injectedJavaScriptBeforeContentLoaded) { + ((RNCWebView) view).setInjectedJavaScriptBeforeContentLoaded(injectedJavaScriptBeforeContentLoaded); + } + + @ReactProp(name = "injectedJavaScriptForMainFrameOnly") + public void setInjectedJavaScriptForMainFrameOnly(WebView view, boolean enabled) { + ((RNCWebView) view).setInjectedJavaScriptForMainFrameOnly(enabled); + } + + @ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly") + public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(WebView view, boolean enabled) { + ((RNCWebView) view).setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(enabled); + } + @ReactProp(name = "messagingEnabled") public void setMessagingEnabled(WebView view, boolean enabled) { ((RNCWebView) view).setMessagingEnabled(enabled); } + @ReactProp(name = "messagingModuleName") + public void setMessagingModuleName(WebView view, String moduleName) { + ((RNCWebView) view).setMessagingModuleName(moduleName); + } + @ReactProp(name = "incognito") public void setIncognito(WebView view, boolean enabled) { + // Don't do anything when incognito is disabled + if (!enabled) { + return; + } + // Remove all previous cookies if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CookieManager.getInstance().removeAllCookies(null); @@ -456,14 +520,13 @@ public void setIncognito(WebView view, boolean enabled) { // Disable caching view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - view.getSettings().setAppCacheEnabled(!enabled); view.clearHistory(); - view.clearCache(enabled); + view.clearCache(true); // No form data or autofill enabled view.clearFormData(); - view.getSettings().setSavePassword(!enabled); - view.getSettings().setSaveFormData(!enabled); + view.getSettings().setSavePassword(false); + view.getSettings().setSaveFormData(false); } @ReactProp(name = "source") @@ -593,6 +656,7 @@ public Map getExportedCustomDirectEventTypeConstants() { export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest")); export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll")); export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError")); + export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone")); return export; } @@ -662,6 +726,7 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray if (args == null) { throw new RuntimeException("Arguments for loading an url are null!"); } + ((RNCWebView) root).progressChangedFilter.setWaitingForCommandLoadUrl(false); root.loadUrl(args.getString(0)); break; case COMMAND_SCROLL_TO_OFFSET: { @@ -709,6 +774,7 @@ public void onDropViewInstance(WebView webView) { super.onDropViewInstance(webView); ((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((RNCWebView) webView); ((RNCWebView) webView).cleanupCallbacksAndDestroy(); + mWebChromeClient = null; } public static RNCWebViewModule getModule(ReactContext reactContext) { @@ -719,6 +785,11 @@ protected void setupWebChromeClient(ReactContext reactContext, WebView webView) if (mAllowsFullscreenVideo) { int initialRequestedOrientation = reactContext.getCurrentActivity().getRequestedOrientation(); mWebChromeClient = new RNCWebChromeClient(reactContext, webView) { + @Override + public Bitmap getDefaultVideoPoster() { + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + } + @Override public void onShowCustomView(View view, CustomViewCallback callback) { if (mVideoView != null) { @@ -737,8 +808,25 @@ public void onShowCustomView(View view, CustomViewCallback callback) { } mVideoView.setBackgroundColor(Color.BLACK); - getRootView().addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS); - mWebView.setVisibility(View.GONE); + + // since RN's Modals interfere with the View hierarchy + // we will decide which View to Hide if the hierarchy + // does not match (i.e., the webview is within a Modal) + // NOTE: We could use mWebView.getRootView() instead of getRootView() + // but that breaks the Modal's styles and layout, so we need this to render + // in the main View hierarchy regardless. + ViewGroup rootView = getRootView(); + rootView.addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS); + + // Different root views, we are in a Modal + if(rootView.getRootView() != mWebView.getRootView()){ + mWebView.getRootView().setVisibility(View.GONE); + } + + // Same view hierarchy (no Modal), just hide the webview then + else{ + mWebView.setVisibility(View.GONE); + } mReactContext.addLifecycleEventListener(this); } @@ -749,18 +837,28 @@ public void onHideCustomView() { return; } - mVideoView.setVisibility(View.GONE); - getRootView().removeView(mVideoView); - mCustomViewCallback.onCustomViewHidden(); + // same logic as above + ViewGroup rootView = getRootView(); - mVideoView = null; - mCustomViewCallback = null; + if(rootView.getRootView() != mWebView.getRootView()){ + mWebView.getRootView().setVisibility(View.VISIBLE); + } - mWebView.setVisibility(View.VISIBLE); + // Same view hierarchy (no Modal) + else{ + mWebView.setVisibility(View.VISIBLE); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mReactContext.getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } + + rootView.removeView(mVideoView); + mCustomViewCallback.onCustomViewHidden(); + + mVideoView = null; + mCustomViewCallback = null; + mReactContext.getCurrentActivity().setRequestedOrientation(initialRequestedOrientation); mReactContext.removeLifecycleEventListener(this); @@ -771,7 +869,12 @@ public void onHideCustomView() { if (mWebChromeClient != null) { mWebChromeClient.onHideCustomView(); } - mWebChromeClient = new RNCWebChromeClient(reactContext, webView); + mWebChromeClient = new RNCWebChromeClient(reactContext, webView) { + @Override + public Bitmap getDefaultVideoPoster() { + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + } + }; webView.setWebChromeClient(mWebChromeClient); } } @@ -781,6 +884,8 @@ protected static class RNCWebViewClient extends WebViewClient { protected boolean mLastLoadFailed = false; protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent; + protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; + protected @Nullable String ignoreErrFailedForThisURL = null; protected float mZoomScale = -1.0f; public float getZoomScale() { @@ -791,6 +896,11 @@ public float getZoomScale() { public void onScaleChanged(WebView view, float oldScale, float newScale) { mZoomScale = newScale; } + + + public void setIgnoreErrFailedForThisURL(@Nullable String url) { + ignoreErrFailedForThisURL = url; + } @Override public void onPageFinished(WebView webView, String url) { @@ -810,6 +920,9 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) { super.onPageStarted(webView, url, favicon); mLastLoadFailed = false; + RNCWebView reactWebView = (RNCWebView) webView; + reactWebView.callInjectedJavaScriptBeforeContentLoaded(); + dispatchEvent( webView, new TopLoadingStartEvent( @@ -819,15 +932,52 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - activeUrl = url; - dispatchEvent( - view, - new TopShouldStartLoadWithRequestEvent( - view.getId(), - createWebViewEvent(view, url))); - return true; - } + final RNCWebView rncWebView = (RNCWebView) view; + final boolean isJsDebugging = ((ReactContext) view.getContext()).getJavaScriptContextHolder().get() == 0; + + if (!isJsDebugging && rncWebView.mCatalystInstance != null) { + 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); + + try { + assert lockObject != null; + synchronized (lockObject) { + final long startTime = SystemClock.elapsedRealtime(); + while (lockObject.get() == ShouldOverrideCallbackState.UNDECIDED) { + if (SystemClock.elapsedRealtime() - startTime > SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) { + FLog.w(TAG, "Did not receive response to shouldOverrideUrlLoading in time, defaulting to allow loading."); + RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); + return false; + } + lockObject.wait(SHOULD_OVERRIDE_URL_LOADING_TIMEOUT); + } + } + } catch (InterruptedException e) { + FLog.e(TAG, "shouldOverrideUrlLoading was interrupted while waiting for result.", e); + RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); + return false; + } + + final boolean shouldOverride = lockObject.get() == ShouldOverrideCallbackState.SHOULD_OVERRIDE; + RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); + + return shouldOverride; + } 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); + dispatchEvent( + view, + new TopShouldStartLoadWithRequestEvent( + view.getId(), + createWebViewEvent(view, url))); + return true; + } + } @TargetApi(Build.VERSION_CODES.N) @Override @@ -836,12 +986,86 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request 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; + } + + 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; + } + + description = descriptionPrefix + description; + + this.onReceivedError( + webView, + code, + description, + failingUrl + ); + } + @Override public void onReceivedError( WebView webView, int errorCode, String description, String failingUrl) { + + if (ignoreErrFailedForThisURL != null + && 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: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678 + // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635 + // This entire commit should be reverted once this bug is resolved in chromium. + setIgnoreErrFailedForThisURL(null); + return; + } + super.onReceivedError(webView, errorCode, description, failingUrl); mLastLoadFailed = true; @@ -877,6 +1101,41 @@ public void onReceivedHttpError( } } + @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); + + 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; + } + + WritableMap event = createWebViewEvent(webView, webView.getUrl()); + event.putBoolean("didCrash", detail.didCrash()); + + dispatchEvent( + webView, + new TopRenderProcessGoneEvent(webView.getId(), event) + ); + + // returning false would crash the app. + return true; + } + protected void emitFinishEvent(WebView webView, String url) { dispatchEvent( webView, @@ -901,6 +1160,10 @@ protected WritableMap createWebViewEvent(WebView webView, String url) { public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) { mUrlPrefixesForDefaultIntent = specialUrls; } + + public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) { + progressChangedFilter = filter; + } } protected static class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener { @@ -922,11 +1185,23 @@ protected static class RNCWebChromeClient extends WebChromeClient implements Lif protected View mVideoView; protected WebChromeClient.CustomViewCallback mCustomViewCallback; + protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; + public RNCWebChromeClient(ReactContext reactContext, WebView webView) { this.mReactContext = reactContext; this.mWebView = webView; } + public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + + final WebView newWebView = new WebView(view.getContext()); + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + @Override public boolean onConsoleMessage(ConsoleMessage cm) { try { @@ -970,6 +1245,8 @@ public void onPermissionRequest(final PermissionRequest request) { permissions.add(Manifest.permission.RECORD_AUDIO); } else if (requestedResources[i].equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { permissions.add(Manifest.permission.CAMERA); + } else if(requestedResources[i].equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { + permissions.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID); } // TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID. } @@ -982,6 +1259,8 @@ public void onPermissionRequest(final PermissionRequest request) { grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE); } else if (permissions.get(i).equals(Manifest.permission.CAMERA)) { grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE); + } else if (permissions.get(i).equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { + grantedPermissions.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID); } } @@ -998,11 +1277,7 @@ public void onPermissionRequest(final PermissionRequest request) { public void onProgressChanged(WebView webView, int newProgress) { super.onProgressChanged(webView, newProgress); final String url = webView.getUrl(); - if ( - url != null - && activeUrl != null - && !url.equals(activeUrl) - ) { + if (progressChangedFilter.isWaitingForCommandLoadUrl()) { return; } WritableMap event = Arguments.createMap(); @@ -1041,8 +1316,7 @@ protected void openFileChooser(ValueCallback filePathCallback, String accep public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { String[] acceptTypes = fileChooserParams.getAcceptTypes(); boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE; - Intent intent = fileChooserParams.createIntent(); - return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple); + return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple); } @Override @@ -1061,6 +1335,10 @@ public void onHostDestroy() { } protected ViewGroup getRootView() { return (ViewGroup) mReactContext.getCurrentActivity().findViewById(android.R.id.content); } + + public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) { + progressChangedFilter = filter; + } } /** @@ -1070,13 +1348,28 @@ protected ViewGroup getRootView() { protected static class RNCWebView extends WebView implements LifecycleEventListener { protected @Nullable String injectedJS; + protected @Nullable + String injectedJSBeforeContentLoaded; + + /** + * android.webkit.WebChromeClient fundamentally does not support JS injection into frames other + * than the main frame, so these two properties are mostly here just for parity with iOS & macOS. + */ + protected boolean injectedJavaScriptForMainFrameOnly = true; + protected boolean injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true; + protected boolean messagingEnabled = false; protected @Nullable + String messagingModuleName; + protected @Nullable RNCWebViewClient mRNCWebViewClient; + protected @Nullable + CatalystInstance mCatalystInstance; protected boolean sendContentSizeChangeEvents = false; private OnScrollDispatchHelper mOnScrollDispatchHelper; protected boolean hasScrollEvent = false; private ValueAnimator currentAnimator; + protected ProgressChangedFilter progressChangedFilter; /** * WebView must be created with an context of the current activity @@ -1086,6 +1379,12 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe */ public RNCWebView(ThemedReactContext reactContext) { super(reactContext); + this.createCatalystInstance(); + progressChangedFilter = new ProgressChangedFilter(); + } + + public void setIgnoreErrFailedForThisURL(String url) { + mRNCWebViewClient.setIgnoreErrFailedForThisURL(url); } public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) { @@ -1132,6 +1431,17 @@ public void setWebViewClient(WebViewClient client) { super.setWebViewClient(client); if (client instanceof RNCWebViewClient) { mRNCWebViewClient = (RNCWebViewClient) client; + mRNCWebViewClient.setProgressChangedFilter(progressChangedFilter); + } + } + + WebChromeClient mWebChromeClient; + @Override + public void setWebChromeClient(WebChromeClient client) { + this.mWebChromeClient = client; + super.setWebChromeClient(client); + if (client instanceof RNCWebChromeClient) { + ((RNCWebChromeClient) client).setProgressChangedFilter(progressChangedFilter); } } @@ -1144,10 +1454,30 @@ public void setInjectedJavaScript(@Nullable String js) { injectedJS = js; } + public void setInjectedJavaScriptBeforeContentLoaded(@Nullable String js) { + injectedJSBeforeContentLoaded = js; + } + + public void setInjectedJavaScriptForMainFrameOnly(boolean enabled) { + injectedJavaScriptForMainFrameOnly = enabled; + } + + public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(boolean enabled) { + injectedJavaScriptBeforeContentLoadedForMainFrameOnly = enabled; + } + protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) { return new RNCWebViewBridge(webView); } + protected void createCatalystInstance() { + ReactContext reactContext = (ReactContext) this.getContext(); + + if (reactContext != null) { + mCatalystInstance = reactContext.getCatalystInstance(); + } + } + @SuppressLint("AddJavascriptInterface") public void setMessagingEnabled(boolean enabled) { if (messagingEnabled == enabled) { @@ -1163,6 +1493,10 @@ public void setMessagingEnabled(boolean enabled) { } } + public void setMessagingModuleName(String moduleName) { + messagingModuleName = moduleName; + } + protected void evaluateJavascriptWithFallback(String script) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { evaluateJavascript(script, null); @@ -1185,7 +1519,18 @@ public void callInjectedJavaScript() { } } + public void callInjectedJavaScriptBeforeContentLoaded() { + if (getSettings().getJavaScriptEnabled() && + injectedJSBeforeContentLoaded != null && + !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { + evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();"); + } + } + public void onMessage(String message) { + ReactContext reactContext = (ReactContext) this.getContext(); + RNCWebView mContext = this; + if (mRNCWebViewClient != null) { WebView webView = this; webView.post(new Runnable() { @@ -1196,13 +1541,23 @@ public void run() { } WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, webView.getUrl()); data.putString("data", message); - dispatchEvent(webView, new TopMessageEvent(webView.getId(), data)); + + if (mCatalystInstance != null) { + mContext.sendDirectMessage("onMessage", data); + } else { + dispatchEvent(webView, new TopMessageEvent(webView.getId(), data)); + } } }); } else { WritableMap eventData = Arguments.createMap(); eventData.putString("data", message); - dispatchEvent(this, new TopMessageEvent(this.getId(), eventData)); + + if (mCatalystInstance != null) { + this.sendDirectMessage("onMessage", eventData); + } else { + dispatchEvent(this, new TopMessageEvent(this.getId(), eventData)); + } } } @@ -1362,6 +1717,16 @@ private void setScrollTo(int x, int y) { } } + protected void sendDirectMessage(final String method, WritableMap data) { + WritableNativeMap event = new WritableNativeMap(); + event.putMap("nativeEvent", data); + + WritableNativeArray params = new WritableNativeArray(); + params.pushMap(event); + + mCatalystInstance.callFunction(messagingModuleName, method, params); + } + protected void onScrollChanged(int x, int y, int oldX, int oldY) { super.onScrollChanged(x, y, oldX, oldY); @@ -1395,6 +1760,14 @@ protected void cleanupCallbacksAndDestroy() { destroy(); } + @Override + public void destroy() { + if (mWebChromeClient != null) { + mWebChromeClient.onHideCustomView(); + } + super.destroy(); + } + protected class RNCWebViewBridge { RNCWebView mContext; @@ -1411,5 +1784,17 @@ public void postMessage(String message) { mContext.onMessage(message); } } + + protected static class ProgressChangedFilter { + private boolean waitingForCommandLoadUrl = false; + + public void setWaitingForCommandLoadUrl(boolean isWaiting) { + waitingForCommandLoadUrl = isWaiting; + } + + public boolean isWaitingForCommandLoadUrl() { + return waitingForCommandLoadUrl; + } + } } } diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java index b730535b9..34fd44b24 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java @@ -11,9 +11,13 @@ import android.os.Environment; import android.os.Parcelable; import android.provider.MediaStore; + +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; +import androidx.core.util.Pair; + import android.util.Log; import android.webkit.MimeTypeMap; import android.webkit.ValueCallback; @@ -32,6 +36,9 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; import static android.app.Activity.RESULT_OK; @@ -41,11 +48,53 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti private static final int PICKER = 1; private static final int PICKER_LEGACY = 3; private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1; - final String DEFAULT_MIME_TYPES = "*/*"; private ValueCallback filePathCallbackLegacy; private ValueCallback filePathCallback; - private Uri outputFileUri; + private File outputImage; + private File outputVideo; private DownloadManager.Request downloadRequest; + + protected static class ShouldOverrideUrlLoadingLock { + protected enum ShouldOverrideCallbackState { + UNDECIDED, + SHOULD_OVERRIDE, + DO_NOT_OVERRIDE, + } + + private int nextLockIdentifier = 1; + private final HashMap> shouldOverrideLocks = new HashMap<>(); + + public synchronized Pair> getNewLock() { + final int lockIdentifier = nextLockIdentifier++; + final AtomicReference shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED); + shouldOverrideLocks.put(lockIdentifier, shouldOverride); + return new Pair<>(lockIdentifier, shouldOverride); + } + + @Nullable + public synchronized AtomicReference getLock(Integer lockIdentifier) { + return shouldOverrideLocks.get(lockIdentifier); + } + + public synchronized void removeLock(Integer lockIdentifier) { + shouldOverrideLocks.remove(lockIdentifier); + } + } + + protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock(); + + private enum MimeType { + DEFAULT("*/*"), + IMAGE("image"), + VIDEO("video"); + + private final String value; + + MimeType(String value) { + this.value = value; + } + } + private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() { @Override public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { @@ -89,12 +138,33 @@ public void isFileUploadSupported(final Promise promise) { promise.resolve(result); } + @ReactMethod(isBlockingSynchronousMethod = true) + public void onShouldStartLoadWithRequestCallback(final boolean shouldStart, final int lockIdentifier) { + final AtomicReference lockObject = shouldOverrideUrlLoadingLock.getLock(lockIdentifier); + if (lockObject != null) { + synchronized (lockObject) { + lockObject.set(shouldStart ? ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.DO_NOT_OVERRIDE : ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE); + lockObject.notify(); + } + } + } + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { if (filePathCallback == null && filePathCallbackLegacy == null) { return; } + boolean imageTaken = false; + boolean videoTaken = false; + + if (outputImage != null && outputImage.length() > 0) { + imageTaken = true; + } + if (outputVideo != null && outputVideo.length() > 0) { + videoTaken = true; + } + // based off of which button was pressed, we get an activity result and a file // the camera activity doesn't properly return the filename* (I think?) so we use // this filename instead @@ -105,23 +175,42 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, filePathCallback.onReceiveValue(null); } } else { - Uri result[] = this.getSelectedFiles(data, resultCode); - if (result != null) { - filePathCallback.onReceiveValue(result); + if (imageTaken) { + filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputImage)}); + } else if (videoTaken) { + filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputVideo)}); } else { - filePathCallback.onReceiveValue(new Uri[]{outputFileUri}); + filePathCallback.onReceiveValue(this.getSelectedFiles(data, resultCode)); } } break; case PICKER_LEGACY: - Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData(); - filePathCallbackLegacy.onReceiveValue(result); + if (resultCode != RESULT_OK) { + filePathCallbackLegacy.onReceiveValue(null); + } else { + if (imageTaken) { + filePathCallbackLegacy.onReceiveValue(getOutputUri(outputImage)); + } else if (videoTaken) { + filePathCallbackLegacy.onReceiveValue(getOutputUri(outputVideo)); + } else { + filePathCallbackLegacy.onReceiveValue(data.getData()); + } + } break; } + + if (outputImage != null && !imageTaken) { + outputImage.delete(); + } + if (outputVideo != null && !videoTaken) { + outputVideo.delete(); + } + filePathCallback = null; filePathCallbackLegacy = null; - outputFileUri = null; + outputImage = null; + outputVideo = null; } public void onNewIntent(Intent intent) { @@ -132,15 +221,6 @@ private Uri[] getSelectedFiles(Intent data, int resultCode) { return null; } - // we have one file selected - if (data.getData() != null) { - if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return WebChromeClient.FileChooserParams.parseResult(resultCode, data); - } else { - return null; - } - } - // we have multiple files selected if (data.getClipData() != null) { final int numSelectedFiles = data.getClipData().getItemCount(); @@ -150,6 +230,12 @@ private Uri[] getSelectedFiles(Intent data, int resultCode) { } return result; } + + // we have one file selected + if (data.getData() != null && resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return WebChromeClient.FileChooserParams.parseResult(resultCode, data); + } + return null; } @@ -161,10 +247,16 @@ public void startPhotoPickerIntent(ValueCallback filePathCallback, String a ArrayList extraIntents = new ArrayList<>(); if (acceptsImages(acceptType)) { - extraIntents.add(getPhotoIntent()); + Intent photoIntent = getPhotoIntent(); + if (photoIntent != null) { + extraIntents.add(photoIntent); + } } if (acceptsVideo(acceptType)) { - extraIntents.add(getVideoIntent()); + Intent videoIntent = getVideoIntent(); + if (videoIntent != null) { + extraIntents.add(videoIntent); + } } chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); @@ -176,15 +268,23 @@ public void startPhotoPickerIntent(ValueCallback filePathCallback, String a } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public boolean startPhotoPickerIntent(final ValueCallback callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) { + public boolean startPhotoPickerIntent(final ValueCallback callback, final String[] acceptTypes, final boolean allowMultiple) { filePathCallback = callback; ArrayList extraIntents = new ArrayList<>(); - if (acceptsImages(acceptTypes)) { - extraIntents.add(getPhotoIntent()); - } - if (acceptsVideo(acceptTypes)) { - extraIntents.add(getVideoIntent()); + if (!needsCameraPermission()) { + if (acceptsImages(acceptTypes)) { + Intent photoIntent = getPhotoIntent(); + if (photoIntent != null) { + extraIntents.add(photoIntent); + } + } + if (acceptsVideo(acceptTypes)) { + Intent videoIntent = getVideoIntent(); + if (videoIntent != null) { + extraIntents.add(videoIntent); + } + } } Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple); @@ -216,16 +316,13 @@ public void downloadFile() { } public boolean grantFileDownloaderPermissions() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Permission not required for Android Q and above + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { return true; } - boolean result = true; - if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - result = false; - } - - if (!result) { + boolean result = ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + if (!result && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PermissionAwareActivity activity = getPermissionAwareActivity(); activity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener); } @@ -233,24 +330,59 @@ public boolean grantFileDownloaderPermissions() { return result; } + protected boolean needsCameraPermission() { + boolean needed = false; + + PackageManager packageManager = getCurrentActivity().getPackageManager(); + try { + String[] requestedPermissions = packageManager.getPackageInfo(getReactApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; + if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA) + && ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + needed = true; + } + } catch (PackageManager.NameNotFoundException e) { + needed = true; + } + + return needed; + } + private Intent getPhotoIntent() { - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); + Intent intent = null; + + try { + outputImage = getCapturedFile(MimeType.IMAGE); + Uri outputImageUri = getOutputUri(outputImage); + intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputImageUri); + } catch (IOException | IllegalArgumentException e) { + Log.e("CREATE FILE", "Error occurred while creating the File", e); + e.printStackTrace(); + } + return intent; } private Intent getVideoIntent() { - Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - outputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); + Intent intent = null; + + try { + outputVideo = getCapturedFile(MimeType.VIDEO); + Uri outputVideoUri = getOutputUri(outputVideo); + intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri); + } catch (IOException | IllegalArgumentException e) { + Log.e("CREATE FILE", "Error occurred while creating the File", e); + e.printStackTrace(); + } + return intent; } private Intent getFileChooserIntent(String acceptTypes) { String _acceptTypes = acceptTypes; if (acceptTypes.isEmpty()) { - _acceptTypes = DEFAULT_MIME_TYPES; + _acceptTypes = MimeType.DEFAULT.value; } if (acceptTypes.matches("\\.\\w+")) { _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", "")); @@ -264,7 +396,7 @@ private Intent getFileChooserIntent(String acceptTypes) { private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); + intent.setType(MimeType.DEFAULT.value); intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes)); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); return intent; @@ -275,25 +407,33 @@ private Boolean acceptsImages(String types) { if (types.matches("\\.\\w+")) { mimeType = getMimeTypeFromExtension(types.replace(".", "")); } - return mimeType.isEmpty() || mimeType.toLowerCase().contains("image"); + return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.IMAGE.value); } private Boolean acceptsImages(String[] types) { String[] mimeTypes = getAcceptedMimeType(types); - return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image"); + return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.IMAGE.value); } private Boolean acceptsVideo(String types) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + String mimeType = types; if (types.matches("\\.\\w+")) { mimeType = getMimeTypeFromExtension(types.replace(".", "")); } - return mimeType.isEmpty() || mimeType.toLowerCase().contains("video"); + return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.VIDEO.value); } private Boolean acceptsVideo(String[] types) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + String[] mimeTypes = getAcceptedMimeType(types); - return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "video"); + return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value); } private Boolean arrayContainsString(String[] array, String pattern) { @@ -306,8 +446,8 @@ private Boolean arrayContainsString(String[] array, String pattern) { } private String[] getAcceptedMimeType(String[] types) { - if (isArrayEmpty(types)) { - return new String[]{DEFAULT_MIME_TYPES}; + if (noAcceptTypesSet(types)) { + return new String[]{MimeType.DEFAULT.value}; } String[] mimeTypes = new String[types.length]; for (int i = 0; i < types.length; i++) { @@ -315,7 +455,11 @@ private String[] getAcceptedMimeType(String[] types) { // convert file extensions to mime types if (t.matches("\\.\\w+")) { String mimeType = getMimeTypeFromExtension(t.replace(".", "")); - mimeTypes[i] = mimeType; + if(mimeType != null) { + mimeTypes[i] = mimeType; + } else { + mimeTypes[i] = t; + } } else { mimeTypes[i] = t; } @@ -331,15 +475,7 @@ private String getMimeTypeFromExtension(String extension) { return type; } - private Uri getOutputUri(String intentType) { - File capturedFile = null; - try { - capturedFile = getCapturedFile(intentType); - } catch (IOException e) { - Log.e("CREATE FILE", "Error occurred while creating the File", e); - e.printStackTrace(); - } - + private Uri getOutputUri(File capturedFile) { // for versions below 6.0 (23) we use the old File creation & permissions model if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return Uri.fromFile(capturedFile); @@ -350,41 +486,50 @@ private Uri getOutputUri(String intentType) { return FileProvider.getUriForFile(getReactApplicationContext(), packageName + ".fileprovider", capturedFile); } - private File getCapturedFile(String intentType) throws IOException { + private File getCapturedFile(MimeType mimeType) throws IOException { String prefix = ""; String suffix = ""; String dir = ""; - String filename = ""; - if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) { - prefix = "image-"; - suffix = ".jpg"; - dir = Environment.DIRECTORY_PICTURES; - } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) { - prefix = "video-"; - suffix = ".mp4"; - dir = Environment.DIRECTORY_MOVIES; + switch (mimeType) { + case IMAGE: + prefix = "image-"; + suffix = ".jpg"; + dir = Environment.DIRECTORY_PICTURES; + break; + case VIDEO: + prefix = "video-"; + suffix = ".mp4"; + dir = Environment.DIRECTORY_MOVIES; + break; + + default: + break; } - filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix; + String filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix; + File outputFile = null; // for versions below 6.0 (23) we use the old File creation & permissions model if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // only this Directory works on all tested Android versions // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21) File storageDir = Environment.getExternalStoragePublicDirectory(dir); - return new File(storageDir, filename); + outputFile = new File(storageDir, filename); + } else { + File storageDir = getReactApplicationContext().getExternalFilesDir(null); + outputFile = File.createTempFile(prefix, suffix, storageDir); } - File storageDir = getReactApplicationContext().getExternalFilesDir(null); - return File.createTempFile(filename, suffix, storageDir); + return outputFile; } - private Boolean isArrayEmpty(String[] arr) { + private Boolean noAcceptTypesSet(String[] types) { // when our array returned from getAcceptTypes() has no values set from the webview // i.e. , without any "accept" attr // will be an array with one empty string element, afaik - return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0); + + return types.length == 0 || (types.length == 1 && types[0] != null && types[0].length() == 0); } private PermissionAwareActivity getPermissionAwareActivity() { diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java deleted file mode 100644 index e75edf40d..000000000 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.reactnativecommunity.webview; - -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; - -import java.util.Collections; -import java.util.List; - -public class RNCWebViewPackage implements ReactPackage { - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.singletonList(new RNCWebViewModule(reactContext)); - } - - // Deprecated from RN 0.47 - public List> createJSModules() { - return Collections.emptyList(); - } - - @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.singletonList(new RNCWebViewManager()); - } -} diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt new file mode 100644 index 000000000..2b74c74aa --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.kt @@ -0,0 +1,15 @@ +package com.reactnativecommunity.webview + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.ReactApplicationContext + + +class RNCWebViewPackage: ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext) = listOf( + RNCWebViewModule(reactContext) + ) + + override fun createViewManagers(reactContext: ReactApplicationContext) = listOf( + RNCWebViewManager() + ) +} diff --git a/android/src/main/java/com/reactnativecommunity/webview/events/TopRenderProcessGoneEvent.kt b/android/src/main/java/com/reactnativecommunity/webview/events/TopRenderProcessGoneEvent.kt new file mode 100644 index 000000000..b87f4fab3 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/events/TopRenderProcessGoneEvent.kt @@ -0,0 +1,26 @@ +package com.reactnativecommunity.webview.events + +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +/** + * Event emitted when the WebView's process has crashed or + was killed by the OS. + */ +class TopRenderProcessGoneEvent(viewId: Int, private val mEventData: WritableMap) : + Event(viewId) { + companion object { + const val EVENT_NAME = "topRenderProcessGone" + } + + override fun getEventName(): String = EVENT_NAME + + override fun canCoalesce(): Boolean = false + + override fun getCoalescingKey(): Short = 0 + + override fun dispatch(rctEventEmitter: RCTEventEmitter) = + rctEventEmitter.receiveEvent(viewTag, eventName, mEventData) + +} diff --git a/android/src/main/java/com/reactnativecommunity/webview/events/TopShouldStartLoadWithRequestEvent.kt b/android/src/main/java/com/reactnativecommunity/webview/events/TopShouldStartLoadWithRequestEvent.kt index 0f80cedfc..da4eb9664 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/events/TopShouldStartLoadWithRequestEvent.kt +++ b/android/src/main/java/com/reactnativecommunity/webview/events/TopShouldStartLoadWithRequestEvent.kt @@ -14,6 +14,8 @@ class TopShouldStartLoadWithRequestEvent(viewId: Int, private val mData: Writabl init { mData.putString("navigationType", "other") + // Android does not raise shouldOverrideUrlLoading for inner frames + mData.putBoolean("isTopFrame", true) } override fun getEventName(): String = EVENT_NAME diff --git a/ios/RNCWKProcessPoolManager.h b/apple/RNCWKProcessPoolManager.h similarity index 100% rename from ios/RNCWKProcessPoolManager.h rename to apple/RNCWKProcessPoolManager.h diff --git a/ios/RNCWKProcessPoolManager.m b/apple/RNCWKProcessPoolManager.m similarity index 100% rename from ios/RNCWKProcessPoolManager.m rename to apple/RNCWKProcessPoolManager.m diff --git a/ios/RNCWebView.h b/apple/RNCWebView.h similarity index 65% rename from ios/RNCWebView.h rename to apple/RNCWebView.h index c55456859..4bd87bd0f 100644 --- a/ios/RNCWebView.h +++ b/apple/RNCWebView.h @@ -19,14 +19,26 @@ @end +@interface RNCWeakScriptMessageDelegate : NSObject + +@property (nonatomic, weak, nullable) id scriptDelegate; + +- (nullable instancetype)initWithDelegate:(id _Nullable)scriptDelegate; + +@end + @interface RNCWebView : RCTView @property (nonatomic, weak) id _Nullable delegate; @property (nonatomic, copy) NSDictionary * _Nullable source; @property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, copy) NSString * _Nullable injectedJavaScript; +@property (nonatomic, copy) NSString * _Nullable injectedJavaScriptBeforeContentLoaded; +@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly; +@property (nonatomic, assign) BOOL injectedJavaScriptBeforeContentLoadedForMainFrameOnly; @property (nonatomic, assign) BOOL scrollEnabled; @property (nonatomic, assign) BOOL sharedCookiesEnabled; +@property (nonatomic, assign) BOOL autoManageStatusBarEnabled; @property (nonatomic, assign) BOOL pagingEnabled; @property (nonatomic, assign) CGFloat decelerationRate; @property (nonatomic, assign) BOOL allowsInlineMediaPlayback; @@ -46,11 +58,27 @@ @property (nonatomic, copy) NSString * _Nullable applicationNameForUserAgent; @property (nonatomic, assign) BOOL cacheEnabled; @property (nonatomic, assign) BOOL javaScriptEnabled; +@property (nonatomic, assign) BOOL javaScriptCanOpenWindowsAutomatically; +@property (nonatomic, assign) BOOL allowFileAccessFromFileURLs; +@property (nonatomic, assign) BOOL allowUniversalAccessFromFileURLs; @property (nonatomic, assign) BOOL allowsLinkPreview; @property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; @property (nonatomic, assign) BOOL showsVerticalScrollIndicator; @property (nonatomic, assign) BOOL directionalLockEnabled; +@property (nonatomic, assign) BOOL ignoreSilentHardwareSwitch; @property (nonatomic, copy) NSString * _Nullable allowingReadAccessToURL; +@property (nonatomic, assign) BOOL pullToRefreshEnabled; +#if !TARGET_OS_OSX +@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl; +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ +@property (nonatomic, assign) WKContentMode contentMode; +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ +@property (nonatomic, assign) BOOL limitsNavigationsToAppBoundDomains; +#endif + (void)setClientAuthenticationCredential:(nullable NSURLCredential*)credential; + (void)setCustomCertificatesForHost:(nullable NSDictionary *)certificates; @@ -63,5 +91,9 @@ - (void)scrollToOffset:(CGPoint)point animated:(BOOL)animated; - (void)setZoomScale:(CGFloat)scale animated:(BOOL)animated; - (void)zoomToRect:(CGRect)rect withScale:(CGFloat)scale animated:(BOOL)animated; +#if !TARGET_OS_OSX +- (void)addPullToRefreshControl; +- (void)pullToRefresh:(UIRefreshControl *_Nonnull)refreshControl; +#endif @end diff --git a/ios/RNCWebView.m b/apple/RNCWebView.m similarity index 55% rename from ios/RNCWebView.m rename to apple/RNCWebView.m index ebb341100..a235af619 100644 --- a/ios/RNCWebView.m +++ b/apple/RNCWebView.m @@ -9,15 +9,21 @@ #import #import #import "RNCWKProcessPoolManager.h" +#if !TARGET_OS_OSX #import +#else +#import +#endif // !TARGET_OS_OSX #import "objc/runtime.h" static NSTimer *keyboardTimer; +static NSString *const HistoryShimName = @"ReactNativeHistoryShim"; static NSString *const MessageHandlerName = @"ReactNativeWebView"; static NSURLCredential* clientAuthenticationCredential; static NSDictionary* customCertificatesForHost; +#if !TARGET_OS_OSX // runtime trick to remove WKWebView keyboard default toolbar // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279 @interface _SwizzleHelperWK : UIView @@ -38,8 +44,31 @@ -(id)inputAccessoryView return nil; } @end +#endif // !TARGET_OS_OSX + +#if TARGET_OS_OSX +@interface RNCWKWebView : WKWebView +@end +@implementation RNCWKWebView +- (void)scrollWheel:(NSEvent *)theEvent { + RNCWebView *rncWebView = (RNCWebView *)[self superview]; + RCTAssert([rncWebView isKindOfClass:[rncWebView class]], @"superview must be an RNCWebView"); + if (![rncWebView scrollEnabled]) { + [[self nextResponder] scrollWheel:theEvent]; + return; + } + [super scrollWheel:theEvent]; +} +@end +#endif // TARGET_OS_OSX + +@interface RNCWebView () -@interface RNCWebView () +@property (nonatomic, copy) RCTDirectEventBlock onFileDownload; @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; @@ -49,40 +78,67 @@ @interface RNCWebView () = 110000 /* __IPHONE_11_0 */ UIScrollViewContentInsetAdjustmentBehavior _savedContentInsetAdjustmentBehavior; #endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ + BOOL _savedAutomaticallyAdjustsScrollIndicatorInsets; +#endif } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { + #if !TARGET_OS_OSX super.backgroundColor = [UIColor clearColor]; + #else + super.backgroundColor = [RCTUIColor clearColor]; + #endif // !TARGET_OS_OSX _bounces = YES; _scrollEnabled = YES; _showsHorizontalScrollIndicator = YES; _showsVerticalScrollIndicator = YES; _directionalLockEnabled = YES; _automaticallyAdjustContentInsets = YES; + _autoManageStatusBarEnabled = YES; _contentInset = UIEdgeInsetsZero; _savedKeyboardDisplayRequiresUserAction = YES; + #if !TARGET_OS_OSX _savedStatusBarStyle = RCTSharedApplication().statusBarStyle; _savedStatusBarHidden = RCTSharedApplication().statusBarHidden; + #endif // !TARGET_OS_OSX + _injectedJavaScript = nil; + _injectedJavaScriptForMainFrameOnly = YES; + _injectedJavaScriptBeforeContentLoaded = nil; + _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES; #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ if (@available(iOS 11.0, *)) { @@ -91,8 +147,22 @@ - (instancetype)initWithFrame:(CGRect)frame // Fallback on earlier versions } #endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ + _savedAutomaticallyAdjustsScrollIndicatorInsets = NO; +#endif + } +#if !TARGET_OS_OSX + [[NSNotificationCenter defaultCenter]addObserver:self + selector:@selector(appDidBecomeActive) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + [[NSNotificationCenter defaultCenter]addObserver:self + selector:@selector(appWillResignActive) + name:UIApplicationWillResignActiveNotification + object:nil]; if (@available(iOS 12.0, *)) { // Workaround for a keyboard dismissal bug present in iOS 12 // https://openradar.appspot.com/radar?id=5018321736957952 @@ -106,7 +176,7 @@ - (instancetype)initWithFrame:(CGRect)frame name:UIKeyboardWillShowNotification object:nil]; // Workaround for StatusBar appearance bug for iOS 12 - // https://github.com/react-native-community/react-native-webview/issues/62 + // https://github.com/react-native-webview/react-native-webview/issues/62 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showFullScreenVideoStatusBars) name:UIWindowDidBecomeVisibleNotification @@ -116,8 +186,9 @@ - (instancetype)initWithFrame:(CGRect)frame selector:@selector(hideFullScreenVideoStatusBars) name:UIWindowDidBecomeHiddenNotification object:nil]; - } + } +#endif // !TARGET_OS_OSX return self; } @@ -137,133 +208,109 @@ - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWe return nil; } -- (void)didMoveToWindow +- (WKWebViewConfiguration *)setUpWkWebViewConfig { - if (self.window != nil && _webView == nil) { - WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new]; - WKPreferences *prefs = [[WKPreferences alloc]init]; - if (!_javaScriptEnabled) { - prefs.javaScriptEnabled = NO; - wkWebViewConfig.preferences = prefs; - } - if (_incognito) { - wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; - } else if (_cacheEnabled) { - wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; - } - if(self.useSharedProcessPool) { - wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool]; - } - wkWebViewConfig.userContentController = [WKUserContentController new]; - [wkWebViewConfig.preferences setValue:@TRUE forKey:@"allowFileAccessFromFileURLs"]; - if (_messagingEnabled) { - [wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName]; - - NSString *source = [NSString stringWithFormat: - @"window.%@ = {" - " postMessage: function (data) {" - " window.webkit.messageHandlers.%@.postMessage(String(data));" - " }" - "};", MessageHandlerName, MessageHandlerName - ]; - - WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; - [wkWebViewConfig.userContentController addUserScript:script]; - - NSString* consoleSource = @"function captureInfo(msg) { window.webkit.messageHandlers.consoleInfoHandler.postMessage(msg); } window.console.info = captureInfo;"; + WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new]; + WKPreferences *prefs = [[WKPreferences alloc]init]; + BOOL _prefsUsed = NO; + if (!_javaScriptEnabled) { + prefs.javaScriptEnabled = NO; + _prefsUsed = YES; + } + if (_allowUniversalAccessFromFileURLs) { + [wkWebViewConfig setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"]; + } + if (_allowFileAccessFromFileURLs) { + [prefs setValue:@TRUE forKey:@"allowFileAccessFromFileURLs"]; + _prefsUsed = YES; + } + if (_javaScriptCanOpenWindowsAutomatically) { + [prefs setValue:@TRUE forKey:@"javaScriptCanOpenWindowsAutomatically"]; + _prefsUsed = YES; + } + if (_prefsUsed) { + wkWebViewConfig.preferences = prefs; + } + if (_incognito) { + wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; + } else if (_cacheEnabled) { + wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; + } + if(self.useSharedProcessPool) { + wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool]; + } + wkWebViewConfig.userContentController = [WKUserContentController new]; + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ + if (@available(iOS 13.0, *)) { + WKWebpagePreferences *pagePrefs = [[WKWebpagePreferences alloc]init]; + pagePrefs.preferredContentMode = _contentMode; + wkWebViewConfig.defaultWebpagePreferences = pagePrefs; + } +#endif - WKUserScript *consoleScript = [[WKUserScript alloc] initWithSource:consoleSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; - [wkWebViewConfig.userContentController addUserScript:consoleScript]; - [wkWebViewConfig.userContentController addScriptMessageHandler:self name:@"consoleInfoHandler"]; +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ + if (@available(iOS 14.0, *)) { + if ([wkWebViewConfig respondsToSelector:@selector(limitsNavigationsToAppBoundDomains)]) { + if (_limitsNavigationsToAppBoundDomains) { + wkWebViewConfig.limitsNavigationsToAppBoundDomains = YES; + } } + } +#endif + + // Shim the HTML5 history API: + [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] + name:HistoryShimName]; + [self resetupScripts:wkWebViewConfig]; - wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback; +#if !TARGET_OS_OSX + wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback; #if WEBKIT_IOS_10_APIS_AVAILABLE - wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction - ? WKAudiovisualMediaTypeAll - : WKAudiovisualMediaTypeNone; - wkWebViewConfig.dataDetectorTypes = _dataDetectorTypes; + wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction + ? WKAudiovisualMediaTypeAll + : WKAudiovisualMediaTypeNone; + wkWebViewConfig.dataDetectorTypes = _dataDetectorTypes; #else - wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction; + wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction; #endif +#endif // !TARGET_OS_OSX - if (_applicationNameForUserAgent) { - wkWebViewConfig.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", wkWebViewConfig.applicationNameForUserAgent, _applicationNameForUserAgent]; - } - - if(_sharedCookiesEnabled) { - // More info to sending cookies with WKWebView - // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303 - if (@available(iOS 11.0, *)) { - // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies - // See also https://forums.developer.apple.com/thread/97194 - // check if websiteDataStore has not been initialized before - if(!_incognito && !_cacheEnabled) { - wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; - } - for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { - [wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil]; - } - } else { - NSMutableString *script = [NSMutableString string]; - - // Clear all existing cookies in a direct called function. This ensures that no - // javascript error will break the web content javascript. - // We keep this code here, if someone requires that Cookies are also removed within the - // the WebView and want to extends the current sharedCookiesEnabled option with an - // additional property. - // Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;" - // for each cookie which is already available in the WebView context. - /* - [script appendString:@"(function () {\n"]; - [script appendString:@" var cookies = document.cookie.split('; ');\n"]; - [script appendString:@" for (var i = 0; i < cookies.length; i++) {\n"]; - [script appendString:@" if (cookies[i].indexOf('=') !== -1) {\n"]; - [script appendString:@" document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"]; - [script appendString:@" }\n"]; - [script appendString:@" }\n"]; - [script appendString:@"})();\n\n"]; - */ - - // Set cookies in a direct called function. This ensures that no - // javascript error will break the web content javascript. - // Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;" - // for each cookie which is available in the application context. - [script appendString:@"(function () {\n"]; - for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { - [script appendFormat:@"document.cookie = %@ + '=' + %@", - RCTJSONStringify(cookie.name, NULL), - RCTJSONStringify(cookie.value, NULL)]; - if (cookie.path) { - [script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)]; - } - if (cookie.expiresDate) { - [script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()", - cookie.expiresDate.timeIntervalSince1970 * 1000 - ]; - } - [script appendString:@";\n"]; - } - [script appendString:@"})();\n"]; + if (_applicationNameForUserAgent) { + wkWebViewConfig.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", wkWebViewConfig.applicationNameForUserAgent, _applicationNameForUserAgent]; + } - WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script - injectionTime:WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly:YES]; - [wkWebViewConfig.userContentController addUserScript:cookieInScript]; - } - } + return wkWebViewConfig; +} +- (void)didMoveToWindow +{ + if (self.window != nil && _webView == nil) { + WKWebViewConfiguration *wkWebViewConfig = [self setUpWkWebViewConfig]; +#if !TARGET_OS_OSX _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; +#else + _webView = [[RNCWKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; +#endif // !TARGET_OS_OSX + [self setBackgroundColor: _savedBackgroundColor]; +#if !TARGET_OS_OSX _webView.scrollView.delegate = self; +#endif // !TARGET_OS_OSX _webView.UIDelegate = self; _webView.navigationDelegate = self; +#if !TARGET_OS_OSX + if (_pullToRefreshEnabled) { + [self addPullToRefreshControl]; + } _webView.scrollView.scrollEnabled = _scrollEnabled; _webView.scrollView.pagingEnabled = _pagingEnabled; - _webView.scrollView.bounces = _bounces; + //For UIRefreshControl to work correctly, the bounces should always be true + _webView.scrollView.bounces = _pullToRefreshEnabled || _bounces; _webView.scrollView.showsHorizontalScrollIndicator = _showsHorizontalScrollIndicator; _webView.scrollView.showsVerticalScrollIndicator = _showsVerticalScrollIndicator; _webView.scrollView.directionalLockEnabled = _directionalLockEnabled; +#endif // !TARGET_OS_OSX _webView.allowsLinkPreview = _allowsLinkPreview; [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures; @@ -276,6 +323,11 @@ - (void)didMoveToWindow _webView.scrollView.contentInsetAdjustmentBehavior = _savedContentInsetAdjustmentBehavior; } #endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ + if (@available(iOS 13.0, *)) { + _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = _savedAutomaticallyAdjustsScrollIndicatorInsets; + } +#endif [self addSubview:_webView]; [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView]; @@ -290,26 +342,37 @@ - (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigation _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures; } - - (void)removeFromSuperview { if (_webView) { + [_webView.configuration.userContentController removeScriptMessageHandlerForName:HistoryShimName]; [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName]; [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; [_webView removeFromSuperview]; +#if !TARGET_OS_OSX _webView.scrollView.delegate = nil; +#endif // !TARGET_OS_OSX _webView = nil; + if (_onContentProcessDidTerminate) { + NSMutableDictionary *event = [self baseEvent]; + _onContentProcessDidTerminate(event); + } } [super removeFromSuperview]; } +#if !TARGET_OS_OSX -(void)showFullScreenVideoStatusBars { #pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (!_autoManageStatusBarEnabled) { + return; + } + _isFullScreenVideoOpen = YES; RCTUnsafeExecuteOnMainQueueSync(^{ - [RCTSharedApplication() setStatusBarStyle:UIStatusBarStyleLightContent animated:YES]; + [RCTSharedApplication() setStatusBarStyle:self->_savedStatusBarStyle animated:YES]; }); #pragma clang diagnostic pop } @@ -317,6 +380,10 @@ -(void)showFullScreenVideoStatusBars -(void)hideFullScreenVideoStatusBars { #pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (!_autoManageStatusBarEnabled) { + return; + } + _isFullScreenVideoOpen = NO; RCTUnsafeExecuteOnMainQueueSync(^{ [RCTSharedApplication() setStatusBarHidden:self->_savedStatusBarHidden animated:YES]; @@ -351,6 +418,7 @@ -(void)keyboardDisplacementFix }]; } } +#endif // !TARGET_OS_OSX - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) { @@ -364,7 +432,11 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } } +#if !TARGET_OS_OSX - (void)setBackgroundColor:(UIColor *)backgroundColor +#else +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor +#endif // !TARGET_OS_OSX { _savedBackgroundColor = backgroundColor; if (_webView == nil) { @@ -372,9 +444,20 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor } CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); - self.opaque = _webView.opaque = (alpha == 1.0); + BOOL opaque = (alpha == 1.0); +#if !TARGET_OS_OSX + self.opaque = _webView.opaque = opaque; _webView.scrollView.backgroundColor = backgroundColor; _webView.backgroundColor = backgroundColor; +#else + // https://stackoverflow.com/questions/40007753/macos-wkwebview-background-transparency + NSOperatingSystemVersion version = { 10, 12, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + [_webView setValue:@(opaque) forKey: @"drawsBackground"]; + } else { + [_webView setValue:@(!opaque) forKey: @"drawsTransparentBackground"]; + } +#endif // !TARGET_OS_OSX } #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ @@ -392,7 +475,17 @@ - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBeh } } #endif - +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ +- (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjustsScrollIndicatorInsets{ + _savedAutomaticallyAdjustsScrollIndicatorInsets = automaticallyAdjustsScrollIndicatorInsets; + if (_webView == nil) { + return; + } + if ([_webView.scrollView respondsToSelector:@selector(setAutomaticallyAdjustsScrollIndicatorInsets:)]) { + _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = automaticallyAdjustsScrollIndicatorInsets; + } +} +#endif /** * This method is called whenever JavaScript running within the web view calls: * - window.webkit.messageHandlers[MessageHandlerName].postMessage @@ -400,10 +493,20 @@ - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBeh - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { - if (_onMessage != nil) { + if ([message.name isEqualToString:HistoryShimName]) { + + if (_onLoadingFinish) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{@"navigationType": message.body}]; + _onLoadingFinish(event); + } + + } else if ([message.name isEqualToString:MessageHandlerName]) { + NSMutableDictionary *event = [self baseEvent]; - if ([message.name isEqualToString:MessageHandlerName]) { - [event addEntriesFromDictionary: @{@"data": message.body}]; + + if (_onMessage) { + [event addEntriesFromDictionary: @{@"data": message.body}]; } else if([message.name isEqualToString: @"consoleInfoHandler"]) { NSDictionary* consoleData = @{ @"type": @"WebAppConsoleMessage", @@ -450,6 +553,7 @@ - (void)setAllowingReadAccessToURL:(NSString *)allowingReadAccessToURL } } +#if !TARGET_OS_OSX - (void)setContentInset:(UIEdgeInsets)contentInset { _contentInset = contentInset; @@ -464,6 +568,7 @@ - (void)refreshContentInset withScrollView:_webView.scrollView updateOffset:YES]; } +#endif // !TARGET_OS_OSX - (void)visitSource { @@ -476,6 +581,16 @@ - (void)visitSource } [_webView loadHTMLString:html baseURL:baseURL]; return; + } + //Add cookie for subsequent resource requests sent by page itself, if cookie was set in headers on WebView + NSString *headerCookie = [RCTConvert NSString:_source[@"headers"][@"cookie"]]; + if(headerCookie) { + NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:headerCookie,@"Set-Cookie",nil]; + NSURL *urlString = [NSURL URLWithString:_source[@"uri"]]; + NSArray *httpCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:urlString]; + for (NSHTTPCookie *httpCookie in httpCookies) { + [_webView.configuration.websiteDataStore.httpCookieStore setCookie:httpCookie completionHandler:nil]; + } } NSURLRequest *request = [self requestForSource:_source]; @@ -500,6 +615,7 @@ - (void)visitSource } } +#if !TARGET_OS_OSX -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAction { if (_webView == nil) { @@ -605,17 +721,23 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView object_setClass(subview, newClass); } +// UIScrollViewDelegate method - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { scrollView.decelerationRate = _decelerationRate; } +#endif // !TARGET_OS_OSX - (void)setScrollEnabled:(BOOL)scrollEnabled { _scrollEnabled = scrollEnabled; +#if !TARGET_OS_OSX _webView.scrollView.scrollEnabled = scrollEnabled; +#endif // !TARGET_OS_OSX } +#if !TARGET_OS_OSX +// UIScrollViewDelegate method - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // Don't allow scrolling the scrollView. @@ -677,6 +799,7 @@ - (void)setShowsVerticalScrollIndicator:(BOOL)showsVerticalScrollIndicator _showsVerticalScrollIndicator = showsVerticalScrollIndicator; _webView.scrollView.showsVerticalScrollIndicator = showsVerticalScrollIndicator; } +#endif // !TARGET_OS_OSX - (void)postMessage:(NSString *)message { @@ -694,7 +817,9 @@ - (void)layoutSubviews // Ensure webview takes the position and dimensions of RNCWebView _webView.frame = self.bounds; +#if !TARGET_OS_OSX _webView.scrollView.contentInset = _contentInset; +#endif // !TARGET_OS_OSX } - (NSMutableDictionary *)baseEvent @@ -756,91 +881,103 @@ - (void) webView:(WKWebView *)webView #pragma mark - WKNavigationDelegate methods /** -* alert -*/ + * alert + */ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { - 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]; - +#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]; +#else + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:message]; + [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(__unused NSModalResponse response){ + completionHandler(); + }]; +#endif // !TARGET_OS_OSX } /** -* confirm -*/ + * confirm + */ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{ - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - completionHandler(YES); - }]]; - [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { - completionHandler(NO); - }]]; - [[self topViewController] presentViewController:alert animated:YES completion:NULL]; +#if !TARGET_OS_OSX + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + completionHandler(YES); + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + completionHandler(NO); + }]]; + [[self topViewController] presentViewController:alert animated:YES completion:NULL]; +#else + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:message]; + [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; + [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel button")]; + void (^callbacksHandlers)(NSModalResponse response) = ^void(NSModalResponse response) { + completionHandler(response == NSAlertFirstButtonReturn); + }; + [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:callbacksHandlers]; +#endif // !TARGET_OS_OSX } /** -* prompt -*/ + * prompt + */ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{ - 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]; +#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 } +#if !TARGET_OS_OSX /** * topViewController */ -(UIViewController *)topViewController{ - UIViewController *controller = [self topViewControllerWithRootViewController:[self getCurrentWindow].rootViewController]; - return controller; -} - -/** - * topViewControllerWithRootViewController - */ --(UIViewController *)topViewControllerWithRootViewController:(UIViewController *)viewController{ - if (viewController==nil) return nil; - if (viewController.presentedViewController!=nil) { - return [self topViewControllerWithRootViewController:viewController.presentedViewController]; - } else if ([viewController isKindOfClass:[UITabBarController class]]){ - return [self topViewControllerWithRootViewController:[(UITabBarController *)viewController selectedViewController]]; - } else if ([viewController isKindOfClass:[UINavigationController class]]){ - return [self topViewControllerWithRootViewController:[(UINavigationController *)viewController visibleViewController]]; - } else { - return viewController; - } -} -/** - * getCurrentWindow - */ --(UIWindow *)getCurrentWindow{ - UIWindow *window = [UIApplication sharedApplication].keyWindow; - if (window.windowLevel!=UIWindowLevelNormal) { - for (UIWindow *wid in [UIApplication sharedApplication].windows) { - if (window.windowLevel==UIWindowLevelNormal) { - window = wid; - break; - } - } - } - return window; + return RCTPresentedViewController(); } +#endif // !TARGET_OS_OSX /** * Decides whether to allow or cancel a navigation. @@ -866,13 +1003,15 @@ - (void) webView:(WKWebView *)webView WKNavigationType navigationType = navigationAction.navigationType; NSURLRequest *request = navigationAction.request; + BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; if (_onShouldStartLoadWithRequest) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ @"url": (request.URL).absoluteString, @"mainDocumentURL": (request.mainDocumentURL).absoluteString, - @"navigationType": navigationTypes[@(navigationType)] + @"navigationType": navigationTypes[@(navigationType)], + @"isTopFrame": @(isTopFrame) }]; if (![self.delegate webView:self shouldStartLoadForRequest:event @@ -884,7 +1023,6 @@ - (void) webView:(WKWebView *)webView if (_onLoadingStart) { // We have this check to filter out iframe requests and whatnot - BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; if (isTopFrame) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ @@ -920,24 +1058,42 @@ - (void) webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; if (_onHttpError && navigationResponse.forMainFrame) { if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; NSInteger statusCode = response.statusCode; if (statusCode >= 400) { - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{ + NSMutableDictionary *httpErrorEvent = [self baseEvent]; + [httpErrorEvent addEntriesFromDictionary: @{ @"url": response.URL.absoluteString, @"statusCode": @(statusCode) }]; - _onHttpError(event); + _onHttpError(httpErrorEvent); + } + + NSString *disposition = nil; + if (@available(iOS 13, *)) { + disposition = [response valueForHTTPHeaderField:@"Content-Disposition"]; + } + BOOL isAttachment = disposition != nil && [disposition hasPrefix:@"attachment"]; + if (isAttachment || !navigationResponse.canShowMIMEType) { + if (_onFileDownload) { + policy = WKNavigationResponsePolicyCancel; + + NSMutableDictionary *downloadEvent = [self baseEvent]; + [downloadEvent addEntriesFromDictionary: @{ + @"downloadUrl": (response.URL).absoluteString, + }]; + _onFileDownload(downloadEvent); + } } } } - decisionHandler(WKNavigationResponsePolicyAllow); + decisionHandler(policy); } /** @@ -957,7 +1113,8 @@ - (void) webView:(WKWebView *)webView return; } - if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) { + if ([error.domain isEqualToString:@"WebKitErrorDomain"] && + (error.code == 102 || error.code == 101)) { // Error code 102 "Frame load interrupted" is raised by the WKWebView // when the URL is from an http redirect. This is a common pattern when // implementing OAuth with a WebView. @@ -988,23 +1145,49 @@ - (void)evaluateJS:(NSString *)js }]; } +-(void)forceIgnoreSilentHardwareSwitch:(BOOL)initialSetup +{ + NSString *mp3Str = @"data:audio/mp3;base64,//tAxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAESAAzMzMzMzMzMzMzMzMzMzMzMzMzZmZmZmZmZmZmZmZmZmZmZmZmZmaZmZmZmZmZmZmZmZmZmZmZmZmZmczMzMzMzMzMzMzMzMzMzMzMzMzM//////////////////////////8AAAA5TEFNRTMuMTAwAZYAAAAAAAAAABQ4JAMGQgAAOAAABEhNIZS0AAAAAAD/+0DEAAPH3Yz0AAR8CPqyIEABp6AxjG/4x/XiInE4lfQDFwIIRE+uBgZoW4RL0OLMDFn6E5v+/u5ehf76bu7/6bu5+gAiIQGAABQIUJ0QolFghEn/9PhZQpcUTpXMjo0OGzRCZXyKxoIQzB2KhCtGobpT9TRVj/3Pmfp+f8X7Pu1B04sTnc3s0XhOlXoGVCMNo9X//9/r6a10TZEY5DsxqvO7mO5qFvpFCmKIjhpSItGsUYcRO//7QsQRgEiljQIAgLFJAbIhNBCa+JmorCbOi5q9nVd2dKnusTMQg4MFUlD6DQ4OFijwGAijRMfLbHG4nLVTjydyPlJTj8pfPflf9/5GD950A5e+jsrmNZSjSirjs1R7hnkia8vr//l/7Nb+crvr9Ok5ZJOylUKRxf/P9Zn0j2P4pJYXyKkeuy5wUYtdmOu6uobEtFqhIJViLEKIjGxchGev/L3Y0O3bwrIOszTBAZ7Ih28EUaSOZf/7QsQfg8fpjQIADN0JHbGgQBAZ8T//y//t/7d/2+f5m7MdCeo/9tdkMtGLbt1tqnabRroO1Qfvh20yEbei8nfDXP7btW7f9/uO9tbe5IvHQbLlxpf3DkAk0ojYcv///5/u3/7PTfGjPEPUvt5D6f+/3Lea4lz4tc4TnM/mFPrmalWbboeNiNyeyr+vufttZuvrVrt/WYv3T74JFo8qEDiJqJrmDTs///v99xDku2xG02jjunrICP/7QsQtA8kpkQAAgNMA/7FgQAGnobgfghgqA+uXwWQ3XFmGimSbe2X3ksY//KzK1a2k6cnNWOPJnPWUsYbKqkh8RJzrVf///P///////4vyhLKHLrCb5nIrYIUss4cthigL1lQ1wwNAc6C1pf1TIKRSkt+a//z+yLVcwlXKSqeSuCVQFLng2h4AFAFgTkH+Z/8jTX/zr//zsJV/5f//5UX/0ZNCNCCaf5lTCTRkaEdhNP//n/KUjf/7QsQ5AEhdiwAAjN7I6jGddBCO+WGTQ1mXrYatSAgaykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg=="; + NSString *scr; + if (initialSetup) { + scr = [NSString stringWithFormat:@"var s=new Audio('%@');s.id='wkwebviewAudio';s.controls=false;s.loop=true;s.play();document.body.appendChild(s);true", mp3Str]; + } else { + scr = [NSString stringWithFormat:@"var s=document.getElementById('wkwebviewAudio');s.src=null;s.parentNode.removeChild(s);s=null;s=new Audio('%@');s.id='wkwebviewAudio';s.controls=false;s.loop=true;s.play();document.body.appendChild(s);true", mp3Str]; + } + [self evaluateJS: scr thenCall: nil]; +} + +-(void)disableIgnoreSilentSwitch +{ + [self evaluateJS: @"document.getElementById('wkwebviewAudio').src=null;true" thenCall: nil]; +} + +-(void)appDidBecomeActive +{ + if (_ignoreSilentHardwareSwitch) { + [self forceIgnoreSilentHardwareSwitch:false]; + } +} + +-(void)appWillResignActive +{ + if (_ignoreSilentHardwareSwitch) { + [self disableIgnoreSilentSwitch]; + } +} + /** * Called when the navigation is complete. * @see https://fburl.com/rtys6jlb */ -- (void) webView:(WKWebView *)webView +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { - if (_injectedJavaScript) { - [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) { - NSMutableDictionary *event = [self baseEvent]; - event[@"jsEvaluationValue"] = jsEvaluationValue; + if (_ignoreSilentHardwareSwitch) { + [self forceIgnoreSilentHardwareSwitch:true]; + } - if (self.onLoadingFinish) { - self.onLoadingFinish(event); - } - }]; - } else if (_onLoadingFinish) { + if (_onLoadingFinish) { _onLoadingFinish([self baseEvent]); } } @@ -1039,16 +1222,215 @@ - (void)reload [_webView reload]; } } +#if !TARGET_OS_OSX +- (void)addPullToRefreshControl +{ + UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; + _refreshControl = refreshControl; + [_webView.scrollView addSubview: refreshControl]; + [refreshControl addTarget:self action:@selector(pullToRefresh:) forControlEvents: UIControlEventValueChanged]; +} + +- (void)pullToRefresh:(UIRefreshControl *)refreshControl +{ + [self reload]; + [refreshControl endRefreshing]; +} + + +- (void)setPullToRefreshEnabled:(BOOL)pullToRefreshEnabled +{ + _pullToRefreshEnabled = pullToRefreshEnabled; + + if (pullToRefreshEnabled) { + [self addPullToRefreshControl]; + } else { + [_refreshControl removeFromSuperview]; + } + + [self setBounces:_bounces]; +} +#endif // !TARGET_OS_OSX - (void)stopLoading { [_webView stopLoading]; } +#if !TARGET_OS_OSX - (void)setBounces:(BOOL)bounces { _bounces = bounces; - _webView.scrollView.bounces = bounces; + //For UIRefreshControl to work correctly, the bounces should always be true + _webView.scrollView.bounces = _pullToRefreshEnabled || bounces; +} +#endif // !TARGET_OS_OSX + +- (void)setInjectedJavaScript:(NSString *)source { + _injectedJavaScript = source; + + self.atEndScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source + injectionTime:WKUserScriptInjectionTimeAtDocumentEnd + forMainFrameOnly:_injectedJavaScriptForMainFrameOnly]; + + if(_webView != nil){ + [self resetupScripts:_webView.configuration]; + } +} + +- (void)setInjectedJavaScriptBeforeContentLoaded:(NSString *)source { + _injectedJavaScriptBeforeContentLoaded = source; + + self.atStartScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:_injectedJavaScriptBeforeContentLoadedForMainFrameOnly]; + + if(_webView != nil){ + [self resetupScripts:_webView.configuration]; + } +} + +- (void)setInjectedJavaScriptForMainFrameOnly:(BOOL)mainFrameOnly { + _injectedJavaScriptForMainFrameOnly = mainFrameOnly; + [self setInjectedJavaScript:_injectedJavaScript]; +} + +- (void)setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly:(BOOL)mainFrameOnly { + _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = mainFrameOnly; + [self setInjectedJavaScriptBeforeContentLoaded:_injectedJavaScriptBeforeContentLoaded]; +} + +- (void)setMessagingEnabled:(BOOL)messagingEnabled { + _messagingEnabled = messagingEnabled; + + self.postMessageScript = _messagingEnabled ? + [ + [WKUserScript alloc] + initWithSource: [ + NSString + stringWithFormat: + @"window.%@ = {" + " postMessage: function (data) {" + " window.webkit.messageHandlers.%@.postMessage(String(data));" + " }" + "};", MessageHandlerName, MessageHandlerName + ] + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + /* TODO: For a separate (minor) PR: use logic like this (as react-native-wkwebview does) so that messaging can be used in all frames if desired. + * I am keeping it as YES for consistency with previous behaviour. */ + // forMainFrameOnly:_messagingEnabledForMainFrameOnly + forMainFrameOnly:YES + ] : + nil; + + if(_webView != nil){ + [self resetupScripts:_webView.configuration]; + } +} + +- (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { + [wkWebViewConfig.userContentController removeAllUserScripts]; + [wkWebViewConfig.userContentController removeScriptMessageHandlerForName:MessageHandlerName]; + + NSString *html5HistoryAPIShimSource = [NSString stringWithFormat: + @"(function(history) {\n" + " function notify(type) {\n" + " setTimeout(function() {\n" + " window.webkit.messageHandlers.%@.postMessage(type)\n" + " }, 0)\n" + " }\n" + " function shim(f) {\n" + " return function pushState() {\n" + " notify('other')\n" + " return f.apply(history, arguments)\n" + " }\n" + " }\n" + " history.pushState = shim(history.pushState)\n" + " history.replaceState = shim(history.replaceState)\n" + " window.addEventListener('popstate', function() {\n" + " notify('backforward')\n" + " })\n" + "})(window.history)\n", HistoryShimName + ]; + WKUserScript *script = [[WKUserScript alloc] initWithSource:html5HistoryAPIShimSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; + [wkWebViewConfig.userContentController addUserScript:script]; + + if(_sharedCookiesEnabled) { + // More info to sending cookies with WKWebView + // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303 + if (@available(iOS 11.0, *)) { + // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies + // See also https://forums.developer.apple.com/thread/97194 + // check if websiteDataStore has not been initialized before + if(!_incognito && !_cacheEnabled) { + wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; + } + for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { + [wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil]; + } + } else { + NSMutableString *script = [NSMutableString string]; + + // Clear all existing cookies in a direct called function. This ensures that no + // javascript error will break the web content javascript. + // We keep this code here, if someone requires that Cookies are also removed within the + // the WebView and want to extends the current sharedCookiesEnabled option with an + // additional property. + // Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;" + // for each cookie which is already available in the WebView context. + /* + [script appendString:@"(function () {\n"]; + [script appendString:@" var cookies = document.cookie.split('; ');\n"]; + [script appendString:@" for (var i = 0; i < cookies.length; i++) {\n"]; + [script appendString:@" if (cookies[i].indexOf('=') !== -1) {\n"]; + [script appendString:@" document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"]; + [script appendString:@" }\n"]; + [script appendString:@" }\n"]; + [script appendString:@"})();\n\n"]; + */ + + // Set cookies in a direct called function. This ensures that no + // javascript error will break the web content javascript. + // Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;" + // for each cookie which is available in the application context. + [script appendString:@"(function () {\n"]; + for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { + [script appendFormat:@"document.cookie = %@ + '=' + %@", + RCTJSONStringify(cookie.name, NULL), + RCTJSONStringify(cookie.value, NULL)]; + if (cookie.path) { + [script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)]; + } + if (cookie.expiresDate) { + [script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()", + cookie.expiresDate.timeIntervalSince1970 * 1000 + ]; + } + [script appendString:@";\n"]; + } + [script appendString:@"})();\n"]; + + WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:YES]; + [wkWebViewConfig.userContentController addUserScript:cookieInScript]; + } + } + + if(_messagingEnabled){ + if (self.postMessageScript){ + [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] + name:MessageHandlerName]; + [wkWebViewConfig.userContentController addUserScript:self.postMessageScript]; + } + if (self.atEndScript) { + [wkWebViewConfig.userContentController addUserScript:self.atEndScript]; + } + } + // Whether or not messaging is enabled, add the startup script if it exists. + if (self.atStartScript) { + [wkWebViewConfig.userContentController addUserScript:self.atStartScript]; + } } - (NSURLRequest *)requestForSource:(id)json { @@ -1060,7 +1442,7 @@ - (NSURLRequest *)requestForSource:(id)json { if (_sharedCookiesEnabled) { if (@available(iOS 11.0, *)) { // see WKWebView initialization for added cookies - } else { + } else if (request != nil) { NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]; NSDictionary *cookieHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; NSMutableURLRequest *mutableRequest = [request mutableCopy]; @@ -1072,3 +1454,19 @@ - (NSURLRequest *)requestForSource:(id)json { } @end + +@implementation RNCWeakScriptMessageDelegate + +- (instancetype)initWithDelegate:(id)scriptDelegate { + self = [super init]; + if (self) { + _scriptDelegate = scriptDelegate; + } + return self; +} + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { + [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +@end diff --git a/ios/RNCWebViewManager.h b/apple/RNCWebViewManager.h similarity index 100% rename from ios/RNCWebViewManager.h rename to apple/RNCWebViewManager.h diff --git a/ios/RNCWebViewManager.m b/apple/RNCWebViewManager.m similarity index 85% rename from ios/RNCWebViewManager.m rename to apple/RNCWebViewManager.m index 0930263f4..72035ee7c 100644 --- a/ios/RNCWebViewManager.m +++ b/apple/RNCWebViewManager.m @@ -14,17 +14,14 @@ @interface RNCWebViewManager () @end -@implementation RCTConvert (UIScrollView) - -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ -RCT_ENUM_CONVERTER(UIScrollViewContentInsetAdjustmentBehavior, (@{ - @"automatic": @(UIScrollViewContentInsetAdjustmentAutomatic), - @"scrollableAxes": @(UIScrollViewContentInsetAdjustmentScrollableAxes), - @"never": @(UIScrollViewContentInsetAdjustmentNever), - @"always": @(UIScrollViewContentInsetAdjustmentAlways), - }), UIScrollViewContentInsetAdjustmentNever, integerValue) +@implementation RCTConvert (WKWebView) +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ +RCT_ENUM_CONVERTER(WKContentMode, (@{ + @"recommended": @(WKContentModeRecommended), + @"mobile": @(WKContentModeMobile), + @"desktop": @(WKContentModeDesktop), +}), WKContentModeRecommended, integerValue) #endif - @end @implementation RNCWebViewManager @@ -35,7 +32,11 @@ @implementation RNCWebViewManager RCT_EXPORT_MODULE() +#if !TARGET_OS_OSX - (UIView *)view +#else +- (RCTUIView *)view +#endif // !TARGET_OS_OSX { RNCWebView *webView = [RNCWebView new]; webView.delegate = self; @@ -43,6 +44,7 @@ - (UIView *)view } RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) +RCT_EXPORT_VIEW_PROPERTY(onFileDownload, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) @@ -51,7 +53,13 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onContentProcessDidTerminate, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) +RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoaded, NSString) +RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL) +RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoadedForMainFrameOnly, BOOL) RCT_EXPORT_VIEW_PROPERTY(javaScriptEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(javaScriptCanOpenWindowsAutomatically, BOOL) +RCT_EXPORT_VIEW_PROPERTY(allowFileAccessFromFileURLs, BOOL) +RCT_EXPORT_VIEW_PROPERTY(allowUniversalAccessFromFileURLs, BOOL) RCT_EXPORT_VIEW_PROPERTY(allowsInlineMediaPlayback, BOOL) RCT_EXPORT_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, BOOL) #if WEBKIT_IOS_10_APIS_AVAILABLE @@ -59,6 +67,7 @@ - (UIView *)view #endif RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) +RCT_EXPORT_VIEW_PROPERTY(autoManageStatusBarEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL) RCT_EXPORT_VIEW_PROPERTY(allowsBackForwardNavigationGestures, BOOL) RCT_EXPORT_VIEW_PROPERTY(incognito, BOOL) @@ -72,6 +81,17 @@ - (UIView *)view #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) #endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ +RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustsScrollIndicatorInsets, BOOL) +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ +RCT_EXPORT_VIEW_PROPERTY(contentMode, WKContentMode) +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ +RCT_EXPORT_VIEW_PROPERTY(limitsNavigationsToAppBoundDomains, BOOL) +#endif /** * Expose methods to enable messaging the webview. @@ -92,6 +112,10 @@ - (UIView *)view }]; } +RCT_CUSTOM_VIEW_PROPERTY(pullToRefreshEnabled, BOOL, RNCWebView) { + view.pullToRefreshEnabled = json == nil ? false : [RCTConvert BOOL: json]; +} + RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RNCWebView) { view.bounces = json == nil ? true : [RCTConvert BOOL: json]; } @@ -108,9 +132,11 @@ - (UIView *)view view.sharedCookiesEnabled = json == nil ? false : [RCTConvert BOOL: json]; } +#if !TARGET_OS_OSX RCT_CUSTOM_VIEW_PROPERTY(decelerationRate, CGFloat, RNCWebView) { view.decelerationRate = json == nil ? UIScrollViewDecelerationRateNormal : [RCTConvert CGFloat: json]; } +#endif // !TARGET_OS_OSX RCT_CUSTOM_VIEW_PROPERTY(directionalLockEnabled, BOOL, RNCWebView) { view.directionalLockEnabled = json == nil ? true : [RCTConvert BOOL: json]; diff --git a/babel.config.js b/babel.config.js index 60dc277b2..0fb97e9eb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -6,5 +6,6 @@ module.exports = function(api) { presets: ['module:metro-react-native-babel-preset'], }, }, + presets: ['module:metro-react-native-babel-preset'], }; }; diff --git a/bin/setup b/bin/setup index 7ad521fec..38212df6c 100755 --- a/bin/setup +++ b/bin/setup @@ -18,7 +18,7 @@ fi # React Native installed? if ! [ -x "$(command -v react-native)" ]; then echo 'Error: React Native is not installed.' >&2 - echo 'Go here: https://facebook.github.io/react-native/docs/getting-started.html' >&2 + echo 'Go here: https://reactnative.dev/docs/getting-started.html' >&2 echo 'Use the "Building Projects With Native Code" option.' exit 1 fi diff --git a/docs/Contributing.md b/docs/Contributing.md index 2390e3a21..ad9a3eb89 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -8,6 +8,52 @@ Secondly, we'd like the contribution experience to be as good as possible. While After you fork the repo, clone it to your machine, and make your changes, you'll want to test them in an app. +There are two methods of testing: +1) Testing within a clone of react-native-webview +2) Testing in a new `react-native init` project + +### Testing within react-native-webview + +#### For all platforms: +``` +$ yarn install +``` + +#### For Android: +``` +$ yarn start:android +``` + +The Android example app will built, the Metro Bundler will launch, and the example app will be installed and started in the Android emulator. + +#### For iOS: +``` +$ cd example/ios +$ pod install +$ cd ../.. +$ yarn start:ios +``` + +The iOS example app will be built, the Metro bundler will launch, and the example app will be install and started in the Simulator. + +#### for macOS: +``` +$ open example/macos/example.xcodeproj +$ yarn start:macos +``` + +The Metro Bundler will now be running in the Terminal for react-native-macos. In XCode select the `example-macos` target and Run. + +#### For Windows: +``` +$ yarn start:windows +$ open example/windows/WebViewWindows.sln and click run button. +``` + +The Metro Bundler will now be running in the Terminal for react-native-windows and the example app will be install and started + +### Testing in a new `react-native init` project + In a new `react-native init` project, do this: ``` diff --git a/docs/Custom-Android.md b/docs/Custom-Android.md index ec581d39a..39ccccf97 100644 --- a/docs/Custom-Android.md +++ b/docs/Custom-Android.md @@ -1,14 +1,14 @@ While the built-in web view has a lot of features, it is not possible to handle every use-case in React Native. You can, however, extend the web view with native code without forking React Native or duplicating all the existing web view code. -Before you do this, you should be familiar with the concepts in [native UI components](https://facebook.github.io/react-native/docs/native-components-android). You should also familiarise yourself with the [native code for web views](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java), as you will have to use this as a reference when implementing new features—although a deep understanding is not required. +Before you do this, you should be familiar with the concepts in [native UI components](https://reactnative.dev/docs/native-components-android). You should also familiarise yourself with the [native code for web views](https://github.com/react-native-webview/react-native-webview/blob/master/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java), as you will have to use this as a reference when implementing new features—although a deep understanding is not required. ## Native Code -To get started, you'll need to create a subclass of `RNCWebViewManager`, `RNCWebView`, and `ReactWebViewClient`. In your view manager, you'll then need to override: +To get started, you'll need to create a subclass of `RNCWebViewManager`, `RNCWebView`, and `RNCWebViewClient`. In your view manager, you'll then need to override: -* `createReactWebViewInstance` -* `getName` -* `addEventEmitters` +- `createReactWebViewInstance` +- `getName` +- `addEventEmitters` ```java @ReactModule(name = CustomWebViewManager.REACT_CLASS) @@ -16,7 +16,7 @@ public class CustomWebViewManager extends RNCWebViewManager { /* This name must match what we're referring to in JS */ protected static final String REACT_CLASS = "RCTCustomWebView"; - protected static class CustomWebViewClient extends ReactWebViewClient { } + protected static class CustomWebViewClient extends RNCWebViewClient { } protected static class CustomWebView extends RNCWebView { public CustomWebView(ThemedReactContext reactContext) { @@ -105,7 +105,7 @@ public class NavigationCompletedEvent extends Event { You can trigger the event in your web view client. You can hook existing handlers if your events are based on them. -You should refer to [RNCWebViewManager.java](https://github.com/react-native-community/react-native-webview/blob/master/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java) in the react-native-webview codebase to see what handlers are available and how they are implemented. You can extend any methods here to provide extra functionality. +You should refer to [RNCWebViewManager.java](https://github.com/react-native-webview/react-native-webview/blob/master/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java) in the react-native-webview codebase to see what handlers are available and how they are implemented. You can extend any methods here to provide extra functionality. ```java public class NavigationCompletedEvent extends Event { @@ -168,22 +168,22 @@ public class CustomWebViewManager extends RNCWebViewManager { To use your custom web view, you'll need to create a class for it. Your class must: -* Export all the prop types from `WebView.propTypes` -* Return a `WebView` component with the prop `nativeConfig.component` set to your native component (see below) +- Export all the prop types from `WebView.propTypes` +- Return a `WebView` component with the prop `nativeConfig.component` set to your native component (see below) To get your native component, you must use `requireNativeComponent`: the same as for regular custom components. However, you must pass in an extra third argument, `WebView.extraNativeComponentConfig`. This third argument contains prop types that are only required for native code. ```javascript -import React, {Component, PropTypes} from 'react'; -import {requireNativeComponent} from 'react-native'; -import {WebView} from 'react-native-webview'; +import React, { Component, PropTypes } from 'react'; +import { requireNativeComponent } from 'react-native'; +import { WebView } from 'react-native-webview'; export default class CustomWebView extends Component { static propTypes = WebView.propTypes; render() { return ( - + ); } } @@ -191,7 +191,7 @@ export default class CustomWebView extends Component { const RCTCustomWebView = requireNativeComponent( 'RCTCustomWebView', CustomWebView, - WebView.extraNativeComponentConfig + WebView.extraNativeComponentConfig, ); ``` @@ -199,7 +199,7 @@ If you want to add custom props to your native component, you can use `nativeCon For events, the event handler must always be set to a function. This means it isn't safe to use the event handler directly from `this.props`, as the user might not have provided one. The standard approach is to create a event handler in your class, and then invoking the event handler given in `this.props` if it exists. -If you are unsure how something should be implemented from the JS side, look at [WebView.android.js](https://github.com/react-native-community/react-native-webview/blob/master/js/WebView.android.js) in the React Native source. +If you are unsure how something should be implemented from the JS side, look at [WebView.android.js](https://github.com/react-native-webview/react-native-webview/blob/master/js/WebView.android.js) in the React Native source. ```javascript export default class CustomWebView extends Component { @@ -214,7 +214,7 @@ export default class CustomWebView extends Component { }; _onNavigationCompleted = (event) => { - const {onNavigationCompleted} = this.props; + const { onNavigationCompleted } = this.props; onNavigationCompleted && onNavigationCompleted(event); }; @@ -249,6 +249,6 @@ const RCTCustomWebView = requireNativeComponent( ...WebView.extraNativeComponentConfig.nativeOnly, onScrollToBottom: true, }, - } + }, ); ``` diff --git a/docs/Debugging.md b/docs/Debugging.md index b70800170..120742dcf 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -15,12 +15,14 @@ It's possible to debug WebView contents in the iOS simulator or on a device usin 3. Safari -> Develop -> [device name] -> [app name] -> [url - title] 4. You can now debug the WebView contents just as you would on the web -##### Note: +##### Notes: When debugging on device you must enable Web Inspector in your device settings: Settings -> Safari -> Advanced -> Web Inspector +Also, if you don't see your device in the Develop menu, and you started Safari before you started your simulator, try restarting Safari. + ### Android & Chrome It's possible to debug WebView contents in the Android emulator or on a device using Chrome DevTools. diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 0df45099a..8631a4843 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -2,19 +2,21 @@ Here's how to get started quickly with the React Native WebView. -#### 1. Add react-native-webview to your dependencies +## 1. Add react-native-webview to your dependencies ``` $ yarn add react-native-webview ``` - (or) - - For npm use + +(or) + +For npm use + ``` $ npm install --save react-native-webview ``` -#### 2. Link native dependencies +## 2. Link native dependencies From react-native 0.60 autolinking will take care of the link step but don't forget to run `pod install` @@ -24,13 +26,21 @@ React Native modules that include native Objective-C, Swift, Java, or Kotlin cod $ react-native link react-native-webview ``` -iOS: +_NOTE: If you ever need to uninstall React Native WebView, run `react-native unlink react-native-webview` to unlink it._ + +### iOS & macOS: + +If using CocoaPods, in the `ios/` or `macos/` directory run: -If using cocoapods in the `ios/` directory run ``` $ pod install ``` +While you can manually link the old way using [react-native own tutorial](https://reactnative.dev/docs/linking-libraries-ios), we find it easier to use CocoaPods. +If you wish to use CocoaPods and haven't set it up yet, please instead refer to [that article](https://engineering.brigad.co/demystifying-react-native-modules-linking-ae6c017a6b4a). + +### Android: + Android - react-native-webview version <6: This module does not require any extra step after running the link command 🎉 @@ -44,12 +54,36 @@ android.enableJetifier=true For Android manual installation, please refer to [this article](https://engineering.brigad.co/demystifying-react-native-modules-linking-964399ec731b) where you can find detailed step on how to link any react-native project. -For iOS, while you can manually link the old way using [react-native own tutorial](https://facebook.github.io/react-native/docs/linking-libraries-ios), we find it easier to use cocoapods. -If you wish to use cocoapods and haven't set it up yet, please instead refer to [that article](https://engineering.brigad.co/demystifying-react-native-modules-linking-ae6c017a6b4a). +### Windows: -_NOTE: If you ever need to uninstall React Native WebView, run `react-native unlink react-native-webview` to unlink it._ +Autolinking is not yet supported for ReactNativeWindows. Make following additions to the given files manually: + +#### **windows/myapp.sln** + +Add the `ReactNativeWebView` project to your solution. + +1. Open the solution in Visual Studio 2019 +2. Right-click Solution icon in Solution Explorer > Add > Existing Project + Select `node_modules\react-native-webview\windows\ReactNativeWebView\ReactNativeWebView.vcxproj` + +#### **windows/myapp/myapp.vcxproj** + +Add a reference to `ReactNativeWebView` to your main application project. From Visual Studio 2019: + +1. Right-click main application project > Add > Reference... + Check `ReactNativeWebView` from Solution Projects. + +2. Modify files below to add the package providers to your main application project + +#### **pch.h** + +Add `#include "winrt/ReactNativeWebView.h"`. + +#### **app.cpp** + +Add `PackageProviders().Append(winrt::ReactNativeWebView::ReactPackageProvider());` before `InitializeComponent();`. -#### 3. Import the webview into your component +## 3. Import the webview into your component ```js import React, { Component } from 'react'; diff --git a/docs/Guide.md b/docs/Guide.md index 37ef62a18..9fa649306 100644 --- a/docs/Guide.md +++ b/docs/Guide.md @@ -48,33 +48,33 @@ import { WebView } from 'react-native-webview'; class MyWeb extends Component { render() { - return ( - - ); + return ; } } ``` ### Loading local HTML files -Sometimes you would have bundled an HTML file along with the app and would like to load the HTML asset into your WebView. To do this on iOS, you can just import the html file like any other asset as shown below. +Note: This is currently not working as discussed in [#428](https://github.com/react-native-webview/react-native-webview/issues/428) and [#518](https://github.com/react-native-webview/react-native-webview/issues/518). Possible workarounds include bundling all assets with webpack or similar, or running a [local webserver](https://github.com/futurepress/react-native-static-server). + +
Show non-working method + +Sometimes you would have bundled an HTML file along with the app and would like to load the HTML asset into your WebView. To do this on iOS and Windows, you can just import the html file like any other asset as shown below. ```js import React, { Component } from 'react'; import { WebView } from 'react-native-webview'; -const myHtmlFile = require("./my-asset-folder/local-site.html"); +const myHtmlFile = require('./my-asset-folder/local-site.html'); class MyWeb extends Component { render() { - return ( - - ); + return ; } } ``` -However on Android, you need to place the HTML file inside your android project's asset directory. For example, if `local-site.html` is your HTML file and you'd like to load it into the webview, you should move the file to your project's android asset directory which is `your-project/android/src/main/assets/`. Then you can load the html file as shown in the following code block +However on Android, you need to place the HTML file inside your android project's asset directory. For example, if `local-site.html` is your HTML file and you'd like to load it into the webview, you should move the file to your project's android asset directory which is `your-project/android/app/src/main/assets/`. Then you can load the html file as shown in the following code block ```js import React, { Component } from 'react'; @@ -83,12 +83,14 @@ import { WebView } from 'react-native-webview'; class MyWeb extends Component { render() { return ( - + ); } } ``` +
+ ### Controlling navigation state changes Sometimes you want to intercept a user tapping on a link in your webview and do something different than navigating there in the webview. Here's some example code on how you might do that using the `onNavigationStateChange` function. @@ -103,14 +105,14 @@ class MyWeb extends Component { render() { return ( (this.webview = ref)} - source={{ uri: 'https://facebook.github.io/react-native/' }} + ref={(ref) => (this.webview = ref)} + source={{ uri: 'https://reactnative.dev/' }} onNavigationStateChange={this.handleWebViewNavigationStateChange} /> ); } - handleWebViewNavigationStateChange = newNavState => { + handleWebViewNavigationStateChange = (newNavState) => { // newNavState looks something like this: // { // url?: string; @@ -141,7 +143,7 @@ class MyWeb extends Component { // redirect somewhere else if (url.includes('google.com')) { - const newURL = 'https://facebook.github.io/react-native/'; + const newURL = 'https://reactnative.dev/'; const redirectTo = 'window.location = "' + newURL + '"'; this.webview.injectJavaScript(redirectTo); } @@ -149,44 +151,6 @@ class MyWeb extends Component { } ``` -#### Intercepting hash URL changes - -While `onNavigationStateChange` will trigger on URL changes, it does not trigger when only the hash URL ("anchor") changes, e.g. from `https://example.com/users#list` to `https://example.com/users#help`. - -You can inject some JavaScript to wrap the history functions in order to intercept these hash URL changes. - -```jsx - { - if (state.data === 'navigationStateChange') { - // Navigation state updated, can check state.canGoBack, etc. - } - }} -/> -``` - -Thanks to [Janic Duplessis](https://github.com/react-native-community/react-native-webview/issues/24#issuecomment-483956651) for this workaround. - ### Add support for File Upload ##### iOS @@ -229,6 +193,12 @@ Add permission in AndroidManifest.xml: ``` +###### Camera option availability in uploading for Android + +If the file input indicates that images or video is desired with [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept), then the WebView will attempt to provide options to the user to use their camera to take a picture or video. + +Normally, apps that do not have permission to use the camera can prompt the user to use an external app so that the requesting app has no need for permission. However, Android has made a special exception for this around the camera to reduce confusion for users. If an app _can_ request the camera permission because it has been declared, and the user has not granted the permission, it may not fire an intent that would use the camera (`MediaStore.ACTION_IMAGE_CAPTURE` or `MediaStore.ACTION_VIDEO_CAPTURE`). In this scenario, it is up to the developer to request camera permission before a file upload directly using the camera is necessary. + ##### Check for File Upload support, with `static isFileUploadSupported()` File Upload using `` is not supported for Android 4.4 KitKat (see [details](https://github.com/delight-im/Android-AdvancedWebView/issues/4#issuecomment-70372146)): @@ -262,9 +232,24 @@ You can control **single** or **multiple** file selection by specifing the [`mul ##### iOS -For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file: +On iOS, you are going to have to supply your own code to download files. You can supply an `onFileDownload` callback +to the WebView component as a prop. If RNCWebView determines that a file download needs to take place, the URL where you can download the file +will be given to `onFileDownload`. From that callback you can then download that file however you would like to do so. -Save to gallery: +NOTE: iOS 13+ is needed for the best possible download experience. On iOS 13 Apple added an API for accessing HTTP response headers, which +is used to determine if an HTTP response should be a download. On iOS 12 or older, only MIME types that cannot be rendered by the webview will +trigger calls to `onFileDownload`. + +Example: + +```javascript +onFileDownload = ({ nativeEvent }) => { + const { downloadUrl } = nativeEvent; + // --> Your download code goes here <-- +}; +``` + +To be able to save images to the gallery you need to specify this permission in your `ios/[project]/Info.plist` file: ``` NSPhotoLibraryAddUsageDescription @@ -273,13 +258,14 @@ Save to gallery: ##### Android -Add permission in AndroidManifest.xml: +On Android, integration with the DownloadManager is built-in. +Add this permisison in AndroidManifest.xml (only required if your app supports Android versions lower than 10): ```xml ...... - + ...... @@ -316,9 +302,9 @@ export default class App extends Component { {}} injectedJavaScript={runFirst} /> @@ -327,15 +313,54 @@ export default class App extends Component { } ``` -This runs the JavaScript in the `runFirst` string once the page is loaded. In this case, you can see that both the body style was changed to red and the alert showed up after 2 seconds. +This runs the JavaScript in the `runFirst` string once the page is loaded. In this case, you can see that both the body style was changed to red and the alert showed up after 2 seconds. An `onMessage` event is required as well to inject the JavaScript code into the WebView. + +By setting `injectedJavaScriptForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the main frame) if supported for the given platform. For example, if a page contains an iframe, the javascript will be injected into that iframe as well with this set to `false`. (Note this is not supported on Android.) There is also `injectedJavaScriptBeforeContentLoadedForMainFrameOnly` for injecting prior to content loading. Read more about this in the [Reference](./Reference.md#injectedjavascriptformainframeonly). screenshot of Github repo _Under the hood_ -> On iOS, `injectedJavaScript` runs a method on WebView called `evaluateJavaScript:completionHandler:` +> On iOS, ~~`injectedJavaScript` runs a method on WebView called `evaluateJavaScript:completionHandler:`~~ – this is no longer true as of version `8.2.0`. Instead, we use a `WKUserScript` with injection time `WKUserScriptInjectionTimeAtDocumentEnd`. As a consequence, `injectedJavaScript` no longer returns an evaluation value nor logs a warning to the console. In the unlikely event that your app depended upon this behaviour, please see migration steps [here](https://github.com/react-native-webview/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour. > On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback` +#### The `injectedJavaScriptBeforeContentLoaded` prop + +This is a script that runs **before** the web page loads for the first time. It only runs once, even if the page is reloaded or navigated away. This is useful if you want to inject anything into the window, localstorage, or document prior to the web code executing. + +```jsx +import React, { Component } from 'react'; +import { View } from 'react-native'; +import { WebView } from 'react-native-webview'; + +export default class App extends Component { + render() { + const runFirst = ` + window.isNativeApp = true; + true; // note: this is required, or you'll sometimes get silent failures + `; + return ( + + + + ); + } +} +``` + +This runs the JavaScript in the `runFirst` string before the page is loaded. In this case, the value of `window.isNativeApp` will be set to true before the web code executes. + +By setting `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform. However, although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-webview/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended. + +> On iOS, ~~`injectedJavaScriptBeforeContentLoaded` runs a method on WebView called `evaluateJavaScript:completionHandler:`~~ – this is no longer true as of version `8.2.0`. Instead, we use a `WKUserScript` with injection time `WKUserScriptInjectionTimeAtDocumentStart`. As a consequence, `injectedJavaScriptBeforeContentLoaded` no longer returns an evaluation value nor logs a warning to the console. In the unlikely event that your app depended upon this behaviour, please see migration steps [here](https://github.com/react-native-webview/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour. +> On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback` +> Note on Android Compatibility: For applications targeting `Build.VERSION_CODES.N` or later, JavaScript state from an empty WebView is no longer persisted across navigations like `loadUrl(java.lang.String)`. For example, global variables and functions defined before calling `loadUrl(java.lang.String)` will not exist in the loaded page. Applications should use the Android Native API `addJavascriptInterface(Object, String)` instead to persist JavaScript objects across navigations. + #### The `injectJavaScript` method While convenient, the downside to the previously mentioned `injectedJavaScript` prop is that it only runs once. That's why we also expose a method on the webview ref called `injectJavaScript` (note the slightly different name!). @@ -359,10 +384,9 @@ export default class App extends Component { return ( (this.webref = r)} + ref={(r) => (this.webref = r)} source={{ - uri: - 'https://github.com/react-native-community/react-native-webview', + uri: 'https://github.com/react-native-webview/react-native-webview', }} /> @@ -412,7 +436,7 @@ export default class App extends Component { { + onMessage={(event) => { alert(event.nativeEvent.data); }} /> @@ -448,7 +472,7 @@ This will set the header on the first load, but not on subsequent page navigatio In order to work around this, you can track the current URL, intercept new page loads, and navigate to them yourself ([original credit for this technique to Chirag Shah from Big Binary](https://blog.bigbinary.com/2016/07/26/passing-request-headers-on-each-webview-request-in-react-native.html)): ```jsx -const CustomHeaderWebView = props => { +const CustomHeaderWebView = (props) => { const { uri, onLoadStart, ...restProps } = props; const [currentURI, setURI] = useState(props.source.uri); const newSource = { ...props.source, uri: currentURI }; @@ -457,7 +481,7 @@ const CustomHeaderWebView = props => { { + onShouldStartLoadWithRequest={(request) => { // If we're loading the current URI, allow it to load if (request.url === currentURI) return true; // We're loading a new URL -- change state first @@ -480,9 +504,9 @@ const CustomHeaderWebView = props => { #### Managing Cookies -You can set cookies on the React Native side using the [react-native-cookies](https://github.com/joeferraro/react-native-cookies) package. +You can set cookies on the React Native side using the [@react-native-community/cookies](https://github.com/react-native-community/cookies) package. -When you do, you'll likely want to enable the [sharedCookiesEnabled](Reference#sharedCookiesEnabled) prop as well. +When you do, you'll likely want to enable the [sharedCookiesEnabled](Reference.md#sharedCookiesEnabled) prop as well. ```jsx const App = () => { @@ -514,3 +538,11 @@ const App = () => { ``` Note that these cookies will only be sent on the first request unless you use the technique above for [setting custom headers on each page load](#Setting-Custom-Headers). + +### Hardware Silence Switch + +There are some inconsistencies in how the hardware silence switch is handled between embedded `audio` and `video` elements and between iOS and Android platforms. + +Audio on `iOS` will be muted when the hardware silence switch is in the on position, unless the `ignoreSilentHardwareSwitch` parameter is set to true. + +Video on `iOS` will always ignore the hardware silence switch. diff --git a/docs/README.portuguese.md b/docs/README.portuguese.md index 3e72b9c3d..05beaf71a 100644 --- a/docs/README.portuguese.md +++ b/docs/README.portuguese.md @@ -1,9 +1,9 @@ # React Native WebView - Um moderno, multiplataforma WebView para React Native -[![star this repo](http://githubbadges.com/star.svg?user=react-native-community&repo=react-native-webview&style=flat)](https://github.com/react-native-community/react-native-webview) +[![star this repo](http://githubbadges.com/star.svg?user=react-native-webview&repo=react-native-webview&style=flat)](https://github.com/react-native-webview/react-native-webview) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors) -[![Known Vulnerabilities](https://snyk.io/test/github/react-native-community/react-native-webview/badge.svg?style=flat-square)](https://snyk.io/test/github/react-native-community/react-native-webview) +[![Known Vulnerabilities](https://snyk.io/test/github/react-native-webview/react-native-webview/badge.svg?style=flat-square)](https://snyk.io/test/github/react-native-webview/react-native-webview) **React Native WebView** é um moderno, bem apoiado, e multiplataforma WebView para React Native. É projetado para substituir o WebView embutido(que sera [removido do core](https://github.com/react-native-community/discussions-and-proposals/pull/3)). @@ -19,6 +19,8 @@ _Esse projeto é mantido gratuitamente por essas pessoas usando ambos seu tempo - [x] iOS - [x] Android +- [x] macOS +- [x] Windows _Nota: O suporte da Expo para o React Native WebView começou com [Expo SDK v33.0.0](https://blog.expo.io/expo-sdk-v33-0-0-is-now-available-52d1c99dfe4c)._ @@ -34,19 +36,21 @@ Esse projeto segue [versionamento semântico](https://semver.org/). Não hesitam Versão atual: ![version](https://img.shields.io/npm/v/react-native-webview.svg) -- [7.0.1](https://github.com/react-native-community/react-native-webview/releases/tag/v7.0.1) - UIWebView removido +- [8.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v8.0.0) - onNavigationStateChange agora é disparado quando alterado o hash da URL. -- [6.0.**2**](https://github.com/react-native-community/react-native-webview/releases/tag/v6.0.2) - Update para AndroidX. Tenha certeza de habilitar no `android/gradle.properties` do seu projeto. Veja o [Getting Started Guide](docs/Getting-Started.md). +- [7.0.1](https://github.com/react-native-webview/react-native-webview/releases/tag/v7.0.1) - UIWebView removido. -- [5.0.**1**](https://github.com/react-native-community/react-native-webview/releases/tag/v5.0.0) - Refatorou a antiga implementação postMessage para comunicação da visualização da webview para nativa. -- [4.0.0](https://github.com/react-native-community/react-native-webview/releases/tag/v4.0.0) - Cache adicionada(habilitada por padrão). -- [3.0.0](https://github.com/react-native-community/react-native-webview/releases/tag/v3.0.0) - WKWebview: Adicionado um pool de processos compartilhados para que os cookies e o localStorage sejam compartilhados nas webviews no iOS (habilitadas por padrão) -- [2.0.0](https://github.com/react-native-community/react-native-webview/releases/tag/v2.0.0) - Primeiro lançamento, esta é uma réplica do componente principal do webview. +- [6.0.**2**](https://github.com/react-native-webview/react-native-webview/releases/tag/v6.0.2) - Update para AndroidX. Tenha certeza de habilitar no `android/gradle.properties` do seu projeto. Veja o [Getting Started Guide](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md). + +- [5.0.**1**](https://github.com/react-native-webview/react-native-webview/releases/tag/v5.0.0) - Refatorou a antiga implementação postMessage para comunicação da visualização da webview para nativa. +- [4.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v4.0.0) - Cache adicionada(habilitada por padrão). +- [3.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v3.0.0) - WKWebview: Adicionado um pool de processos compartilhados para que os cookies e o localStorage sejam compartilhados nas webviews no iOS (habilitadas por padrão) +- [2.0.0](https://github.com/react-native-webview/react-native-webview/releases/tag/v2.0.0) - Primeiro lançamento, esta é uma réplica do componente principal do webview. **Seguinte:** - remoção do this.webView.postMessage() ( - nunca documentado e menos flexível que o injectJavascript) -> [Como migrar](https://github.com/react-native-community/react-native-webview/issues/809) + nunca documentado e menos flexível que o injectJavascript) -> [Como migrar](https://github.com/react-native-webview/react-native-webview/issues/809) - Reescrita em Kotlin - talvez reescrita em Swift @@ -62,9 +66,7 @@ import { WebView } from 'react-native-webview'; // ... class MyWebComponent extends Component { render() { - return ( - - ); + return ; } } ``` @@ -77,7 +79,7 @@ Para mais informações, leia a [API Reference](./docs/Reference.md) e o [Guia]( ## Contribuindo -Veja [Contributing.md](https://github.com/react-native-community/react-native-webview/blob/master/docs/Contributing.md) +Veja [Contributing.md](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Contributing.md) ## Contribuidores diff --git a/docs/Reference.md b/docs/Reference.md index 5bf70d6c9..6f52a13ff 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -6,10 +6,15 @@ This document lays out the current public properties and methods for the React N - [`source`](Reference.md#source) - [`automaticallyAdjustContentInsets`](Reference.md#automaticallyadjustcontentinsets) +- [`automaticallyAdjustsScrollIndicatorInsets`](Reference.md#automaticallyAdjustsScrollIndicatorInsets) - [`injectedJavaScript`](Reference.md#injectedjavascript) +- [`injectedJavaScriptBeforeContentLoaded`](Reference.md#injectedjavascriptbeforecontentloaded) +- [`injectedJavaScriptForMainFrameOnly`](Reference.md#injectedjavascriptformainframeonly) +- [`injectedJavaScriptBeforeContentLoadedForMainFrameOnly`](Reference.md#injectedjavascriptbeforecontentloadedformainframeonly) - [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction) - [`nativeConfig`](Reference.md#nativeconfig) - [`onError`](Reference.md#onerror) +- [`onRenderProcessGone`](Reference.md#onRenderProcessGone) - [`onLoad`](Reference.md#onload) - [`onLoadEnd`](Reference.md#onloadend) - [`onLoadStart`](Reference.md#onloadstart) @@ -18,6 +23,7 @@ This document lays out the current public properties and methods for the React N - [`onMessage`](Reference.md#onmessage) - [`onNavigationStateChange`](Reference.md#onnavigationstatechange) - [`onContentProcessDidTerminate`](Reference.md#oncontentprocessdidterminate) +- [`onScroll`](Reference.md#onscroll) - [`originWhitelist`](Reference.md#originwhitelist) - [`renderError`](Reference.md#rendererror) - [`renderLoading`](Reference.md#renderloading) @@ -29,7 +35,9 @@ This document lays out the current public properties and methods for the React N - [`decelerationRate`](Reference.md#decelerationrate) - [`domStorageEnabled`](Reference.md#domstorageenabled) - [`javaScriptEnabled`](Reference.md#javascriptenabled) +- [`javaScriptCanOpenWindowsAutomatically`](Reference.md#javascriptcanopenwindowsautomatically) - [`androidHardwareAccelerationDisabled`](Reference.md#androidHardwareAccelerationDisabled) +- [`androidLayerType`](Reference.md#androidLayerType) - [`mixedContentMode`](Reference.md#mixedcontentmode) - [`thirdPartyCookiesEnabled`](Reference.md#thirdpartycookiesenabled) - [`userAgent`](Reference.md#useragent) @@ -40,6 +48,7 @@ This document lays out the current public properties and methods for the React N - [`overScrollMode`](Reference.md#overscrollmode) - [`contentInset`](Reference.md#contentinset) - [`contentInsetAdjustmentBehavior`](Reference.md#contentInsetAdjustmentBehavior) +- [`contentMode`](Reference.md#contentMode) - [`dataDetectorTypes`](Reference.md#datadetectortypes) - [`scrollEnabled`](Reference.md#scrollenabled) - [`directionalLockEnabled`](Reference.md#directionalLockEnabled) @@ -61,6 +70,12 @@ This document lays out the current public properties and methods for the React N - [`allowsLinkPreview`](Reference.md#allowsLinkPreview) - [`sharedCookiesEnabled`](Reference.md#sharedCookiesEnabled) - [`textZoom`](Reference.md#textZoom) +- [`pullToRefreshEnabled`](Reference.md#pullToRefreshEnabled) +- [`ignoreSilentHardwareSwitch`](Reference.md#ignoreSilentHardwareSwitch) +- [`onFileDownload`](Reference.md#onFileDownload) +- [`limitsNavigationsToAppBoundDomains`](Reference.md#limitsNavigationsToAppBoundDomains) +- [`autoManageStatusBarEnabled`](Reference.md#autoManageStatusBarEnabled) +- [`setSupportMultipleWindows`](Reference.md#setSupportMultipleWindows) ## Methods Index @@ -78,13 +93,15 @@ This document lays out the current public properties and methods for the React N - [`clearCache`](Reference.md#clearCache) - [`clearHistory`](Reference.md#clearHistory) - [`requestFocus`](Reference.md#requestFocus) +- [`postMessage`](Reference.md#postmessagestr) + --- # Reference ## Props -### `source` +### `source`[⬆](#props-index) Loads static HTML or a URI (with optional headers) in the WebView. Note that static HTML will require setting [`originWhitelist`](Reference.md#originwhitelist) to `["*"]`. @@ -110,7 +127,7 @@ _Note that using static HTML requires the WebView property [originWhiteList](Ref --- -### `automaticallyAdjustContentInsets` +### `automaticallyAdjustContentInsets`[⬆](#props-index) Controls whether to adjust the content inset for web views that are placed behind a navigation bar, tab bar, or toolbar. The default value is `true`. @@ -120,13 +137,28 @@ Controls whether to adjust the content inset for web views that are placed behin --- -### `injectedJavaScript` +### `automaticallyAdjustsScrollIndicatorInsets`[⬆](#props-index) -Set this to provide JavaScript that will be injected into the web page when the view loads. Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception. +Controls whether to adjust the scroll indicator inset for web views that are placed behind a navigation bar, tab bar, or toolbar. The default value `false`. (iOS 13+) -| Type | Required | -| ------ | -------- | -| string | No | +| Type | Required | Platform | +| ---- | -------- | -------- | +| bool | No | iOS(13+) | + +--- + +### `injectedJavaScript`[⬆](#props-index) + +Set this to provide JavaScript that will be injected into the web page after the document finishes loading, but before other subresources finish loading. + +Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception. + +On iOS, see [`WKUserScriptInjectionTimeAtDocumentEnd`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentend?language=objc). Be sure +to set an [`onMessage`](Reference.md#onmessage) handler, even if it's a no-op, or the code will not be run. + +| Type | Required | Platform | +| ------ | -------- | ------------------- | +| string | No | iOS, Android, macOS | To learn more, read the [Communicating between JS and Native](Guide.md#communicating-between-js-and-native) guide. @@ -140,7 +172,7 @@ const INJECTED_JAVASCRIPT = `(function() { })();`; ; @@ -148,19 +180,75 @@ const INJECTED_JAVASCRIPT = `(function() { --- -### `mediaPlaybackRequiresUserAction` +### `injectedJavaScriptBeforeContentLoaded`[⬆](#props-index) + +Set this to provide JavaScript that will be injected into the web page after the document element is created, but before other subresources finish loading. + +Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception. + +On iOS, see [`WKUserScriptInjectionTimeAtDocumentStart`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc) + +| Type | Required | Platform | +| ------ | -------- | ---------- | +| string | No | iOS, macOS | + +To learn more, read the [Communicating between JS and Native](Guide.md#communicating-between-js-and-native) guide. + +Example: + +Post message a JSON object of `window.location` to be handled by [`onMessage`](Reference.md#onmessage). `window.ReactNativeWebView.postMessage` _will_ be available at this time. + +```jsx +const INJECTED_JAVASCRIPT = `(function() { + window.ReactNativeWebView.postMessage(JSON.stringify(window.location)); +})();`; + +; +``` + +--- + +### `injectedJavaScriptForMainFrameOnly`[⬆](#props-index) + +If `true` (default; mandatory for Android), loads the `injectedJavaScript` only into the main frame. + +If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. iframes). + +| Type | Required | Platform | +| ---- | -------- | ------------------------------------------------- | +| bool | No | iOS and macOS (only `true` supported for Android) | + +--- + +### `injectedJavaScriptBeforeContentLoadedForMainFrameOnly`[⬆](#props-index) + +If `true` (default; mandatory for Android), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame. + +If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. iframes). + +| Type | Required | Platform | +| ---- | -------- | ------------------------------------------------- | +| bool | No | iOS and macOS (only `true` supported for Android) | + +--- + +### `mediaPlaybackRequiresUserAction`[⬆](#props-index) Boolean that determines whether HTML5 audio and video requires the user to tap them before they start playing. The default value is `true`. (Android API minimum version 17). NOTE: the default `true` value might cause some videos to hang loading on iOS. Setting it to `false` could fix this issue. -| Type | Required | -| ---- | -------- | -| bool | No | +| Type | Required | Platform | +| ---- | -------- | ------------------- | +| bool | No | iOS, Android, macOS | --- -### `nativeConfig` +### `nativeConfig`[⬆](#props-index) Override the native component used to render the WebView. Enables a custom native WebView which uses the same JavaScript as the original WebView. @@ -170,13 +258,13 @@ The `nativeConfig` prop expects an object with the following keys: - `props` (object) - `viewManager` (object) -| Type | Required | -| ------ | -------- | -| object | No | +| Type | Required | Platform | +| ------ | -------- | ------------------- | +| object | No | iOS, Android, macOS | --- -### `onError` +### `onError`[⬆](#props-index) Function that is invoked when the `WebView` load fails. @@ -188,8 +276,8 @@ Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onError={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; console.warn('WebView error: ', nativeEvent); }} @@ -216,7 +304,7 @@ url --- -### `onLoad` +### `onLoad`[⬆](#props-index) Function that is invoked when the `WebView` has finished loading. @@ -228,8 +316,8 @@ Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onLoad={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; this.url = nativeEvent.url; }} @@ -249,7 +337,7 @@ url --- -### `onLoadEnd` +### `onLoadEnd`[⬆](#props-index) Function that is invoked when the `WebView` load succeeds or fails. @@ -261,8 +349,8 @@ Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onLoadEnd={(syntheticEvent) => { // update component to be aware of loading status const { nativeEvent } = syntheticEvent; this.isLoading = nativeEvent.loading; @@ -283,7 +371,7 @@ url --- -### `onLoadStart` +### `onLoadStart`[⬆](#props-index) Function that is invoked when the `WebView` starts loading. @@ -295,8 +383,8 @@ Example: ```jsx { + source={{ uri: 'https://reactnative.dev/=' }} + onLoadStart={(syntheticEvent) => { // update component to be aware of loading status const { nativeEvent } = syntheticEvent; this.isLoading = nativeEvent.loading; @@ -317,19 +405,19 @@ url --- -### `onLoadProgress` +### `onLoadProgress`[⬆](#props-index) Function that is invoked when the `WebView` is loading. -| Type | Required | -| -------- | -------- | -| function | No | +| Type | Required | Platform | +| -------- | -------- | ------------------- | +| function | No | iOS, Android, macOS | Example: ```jsx { this.loadingProgress = nativeEvent.progress; }} @@ -350,7 +438,7 @@ url --- -### `onHttpError` +### `onHttpError`[⬆](#props-index) Function that is invoked when the `WebView` receives an http error. @@ -365,8 +453,8 @@ Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onHttpError={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; console.warn( 'WebView received error status code: ', @@ -394,7 +482,40 @@ url --- -### `onMessage` +### `onRenderProcessGone`[⬆](#props-index) + +Function that is invoked when the `WebView` process crashes or is killed by the OS on Android. + +> **_Note_** +> Android API minimum level 26. Android Only + +| Type | Required | +| -------- | -------- | +| function | No | + +Example: + +```jsx + { + const { nativeEvent } = syntheticEvent; + console.warn( + 'WebView Crashed: ', + nativeEvent.didCrash, + ); + }} +/> +``` + +Function passed to `onRenderProcessGone` is called with a SyntheticEvent wrapping a nativeEvent with these properties: + +``` +didCrash +``` +--- + +### `onMessage`[⬆](#props-index) Function that is invoked when the webview calls `window.ReactNativeWebView.postMessage`. Setting this property will inject this global into your webview. @@ -408,7 +529,7 @@ To learn more, read the [Communicating between JS and Native](Guide.md#communica --- -### `onNavigationStateChange` +### `onNavigationStateChange`[⬆](#props-index) Function that is invoked when the `WebView` loading starts or ends. @@ -420,8 +541,8 @@ Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onNavigationStateChange={(navState) => { // Keep track of going back navigation within component this.canGoBack = navState.canGoBack; }} @@ -434,30 +555,28 @@ The `navState` object includes these properties: canGoBack canGoForward loading -navigationType +navigationType (iOS only) target title url ``` -Note that this method will not be invoked on hash URL changes (e.g. from `https://example.com/users#list` to `https://example.com/users#help`). There is a workaround for this that is described [in the Guide](Guide.md#intercepting-hash-url-changes). - --- -### `onContentProcessDidTerminate` +### `onContentProcessDidTerminate`[⬆](#props-index) Function that is invoked when the `WebView` content process is terminated. -| Type | Required | Platform | -| -------- | -------- | ------------- | -| function | No | iOS WKWebView | +| Type | Required | Platform | +| -------- | -------- | ----------------------- | +| function | No | iOS and macOS WKWebView | Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onContentProcessDidTerminate={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; console.warn('Content process terminated, reloading', nativeEvent); this.refs.webview.reload(); @@ -478,40 +597,73 @@ url --- -### `originWhitelist` +### `onScroll`[⬆](#props-index) + +Function that is invoked when the scroll event is fired in the `WebView`. + +| Type | Required | Platform | +| -------- | -------- | ----------------------- | +| function | No | iOS, macOS, Android, Windows | + +Example: + +```jsx + { + const { contentOffset } = syntheticEvent.nativeEvent + console.table(contentOffset) + }} +/> +``` + +Function passed to `onScroll` is called with a SyntheticEvent wrapping a nativeEvent with these properties: + +``` +contentInset +contentOffset +contentSize +layoutMeasurement +velocity +zoomScale +``` + +--- + +### `originWhitelist`[⬆](#props-index) List of origin strings to allow being navigated to. The strings allow wildcards and get matched against _just_ the origin (not the full URL). If the user taps to navigate to a new page but the new page is not in this whitelist, the URL will be handled by the OS. The default whitelisted origins are "http://*" and "https://*". -| Type | Required | -| ---------------- | -------- | -| array of strings | No | +| Type | Required | Platform | +| ---------------- | -------- | ------------------- | +| array of strings | No | iOS, Android, macOS | Example: ```jsx //only allow URIs that begin with https:// or git:// ``` --- -### `renderError` +### `renderError`[⬆](#props-index) Function that returns a view to show if there's an error. -| Type | Required | -| -------- | -------- | -| function | No | +| Type | Required | Platform | +| -------- | -------- | ------------------- | +| function | No | iOS, Android, macOS | Example: ```jsx } + source={{ uri: 'https://reactnative.dev' }} + renderError={(errorName) => } /> ``` @@ -519,19 +671,19 @@ The function passed to `renderError` will be called with the name of the error --- -### `renderLoading` +### `renderLoading`[⬆](#props-index) Function that returns a loading indicator. The startInLoadingState prop must be set to true in order to use this prop. -| Type | Required | -| -------- | -------- | -| function | No | +| Type | Required | Platform | +| -------- | -------- | ------------------- | +| function | No | iOS, Android, macOS | Example: ```jsx } /> @@ -539,7 +691,7 @@ Example: --- -### `scalesPageToFit` +### `scalesPageToFit`[⬆](#props-index) Boolean that controls whether the web content is scaled to fit the view and enables the user to change the scale. The default value is `true`. @@ -549,24 +701,24 @@ Boolean that controls whether the web content is scaled to fit the view and enab --- -### `onShouldStartLoadWithRequest` +### `onShouldStartLoadWithRequest`[⬆](#props-index) Function that allows custom handling of any web view requests. Return `true` from the function to continue loading the request and `false` to stop loading. On Android, is not called on the first load. -| Type | Required | -| -------- | -------- | -| function | No | +| Type | Required | Platform | +| -------- | -------- | ------------------- | +| function | No | iOS, Android, macOS | Example: ```jsx { + source={{ uri: 'https://reactnative.dev' }} + onShouldStartLoadWithRequest={(request) => { // Only allow navigating within this website - return request.url.startsWith('https://facebook.github.io/react-native'); + return request.url.startsWith('https://reactnative.dev'); }} /> ``` @@ -582,22 +734,23 @@ canGoBack canGoForward lockIdentifier mainDocumentURL (iOS only) -navigationType +navigationType (iOS only) +isTopFrame (iOS only) ``` --- -### `startInLoadingState` +### `startInLoadingState`[⬆](#props-index) Boolean value that forces the `WebView` to show the loading view on the first load. This prop must be set to `true` in order for the `renderLoading` prop to work. -| Type | Required | -| ---- | -------- | -| bool | No | +| Type | Required | Platform | +| ---- | -------- | ------------------- | +| bool | No | iOS, Android, macOS | --- -### `style` +### `style`[⬆](#props-index) A style object that allow you to customize the `WebView` style. Please note that there are default styles (example: you need to add `flex: 0` to the style if you want to use `height` property). @@ -609,14 +762,14 @@ Example: ```jsx ``` --- -### `containerStyle` +### `containerStyle`[⬆](#props-index) A style object that allow you to customize the `WebView` container style. Please note that there are default styles (example: you need to add `flex: 0` to the style if you want to use `height` property). @@ -628,14 +781,14 @@ Example: ```jsx ``` --- -### `decelerationRate` +### `decelerationRate`[⬆](#props-index) A floating-point number that determines how quickly the scroll view decelerates after the user lifts their finger. You may also use the string shortcuts `"normal"` and `"fast"` which match the underlying iOS settings for `UIScrollViewDecelerationRateNormal` and `UIScrollViewDecelerationRateFast` respectively: @@ -648,7 +801,7 @@ A floating-point number that determines how quickly the scroll view decelerates --- -### `domStorageEnabled` +### `domStorageEnabled`[⬆](#props-index) Boolean value to control whether DOM Storage is enabled. Used only in Android. @@ -658,19 +811,29 @@ Boolean value to control whether DOM Storage is enabled. Used only in Android. --- -### `javaScriptEnabled` +### `javaScriptEnabled`[⬆](#props-index) -Boolean value to enable JavaScript in the `WebView`. Used on Android only as JavaScript is enabled by default on iOS. The default value is `true`. +Boolean value to enable JavaScript in the `WebView`. The default value is `true`. -| Type | Required | Platform | -| ---- | -------- | -------- | -| bool | No | Android | +| Type | Required | +| ---- | -------- | +| bool | No | + +--- + +### `javaScriptCanOpenWindowsAutomatically`[⬆](#props-index) + +A Boolean value indicating whether JavaScript can open windows without user interaction. The default value is `false`. + +| Type | Required | +| ---- | -------- | +| bool | No | --- -### `androidHardwareAccelerationDisabled` +### `androidHardwareAccelerationDisabled`[⬆](#props-index) -Boolean value to disable Hardware Acceleration in the `WebView`. Used on Android only as Hardware Acceleration is a feature only for Android. The default value is `false`. +**Deprecated.** Use the `androidLayerType` prop instead. | Type | Required | Platform | | ---- | -------- | -------- | @@ -678,7 +841,25 @@ Boolean value to disable Hardware Acceleration in the `WebView`. Used on Android --- -### `mixedContentMode` +### `androidLayerType`[⬆](#props-index) + +Specifies the layer type. + +Possible values for `androidLayerType` are: + +- `none` (default) - The view does not have a layer. +- `software` - The view has a software layer. A software layer is backed by a bitmap and causes the view to be rendered using Android's software rendering pipeline, even if hardware acceleration is enabled. +- `hardware` - The view has a hardware layer. A hardware layer is backed by a hardware specific texture and causes the view to be rendered using Android's hardware rendering pipeline, but only if hardware acceleration is turned on for the view hierarchy. + +Avoid setting both `androidLayerType` and `androidHardwareAccelerationDisabled` props at the same time, as this may cause undefined behaviour. + +| Type | Required | Platform | +| ------ | -------- | -------- | +| string | No | Android | + +--- + +### `mixedContentMode`[⬆](#props-index) Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin. @@ -694,7 +875,7 @@ Possible values for `mixedContentMode` are: --- -### `thirdPartyCookiesEnabled` +### `thirdPartyCookiesEnabled`[⬆](#props-index) Boolean value to enable third party cookies in the `WebView`. Used on Android Lollipop and above only as third party cookies are enabled by default on Android Kitkat and below and on iOS. The default value is `true`. For more on cookies, read the [Guide](Guide.md#Managing-Cookies) @@ -704,27 +885,27 @@ Boolean value to enable third party cookies in the `WebView`. Used on Android Lo --- -### `userAgent` +### `userAgent`[⬆](#props-index) Sets the user-agent for the `WebView`. -| Type | Required | -| ------ | -------- | -| string | No | +| Type | Required | Platform | +| ------ | -------- | ------------------- | +| string | No | iOS, Android, macOS | --- -### `applicationNameForUserAgent` +### `applicationNameForUserAgent`[⬆](#props-index) Append to the existing user-agent. Setting `userAgent` will override this. -| Type | Required | -| ------ | -------- | -| string | No | +| Type | Required | Platform | +| ------ | -------- | ------------------- | +| string | No | iOS, Android, macOS | ```jsx // Resulting User-Agent will look like: @@ -732,7 +913,7 @@ Append to the existing user-agent. Setting `userAgent` will override this. // Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 DemoApp/1.1.0 ``` -### `allowsFullscreenVideo` +### `allowsFullscreenVideo`[⬆](#props-index) Boolean that determines whether videos are allowed to be played in fullscreen. The default value is `false`. @@ -742,7 +923,7 @@ Boolean that determines whether videos are allowed to be played in fullscreen. T --- -### `allowsInlineMediaPlayback` +### `allowsInlineMediaPlayback`[⬆](#props-index) Boolean that determines whether HTML5 videos play inline or use the native full-screen controller. The default value is `false`. @@ -756,7 +937,7 @@ Boolean that determines whether HTML5 videos play inline or use the native full- --- -### `bounces` +### `bounces`[⬆](#props-index) Boolean value that determines whether the web view bounces when it reaches the edge of the content. The default value is `true`. @@ -766,7 +947,7 @@ Boolean value that determines whether the web view bounces when it reaches the e --- -### `overScrollMode` +### `overScrollMode`[⬆](#props-index) Specifies the over scroll mode. @@ -782,7 +963,7 @@ Possible values for `overScrollMode` are: --- -### `contentInset` +### `contentInset`[⬆](#props-index) The amount by which the web view content is inset from the edges of the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}. @@ -792,7 +973,7 @@ The amount by which the web view content is inset from the edges of the scroll v --- -### `contentInsetAdjustmentBehavior` +### `contentInsetAdjustmentBehavior`[⬆](#props-index) This property specifies how the safe area insets are used to modify the content area of the scroll view. The default value of this property is "never". Available on iOS 11 and later. Defaults to `never`. @@ -809,7 +990,25 @@ Possible values: --- -### `dataDetectorTypes` +### `contentMode`[⬆](#props-index) + +Controls the type of content to load. Available on iOS 13 and later. Defaults to `recommended`, which loads mobile content on iPhone & iPad Mini but desktop content on larger iPads. + +See [Introducing Desktop-class Browsing on iPad](https://developer.apple.com/videos/play/wwdc2019/203/) for more. + +Possible values: + +- `recommended` +- `mobile` +- `desktop` + +| Type | Required | Platform | +| ------ | -------- | -------- | +| string | No | iOS | + +--- + +### `dataDetectorTypes`[⬆](#props-index) Determines the types of data converted to clickable URLs in the web view's content. By default only phone numbers are detected. @@ -833,17 +1032,17 @@ Possible values for `dataDetectorTypes` are: --- -### `scrollEnabled` +### `scrollEnabled`[⬆](#props-index) Boolean value that determines whether scrolling is enabled in the `WebView`. The default value is `true`. Setting this to `false` will prevent the webview from moving the document body when the keyboard appears over an input. -| Type | Required | Platform | -| ---- | -------- | -------- | -| bool | No | iOS | +| Type | Required | Platform | +| ---- | -------- | ------------- | +| bool | No | iOS and macOS | --- -### `directionalLockEnabled` +### `directionalLockEnabled`[⬆](#props-index) A Boolean value that determines whether scrolling is disabled in a particular direction. The default value is `true`. @@ -854,27 +1053,27 @@ The default value is `true`. --- -### `showsHorizontalScrollIndicator` +### `showsHorizontalScrollIndicator`[⬆](#props-index) Boolean value that determines whether a horizontal scroll indicator is shown in the `WebView`. The default value is `true`. -| Type | Required | -| ---- | -------- | -| bool | No | +| Type | Required | Platform | +| ---- | -------- | ------------------- | +| bool | No | iOS, Android, macOS | --- -### `showsVerticalScrollIndicator` +### `showsVerticalScrollIndicator`[⬆](#props-index) Boolean value that determines whether a vertical scroll indicator is shown in the `WebView`. The default value is `true`. -| Type | Required | -| ---- | -------- | -| bool | No | +| Type | Required | Platform | +| ---- | -------- | ------------------- | +| bool | No | iOS, Android, macOS | --- -### `geolocationEnabled` +### `geolocationEnabled`[⬆](#props-index) Set whether Geolocation is enabled in the `WebView`. The default value is `false`. Used only in Android. @@ -884,37 +1083,37 @@ Set whether Geolocation is enabled in the `WebView`. The default value is `false --- -### `allowFileAccessFromFileURLs` +### `allowFileAccessFromFileURLs`[⬆](#props-index) Boolean that sets whether JavaScript running in the context of a file scheme URL should be allowed to access content from other file scheme URLs. The default value is `false`. -| Type | Required | Platform | -| ---- | -------- | -------- | -| bool | No | Android | +| Type | Required | Platform | +| ---- | -------- | ------------------- | +| bool | No | iOS, Android, macOS | --- -### `allowUniversalAccessFromFileURLs` +### `allowUniversalAccessFromFileURLs`[⬆](#props-index) Boolean that sets whether JavaScript running in the context of a file scheme URL should be allowed to access content from any origin. Including accessing content from other file scheme URLs. The default value is `false`. -| Type | Required | Platform | -| ---- | -------- | -------- | -| bool | No | Android | +| Type | Required | Platform | +| ---- | -------- | -------------------- | +| bool | No | iOS, Android, macOS | --- -### `allowingReadAccessToURL` +### `allowingReadAccessToURL`[⬆](#props-index) A String value that indicates which URLs the WebView's file can then reference in scripts, AJAX requests, and CSS imports. This is only used in for WebViews that are loaded with a source.uri set to a `'file://'` URL. If not provided, the default is to only allow read access to the URL provided in source.uri itself. -| Type | Required | Platform | -| ------ | -------- | -------- | -| string | No | iOS | +| Type | Required | Platform | +| ------ | -------- | ------------- | +| string | No | iOS and macOS | --- -### `url` +### `url`[⬆](#props-index) **Deprecated.** Use the `source` prop instead. @@ -924,7 +1123,7 @@ A String value that indicates which URLs the WebView's file can then reference i --- -### `html` +### `html`[⬆](#props-index) **Deprecated.** Use the `source` prop instead. @@ -934,7 +1133,7 @@ A String value that indicates which URLs the WebView's file can then reference i --- -### `keyboardDisplayRequiresUserAction` +### `keyboardDisplayRequiresUserAction`[⬆](#props-index) If false, web content can programmatically display the keyboard. The default value is `true`. @@ -944,7 +1143,7 @@ If false, web content can programmatically display the keyboard. The default val --- -### `hideKeyboardAccessoryView` +### `hideKeyboardAccessoryView`[⬆](#props-index) If true, this will hide the keyboard accessory view (< > and Done). @@ -954,27 +1153,27 @@ If true, this will hide the keyboard accessory view (< > and Done). --- -### `allowsBackForwardNavigationGestures` +### `allowsBackForwardNavigationGestures`[⬆](#props-index) If true, this will be able horizontal swipe gestures. The default value is `false`. -| Type | Required | Platform | -| ------- | -------- | -------- | -| boolean | No | iOS | +| Type | Required | Platform | +| ------- | -------- | ------------- | +| boolean | No | iOS and macOS | --- -### `incognito` +### `incognito`[⬆](#props-index) Does not store any data within the lifetime of the WebView. -| Type | Required | -| ------- | -------- | -| boolean | No | +| Type | Required | Platform | +| ------- | -------- | ------------------- | +| boolean | No | iOS, Android, macOS | --- -### `allowFileAccess` +### `allowFileAccess`[⬆](#props-index) If true, this will allow access to the file system via `file://` URI's. The default value is `false`. @@ -984,7 +1183,7 @@ If true, this will allow access to the file system via `file://` URI's. The defa --- -### `saveFormDataDisabled` +### `saveFormDataDisabled`[⬆](#props-index) Sets whether the WebView should disable saving form data. The default value is `false`. This function does not have any effect from Android API level 26 onwards as there is an Autofill feature which stores form data. @@ -994,17 +1193,17 @@ Sets whether the WebView should disable saving form data. The default value is ` --- -### `cacheEnabled` +### `cacheEnabled`[⬆](#props-index) Sets whether WebView should use browser caching. -| Type | Required | Default | -| ------- | -------- | ------- | -| boolean | No | true | +| Type | Required | Default | Platform | +| ------- | -------- | ------- | ------------------- | +| boolean | No | true | iOS, Android, macOS | --- -### `cacheMode` +### `cacheMode`[⬆](#props-index) Overrides the way the cache is used. The way the cache is used is based on the navigation type. For a normal page load, the cache is checked and content is re-validated as needed. When navigating back, content is not revalidated, instead the content is just retrieved from the cache. This property allows the client to override this behavior. @@ -1021,7 +1220,7 @@ Possible values are: --- -### `pagingEnabled` +### `pagingEnabled`[⬆](#props-index) If the value of this property is true, the scroll view stops on multiples of the scroll view’s bounds when the user scrolls. The default value is false. @@ -1031,27 +1230,27 @@ If the value of this property is true, the scroll view stops on multiples of the --- -### `allowsLinkPreview` +### `allowsLinkPreview`[⬆](#props-index) A Boolean value that determines whether pressing on a link displays a preview of the destination for the link. In iOS this property is available on devices that support 3D Touch. In iOS 10 and later, the default value is true; before that, the default value is false. -| Type | Required | Platform | -| ------- | -------- | -------- | -| boolean | No | iOS | +| Type | Required | Platform | +| ------- | -------- | ------------- | +| boolean | No | iOS and macOS | --- -### `sharedCookiesEnabled` +### `sharedCookiesEnabled`[⬆](#props-index) Set `true` if shared cookies from `[NSHTTPCookieStorage sharedHTTPCookieStorage]` should used for every load request in the WebView. The default value is `false`. For more on cookies, read the [Guide](Guide.md#Managing-Cookies) -| Type | Required | Platform | -| ------- | -------- | -------- | -| boolean | No | iOS | +| Type | Required | Platform | +| ------- | -------- | ------------- | +| boolean | No | iOS and macOS | --- -### `textZoom` +### `textZoom`[⬆](#props-index) If the user has set a custom font size in the Android system, an undesirable scale of the site interface in WebView occurs. @@ -1065,15 +1264,118 @@ Example: `` +--- + +### `pullToRefreshEnabled`[⬆](#props-index) + +Boolean value that determines whether a pull to refresh gesture is available in the `WebView`. The default value is `false`. If `true`, sets `bounces` automatically to `true`. + +| Type | Required | Platform | +| ------- | -------- | -------- | +| boolean | No | iOS | + +### `ignoreSilentHardwareSwitch`[⬆](#props-index) + +(ios only) + +When set to true the hardware silent switch is ignored. Default: `false` + +| Type | Required | Platform | +| ------- | -------- | -------- | +| boolean | No | iOS | + +### `onFileDownload`[⬆](#props-index) +This property is iOS-only. + +Function that is invoked when the client needs to download a file. + +iOS 13+ only: If the webview navigates to a URL that results in an HTTP +response with a Content-Disposition header 'attachment...', then +this will be called. + +iOS 8+: If the MIME type indicates that the content is not renderable by the +webview, that will also cause this to be called. On iOS versions before 13, +this is the only condition that will cause this function to be called. + +The application will need to provide its own code to actually download +the file. + +If not provided, the default is to let the webview try to render the file. + +Example: + +```jsx + { + // You use downloadUrl which is a string to download files however you want. + }} +/> +``` + +| Type | Required | Platform | +| -------- | -------- | -------- | +| function | No | iOS | + +--- + +### `limitsNavigationsToAppBoundDomains`[⬆](#props-index) + +If true indicates to WebKit that a WKWebView will only navigate to app-bound domains. Only applicable for iOS 14 or greater. + +Once set, any attempt to navigate away from an app-bound domain will fail with the error “App-bound domain failure.” +Applications can specify up to 10 “app-bound” domains using a new Info.plist key `WKAppBoundDomains`. For more information see [App-Bound Domains](https://webkit.org/blog/10882/app-bound-domains/). + +| Type | Required | Platform | +| ------- | -------- | -------- | +| boolean | No | iOS | + +Example: + +```jsx + +``` + +--- + +### `autoManageStatusBarEnabled` + +If set to `true`, the status bar will be automatically hidden/shown by WebView, specifically when full screen video is being watched. If `false`, WebView will not manage the status bar at all. The default value is `true`. + +| Type | Required | Platform | +| ------- | -------- | -------- | +| boolean | No | iOS | + +Example: + +```javascript + +``` + +### `setSupportMultipleWindows` + +Sets whether the WebView supports multiple windows. See [Android documentation]('https://developer.android.com/reference/android/webkit/WebSettings#setSupportMultipleWindows(boolean)') for more information. +Setting this to false can expose the application to this [vulnerability](https://alesandroortiz.com/articles/uxss-android-webview-cve-2020-6506/) allowing a malicious iframe to escape into the top layer DOM. + +| Type | Required | Default | Platform | +| ------- | -------- | ------- | -------- | +| boolean | No | true | Android | + +Example: + +```javascript + +``` + ## Methods -### `extraNativeComponentConfig()` +### `extraNativeComponentConfig()`[⬆](#methods-index) ```javascript static extraNativeComponentConfig() ``` -### `goForward()` +### `goForward()`[⬆](#methods-index) ```javascript goForward(); @@ -1081,7 +1383,7 @@ goForward(); Go forward one page in the web view's history. -### `goBack()` +### `goBack()`[⬆](#methods-index) ```javascript goBack(); @@ -1089,7 +1391,7 @@ goBack(); Go back one page in the web view's history. -### `reload()` +### `reload()`[⬆](#methods-index) ```javascript reload(); @@ -1097,7 +1399,7 @@ reload(); Reloads the current page. -### `stopLoading()` +### `stopLoading()`[⬆](#methods-index) ```javascript stopLoading(); @@ -1105,7 +1407,7 @@ stopLoading(); Stop loading the current page. -### `injectJavaScript(str)` +### `injectJavaScript(str)`[⬆](#methods-index) ```javascript injectJavaScript('... javascript string ...'); @@ -1138,7 +1440,7 @@ zoomToRect({x: 5, y: 5, width: 100, height: 100}, 1.0, true); ``` Zooms to a specific area of the content so that it is visible in the webview. -### `requestFocus()` +### `requestFocus()`[⬆](#methods-index) ```javascript requestFocus(); @@ -1146,31 +1448,43 @@ requestFocus(); Request the webView to ask for focus. (People working on TV apps might want having a look at this!) -### `clearFormData()` +### `postMessage(str)`[⬆](#methods-index) + +```javascript +postMessage('message'); +``` + +Post a message to WebView, handled by [`onMessage`](Reference.md#onmessage). + +### `clearFormData()`[⬆](#methods-index) + (android only) ```javascript clearFormData(); ``` -Removes the autocomplete popup from the currently focused form field, if present. [developer.android.com reference](https://developer.android.com/reference/android/webkit/WebView.html#clearFormData()) +Removes the autocomplete popup from the currently focused form field, if present. [developer.android.com reference]() + +### `clearCache(bool)`[⬆](#methods-index) -### `clearCache(bool)` (android only) + ```javascript clearCache(true); ``` -Clears the resource cache. Note that the cache is per-application, so this will clear the cache for all WebViews used. [developer.android.com reference](https://developer.android.com/reference/android/webkit/WebView.html#clearCache(boolean)) +Clears the resource cache. Note that the cache is per-application, so this will clear the cache for all WebViews used. [developer.android.com reference]() +### `clearHistory()`[⬆](#methods-index) -### `clearHistory()` (android only) + ```javascript clearHistory(); ``` -Tells this WebView to clear its internal back/forward list. [developer.android.com reference](https://developer.android.com/reference/android/webkit/WebView.html#clearHistory()) +Tells this WebView to clear its internal back/forward list. [developer.android.com reference]() ## Other Docs diff --git a/example/.gitattributes b/example/.gitattributes new file mode 100644 index 000000000..d42ff1835 --- /dev/null +++ b/example/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/example/.gitignore b/example/.gitignore new file mode 100755 index 000000000..3a3f3a5c9 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,67 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +# exclude project.xcworkspace except for xcshareddata/WorkspaceSettings.xcsettings +project.xcworkspace/* +**/project.xcworkspace/contents.xcworkspacedata +**/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# Visual Studio Code +# +.vscode/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ diff --git a/example/.prettierrc.js b/example/.prettierrc.js new file mode 100644 index 000000000..5c4de1a4f --- /dev/null +++ b/example/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: false, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/example/.watchmanconfig b/example/.watchmanconfig new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/example/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/example/App.tsx b/example/App.tsx new file mode 100644 index 000000000..b2d563465 --- /dev/null +++ b/example/App.tsx @@ -0,0 +1,248 @@ +import React, {Component} from 'react'; +import { + StyleSheet, + SafeAreaView, + Text, + TouchableOpacity, + View, + Keyboard, + Button, + Platform, +} from 'react-native'; + +import Alerts from './examples/Alerts'; +import Scrolling from './examples/Scrolling'; +import Background from './examples/Background'; +import Downloads from './examples/Downloads'; +import Uploads from './examples/Uploads'; +import Injection from './examples/Injection'; +import LocalPageLoad from './examples/LocalPageLoad'; +import Messaging from './examples/Messaging'; +import NativeWebpage from './examples/NativeWebpage'; + +const TESTS = { + Messaging: { + title: 'Messaging', + testId: 'messaging', + description: 'js-webview postMessage messaging test', + render() { + return ; + }, + }, + Alerts: { + title: 'Alerts', + testId: 'alerts', + description: 'Alerts tests', + render() { + return ; + }, + }, + Scrolling: { + title: 'Scrolling', + testId: 'scrolling', + description: 'Scrolling event test', + render() { + return ; + }, + }, + Background: { + title: 'Background', + testId: 'background', + description: 'Background color test', + render() { + return ; + }, + }, + Downloads: { + title: 'Downloads', + testId: 'downloads', + description: 'File downloads test', + render() { + return ; + }, + }, + Uploads: { + title: 'Uploads', + testId: 'uploads', + description: 'Upload test', + render() { + return ; + }, + }, + Injection: { + title: 'Injection', + testId: 'injection', + description: 'Injection test', + render() { + return ; + }, + }, + PageLoad: { + title: 'LocalPageLoad', + testId: 'LocalPageLoad', + description: 'Local Page load test', + render() { + return ; + }, + }, + NativeWebpage: { + title: 'NativeWebpage', + testId: 'NativeWebpage', + description: 'Test to open a new webview with a link', + render() { + return ; + }, + }, +}; + +type Props = {}; +type State = {restarting: boolean; currentTest: Object}; + +export default class App extends Component { + state = { + restarting: false, + currentTest: TESTS.Alerts, + }; + + _simulateRestart = () => { + this.setState({restarting: true}, () => this.setState({restarting: false})); + }; + + _changeTest = (testName) => { + this.setState({currentTest: TESTS[testName]}); + }; + + render() { + const {restarting, currentTest} = this.state; + return ( + + Keyboard.dismiss()} + testID="closeKeyboard" + /> + + + Simulate Restart + + + + + + +

+ + + +`; + +type Props = {}; +type State = {}; + +export default class Alerts extends Component { + state = {}; + + render() { + return ( + + + + ); + } +} diff --git a/example/examples/Background.tsx b/example/examples/Background.tsx new file mode 100644 index 000000000..79a360eb3 --- /dev/null +++ b/example/examples/Background.tsx @@ -0,0 +1,54 @@ +import React, {Component} from 'react'; +import {Text, View} from 'react-native'; + +import WebView from 'react-native-webview'; + +const HTML = ` +\n + + + Hello World + + + + + +

HTML content in transparent body.

+ + +`; + +type Props = {}; +type State = { + backgroundColor: string, +}; + +export default class Background extends Component { + state = { + backgroundColor: '#FF00FF00' + }; + + render() { + return ( + + + + + + + WebView is transparent contained in a View with a red backgroundColor + + ); + } +} diff --git a/example/examples/Downloads.tsx b/example/examples/Downloads.tsx new file mode 100644 index 000000000..2fa554704 --- /dev/null +++ b/example/examples/Downloads.tsx @@ -0,0 +1,55 @@ +import React, {Component} from 'react'; +import {Alert, Platform, View} from 'react-native'; + +import WebView, {FileDownload} from 'react-native-webview'; + +const HTML = ` +\n + + + Downloads + + + + + + Example zip file download + + +`; + +type Props = {}; +type State = {}; + +export default class Downloads extends Component { + state = {}; + + onFileDownload = ({ nativeEvent }: { nativeEvent: FileDownload } ) => { + Alert.alert("File download detected", nativeEvent.downloadUrl); + }; + + render() { + const platformProps = Platform.select({ + ios: { + onFileDownload: this.onFileDownload, + }, + }); + + return ( + + + + ); + } +} diff --git a/example/examples/Injection.tsx b/example/examples/Injection.tsx new file mode 100644 index 000000000..e1d52a2db --- /dev/null +++ b/example/examples/Injection.tsx @@ -0,0 +1,161 @@ +import React, {Component} from 'react'; +import {Text, View, ScrollView} from 'react-native'; + +import WebView from 'react-native-webview'; + +const HTML = ` + + + + + + iframe test + + +

beforeContentLoaded on the top frame failed!

+

afterContentLoaded on the top frame failed!

+ + + + + +`; + +type Props = {}; +type State = { + backgroundColor: string, +}; + +export default class Injection extends Component { + state = { + backgroundColor: '#FF00FF00' + }; + + render() { + return ( + + + + {}} + injectedJavaScriptBeforeContentLoadedForMainFrameOnly={false} + injectedJavaScriptForMainFrameOnly={false} + + /* We set this property in each frame */ + injectedJavaScriptBeforeContentLoaded={` + console.log("executing injectedJavaScriptBeforeContentLoaded... " + (new Date()).toString()); + if(typeof window.top.injectedIframesBeforeContentLoaded === "undefined"){ + window.top.injectedIframesBeforeContentLoaded = []; + } + window.self.colourToUse = "orange"; + if(window.self === window.top){ + console.log("Was window.top. window.frames.length is:", window.frames.length); + window.self.numberOfFramesAtBeforeContentLoaded = window.frames.length; + function declareSuccessOfBeforeContentLoaded(head){ + var style = window.self.document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = "#before_failed { display: none !important; }#before_succeeded { display: inline-block !important; }"; + head.appendChild(style); + } + + const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]); + + if(head){ + declareSuccessOfBeforeContentLoaded(head); + } else { + window.self.document.addEventListener("DOMContentLoaded", function (event) { + const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]); + declareSuccessOfBeforeContentLoaded(head); + }); + } + } else { + window.top.injectedIframesBeforeContentLoaded.push(window.self.name); + console.log("wasn't window.top."); + console.log("wasn't window.top. Still going..."); + } + `} + + /* We read the colourToUse property in each frame to recolour each frame */ + injectedJavaScript={` + console.log("executing injectedJavaScript... " + (new Date()).toString()); + if(typeof window.top.injectedIframesAfterContentLoaded === "undefined"){ + window.top.injectedIframesAfterContentLoaded = []; + } + + if(window.self.colourToUse){ + window.self.document.body.style.backgroundColor = window.self.colourToUse; + } else { + window.self.document.body.style.backgroundColor = "cyan"; + } + + if(window.self === window.top){ + function declareSuccessOfAfterContentLoaded(head){ + var style = window.self.document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = "#after_failed { display: none !important; }#after_succeeded { display: inline-block !important; }"; + head.appendChild(style); + } + + declareSuccessOfAfterContentLoaded(window.self.document.head || window.self.document.getElementsByTagName('head')[0]); + + // var numberOfFramesAtBeforeContentLoadedEle = document.createElement('p'); + // numberOfFramesAtBeforeContentLoadedEle.textContent = "Number of iframes upon the main frame's beforeContentLoaded: " + + // window.self.numberOfFramesAtBeforeContentLoaded; + + // var numberOfFramesAtAfterContentLoadedEle = document.createElement('p'); + // numberOfFramesAtAfterContentLoadedEle.textContent = "Number of iframes upon the main frame's afterContentLoaded: " + window.frames.length; + // numberOfFramesAtAfterContentLoadedEle.id = "numberOfFramesAtAfterContentLoadedEle"; + + var namedFramesAtBeforeContentLoadedEle = document.createElement('p'); + namedFramesAtBeforeContentLoadedEle.textContent = "Names of iframes that called beforeContentLoaded: " + JSON.stringify(window.top.injectedIframesBeforeContentLoaded || []); + namedFramesAtBeforeContentLoadedEle.id = "namedFramesAtBeforeContentLoadedEle"; + + var namedFramesAtAfterContentLoadedEle = document.createElement('p'); + namedFramesAtAfterContentLoadedEle.textContent = "Names of iframes that called afterContentLoaded: " + JSON.stringify(window.top.injectedIframesAfterContentLoaded); + namedFramesAtAfterContentLoadedEle.id = "namedFramesAtAfterContentLoadedEle"; + + // document.body.appendChild(numberOfFramesAtBeforeContentLoadedEle); + // document.body.appendChild(numberOfFramesAtAfterContentLoadedEle); + document.body.appendChild(namedFramesAtBeforeContentLoadedEle); + document.body.appendChild(namedFramesAtAfterContentLoadedEle); + } else { + window.top.injectedIframesAfterContentLoaded.push(window.self.name); + window.top.document.getElementById('namedFramesAtAfterContentLoadedEle').textContent = "Names of iframes that called afterContentLoaded: " + JSON.stringify(window.top.injectedIframesAfterContentLoaded); + } + `} + /> + + + This test presents three iframes: iframe_0 (yellow); iframe_1 (pink); and iframe_2 (transparent, because its 'X-Frame-Options' is set to 'SAMEORIGIN'). + Before injection, the main frame's background is the browser's default value (transparent or white) and each frame has its natural colour. + {/*1a) At injection time "beforeContentLoaded", a variable will be set in each frame to set 'orange' as the "colour to be used".*/} + {/*1b) Also upon "beforeContentLoaded", a style element to change the text "beforeContentLoaded failed" -> "beforeContentLoaded succeeded" will be applied as soon as the head has loaded.*/} + {/*2a) At injection time "afterContentLoaded", that variable will be read – if present, the colour orange will be injected into all frames. Otherwise, cyan.*/} + {/*2b) Also upon "afterContentLoaded", a style element to change the text "afterContentLoaded failed" -> "afterContentLoaded succeeded" will be applied as soon as the head has loaded.*/} + ✅ If the main frame becomes orange, then top-frame injection both beforeContentLoaded and afterContentLoaded is supported. + ✅ If iframe_0, and iframe_1 become orange, then multi-frame injection beforeContentLoaded and afterContentLoaded is supported. + ✅ If the two texts say "beforeContentLoaded on the top frame succeeded!" and "afterContentLoaded on the top frame succeeded!", then both injection times are supported at least on the main frame. + ❌ If either of the two iframes become coloured cyan, then for that given frame, JS injection succeeded after the content loaded, but didn't occur before the content loaded. + ❌ If "Names of iframes that called beforeContentLoaded: " is [], then see above. + ❌ If "Names of iframes that called afterContentLoaded: " is [], then afterContentLoaded is not supported in iframes. + ❌ If the main frame becomes coloured cyan, then JS injection succeeded after the content loaded, but didn't occur before the content loaded. + ❌ If the text "beforeContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame before the content loaded. + ❌ If the text "afterContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame after the content loaded. + ❌ If the iframes remain their original colours (yellow and pink), then multi-frame injection is not supported at all. + + ); + } +} diff --git a/example/examples/LocalPageLoad.tsx b/example/examples/LocalPageLoad.tsx new file mode 100644 index 000000000..780e6e697 --- /dev/null +++ b/example/examples/LocalPageLoad.tsx @@ -0,0 +1,16 @@ +import React, {Component} from 'react'; +import {View, Text, Alert, TextInput, Button} from 'react-native'; +import WebView from 'react-native-webview'; +const localHtmlFile = require('../assets/test.html'); + +export default class LocalPageLoad extends Component { + render() { + return ( + + + + + + ); + } + } \ No newline at end of file diff --git a/example/examples/Messaging.tsx b/example/examples/Messaging.tsx new file mode 100644 index 000000000..afd6a6088 --- /dev/null +++ b/example/examples/Messaging.tsx @@ -0,0 +1,63 @@ +import React, {Component} from 'react'; +import {View, Alert} from 'react-native'; + +import WebView from 'react-native-webview'; + +const HTML = `\n + + + Messaging + + + + + + +

+ + +`; + +type Props = {}; +type State = {}; + +export default class Messaging extends Component { + state = {}; + + constructor(props) { + super(props); + this.webView = React.createRef(); + } + + render() { + return ( + + {this.webView.current.postMessage('Hello from RN');}} + automaticallyAdjustContentInsets={false} + onMessage={(e: {nativeEvent: {data?: string}}) => { + Alert.alert('Message received from JS: ', e.nativeEvent.data); + }} + /> + + ); + } +} diff --git a/example/examples/NativeWebpage.tsx b/example/examples/NativeWebpage.tsx new file mode 100644 index 000000000..7ec9da0c5 --- /dev/null +++ b/example/examples/NativeWebpage.tsx @@ -0,0 +1,23 @@ +import React, {Component} from 'react'; +import {View} from 'react-native'; + +import WebView from 'react-native-webview'; + +type Props = {}; +type State = {}; + +export default class NativeWebpage extends Component { + state = {}; + + render() { + return ( + + + + ); + } +} diff --git a/example/examples/Scrolling.tsx b/example/examples/Scrolling.tsx new file mode 100644 index 000000000..7f3564522 --- /dev/null +++ b/example/examples/Scrolling.tsx @@ -0,0 +1,68 @@ +import React, {Component} from 'react'; +import {Button, Text, View} from 'react-native'; + +import WebView from 'react-native-webview'; + +const HTML = ` +\n + + + Hello World + + + + + +

Lorem ipsum dolor sit amet, virtute utroque voluptaria et duo, probo aeque partiendo pri at. Mea ut stet aliquip deterruisset. Inani erroribus principes ei mel, no dicit recteque delicatissimi usu. Ne has dolore nominavi, feugait hendrerit interesset vis ea, amet regione ne pri. Te cum amet etiam.

+

Ut adipiscing neglegentur mediocritatem sea, suas abhorreant ius cu, ne nostrud feugiat per. Nam paulo facete iudicabit an, an brute mundi suavitate has, ex utamur numquam duo. Sea platonem argumentum instructior in, quo no prima inani perfecto. Ex illum postea copiosae has, ei mea sonet ocurreret.

+

Has convenire erroribus cu, quo homero facilisis inciderint ea. Vix choro gloriatur definitionem an, te exerci debitis voluptaria pri, mea admodum antiopam neglegentur te. His ea iisque splendide, nam id malorum pertinacia. Iusto tempor in eos, vis debet erant an. An nostrum rationibus sit, et sed dicta delenit suscipiantur. Est dolore vituperatoribus in, ubique explicari est cu. Legere tractatos ut usu, probo atqui vituperata in usu, mazim nemore praesent pro no.

+

Ei pri facilisi accusamus. Ut partem quaestio sit, an usu audiam quaerendum, ei qui hinc soleat. Fabulas phaedrum erroribus ut est. Intellegebat delicatissimi vis cu. His ea vidit libris facilis. Usu ne scripta legimus intellegam. Hendrerit urbanitas accommodare mei in.

+

Brute appetere efficiendi has ne. Ei ornatus labores vis. Vel harum fierent no, ad erat partiendo vis, harum democritum duo at. Has no labitur vulputate. Has cu autem aperiam hendrerit, sed eu justo verear menandri.

+ + +`; + +type Props = {}; +type State = { + scrollEnabled: boolean, + lastScrollEvent: string +}; + +export default class Scrolling extends Component { + state = { + scrollEnabled: true, + lastScrollEvent: '' + }; + + render() { + return ( + + + + +