Skip to content

Commit

Permalink
WALLET-486: Fix timeout sending user back to login screen (#66)
Browse files Browse the repository at this point in the history
* The issue occurred because the useQuery hook was running a fetch request before the WebView was ready after the app moved from inactive to active. As a result, cookies were not available for the fetch request, causing a 401 error. I moved the useQuery API call to a useEffect hook to ensure that all components are rendered before the fetch occurs.

* Show ActivityIndicator while login page is loading in Webview
* Make sure logout trigger in useEffect

---------

Co-authored-by: Pete Edwards <[email protected]>
  • Loading branch information
quanvo298Wizeline and edwardsph authored Sep 19, 2024
1 parent e25ab08 commit 129b215
Show file tree
Hide file tree
Showing 18 changed files with 254 additions and 197 deletions.
2 changes: 1 addition & 1 deletion api/apiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const makeApiRequest = async <T>(
);

if (response.status === 401) {
router.replace("/login?logout=true");
router.replace(`/login?logout=${Date.now()}`);
throw new Error(`Unauthorized: ${response.status}`);
}

Expand Down
69 changes: 3 additions & 66 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
// limitations under the License.
//

import { Redirect, Tabs } from "expo-router";
import { Tabs } from "expo-router";
import type { MutableRefObject } from "react";
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import HouseOutLine from "@/assets/images/house-outline.svg";
import AccessOutLine from "@/assets/images/access-outline.svg";
import AccessSolid from "@/assets/images/access-solid.svg";
Expand All @@ -25,64 +25,19 @@ import { FontAwesome6 } from "@expo/vector-icons";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
import { faBell } from "@fortawesome/free-solid-svg-icons/faBell";
import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
import { useSession } from "@/hooks/session";
import {
Dimensions,
StyleSheet,
TouchableOpacity,
View,
Image,
} from "react-native";
// import { Image } from "expo-image";
import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
import PopupMenu from "@/components/PopupMenu";
import { ThemedText } from "@/components/ThemedText";
import { useMutation, useQuery } from "@tanstack/react-query";
import { getUserInfo, signup } from "@/api/user";
import type { UserInfo } from "@/constants/user";

const { width, height } = Dimensions.get("window");

export default function TabLayout() {
const [isShowSplash, setIsShowSplash] = useState(true);

const { session, signOut } = useSession();
const {
isFetching,
data: userInfo,
refetch,
} = useQuery<UserInfo>({
queryKey: ["userInfo"],
queryFn: getUserInfo,
enabled: !!session,
});

const signupMutation = useMutation({
mutationFn: signup,
onSuccess: async () => {
setIsShowSplash(false);
await refetch();
},
onError: () => {
setIsShowSplash(true);
},
});

const [menuVisible, setMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const [positionType, setPositionType] = useState<
"topMiddle" | "bottomLeft" | "bottomMiddle"
>("bottomMiddle");

useEffect(() => {
if (userInfo && userInfo.signupRequired && session) {
signupMutation.mutateAsync().catch((error) => {
console.error("Error while signup", error);
signOut();
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userInfo, session]);

const tabBarAddButtonRef = useRef(null);
const rightHeaderAddButtonRef = useRef(null);
const toggleMenu = (
Expand Down Expand Up @@ -120,24 +75,6 @@ export default function TabLayout() {
}
};

if (!session) {
return <Redirect href="/login" />;
}

if (isFetching || (userInfo!.signupRequired && isShowSplash)) {
return (
<View style={styles.container}>
{!isFetching && (
<Image
// eslint-disable-next-line global-require
source={require("../../assets/images/signup-splash.png")}
style={styles.image}
/>
)}
</View>
);
}

return (
<>
<Tabs
Expand Down
6 changes: 5 additions & 1 deletion app/(tabs)/accesses/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ import { AccessRequestMode } from "@/types/enums";
import type { AccessGrant } from "@/types/accessGrant";
import GrantsScreen from "./index";

const mockRefetch = jest.fn().mockImplementation(() => {
return Promise.resolve({ data: [], status: "success" });
});

function mockUseQuery(
data: AccessGrant[]
): ReturnType<typeof ReactQuery.useQuery> {
return {
data,
isLoading: false,
isFetching: false,
refetch: jest.fn<ReturnType<typeof ReactQuery.useQuery>["refetch"]>(),
refetch: mockRefetch,
} as unknown as ReturnType<typeof ReactQuery.useQuery>;
}

Expand Down
10 changes: 10 additions & 0 deletions app/(tabs)/accesses/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { StyleSheet, View } from "react-native";
import WebIdAccessGrantList from "@/components/accessGrants/WebIdAccessGrantList";
import useRefreshOnFocus from "@/hooks/useRefreshOnFocus";
import type { AccessGrantGroup } from "@/types/accessGrant";
import { useEffect } from "react";

export default function AccessGrantScreen() {
const {
Expand All @@ -29,7 +30,16 @@ export default function AccessGrantScreen() {
} = useQuery<AccessGrantGroup[]>({
queryKey: ["accessGrants"],
queryFn: getAccessGrants,
enabled: false,
});

useEffect(() => {
refetch().catch((err) => console.log(err));
return () => {
console.debug("Unmount AccessGrantScreen");
};
}, [refetch]);

useRefreshOnFocus(refetch);

return (
Expand Down
6 changes: 5 additions & 1 deletion app/(tabs)/home/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ jest.mock("expo-router", () => {
};
});

const mockRefetch = jest.fn().mockImplementation(() => {
return Promise.resolve({ data: [], status: "success" });
});

function mockUseQuery(
data: WalletFile[]
): ReturnType<typeof ReactQuery.useQuery> {
return {
data,
isLoading: false,
isFetching: false,
refetch: jest.fn<ReturnType<typeof ReactQuery.useQuery>["refetch"]>(),
refetch: mockRefetch,
} as unknown as ReturnType<typeof ReactQuery.useQuery>;
}

Expand Down
11 changes: 10 additions & 1 deletion app/(tabs)/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import CustomButton from "@/components/Button";
import PopupMenu from "@/components/PopupMenu";
import { ThemedText } from "@/components/ThemedText";
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { View, StyleSheet, TouchableOpacity } from "react-native";
import { useQuery } from "@tanstack/react-query";
import { fetchFiles } from "@/api/files";
Expand All @@ -29,7 +29,16 @@ const HomeScreen = () => {
const { data, isLoading, isFetching, refetch } = useQuery<WalletFile[]>({
queryKey: ["files"],
queryFn: fetchFiles,
enabled: false,
});

useEffect(() => {
refetch().catch((err) => console.error(err));
return () => {
console.debug("Unmount HomeScreen");
};
}, [refetch]);

useRefreshOnFocus(refetch);

const [menuVisible, setMenuVisible] = useState(false);
Expand Down
6 changes: 5 additions & 1 deletion app/(tabs)/profile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ import ProfileScreen from "./profile";

const { useSession } = SessionHooks;

const mockRefetch = jest.fn().mockImplementation(() => {
return Promise.resolve({ data: {}, status: "success" });
});

function mockUseQuery(data: UserInfo): ReturnType<typeof ReactQuery.useQuery> {
return {
data,
isLoading: false,
isFetching: false,
refetch: jest.fn<ReturnType<typeof ReactQuery.useQuery>["refetch"]>(),
refetch: mockRefetch,
} as unknown as ReturnType<typeof ReactQuery.useQuery>;
}

Expand Down
13 changes: 11 additions & 2 deletions app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,22 @@ import { BottomSheetModal, BottomSheetView } from "@gorhom/bottom-sheet";
import type { UserInfo } from "@/constants/user";
import AccessSolid from "@/assets/images/access-solid.svg";
import { formatResourceName } from "@/utils/fileUtils";
import { getUserInfo } from "@/api/user";

export default function Profile() {
const { data: userInfo } = useQuery<UserInfo>({
const { data: userInfo, refetch } = useQuery<UserInfo>({
queryKey: ["userInfo"],
queryFn: getUserInfo,
enabled: false,
});

useEffect(() => {
refetch().catch((err) => console.error(err));
return () => {
console.debug("Unmount Profile");
};
}, [refetch]);

const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const navigation = useNavigation();

Expand Down Expand Up @@ -115,7 +124,7 @@ export default function Profile() {
style={styles.logoutContainer}
onPress={() => {
bottomSheetModalRef.current?.close();
router.navigate("/login?logout=true");
router.navigate(`/login?logout=${Date.now()}`);
}}
>
<FontAwesomeIcon icon={faRightFromBracket} size={22} />
Expand Down
6 changes: 5 additions & 1 deletion app/(tabs)/requests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ import type { AccessRequest } from "@/types/accessRequest";
import { AccessRequestMode } from "@/types/enums";
import RequestsScreen from "./index";

const mockRefetch = jest.fn().mockImplementation(() => {
return Promise.resolve({ data: [], status: "success" });
});

function mockUseQuery(
data: AccessRequest[]
): ReturnType<typeof ReactQuery.useQuery> {
return {
data,
isLoading: false,
isFetching: false,
refetch: jest.fn<ReturnType<typeof ReactQuery.useQuery>["refetch"]>(),
refetch: mockRefetch,
} as unknown as ReturnType<typeof ReactQuery.useQuery>;
}

Expand Down
9 changes: 9 additions & 0 deletions app/(tabs)/requests/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ export default function AccessRequestScreen() {
} = useQuery<AccessRequest[]>({
queryKey: ["accessRequests"],
queryFn: getAccessRequests,
enabled: false,
});

useEffect(() => {
refetch().catch((err) => console.error(err));
return () => {
console.debug("Unmount AccessRequestScreen");
};
}, [refetch]);

useRefreshOnFocus(refetch);

const navigation = useNavigation("/(tabs)");
Expand Down
57 changes: 29 additions & 28 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ import { DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import {
QueryClientProvider,
QueryClient,
focusManager,
} from "@tanstack/react-query";

import { SessionProvider } from "@/hooks/session";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import type { AppStateStatus } from "react-native";
Expand All @@ -40,9 +39,13 @@ SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();

export default function RootLayout() {
const [appStateStatus, setAppStateStatus] =
useState<AppStateStatus>("active");

function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== "web") {
focusManager.setFocused(status === "active");
setAppStateStatus(status);
}
}
useEffect(() => {
Expand Down Expand Up @@ -80,34 +83,32 @@ export default function RootLayout() {
<QueryClientProvider client={queryClient}>
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<SessionProvider>
<LoginWebViewProvider>
<ErrorViewProvider>
<Stack
screenOptions={{
<LoginWebViewProvider appStateStatus={appStateStatus}>
<ErrorViewProvider>
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen
name="login"
options={{
animation: "none",
}}
/>
<Stack.Screen
name="scan-qr"
options={{
headerShown: false,
// Set the presentation mode to modal for our modal route.
// presentation: "fullScreenModal",
animation: "slide_from_bottom",
animationDuration: 200,
}}
>
<Stack.Screen
name="login"
options={{
animation: "none",
}}
/>
<Stack.Screen
name="scan-qr"
options={{
headerShown: false,
// Set the presentation mode to modal for our modal route.
// presentation: "fullScreenModal",
animation: "slide_from_bottom",
animationDuration: 200,
}}
/>
</Stack>
</ErrorViewProvider>
</LoginWebViewProvider>
</SessionProvider>
/>
</Stack>
</ErrorViewProvider>
</LoginWebViewProvider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</QueryClientProvider>
Expand Down
Loading

0 comments on commit 129b215

Please sign in to comment.