diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index 483b181..83e2474 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -32,6 +32,7 @@ import android.webkit.URLUtil; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -58,6 +59,7 @@ import com.facebook.react.uimanager.events.ContentSizeChangeEvent; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.EventDispatcher; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; +import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; import com.reactnativecommunity.webview.events.TopLoadingProgressEvent; import com.reactnativecommunity.webview.events.TopLoadingStartEvent; @@ -512,6 +514,7 @@ public class RNCWebViewManager extends SimpleViewManager { export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress")); 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")); return export; } @@ -725,6 +728,25 @@ public class RNCWebViewManager extends SimpleViewManager { new TopLoadingErrorEvent(webView.getId(), eventData)); } + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError( + WebView webView, + WebResourceRequest request, + WebResourceResponse errorResponse) { + super.onReceivedHttpError(webView, request, errorResponse); + + if (request.isForMainFrame()) { + WritableMap eventData = createWebViewEvent(webView, request.getUrl().toString()); + eventData.putInt("statusCode", errorResponse.getStatusCode()); + eventData.putString("description", errorResponse.getReasonPhrase()); + + dispatchEvent( + webView, + new TopHttpErrorEvent(webView.getId(), eventData)); + } + } + protected void emitFinishEvent(WebView webView, String url) { dispatchEvent( webView, diff --git a/android/src/main/java/com/reactnativecommunity/webview/events/TopHttpErrorEvent.kt b/android/src/main/java/com/reactnativecommunity/webview/events/TopHttpErrorEvent.kt new file mode 100644 index 0000000..15aa089 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/events/TopHttpErrorEvent.kt @@ -0,0 +1,25 @@ +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 a http error is received from the server. + */ +class TopHttpErrorEvent(viewId: Int, private val mEventData: WritableMap) : + Event(viewId) { + companion object { + const val EVENT_NAME = "topHttpError" + } + + 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/docs/Reference.md b/docs/Reference.md index d98ece8..2ce7f1e 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -14,6 +14,7 @@ This document lays out the current public properties and methods for the React N - [`onLoadEnd`](Reference.md#onloadend) - [`onLoadStart`](Reference.md#onloadstart) - [`onLoadProgress`](Reference.md#onloadprogress) +- [`onHttpError`](Reference.md#onhttperror) - [`onMessage`](Reference.md#onmessage) - [`onNavigationStateChange`](Reference.md#onnavigationstatechange) - [`originWhitelist`](Reference.md#originwhitelist) @@ -342,6 +343,46 @@ url --- +### `onHttpError` + +Function that is invoked when the `WebView` receives an http error. +> **_Note_** +> Android API minimum level 23. + +| Type | Required | +| -------- | -------- | +| function | No | + +Example: + +```jsx + { + const { nativeEvent } = syntheticEvent; + console.warn('WebView received error status code: ', nativeEvent.statusCode); + }} +/> +``` + +Function passed to `onHttpError` is called with a SyntheticEvent wrapping a nativeEvent with these properties: + +``` +canGoBack +canGoForward +description +loading +statusCode +target +title +url +``` + +> **_Note_** +> Description is only used on Android + +--- + ### `onMessage` Function that is invoked when the webview calls `window.ReactNativeWebView.postMessage`. Setting this property will inject this global into your webview. diff --git a/ios/RNCWebView.m b/ios/RNCWebView.m index b64f71b..bd59aa6 100644 --- a/ios/RNCWebView.m +++ b/ios/RNCWebView.m @@ -33,6 +33,7 @@ static NSURLCredential* clientAuthenticationCredential; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; @property (nonatomic, copy) RCTDirectEventBlock onLoadingProgress; @property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; +@property (nonatomic, copy) RCTDirectEventBlock onHttpError; @property (nonatomic, copy) RCTDirectEventBlock onMessage; @property (nonatomic, copy) RCTDirectEventBlock onScroll; @property (nonatomic, copy) WKWebView *webView; @@ -806,6 +807,34 @@ static NSURLCredential* clientAuthenticationCredential; decisionHandler(WKNavigationResponsePolicyAllow); } +/** + * Decides whether to allow or cancel a navigation after its response is known. + * @see https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview?language=objc + */ +- (void) webView:(WKWebView *)webView + decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse + decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler +{ + 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: @{ + @"url": response.URL.absoluteString, + @"statusCode": @(statusCode) + }]; + + _onHttpError(event); + } + } + } + + decisionHandler(WKNavigationResponsePolicyAllow); +} + /** * Called when an error occurs while the web view is loading content. * @see https://fburl.com/km6vqenw diff --git a/ios/RNCWebViewManager.m b/ios/RNCWebViewManager.m index 5f823e6..03c7890 100644 --- a/ios/RNCWebViewManager.m +++ b/ios/RNCWebViewManager.m @@ -47,6 +47,7 @@ RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingProgress, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onHttpError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) RCT_EXPORT_VIEW_PROPERTY(javaScriptEnabled, BOOL) diff --git a/src/WebView.android.tsx b/src/WebView.android.tsx index b1d95f1..1151e4d 100644 --- a/src/WebView.android.tsx +++ b/src/WebView.android.tsx @@ -20,6 +20,7 @@ import { } from './WebViewShared'; import { WebViewErrorEvent, + WebViewHttpErrorEvent, WebViewMessageEvent, WebViewNavigationEvent, WebViewProgressEvent, @@ -178,6 +179,13 @@ class WebView extends React.Component { }); }; + onHttpError = (event: WebViewHttpErrorEvent) => { + const { onHttpError } = this.props; + if (onHttpError) { + onHttpError(event); + } + } + onLoadingFinish = (event: WebViewNavigationEvent) => { const { onLoad, onLoadEnd } = this.props; if (onLoad) { @@ -281,6 +289,7 @@ class WebView extends React.Component { onLoadingFinish={this.onLoadingFinish} onLoadingProgress={this.onLoadingProgress} onLoadingStart={this.onLoadingStart} + onHttpError={this.onHttpError} onMessage={this.onMessage} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} ref={this.webViewRef} diff --git a/src/WebView.ios.tsx b/src/WebView.ios.tsx index ab7765b..553c217 100644 --- a/src/WebView.ios.tsx +++ b/src/WebView.ios.tsx @@ -18,6 +18,7 @@ import { } from './WebViewShared'; import { WebViewErrorEvent, + WebViewHttpErrorEvent, WebViewMessageEvent, WebViewNavigationEvent, WebViewProgressEvent, @@ -207,6 +208,13 @@ class WebView extends React.Component { }); }; + onHttpError = (event: WebViewHttpErrorEvent) => { + const { onHttpError } = this.props; + if (onHttpError) { + onHttpError(event); + } + } + onLoadingFinish = (event: WebViewNavigationEvent) => { const { onLoad, onLoadEnd } = this.props; if (onLoad) { @@ -321,6 +329,7 @@ class WebView extends React.Component { onLoadingFinish={this.onLoadingFinish} onLoadingProgress={this.onLoadingProgress} onLoadingStart={this.onLoadingStart} + onHttpError={this.onHttpError} onMessage={this.onMessage} onScroll={this.props.onScroll} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 7ec5fc6..3cec75f 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -109,6 +109,11 @@ export interface WebViewError extends WebViewNativeEvent { description: string; } +export interface WebViewHttpError extends WebViewNativeEvent { + description: string; + statusCode: number; +} + export type WebViewEvent = NativeSyntheticEvent; export type WebViewProgressEvent = NativeSyntheticEvent< @@ -121,6 +126,8 @@ export type WebViewMessageEvent = NativeSyntheticEvent; export type WebViewErrorEvent = NativeSyntheticEvent; +export type WebViewHttpErrorEvent = NativeSyntheticEvent; + export type DataDetectorTypes = | 'phoneNumber' | 'link' @@ -210,6 +217,7 @@ export interface CommonNativeWebViewProps extends ViewProps { onLoadingFinish: (event: WebViewNavigationEvent) => void; onLoadingProgress: (event: WebViewProgressEvent) => void; onLoadingStart: (event: WebViewNavigationEvent) => void; + onHttpError: (event: WebViewHttpErrorEvent) => void; onMessage: (event: WebViewMessageEvent) => void; onShouldStartLoadWithRequest: (event: WebViewNavigationEvent) => void; showsHorizontalScrollIndicator?: boolean; @@ -598,6 +606,12 @@ export interface WebViewSharedProps extends ViewProps { */ onError?: (event: WebViewErrorEvent) => void; + /** + * Function that is invoked when the `WebView` receives an error status code. + * Works on iOS and Android (minimum API level 23). + */ + onHttpError?: (event: WebViewHttpErrorEvent) => void; + /** * Function that is invoked when the `WebView` loading starts or ends. */