mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-05 22:25:57 -04:00
* Allow the in-app browser to open users/delete from settings * When account deletion successful, sign the user out
223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
import {
|
|
RouteProp,
|
|
useFocusEffect,
|
|
useNavigation,
|
|
useRoute
|
|
} from "@react-navigation/native";
|
|
import { getUserAgent } from "api/userAgent";
|
|
import { getJWT } from "components/LoginSignUp/AuthenticationService.ts";
|
|
import { ActivityIndicator, Mortal, ViewWrapper } from "components/SharedComponents";
|
|
import { View } from "components/styledComponents";
|
|
import React, { useEffect, useState } from "react";
|
|
import { Linking } from "react-native";
|
|
import { EventRegister } from "react-native-event-listeners";
|
|
import WebView from "react-native-webview";
|
|
import { log } from "sharedHelpers/logger";
|
|
import { composeEmail } from "sharedHelpers/mail.ts";
|
|
|
|
const logger = log.extend( "FullPageWebView" );
|
|
|
|
export const ALLOWED_DOMAINS = [
|
|
"inaturalist.org",
|
|
|
|
// The following are all required for the donorbox form to work
|
|
"donorbox.org",
|
|
"stripe.com",
|
|
"recaptcha.net",
|
|
"paypal.com",
|
|
"paypalobjects.com",
|
|
"plaid.com",
|
|
"stripecdn.com",
|
|
"stripe.network",
|
|
"hcaptcha.com"
|
|
];
|
|
|
|
const ALLOWED_ORIGINS = ["https://*", "mailto:*"];
|
|
const ALLOWED_AUTH_DOMAINS = ["inaturalist.org"];
|
|
|
|
// eslint-disable-next-line no-undef
|
|
if ( __DEV__ ) {
|
|
ALLOWED_DOMAINS.push( "localhost:3000" );
|
|
ALLOWED_ORIGINS.push( "http://localhost:3000*" );
|
|
ALLOWED_AUTH_DOMAINS.push( "localhost:3000" );
|
|
}
|
|
|
|
// Note that you want flex-2 so it grows into the entire webview container
|
|
const LoadingView = ( ) => (
|
|
<View className="flex-2 justify-center items-center w-full h-full">
|
|
<ActivityIndicator />
|
|
</View>
|
|
);
|
|
|
|
type FullPageWebViewParams = {
|
|
initialUrl: string;
|
|
blurEvent?: string;
|
|
title?: string;
|
|
loggedIn?: boolean;
|
|
skipSetSourceInShouldStartLoadWithRequest?: boolean;
|
|
clickablePathnames?: Array<string>;
|
|
shouldLoadUrl?: ( url: string ) => boolean;
|
|
}
|
|
|
|
type ParamList = {
|
|
FullPageWebView: FullPageWebViewParams
|
|
}
|
|
|
|
type WebViewSource = {
|
|
uri: string;
|
|
headers?: {
|
|
Authorization?: string | null
|
|
}
|
|
}
|
|
|
|
type WebViewRequest = {
|
|
url: string;
|
|
navigationType: "click" | "other"
|
|
}
|
|
|
|
export function onShouldStartLoadWithRequest(
|
|
request: WebViewRequest,
|
|
source: WebViewSource,
|
|
params: FullPageWebViewParams,
|
|
setSource?: ( source: WebViewSource ) => void
|
|
) {
|
|
if ( typeof ( params.shouldLoadUrl ) === "function" ) {
|
|
if ( !params.shouldLoadUrl( request.url ) ) return false;
|
|
}
|
|
|
|
// If we're just loading the same page, that's fine
|
|
if ( request.url === source.uri ) {
|
|
return true;
|
|
}
|
|
|
|
// If we're going to a different anchor on the same page, also fine
|
|
const requestUrl = new URL( request.url );
|
|
const requestDomain = requestUrl.host.split( "." ).slice( -2 ).join( "." );
|
|
const sourceUrl = new URL( source.uri );
|
|
const sourceDomain = sourceUrl.host.split( "." ).slice( -2 ).join( "." );
|
|
|
|
// This should prevent accidentally making a webview with auth for a
|
|
// non-iNat domain
|
|
if ( source.headers?.Authorization && ALLOWED_AUTH_DOMAINS.indexOf( sourceDomain ) < 0 ) {
|
|
throw new Error( "Cannot send Authorization to non-iNat domain" );
|
|
}
|
|
|
|
// We do want to handle requests for the same page
|
|
if (
|
|
requestUrl.host === sourceUrl.host
|
|
&& requestUrl.pathname === sourceUrl.pathname
|
|
&& requestUrl.search === sourceUrl.search
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const emailAddress = request.url.match( /^mailto:(.+)/ )?.[1];
|
|
if ( emailAddress ) {
|
|
composeEmail( emailAddress );
|
|
return false;
|
|
}
|
|
|
|
// Otherwise we might want to open a browser
|
|
if (
|
|
// If the user is about to visit a domain we don't want to handle
|
|
!ALLOWED_DOMAINS.includes( requestDomain )
|
|
// or if this is a click, i.e. even if this is an allowed domain, we want
|
|
// to open a browser unless we were explicitly asked not to. This only
|
|
// works in iOS.
|
|
|| (
|
|
// TODO come up with an Android solution
|
|
request.navigationType === "click"
|
|
&& ( params.clickablePathnames || [] ).indexOf( requestUrl.pathname ) < 0
|
|
)
|
|
) {
|
|
// Note we can't use openExternalWebBrowser here b/c this function needs
|
|
// to be synchronous
|
|
Linking.openURL( request.url ).catch( e => {
|
|
const linkingError = e as Error;
|
|
logger.info( "Failed to open ", request.url, ", error: ", linkingError );
|
|
} );
|
|
return false;
|
|
}
|
|
|
|
if ( params.skipSetSourceInShouldStartLoadWithRequest || !setSource ) {
|
|
return true;
|
|
}
|
|
|
|
// Note: this will cause infinite re-renders if the page has iframes
|
|
setSource( { ...source, uri: request.url } );
|
|
return true;
|
|
}
|
|
|
|
const FullPageWebView = ( ) => {
|
|
const navigation = useNavigation( );
|
|
const { params } = useRoute<RouteProp<ParamList, "FullPageWebView">>( );
|
|
const [source, setSource] = useState<WebViewSource>( { uri: params.initialUrl } );
|
|
|
|
// If the previous screen wanted to know when this one blurs, fire off an
|
|
// event when that happens
|
|
useEffect( ( ) => {
|
|
const unsubscribe = navigation.addListener( "blur", ( ) => {
|
|
if ( params.blurEvent ) {
|
|
EventRegister.emit( params.blurEvent );
|
|
}
|
|
} );
|
|
return unsubscribe;
|
|
}, [navigation, params.blurEvent] );
|
|
|
|
useFocusEffect(
|
|
React.useCallback( () => {
|
|
if ( params.title ) {
|
|
navigation.setOptions( {
|
|
headerTitle: params.title
|
|
} );
|
|
}
|
|
|
|
setSource( {
|
|
...source,
|
|
uri: params.initialUrl
|
|
} );
|
|
|
|
// Make the WebView logged in for the current user
|
|
if ( params.loggedIn ) {
|
|
getJWT().then( jwt => {
|
|
setSource( {
|
|
...source,
|
|
headers: {
|
|
Authorization: jwt
|
|
}
|
|
} );
|
|
} );
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [navigation, params.loggedIn, params.title] )
|
|
);
|
|
|
|
return (
|
|
<Mortal>
|
|
<ViewWrapper>
|
|
{( !params.loggedIn || source.headers ) && (
|
|
<WebView
|
|
className="h-full w-full flex-1"
|
|
source={source}
|
|
onShouldStartLoadWithRequest={
|
|
( request: WebViewRequest ) => onShouldStartLoadWithRequest(
|
|
request,
|
|
source,
|
|
params,
|
|
setSource
|
|
)
|
|
}
|
|
originWhitelist={ALLOWED_ORIGINS}
|
|
renderLoading={LoadingView}
|
|
startInLoadingState
|
|
userAgent={getUserAgent()}
|
|
onOpenWindow={() => true}
|
|
/>
|
|
)}
|
|
</ViewWrapper>
|
|
</Mortal>
|
|
);
|
|
};
|
|
|
|
export default FullPageWebView;
|