Skip to content

Commit

Permalink
Add property to handle non-Turbo link press (#59)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pklatka committed Dec 7, 2023
1 parent 1562fe9 commit db5e486
Show file tree
Hide file tree
Showing 14 changed files with 75 additions and 11 deletions.
6 changes: 6 additions & 0 deletions packages/turbo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 14 additions & 1 deletion packages/turbo/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativeturbowebview">

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="mailto"/>
</intent>
</queries>
<application>
<provider
android:name=".RNWebViewFileProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class RNSession(
webView.settings.setJavaScriptEnabled(true)
webView.addJavascriptInterface(JavaScriptInterface(), "AndroidInterface")
setUserAgentString(webView, applicationNameForUserAgent)
webView.webChromeClient = RNWebChromeClient(reactContext)
webView.webChromeClient = RNWebChromeClient(reactContext, visitableViews)
session.isRunningInAndroidNavigation = false
session
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib
sendEvent(RNVisitableViewEvent.MESSAGE, message)
}

override fun didOpenExternalUrl(url: String) {
sendEvent(RNVisitableViewEvent.OPEN_EXTERNAL_URL, Arguments.createMap().apply {
putString("url", url)
})
}

override fun onReceivedError(errorCode: Int) {
sendEvent(RNVisitableViewEvent.VISIT_ERROR, Arguments.createMap().apply {
putInt("statusCode", errorCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ enum class RNVisitableViewEvent(val jsCallbackName: String) {
VISIT_PROPOSED("onVisitProposal"),
VISIT_ERROR("onVisitError"),
PAGE_LOADED("onLoad"),
MESSAGE("onMessage")
MESSAGE("onMessage"),
OPEN_EXTERNAL_URL("onOpenExternalUrl")
}

enum class RNVisitableViewCommand(val jsCallbackName: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import com.facebook.react.bridge.ReactApplicationContext


class RNWebChromeClient(
private val reactContext: ReactApplicationContext
private val reactContext: ReactApplicationContext,
private val visitableViews: LinkedHashSet<SessionSubscriber>
) : ActivityEventListener, WebChromeClient() {

private val fileChooserDelegate = RNFileChooserDelegate(reactContext)
Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ interface SessionSubscriber: SessionCallbackAdapter {
fun detachWebView()
fun handleMessage(message: WritableMap)
fun injectJavaScript(script: String)
fun didOpenExternalUrl(url: String)
}
6 changes: 2 additions & 4 deletions packages/turbo/ios/RNSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}


Expand Down
1 change: 1 addition & 0 deletions packages/turbo/ios/RNSessionSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ protocol RNSessionSubscriber {
func handleMessage(message: WKScriptMessage)
func didProposeVisit(proposal: VisitProposal)
func didFailRequestForVisitable(visitable: Visitable, error: Error)
func didOpenExternalUrl(url: URL)
}
5 changes: 5 additions & 0 deletions packages/turbo/ios/RNVisitableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -84,6 +85,10 @@ class RNVisitableView: UIView, RNSessionSubscriber {
}
onVisitError?(event)
}

public func didOpenExternalUrl(url: URL) {
onOpenExternalUrl?(["url": url.absoluteString])
}

}

Expand Down
1 change: 1 addition & 0 deletions packages/turbo/ios/RNVisitableViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions packages/turbo/src/RNVisitableView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
StyleProp,
UIManager,
ViewStyle,
Linking,
} from 'react-native';
import { findNodeHandle } from 'react-native';
import type {
Expand All @@ -13,6 +14,7 @@ import type {
MessageEvent,
VisitProposal,
VisitProposalError,
OpenExternalUrlEvent,
} from './types';

// interface should match RNVisitableView exported properties in native code
Expand Down Expand Up @@ -58,6 +60,18 @@ export function dispatchCommand(
);
}

export async function openExternalURL({
url,
}: OpenExternalUrlEvent): Promise<any> {
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<RNVisitableViewProps>('RNVisitableView');

Expand Down
15 changes: 14 additions & 1 deletion packages/turbo/src/VisitableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand All @@ -44,6 +49,7 @@ const VisitableView = React.forwardRef<RefObject, React.PropsWithRef<Props>>(
onVisitError: viewErrorHandler,
onMessage,
onVisitProposal,
onOpenExternalUrl: onOpenExternalUrlCallback = openExternalURL,
} = props;
const visitableViewRef = useRef<typeof RNVisitableView>();

Expand Down Expand Up @@ -99,6 +105,12 @@ const VisitableView = React.forwardRef<RefObject, React.PropsWithRef<Props>>(
[onVisitProposal]
);

const handleOnOpenExternalUrl = useCallback(
(e: NativeSyntheticEvent<OpenExternalUrlEvent>) =>
onOpenExternalUrlCallback(e.nativeEvent),
[onOpenExternalUrlCallback]
);

return (
<>
{stradaComponents?.map((Component, i) => (
Expand All @@ -120,6 +132,7 @@ const VisitableView = React.forwardRef<RefObject, React.PropsWithRef<Props>>(
onVisitProposal={handleVisitProposal}
onMessage={handleOnMessage}
onVisitError={handleVisitError}
onOpenExternalUrl={handleOnOpenExternalUrl}
onLoad={handleOnLoad}
style={styles.container}
/>
Expand Down
4 changes: 4 additions & 0 deletions packages/turbo/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export interface LoadEvent {
url: string;
}

export interface OpenExternalUrlEvent {
url: string;
}

export type MessageEvent = object;

export interface VisitProposalError {
Expand Down

0 comments on commit db5e486

Please sign in to comment.