import { useHeaderHeight } from '@react-navigation/elements' import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ActionSheetIOS, ActivityIndicator, KeyboardAvoidingView, Platform, StyleSheet, View, } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes' import AppAnalytics from 'src/analytics/AppAnalytics' import { DappExplorerEvents, WebViewEvents } from 'src/analytics/Events' import { openDeepLink } from 'src/app/actions' import KeyboardSpacer from 'src/components/KeyboardSpacer' import Touchable from 'src/components/Touchable' import WebView, { WebViewRef } from 'src/components/WebView' import { DEEP_LINK_URL_SCHEME } from 'src/config' import { activeDappSelector } from 'src/dapps/selectors' import { dappSessionEnded } from 'src/dapps/slice' import BackChevron from 'src/icons/BackChevron' import ForwardChevron from 'src/icons/ForwardChevron' import Refresh from 'src/icons/Refresh' import TripleDotVertical from 'src/icons/TripleDotVertical' import { HeaderTitleWithSubtitle } from 'src/navigator/Headers' import { navigateBack } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { TopBarTextButton } from 'src/navigator/TopBarButton' import { StackParamList } from 'src/navigator/types' import { useDispatch, useSelector } from 'src/redux/hooks' import { getDynamicConfigParams } from 'src/statsig' import { DynamicConfigs } from 'src/statsig/constants' import { StatsigDynamicConfigs } from 'src/statsig/types' import colors from 'src/styles/colors' import { iconHitslop } from 'src/styles/variables' import Logger from 'src/utils/Logger' import { navigateToURI } from 'src/utils/linking' import useBackHandler from 'src/utils/useBackHandler' import { isWalletConnectDeepLink } from 'src/walletConnect/walletConnect' import { WebViewAndroidBottomSheet } from 'src/webview/WebViewAndroidBottomSheet' import { parse } from 'url' type RouteProps = NativeStackScreenProps type Props = RouteProps function WebViewScreen({ route, navigation }: Props) { const { uri } = route.params const dispatch = useDispatch() const { t } = useTranslation() const headerHeight = useHeaderHeight() const activeDapp = useSelector(activeDappSelector) const disabledMediaPlaybackRequiresUserActionOrigins = getDynamicConfigParams( DynamicConfigs[StatsigDynamicConfigs.DAPP_WEBVIEW_CONFIG] ).disabledMediaPlaybackRequiresUserActionOrigins const webViewRef = useRef(null) const [canGoBack, setCanGoBack] = useState(false) const [canGoForward, setCanGoForward] = useState(false) const [showingBottomSheet, setShowingBottomSheet] = useState(false) const [currentUrl, setCurrentUrl] = useState('') const handleSetNavigationTitle = useCallback( (url: string, title: string, loading: boolean) => { let hostname = ' ' let displayedTitle = ' ' try { setCurrentUrl(url) hostname = parse(url).hostname ?? ' ' // when first loading, the title of the webpage is unknown and the title // defaults to the url - display a loading placeholder in this case const parsedTitleUrl = parse(title) displayedTitle = loading || !title || (parsedTitleUrl.protocol && parsedTitleUrl.hostname) ? t('loading') : title } catch (error) { Logger.error( 'WebViewScreen', `could not parse url for screen header, with url ${url} and title ${title}`, error ) } navigation.setOptions({ headerTitle: () => ( ), }) }, [navigation] ) useLayoutEffect(() => { navigation.setOptions({ headerLeft: () => ( ), }) }, [navigation]) useEffect(() => { return () => { // end the active dapp session when the screen is unmounted (e.g. screen // dismissed via header close button or via gesture/device back button) // note that the dependency array is empty so the values used here are // captured on screen mount, which works for now but may need to be // refreshed in the future if (activeDapp) { dispatch(dappSessionEnded()) AppAnalytics.track(DappExplorerEvents.dapp_close, { categories: activeDapp.categories, dappId: activeDapp.id, dappName: activeDapp.name, section: activeDapp.openedFrom, }) } } }, []) useBackHandler(() => { // android hardware back button functions as either browser back button or // app back button if (canGoBack) { handleGoBack() } else { navigateBack() } return true }, [canGoBack, webViewRef.current, navigation]) const handleLoadRequest = (event: ShouldStartLoadRequest): boolean => { if (event.url.startsWith(`${DEEP_LINK_URL_SCHEME}://`) || isWalletConnectDeepLink(event.url)) { dispatch(openDeepLink(event.url)) return false } return true } const handleRefresh = () => { webViewRef.current?.reload() } const handleGoForward = () => { webViewRef.current?.goForward() } const handleGoBack = () => { webViewRef.current?.goBack() } const openActionSheet = () => { Platform.OS === 'ios' ? openActionSheetiOS() : toggleBottomSheet() AppAnalytics.track(WebViewEvents.webview_more_options, { currentUrl }) } // iOS Action sheet const openActionSheetiOS = () => { ActionSheetIOS.showActionSheetWithOptions( { options: [t('webView.openExternal'), t('dismiss')], cancelButtonIndex: 1, }, (buttonIndex: number) => { switch (buttonIndex) { case 0: navigateToURI(currentUrl) AppAnalytics.track(WebViewEvents.webview_open_in_browser, { currentUrl }) break default: case 1: break } } ) } // Toggle for Android Bottom Sheet - passed to WebViewAndroidBottomSheet const toggleBottomSheet = () => setShowingBottomSheet((value: boolean) => !value) const { origin } = new URL(uri) const mediaPlaybackRequiresUserAction = !disabledMediaPlaybackRequiresUserActionOrigins.includes(origin) return ( ( )} onNavigationStateChange={(navState) => { setCanGoBack(navState.canGoBack) setCanGoForward(navState.canGoForward) handleSetNavigationTitle(navState.url, navState.title, navState.loading) }} mediaPlaybackRequiresUserAction={mediaPlaybackRequiresUserAction} testID={activeDapp ? `WebViewScreen/${activeDapp.name}` : 'RNWebView'} /> {Platform.OS === 'android' && ( setShowingBottomSheet(false)} toggleBottomSheet={toggleBottomSheet} /> )} ) } const styles = StyleSheet.create({ container: { flex: 1, }, keyboardView: { flexGrow: 1, }, loading: { position: 'absolute', left: 0, right: 0, bottom: 0, top: 0, }, navBar: { height: 52, width: '100%', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 32, borderTopWidth: 1, borderColor: colors.borderPrimary, }, }) export default WebViewScreen