166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
import { callWebviewExpose } from "@follow/shared"
|
|
import { parseSafeUrl, transformVideoUrl } from "@follow/utils"
|
|
import * as Linking from "expo-linking"
|
|
import type { RefObject } from "react"
|
|
import { useCallback, useEffect, useState } from "react"
|
|
import { Platform } from "react-native"
|
|
import type { WebViewNavigation, WebViewProps } from "react-native-webview"
|
|
import { WebView } from "react-native-webview"
|
|
|
|
import { signOut } from "@/src/lib/auth"
|
|
import { useOpenLink } from "@/src/lib/hooks/use-open-link"
|
|
|
|
const presetUri = Platform.select({
|
|
ios: "rn-web/index.html",
|
|
android: "file:///android_asset/raw/index.html",
|
|
default: "https://app.follow.is",
|
|
})
|
|
|
|
const allowHosts = new Set(["app.follow.is"])
|
|
|
|
interface FollowWebViewProps extends WebViewProps {
|
|
customUrl?: string
|
|
}
|
|
|
|
const injectedJavaScript = [
|
|
`window.__RN__ = true`,
|
|
// TODO use more reliable way to detect the page is ready
|
|
`window.setTimeout(() => window.ReactNativeWebView.postMessage("ready"), 2000)`,
|
|
].join(";")
|
|
|
|
const styles = {
|
|
// https://github.com/react-native-webview/react-native-webview/issues/318#issuecomment-503979211
|
|
webview: { backgroundColor: "transparent" },
|
|
webviewContainer: { width: "100%" },
|
|
} as const
|
|
|
|
export const FollowWebView = ({
|
|
webViewRef,
|
|
customUrl,
|
|
...props
|
|
}: { webViewRef: RefObject<WebView> } & FollowWebViewProps) => {
|
|
const { onNavigationStateChange } = useWebViewNavigation({ webViewRef })
|
|
const [ready, setReady] = useState(false)
|
|
useDeepLink({ webViewRef, ready })
|
|
|
|
return (
|
|
<WebView
|
|
ref={webViewRef}
|
|
source={{ uri: customUrl ?? presetUri }}
|
|
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
sharedCookiesEnabled
|
|
originWhitelist={["*"]}
|
|
allowUniversalAccessFromFileURLs
|
|
startInLoadingState
|
|
allowsBackForwardNavigationGestures
|
|
/* Open chrome://inspect/#devices , or Development menu on Safari to debug the WebView. https://github.com/react-native-webview/react-native-webview/blob/master/docs/Debugging.md#debugging-webview-contents */
|
|
webviewDebuggingEnabled={__DEV__}
|
|
style={styles.webview}
|
|
containerStyle={styles.webviewContainer}
|
|
onNavigationStateChange={onNavigationStateChange}
|
|
// onLoad={() => setReady(false)}
|
|
// onLoadProgress={({ nativeEvent }) => setProgress(nativeEvent.progress)}
|
|
onError={(e) => {
|
|
console.error("WebView error:", e)
|
|
}}
|
|
onContentProcessDidTerminate={() => webViewRef.current?.reload()}
|
|
onMessage={(e) => {
|
|
const message = e.nativeEvent.data
|
|
if (message === "sign-out") {
|
|
signOut()
|
|
return
|
|
}
|
|
if (message === "ready") {
|
|
setReady(true)
|
|
return
|
|
}
|
|
}}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const useWebViewNavigation = ({ webViewRef }: { webViewRef: RefObject<WebView> }) => {
|
|
const openLink = useOpenLink()
|
|
|
|
const onNavigationStateChange = useCallback(
|
|
(newNavState: WebViewNavigation) => {
|
|
const { url: urlStr } = newNavState
|
|
const url = parseSafeUrl(urlStr)
|
|
if (!url) return
|
|
if (url.protocol === "file:") return
|
|
if (allowHosts.has(url.host)) return
|
|
|
|
webViewRef.current?.stopLoading()
|
|
|
|
const formattedUrl = transformVideoUrl({ url: urlStr })
|
|
if (formattedUrl) {
|
|
openLink(formattedUrl)
|
|
return
|
|
}
|
|
openLink(urlStr)
|
|
},
|
|
[openLink, webViewRef],
|
|
)
|
|
|
|
return { onNavigationStateChange }
|
|
}
|
|
|
|
// We only need to handle deep link at the first time the app is opened
|
|
let lastInitialUrl: string | null = null
|
|
const useDeepLink = ({
|
|
webViewRef,
|
|
ready = true,
|
|
}: {
|
|
webViewRef: RefObject<WebView>
|
|
ready?: boolean
|
|
}) => {
|
|
const handleDeepLink = useCallback(
|
|
async (url: string) => {
|
|
const { queryParams } = Linking.parse(url)
|
|
if (!queryParams) {
|
|
console.error("Invalid URL! queryParams is not available", url)
|
|
return
|
|
}
|
|
const id = queryParams["id"] ?? undefined
|
|
const isList = queryParams["type"] === "list"
|
|
// const urlParam = queryParams["url"] ?? undefined
|
|
if (!id || typeof id !== "string") {
|
|
console.error("Invalid URL! id is not a string", url)
|
|
return
|
|
}
|
|
const injectJavaScript = webViewRef.current?.injectJavaScript
|
|
if (!injectJavaScript) {
|
|
console.error("injectJavaScript is not available")
|
|
return
|
|
}
|
|
callWebviewExpose(injectJavaScript).follow({ id, isList })
|
|
},
|
|
[webViewRef],
|
|
)
|
|
|
|
// https://reactnative.dev/docs/linking#handling-deep-links
|
|
//
|
|
// The handler added with Linking.addEventListener() is only triggered when app is already open.
|
|
//
|
|
// When the app is not already open and the deep link triggered app launch,
|
|
// the URL can be obtained with Linking.getInitialURL().
|
|
useEffect(() => {
|
|
if (!ready) return
|
|
|
|
const getUrlAsync = async () => {
|
|
const initialUrl = await Linking.getInitialURL()
|
|
if (!initialUrl) return
|
|
if (initialUrl === lastInitialUrl) return
|
|
lastInitialUrl = initialUrl
|
|
handleDeepLink(initialUrl)
|
|
}
|
|
getUrlAsync()
|
|
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
|
|
const deepLinkListener = Linking.addEventListener("url", (event: { url: string }) => {
|
|
handleDeepLink(event.url)
|
|
})
|
|
return () => deepLinkListener.remove()
|
|
}, [handleDeepLink, ready])
|
|
}
|