From db5e4863e91bd8f705efc37a5a31244bb34b071e Mon Sep 17 00:00:00 2001 From: Patryk Klatka Date: Thu, 7 Dec 2023 15:33:08 +0100 Subject: [PATCH] Add property to handle non-Turbo link press (#59) * feat: catch non turbo link press * feat: handle opening external browser on react native side * chore: rename to onOpenExternalUrl * fix: simplify code * feat: rebase and support prop on Android --- packages/turbo/README.md | 6 ++++++ .../turbo/android/src/main/AndroidManifest.xml | 15 ++++++++++++++- .../java/com/reactnativeturbowebview/RNSession.kt | 2 +- .../reactnativeturbowebview/RNVisitableView.kt | 6 ++++++ .../RNVisitableViewManager.kt | 3 ++- .../reactnativeturbowebview/RNWebChromeClient.kt | 7 ++++--- .../reactnativeturbowebview/SessionSubscriber.kt | 1 + packages/turbo/ios/RNSession.swift | 6 ++---- packages/turbo/ios/RNSessionSubscriber.swift | 1 + packages/turbo/ios/RNVisitableView.swift | 5 +++++ packages/turbo/ios/RNVisitableViewManager.m | 1 + packages/turbo/src/RNVisitableView.ts | 14 ++++++++++++++ packages/turbo/src/VisitableView.tsx | 15 ++++++++++++++- packages/turbo/src/types.ts | 4 ++++ 14 files changed, 75 insertions(+), 11 deletions(-) diff --git a/packages/turbo/README.md b/packages/turbo/README.md index 0a378615..a0e43d2c 100644 --- a/packages/turbo/README.md +++ b/packages/turbo/README.md @@ -125,6 +125,12 @@ Callback called when the webview detects turbo visit action. - url - action – e.g "replace" +### `onOpenExternalUrl` + +Callback called when the webview detects non-turbo visit action (e.g. opening external link). + +- url + ### `onLoad` Callback called with screen title and URL when the webview successfully loads. diff --git a/packages/turbo/android/src/main/AndroidManifest.xml b/packages/turbo/android/src/main/AndroidManifest.xml index d99bfe4c..92270813 100644 --- a/packages/turbo/android/src/main/AndroidManifest.xml +++ b/packages/turbo/android/src/main/AndroidManifest.xml @@ -1,6 +1,19 @@ - + + + + + + + + + + + + + + ) : ActivityEventListener, WebChromeClient() { private val fileChooserDelegate = RNFileChooserDelegate(reactContext) @@ -29,8 +30,8 @@ class RNWebChromeClient( ): Boolean { val result = view!!.hitTestResult val data = result.extra - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(data)) - reactContext.startActivityForResult(browserIntent, 0, null) + val uri = Uri.parse(data) + visitableViews.lastOrNull()?.didOpenExternalUrl(uri.toString()) return false } diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt b/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt index a8c79d8a..6dcad61e 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt +++ b/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt @@ -6,4 +6,5 @@ interface SessionSubscriber: SessionCallbackAdapter { fun detachWebView() fun handleMessage(message: WritableMap) fun injectJavaScript(script: String) + fun didOpenExternalUrl(url: String) } diff --git a/packages/turbo/ios/RNSession.swift b/packages/turbo/ios/RNSession.swift index 657c809c..51490dbf 100644 --- a/packages/turbo/ios/RNSession.swift +++ b/packages/turbo/ios/RNSession.swift @@ -82,11 +82,9 @@ extension RNSession: SessionDelegate { visitableViews.last?.didFailRequestForVisitable(visitable: visitable, error: error) } - func webView(_ webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> ()) { - decisionHandler(WKNavigationActionPolicy.cancel) - // Handle non-Turbo links + func session(_ session: Session, openExternalURL url: URL) { + visitableViews.last?.didOpenExternalUrl(url: url) } - } diff --git a/packages/turbo/ios/RNSessionSubscriber.swift b/packages/turbo/ios/RNSessionSubscriber.swift index dab3ef3f..db8307e4 100644 --- a/packages/turbo/ios/RNSessionSubscriber.swift +++ b/packages/turbo/ios/RNSessionSubscriber.swift @@ -14,4 +14,5 @@ protocol RNSessionSubscriber { func handleMessage(message: WKScriptMessage) func didProposeVisit(proposal: VisitProposal) func didFailRequestForVisitable(visitable: Visitable, error: Error) + func didOpenExternalUrl(url: URL) } diff --git a/packages/turbo/ios/RNVisitableView.swift b/packages/turbo/ios/RNVisitableView.swift index fc83a8fd..d343cd2d 100644 --- a/packages/turbo/ios/RNVisitableView.swift +++ b/packages/turbo/ios/RNVisitableView.swift @@ -15,6 +15,7 @@ class RNVisitableView: UIView, RNSessionSubscriber { @objc var url: NSString = "" @objc var onMessage: RCTDirectEventBlock? @objc var onVisitProposal: RCTDirectEventBlock? + @objc var onOpenExternalUrl: RCTDirectEventBlock? @objc var onLoad: RCTDirectEventBlock? @objc var onVisitError: RCTDirectEventBlock? @@ -84,6 +85,10 @@ class RNVisitableView: UIView, RNSessionSubscriber { } onVisitError?(event) } + + public func didOpenExternalUrl(url: URL) { + onOpenExternalUrl?(["url": url.absoluteString]) + } } diff --git a/packages/turbo/ios/RNVisitableViewManager.m b/packages/turbo/ios/RNVisitableViewManager.m index 47d549ba..514fe552 100644 --- a/packages/turbo/ios/RNVisitableViewManager.m +++ b/packages/turbo/ios/RNVisitableViewManager.m @@ -15,6 +15,7 @@ @interface RCT_EXTERN_MODULE(RNVisitableViewManager, NSObject) RCT_EXPORT_VIEW_PROPERTY(sessionHandle, NSString) RCT_EXPORT_VIEW_PROPERTY(applicationNameForUserAgent, NSString) RCT_EXPORT_VIEW_PROPERTY(onVisitProposal, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onOpenExternalUrl, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onVisitError, RCTDirectEventBlock) diff --git a/packages/turbo/src/RNVisitableView.ts b/packages/turbo/src/RNVisitableView.ts index 6ec471fc..7da0c6fc 100644 --- a/packages/turbo/src/RNVisitableView.ts +++ b/packages/turbo/src/RNVisitableView.ts @@ -5,6 +5,7 @@ import { StyleProp, UIManager, ViewStyle, + Linking, } from 'react-native'; import { findNodeHandle } from 'react-native'; import type { @@ -13,6 +14,7 @@ import type { MessageEvent, VisitProposal, VisitProposalError, + OpenExternalUrlEvent, } from './types'; // interface should match RNVisitableView exported properties in native code @@ -58,6 +60,18 @@ export function dispatchCommand( ); } +export async function openExternalURL({ + url, +}: OpenExternalUrlEvent): Promise { + const supported = await Linking.canOpenURL(url); + + if (supported) { + return await Linking.openURL(url); + } else { + console.error(`Don't know how to open this URL: ${url}`); + } +} + const RNVisitableView = requireNativeComponent('RNVisitableView'); diff --git a/packages/turbo/src/VisitableView.tsx b/packages/turbo/src/VisitableView.tsx index b2d18ab9..1bea08e2 100644 --- a/packages/turbo/src/VisitableView.tsx +++ b/packages/turbo/src/VisitableView.tsx @@ -5,13 +5,17 @@ import React, { useMemo, } from 'react'; import { NativeSyntheticEvent, StyleSheet } from 'react-native'; -import RNVisitableView, { dispatchCommand } from './RNVisitableView'; +import RNVisitableView, { + dispatchCommand, + openExternalURL, +} from './RNVisitableView'; import type { OnErrorCallback, LoadEvent, SessionMessageCallback, VisitProposal, VisitProposalError, + OpenExternalUrlEvent, StradaComponent, } from './types'; import { useStradaBridge } from './hooks/useStradaBridge'; @@ -25,6 +29,7 @@ export interface Props { stradaComponents?: StradaComponent[]; onVisitProposal: (proposal: VisitProposal) => void; onLoad?: (params: LoadEvent) => void; + onOpenExternalUrl?: (proposal: OpenExternalUrlEvent) => void; onVisitError?: OnErrorCallback; onMessage?: SessionMessageCallback; } @@ -44,6 +49,7 @@ const VisitableView = React.forwardRef>( onVisitError: viewErrorHandler, onMessage, onVisitProposal, + onOpenExternalUrl: onOpenExternalUrlCallback = openExternalURL, } = props; const visitableViewRef = useRef(); @@ -99,6 +105,12 @@ const VisitableView = React.forwardRef>( [onVisitProposal] ); + const handleOnOpenExternalUrl = useCallback( + (e: NativeSyntheticEvent) => + onOpenExternalUrlCallback(e.nativeEvent), + [onOpenExternalUrlCallback] + ); + return ( <> {stradaComponents?.map((Component, i) => ( @@ -120,6 +132,7 @@ const VisitableView = React.forwardRef>( onVisitProposal={handleVisitProposal} onMessage={handleOnMessage} onVisitError={handleVisitError} + onOpenExternalUrl={handleOnOpenExternalUrl} onLoad={handleOnLoad} style={styles.container} /> diff --git a/packages/turbo/src/types.ts b/packages/turbo/src/types.ts index 2ca3a8f4..46ba2e70 100644 --- a/packages/turbo/src/types.ts +++ b/packages/turbo/src/types.ts @@ -10,6 +10,10 @@ export interface LoadEvent { url: string; } +export interface OpenExternalUrlEvent { + url: string; +} + export type MessageEvent = object; export interface VisitProposalError {