diff --git a/src/components/FullPageWebView/FullPageWebView.tsx b/src/components/FullPageWebView/FullPageWebView.tsx index d6c280b57..fa56a6b75 100644 --- a/src/components/FullPageWebView/FullPageWebView.tsx +++ b/src/components/FullPageWebView/FullPageWebView.tsx @@ -5,7 +5,7 @@ import { useRoute } from "@react-navigation/native"; import { getUserAgent } from "api/userAgent"; -import { getAPIToken } from "components/LoginSignUp/AuthenticationService.ts"; +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"; @@ -32,6 +32,16 @@ export const ALLOWED_DOMAINS = [ "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 = ( ) => ( @@ -40,11 +50,13 @@ const LoadingView = ( ) => ( ); type FullPageWebViewParams = { - initialUrl: string, - blurEvent?: string, - title?: string, - loggedIn?: boolean, - skipSetSourceInShouldStartLoadWithRequest?: boolean + initialUrl: string; + blurEvent?: string; + title?: string; + loggedIn?: boolean; + skipSetSourceInShouldStartLoadWithRequest?: boolean; + clickablePathnames?: Array; + shouldLoadUrl?: ( url: string ) => boolean; } type ParamList = { @@ -69,6 +81,10 @@ export function onShouldStartLoadWithRequest( 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; @@ -82,7 +98,7 @@ export function onShouldStartLoadWithRequest( // This should prevent accidentally making a webview with auth for a // non-iNat domain - if ( source.headers?.Authorization && sourceDomain !== "inaturalist.org" ) { + if ( source.headers?.Authorization && ALLOWED_AUTH_DOMAINS.indexOf( sourceDomain ) < 0 ) { throw new Error( "Cannot send Authorization to non-iNat domain" ); } @@ -108,8 +124,11 @@ export function onShouldStartLoadWithRequest( // 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" + || ( + // 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 @@ -120,12 +139,6 @@ export function onShouldStartLoadWithRequest( return false; } - // This should prevent making any request w/ auth to a non-iNat domain from - // a web page on an iNat domain - if ( source.headers?.Authorization && requestDomain !== "inaturalist.org" ) { - throw new Error( "Cannot send Authorization to non-iNat domain" ); - } - if ( params.skipSetSourceInShouldStartLoadWithRequest || !setSource ) { return true; } @@ -166,11 +179,11 @@ const FullPageWebView = ( ) => { // Make the WebView logged in for the current user if ( params.loggedIn ) { - getAPIToken().then( token => { + getJWT().then( jwt => { setSource( { ...source, headers: { - Authorization: token + Authorization: jwt } } ); } ); @@ -194,7 +207,7 @@ const FullPageWebView = ( ) => { setSource ) } - originWhitelist={["https://*", "mailto:*"]} + originWhitelist={ALLOWED_ORIGINS} renderLoading={LoadingView} startInLoadingState userAgent={getUserAgent()} diff --git a/src/components/Settings/Settings.js b/src/components/Settings/Settings.js index 645f14edb..96ec1b203 100644 --- a/src/components/Settings/Settings.js +++ b/src/components/Settings/Settings.js @@ -3,21 +3,23 @@ import { } from "@react-native-community/netinfo"; import { useNavigation } from "@react-navigation/native"; import { useQueryClient } from "@tanstack/react-query"; -// import fetchAvailableLocales from "api/translations"; import { updateUsers } from "api/users"; +import { + signOut +} from "components/LoginSignUp/AuthenticationService.ts"; import { ActivityIndicator, Body2, Button, Heading4, - // PickerSheet, RadioButtonRow, ScrollViewWrapper } from "components/SharedComponents"; import { RealmContext } from "providers/contexts.ts"; import React, { useCallback, useEffect, useState } from "react"; import { - Alert, StatusBar, + Alert, + StatusBar, View } from "react-native"; import Config from "react-native-config"; @@ -30,7 +32,6 @@ import { useUserMe } from "sharedHooks"; import useStore from "stores/useStore"; -// import useStore, { zustandStorage } from "stores/useStore"; const { useRealm } = RealmContext; @@ -261,7 +262,26 @@ const Settings = ( ) => { title: t( "SETTINGS" ), loggedIn: true, initialUrl: SETTINGS_URL, - blurEvent: FINISHED_WEB_SETTINGS + blurEvent: FINISHED_WEB_SETTINGS, + clickablePathnames: ["/users/delete"], + skipSetSourceInShouldStartLoadWithRequest: true, + shouldLoadUrl: url => { + async function signOutGoHome() { + // sign out + await signOut( { realm, clearRealm: true, queryClient } ); + // navigate to My Obs + navigation.navigate( "ObsList" ); + Alert.alert( + t( "Account-Deleted" ), + t( "It-may-take-up-to-an-hour-to-remove-content" ) + ); + } + if ( url === `${Config.OAUTH_API_URL}/?account_deleted=true` ) { + signOutGoHome( ); + return false; + } + return true; + } } ); }} accessibilityLabel={t( "INATURALIST-SETTINGS" )} diff --git a/src/i18n/l10n/en.ftl b/src/i18n/l10n/en.ftl index 00f177ef7..6f66e4f58 100644 --- a/src/i18n/l10n/en.ftl +++ b/src/i18n/l10n/en.ftl @@ -30,6 +30,8 @@ ABOUT-UMBRELLA-PROJECTS = ABOUT UMBRELLA PROJECTS accessible-comname-sciname = { $commonName } ({ $scientificName }) # Label for a taxon when a user prefers to see or hear the scientific name first accessible-sciname-comname = { $scientificName } ({ $commonName }) +# Alert message shown after account deletion +Account-Deleted = Account Deleted ACTIVITY = ACTIVITY # Label for a button that adds a vote of agreement Add-agreement = Add agreement @@ -489,6 +491,7 @@ Internet-Connection-Required = Internet Connection Required Intl-number = { $val } Introduced = Introduced Introduced-to-place = Introduced to { $place } +It-may-take-up-to-an-hour-to-remove-content = It may take up to an hour to completely delete all associated content # Month of January January = January JOIN = JOIN diff --git a/src/i18n/l10n/en.ftl.json b/src/i18n/l10n/en.ftl.json index 0b51b67c9..56a6f4433 100644 --- a/src/i18n/l10n/en.ftl.json +++ b/src/i18n/l10n/en.ftl.json @@ -20,6 +20,10 @@ "comment": "Label for a taxon when a user prefers to see or hear the scientific name first", "val": "{ $scientificName } ({ $commonName })" }, + "Account-Deleted": { + "comment": "Alert message shown after account deletion", + "val": "Account Deleted" + }, "ACTIVITY": "ACTIVITY", "Add-agreement": { "comment": "Label for a button that adds a vote of agreement", @@ -660,6 +664,7 @@ "Intl-number": "{ $val }", "Introduced": "Introduced", "Introduced-to-place": "Introduced to { $place }", + "It-may-take-up-to-an-hour-to-remove-content": "It may take up to an hour to completely delete all associated content", "January": { "comment": "Month of January", "val": "January" diff --git a/src/i18n/strings.ftl b/src/i18n/strings.ftl index 00f177ef7..6f66e4f58 100644 --- a/src/i18n/strings.ftl +++ b/src/i18n/strings.ftl @@ -30,6 +30,8 @@ ABOUT-UMBRELLA-PROJECTS = ABOUT UMBRELLA PROJECTS accessible-comname-sciname = { $commonName } ({ $scientificName }) # Label for a taxon when a user prefers to see or hear the scientific name first accessible-sciname-comname = { $scientificName } ({ $commonName }) +# Alert message shown after account deletion +Account-Deleted = Account Deleted ACTIVITY = ACTIVITY # Label for a button that adds a vote of agreement Add-agreement = Add agreement @@ -489,6 +491,7 @@ Internet-Connection-Required = Internet Connection Required Intl-number = { $val } Introduced = Introduced Introduced-to-place = Introduced to { $place } +It-may-take-up-to-an-hour-to-remove-content = It may take up to an hour to completely delete all associated content # Month of January January = January JOIN = JOIN diff --git a/tests/unit/components/FullPageWebView/FullPageWebView.test.js b/tests/unit/components/FullPageWebView/FullPageWebView.test.js index 665e9649d..6aaef5dc6 100644 --- a/tests/unit/components/FullPageWebView/FullPageWebView.test.js +++ b/tests/unit/components/FullPageWebView/FullPageWebView.test.js @@ -39,10 +39,10 @@ describe( "FullPageWebView", ( ) => { expect( Linking.openURL ).toHaveBeenCalledWith( request.url ); } ); - it( "should not try to open for any domain on the allowlist if requested", ( ) => { + it( "should not try to open for any domain on the allowlist", ( ) => { const url = "https://www.inaturalist.org"; - const request = { url: "https://www.inaturalist.org/users/edit" }; - expect( ALLOWED_DOMAINS ).toContain( "inaturalist.org" ); + const request = { url: "https://www.donorbox.org" }; + expect( ALLOWED_DOMAINS ).toContain( "donorbox.org" ); const source = { uri: url }; const routeParams = { initialUrl: url, handleLinksForAllowedDomains: true }; expect( onShouldStartLoadWithRequest( request, source, routeParams ) ).toBeTruthy(); @@ -57,6 +57,16 @@ describe( "FullPageWebView", ( ) => { expect( onShouldStartLoadWithRequest( request, source, routeParams ) ).toBeFalsy(); expect( Linking.openURL ).toHaveBeenCalledWith( request.url ); } ); + + it( "should not try to open for pathnames specified in params on click", ( ) => { + const url = "https://www.inaturalist.org"; + const request = { url: "https://www.inaturalist.org/users/delete", navigationType: "click" }; + expect( ALLOWED_DOMAINS ).toContain( "inaturalist.org" ); + const source = { uri: url }; + const routeParams = { initialUrl: url, clickablePathnames: ["/users/delete"] }; + expect( onShouldStartLoadWithRequest( request, source, routeParams ) ).toBeTruthy(); + expect( Linking.openURL ).not.toHaveBeenCalled( ); + } ); } ); } ); } );