fix: prevent iconic taxon common names from disappearing in the local db (#2631)

Ensure useAuthenticatedQuery only executes the query when it knows if the user
is signed in or not, and appends that state to the query key so signed in and
signed out results are distinct.

The problem here was that useIconicTaxa was mysteriously returning stale,
signed out results while offline.

Closes MOB-383
This commit is contained in:
Ken-ichi
2025-01-22 17:17:26 -08:00
committed by GitHub
parent c09cb2f5de
commit 9429fd0996
6 changed files with 58 additions and 29 deletions

View File

@@ -1531,7 +1531,7 @@ SPEC CHECKSUMS:
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390
Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e
RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCTRequired: 77f73950d15b8c1a2b48ba5b79020c3003d1c9b5
RCTTypeSafety: ede1e2576424d89471ef553b2aed09fbbcc038e3
React: 2ddb437e599df2f1bffa9b248de2de4cfa0227f0

View File

@@ -1,29 +1,56 @@
import { useQuery } from "@tanstack/react-query";
import { getJWT } from "components/LoginSignUp/AuthenticationService.ts";
import { getJWT, isLoggedIn } from "components/LoginSignUp/AuthenticationService.ts";
import { useEffect, useState } from "react";
import { reactQueryRetry } from "sharedHelpers/logging";
const LOGGED_IN_UNKNOWN = null;
// Should work like React Query's useQuery except it calls the queryFunction
// with an object that includes the JWT
const useAuthenticatedQuery = (
queryKey,
queryFunction,
queryOptions = {}
) => useQuery( {
queryKey: [...queryKey, queryOptions.allowAnonymousJWT],
queryFn: async ( ) => {
// Note, getJWT() takes care of fetching a new token if the existing
// one is expired. We *could* store the token in state with useState if
// fetching from RNSInfo becomes a performance issue
const apiToken = await getJWT( queryOptions.allowAnonymousJWT );
const options = {
api_token: apiToken
};
return queryFunction( options );
},
retry: ( failureCount, error ) => reactQueryRetry( failureCount, error, {
queryKey
} ),
...queryOptions
} );
) => {
const [userLoggedIn, setUserLoggedIn] = useState( LOGGED_IN_UNKNOWN );
// Whether we perform this query and whether we need to re-perform it
// depends on whether the user is signed in. The possible vulnerability
// here is that this effect might not run frequently enough to change when
// a user signs in or out. The reason we're not using useCurrentUser is it
// doesn't tell us whether we know the user's auth state yet, it only
// returns null when we don't know OR the user is signed out.
useEffect( ( ) => {
isLoggedIn()
.then( result => setUserLoggedIn( result ) )
.catch( ( ) => setUserLoggedIn( LOGGED_IN_UNKNOWN ) );
}, [queryKey, queryOptions] );
// The results will probably be different depending on whether the user is
// signed in or we wouldn't be using useAuthenticatedQuery in the first
// place, so we need to redo this request if the auth state changed
const authQueryKey = [...queryKey, queryOptions.allowAnonymousJWT, userLoggedIn];
return useQuery( {
queryKey: authQueryKey,
queryFn: async ( ) => {
// Note, getJWT() takes care of fetching a new token if the existing
// one is expired. We *could* store the token in state with useState if
// fetching from RNSInfo becomes a performance issue
const apiToken = await getJWT( queryOptions.allowAnonymousJWT );
const options = {
api_token: apiToken
};
return queryFunction( options );
},
retry: ( failureCount, error ) => reactQueryRetry( failureCount, error, {
queryKey
} ),
...queryOptions,
// Authenticated queries should not run until we know whether or not the
// user is signed in
enabled: userLoggedIn !== LOGGED_IN_UNKNOWN && queryOptions.enabled
} );
};
export default useAuthenticatedQuery;

View File

@@ -1,4 +1,5 @@
import { searchTaxa } from "api/taxa";
import type { ApiOpts, ApiTaxon } from "api/types";
import { RealmContext } from "providers/contexts.ts";
import { useEffect, useState } from "react";
import { UpdateMode } from "realm";
@@ -14,12 +15,10 @@ const useIconicTaxa = ( options: { reload: boolean } = { reload: false } ) => {
const [isUpdatingRealm, setIsUpdatingRealm] = useState<boolean>( );
const enabled = !!( reload );
const queryKey = ["searchTaxa", reload];
const queryKey = ["useIconicTaxa", reload];
const { data: iconicTaxa } = useAuthenticatedQuery(
queryKey,
optsWithAuth => searchTaxa( {
iconic: true
}, optsWithAuth ),
( optsWithAuth: ApiOpts ) => searchTaxa( { iconic: true }, optsWithAuth ),
{ enabled }
);
@@ -27,7 +26,7 @@ const useIconicTaxa = ( options: { reload: boolean } = { reload: false } ) => {
if ( iconicTaxa?.length > 0 && !isUpdatingRealm ) {
setIsUpdatingRealm( true );
safeRealmWrite( realm, ( ) => {
iconicTaxa.forEach( taxon => {
iconicTaxa.forEach( ( taxon: ApiTaxon ) => {
realm.create(
"Taxon",
Taxon.forUpdate( taxon, { isIconic: true } ),

View File

@@ -24,9 +24,12 @@ function saveTaxaToRealm( taxa: Taxon[], realm: Realm ) {
}, "saving remote taxon from useTaxonSearch" );
}
const useTaxonSearch = ( taxonQuery = "" ) => {
const useTaxonSearch = ( taxonQueryArg = "" ) => {
const realm = useRealm( );
const iconicTaxa = useIconicTaxa( { reload: false } );
// Remove leading and trailing whitespace, no need to perform new queries or
// potentially get different results b/c of meaningless whitespace
const taxonQuery = taxonQueryArg.trim();
const { data: remoteTaxa, refetch, isLoading } = useAuthenticatedQuery(
["fetchTaxonSuggestions", taxonQuery],

View File

@@ -158,10 +158,10 @@ describe( "TaxonDetails", ( ) => {
await actor.type( searchBar, "b" );
const searchedTaxon = mockTaxaList[0];
const searchedTaxonName = await screen.findByText( searchedTaxon.name );
await waitFor( ( ) => {
expect( searchedTaxonName ).toBeVisible( );
await waitFor( async ( ) => {
expect( await screen.findByText( searchedTaxon.name ) ).toBeVisible( );
} );
const searchedTaxonName = await screen.findByText( searchedTaxon.name );
await actor.press( searchedTaxonName );
const taxonDetailsScreen = await screen.findByTestId( `TaxonDetails.${searchedTaxon.id}` );

View File

@@ -115,7 +115,7 @@ describe( "when there is no local taxon with taxon id", ( ) => {
it( "should return a taxon like a local taxon record if the request succeeds", async ( ) => {
const { result } = renderHook( ( ) => useTaxon( { id: mockTaxon.id } ) );
expect( result.current.taxon ).toHaveProperty( "default_photo" );
await waitFor( ( ) => expect( result.current.taxon ).toHaveProperty( "default_photo" ) );
} );
} );