Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ android/gradle
android/gradlew
android/gradlew.bat

lib/
.classpath
.project
.settings/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,18 @@ public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable

@Override
public void setFraudulentWebsiteWarningEnabled(RNCWebViewWrapper view, boolean value) {}

@Override
public void setTintColor(RNCWebViewWrapper view, double r, double g, double b, double a) {}

@Override
public void setScrollsToTop(RNCWebViewWrapper view, boolean value) {}

@Override
public void setDragInteractionEnabled(RNCWebViewWrapper view, boolean value) {}

@Override
public void setPreventUniversalLinks(RNCWebViewWrapper view, @Nullable ReadableArray value) {}
/* !iOS PROPS - no implemented here */

@Override
Expand Down
7 changes: 7 additions & 0 deletions apple/RNCWebView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,13 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */
REMAP_WEBVIEW_PROP(limitsNavigationsToAppBoundDomains)
#endif
if(oldViewProps.preventUniversalLinks != newViewProps.preventUniversalLinks) {
NSMutableArray *preventUniversalLinks = [NSMutableArray array];
for (const auto &host: newViewProps.preventUniversalLinks) {
[preventUniversalLinks addObject: RCTNSStringFromString(host)];
}
[_view setPreventUniversalLinks:preventUniversalLinks];
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500 /* iOS 14.5 */
REMAP_WEBVIEW_PROP(textInteractionEnabled)
#endif
Expand Down
2 changes: 2 additions & 0 deletions apple/RNCWebViewImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, assign) BOOL limitsNavigationsToAppBoundDomains;
#endif

@property (nonatomic, copy) NSArray<NSString *> * _Nullable preventUniversalLinks;

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500 /* iOS 14.5 */
@property (nonatomic, assign) BOOL textInteractionEnabled;
#endif
Expand Down
45 changes: 45 additions & 0 deletions apple/RNCWebViewImpl.m
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,51 @@ - (void) webView:(WKWebView *)webView
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
BOOL hasTargetFrame = navigationAction.targetFrame != nil;

// Universal Link handoff bypass.
// When the request host matches an entry in `preventUniversalLinks`
// (domain-suffix match with dot boundary), the navigation is canceled
// and re-issued via `[webView loadRequest:]` — the one nav type iOS
// does not consider for Universal Link handoff. Without this, a
// navigation to a UL-eligible URL (e.g. an OAuth redirect targeting a
// host whose AASA registers Universal Links for this app) would hand
// the user off to the installed third-party app and break the
// embedded flow.
//
// Scoped to specified hosts on purpose: reissuing every top-frame nav
// would lose POST bodies and consume single-use auth codes when the
// chain runs through other hosts (e.g. an identity provider during
// OAuth).
//
// An associated-object flag breaks the cancel/reissue loop: the
// reissued nav re-enters this method with the flag set, we clear it
// and fall through to the normal handler.
if (_preventUniversalLinks.count > 0 && isTopFrame && hasTargetFrame) {
static const void *kRNCUlBypassKey = &kRNCUlBypassKey;
NSString *host = request.URL.host.lowercaseString ?: @"";
BOOL hostMatches = NO;
for (NSString *pattern in _preventUniversalLinks) {
NSString *lowered = pattern.lowercaseString ?: @"";
if ([host isEqualToString:lowered] ||
[host hasSuffix:[@"." stringByAppendingString:lowered]]) {
hostMatches = YES;
break;
}
}
if (hostMatches) {
NSNumber *bypassFlag = objc_getAssociatedObject(webView, kRNCUlBypassKey);
if ([bypassFlag boolValue]) {
objc_setAssociatedObject(webView, kRNCUlBypassKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} else {
objc_setAssociatedObject(webView, kRNCUlBypassKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
decisionHandler(WKNavigationActionPolicyCancel);
dispatch_async(dispatch_get_main_queue(), ^{
[webView loadRequest:request];
});
return;
}
}
}

if (_onOpenWindow && !hasTargetFrame) {
// When OnOpenWindow should be called, we want to prevent the navigation
// If not prevented, the `decisionHandler` is called first and after that `createWebViewWithConfiguration` is called
Expand Down
2 changes: 2 additions & 0 deletions apple/RNCWebViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ - (RNCView *)view
RCT_EXPORT_VIEW_PROPERTY(limitsNavigationsToAppBoundDomains, BOOL)
#endif

RCT_EXPORT_VIEW_PROPERTY(preventUniversalLinks, NSArray)

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500 /* iOS 14.5 */
RCT_EXPORT_VIEW_PROPERTY(textInteractionEnabled, BOOL)
#endif
Expand Down
8 changes: 8 additions & 0 deletions lib/NativeRNCWebViewModule.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { TurboModule } from 'react-native';
import { Double } from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
isFileUploadSupported(): Promise<boolean>;
shouldStartLoadWithLockIdentifier(shouldStart: boolean, lockIdentifier: Double): void;
}
declare const _default: Spec;
export default _default;
1 change: 1 addition & 0 deletions lib/NativeRNCWebViewModule.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

249 changes: 249 additions & 0 deletions lib/RNCWebViewNativeComponent.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import type { HostComponent, ViewProps } from 'react-native';
import { DirectEventHandler, Double, Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
export type WebViewNativeEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
}>;
export type WebViewCustomMenuSelectionEvent = Readonly<{
label: string;
key: string;
selectedText: string;
}>;
export type WebViewMessageEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
data: string;
}>;
export type WebViewOpenWindowEvent = Readonly<{
targetUrl: string;
}>;
export type WebViewHttpErrorEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
description: string;
statusCode: Int32;
}>;
export type WebViewErrorEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
domain?: string;
code: Int32;
description: string;
}>;
export type WebViewNativeProgressEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
progress: Double;
}>;
export type WebViewNavigationEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
navigationType: 'click' | 'formsubmit' | 'backforward' | 'reload' | 'formresubmit' | 'other';
mainDocumentURL?: string;
}>;
export type ShouldStartLoadRequestEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
navigationType: 'click' | 'formsubmit' | 'backforward' | 'reload' | 'formresubmit' | 'other';
mainDocumentURL?: string;
isTopFrame: boolean;
}>;
type ScrollEvent = Readonly<{
contentInset: {
bottom: Double;
left: Double;
right: Double;
top: Double;
};
contentOffset: {
y: Double;
x: Double;
};
contentSize: {
height: Double;
width: Double;
};
layoutMeasurement: {
height: Double;
width: Double;
};
targetContentOffset?: {
y: Double;
x: Double;
};
velocity?: {
y: Double;
x: Double;
};
zoomScale?: Double;
responderIgnoreScroll?: boolean;
}>;
type WebViewRenderProcessGoneEvent = Readonly<{
didCrash: boolean;
}>;
type WebViewDownloadEvent = Readonly<{
downloadUrl: string;
}>;
export interface NativeProps extends ViewProps {
allowFileAccess?: boolean;
allowsProtectedMedia?: boolean;
allowsFullscreenVideo?: boolean;
androidLayerType?: WithDefault<'none' | 'software' | 'hardware', 'none'>;
cacheMode?: WithDefault<'LOAD_DEFAULT' | 'LOAD_CACHE_ELSE_NETWORK' | 'LOAD_NO_CACHE' | 'LOAD_CACHE_ONLY', 'LOAD_DEFAULT'>;
domStorageEnabled?: boolean;
downloadingMessage?: string;
forceDarkOn?: boolean;
geolocationEnabled?: boolean;
lackPermissionToDownloadMessage?: string;
messagingModuleName: string;
minimumFontSize?: Int32;
mixedContentMode?: WithDefault<'never' | 'always' | 'compatibility', 'never'>;
nestedScrollEnabled?: boolean;
onContentSizeChange?: DirectEventHandler<WebViewNativeEvent>;
onRenderProcessGone?: DirectEventHandler<WebViewRenderProcessGoneEvent>;
overScrollMode?: string;
saveFormDataDisabled?: boolean;
scalesPageToFit?: WithDefault<boolean, true>;
setBuiltInZoomControls?: WithDefault<boolean, true>;
setDisplayZoomControls?: boolean;
setSupportMultipleWindows?: WithDefault<boolean, true>;
textZoom?: Int32;
thirdPartyCookiesEnabled?: WithDefault<boolean, true>;
hasOnScroll?: boolean;
allowingReadAccessToURL?: string;
allowsBackForwardNavigationGestures?: boolean;
allowsInlineMediaPlayback?: boolean;
allowsPictureInPictureMediaPlayback?: boolean;
allowsAirPlayForMediaPlayback?: boolean;
allowsLinkPreview?: WithDefault<boolean, true>;
automaticallyAdjustContentInsets?: WithDefault<boolean, true>;
autoManageStatusBarEnabled?: WithDefault<boolean, true>;
bounces?: WithDefault<boolean, true>;
contentInset?: Readonly<{
top?: Double;
left?: Double;
bottom?: Double;
right?: Double;
}>;
contentInsetAdjustmentBehavior?: WithDefault<'never' | 'automatic' | 'scrollableAxes' | 'always', 'never'>;
contentMode?: WithDefault<'recommended' | 'mobile' | 'desktop', 'recommended'>;
dataDetectorTypes?: WithDefault<ReadonlyArray<'address' | 'link' | 'calendarEvent' | 'trackingNumber' | 'flightNumber' | 'lookupSuggestion' | 'phoneNumber' | 'all' | 'none'>, 'phoneNumber'>;
decelerationRate?: Double;
directionalLockEnabled?: WithDefault<boolean, true>;
enableApplePay?: boolean;
hideKeyboardAccessoryView?: boolean;
keyboardDisplayRequiresUserAction?: WithDefault<boolean, true>;
limitsNavigationsToAppBoundDomains?: boolean;
preventUniversalLinks?: ReadonlyArray<string>;
mediaCapturePermissionGrantType?: WithDefault<'prompt' | 'grant' | 'deny' | 'grantIfSameHostElsePrompt' | 'grantIfSameHostElseDeny', 'prompt'>;
pagingEnabled?: boolean;
pullToRefreshEnabled?: boolean;
refreshControlLightMode?: boolean;
scrollEnabled?: WithDefault<boolean, true>;
scrollsToTop?: WithDefault<boolean, true>;
sharedCookiesEnabled?: boolean;
dragInteractionEnabled?: WithDefault<boolean, true>;
textInteractionEnabled?: WithDefault<boolean, true>;
useSharedProcessPool?: WithDefault<boolean, true>;
onContentProcessDidTerminate?: DirectEventHandler<WebViewNativeEvent>;
onCustomMenuSelection?: DirectEventHandler<WebViewCustomMenuSelectionEvent>;
onFileDownload?: DirectEventHandler<WebViewDownloadEvent>;
menuItems?: ReadonlyArray<Readonly<{
label: string;
key: string;
}>>;
suppressMenuItems?: Readonly<string>[];
hasOnFileDownload?: boolean;
fraudulentWebsiteWarningEnabled?: WithDefault<boolean, true>;
allowFileAccessFromFileURLs?: boolean;
allowUniversalAccessFromFileURLs?: boolean;
applicationNameForUserAgent?: string;
basicAuthCredential?: Readonly<{
username: string;
password: string;
}>;
cacheEnabled?: WithDefault<boolean, true>;
incognito?: boolean;
injectedJavaScript?: string;
injectedJavaScriptBeforeContentLoaded?: string;
injectedJavaScriptForMainFrameOnly?: WithDefault<boolean, true>;
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: WithDefault<boolean, true>;
javaScriptCanOpenWindowsAutomatically?: boolean;
javaScriptEnabled?: WithDefault<boolean, true>;
webviewDebuggingEnabled?: boolean;
mediaPlaybackRequiresUserAction?: WithDefault<boolean, true>;
messagingEnabled: boolean;
onLoadingError: DirectEventHandler<WebViewErrorEvent>;
onLoadingSubResourceError: DirectEventHandler<WebViewErrorEvent>;
onLoadingFinish: DirectEventHandler<WebViewNavigationEvent>;
onLoadingProgress: DirectEventHandler<WebViewNativeProgressEvent>;
onLoadingStart: DirectEventHandler<WebViewNavigationEvent>;
onHttpError: DirectEventHandler<WebViewHttpErrorEvent>;
onMessage: DirectEventHandler<WebViewMessageEvent>;
onOpenWindow?: DirectEventHandler<WebViewOpenWindowEvent>;
hasOnOpenWindowEvent?: boolean;
onScroll?: DirectEventHandler<ScrollEvent>;
onShouldStartLoadWithRequest: DirectEventHandler<ShouldStartLoadRequestEvent>;
showsHorizontalScrollIndicator?: WithDefault<boolean, true>;
showsVerticalScrollIndicator?: WithDefault<boolean, true>;
indicatorStyle?: WithDefault<'default' | 'black' | 'white', 'default'>;
newSource: Readonly<{
uri?: string;
method?: string;
body?: string;
headers?: ReadonlyArray<Readonly<{
name: string;
value: string;
}>>;
html?: string;
baseUrl?: string;
}>;
userAgent?: string;
injectedJavaScriptObject?: string;
paymentRequestEnabled?: boolean;
}
export interface NativeCommands {
goBack: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
goForward: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
reload: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
stopLoading: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
injectJavaScript: (viewRef: React.ElementRef<HostComponent<NativeProps>>, javascript: string) => void;
requestFocus: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
postMessage: (viewRef: React.ElementRef<HostComponent<NativeProps>>, data: string) => void;
loadUrl: (viewRef: React.ElementRef<HostComponent<NativeProps>>, url: string) => void;
clearFormData: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
clearCache: (viewRef: React.ElementRef<HostComponent<NativeProps>>, includeDiskFiles: boolean) => void;
clearHistory: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
setTintColor: (viewRef: React.ElementRef<HostComponent<NativeProps>>, red: Double, green: Double, blue: Double, alpha: Double) => void;
}
export declare const Commands: NativeCommands;
declare const _default: HostComponent<NativeProps>;
export default _default;
Loading
Loading