Account deletion improvements (#2044)

* Allow the in-app browser to open users/delete from settings
* When account deletion successful, sign the user out
This commit is contained in:
Ken-ichi
2024-09-05 16:30:31 -07:00
committed by GitHub
parent ac76db7c04
commit ee37f6ce76
6 changed files with 80 additions and 26 deletions

View File

@@ -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 = ( ) => (
<View className="flex-2 justify-center items-center w-full h-full">
@@ -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<string>;
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()}

View File

@@ -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" )}

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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( );
} );
} );
} );
} );