Skip to content

Commit

Permalink
Fix/wallet 424 wallet login (#39)
Browse files Browse the repository at this point in the history
* Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed

* Add requestLogout to deps array in useEffect

---------

Co-authored-by: Zwifi <[email protected]>
  • Loading branch information
quanvo298Wizeline and NSeydoux authored Sep 6, 2024
1 parent 93d6d11 commit f93815a
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 131 deletions.
2 changes: 1 addition & 1 deletion api/apiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const makeApiRequest = async <T>(
);

if (response.status === 401) {
router.navigate("/profile?forceLogout=true");
router.replace("/login?logout=true");
throw new Error(`Unauthorized: ${response.status}`);
}
if (!response.ok) {
Expand Down
15 changes: 5 additions & 10 deletions app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { useSession } from "@/hooks/session";
import { View, StyleSheet, Image, TouchableOpacity } from "react-native";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { router, useNavigation } from "expo-router";
import React, { useEffect, useRef } from "react";
import { ThemedText } from "@/components/ThemedText";
import QRCode from "react-native-qrcode-svg";
Expand All @@ -31,7 +30,6 @@ import AccessSolid from "@/assets/images/access-solid.svg";
import { formatResourceName } from "@/utils/fileUtils";

export default function Profile() {
const { signOut } = useSession();
const { data: userInfo } = useQuery<UserInfo>({
queryKey: ["userInfo"],
enabled: false,
Expand All @@ -40,12 +38,6 @@ export default function Profile() {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const navigation = useNavigation();

const { forceLogout } = useLocalSearchParams();
useEffect(() => {
if (forceLogout) {
signOut();
}
}, [forceLogout, signOut]);
useEffect(() => {
navigation.setOptions({
headerTitle: "",
Expand Down Expand Up @@ -121,7 +113,10 @@ export default function Profile() {
<BottomSheetView style={styles.bottomSheetContent}>
<TouchableOpacity
style={styles.logoutContainer}
onPress={() => signOut()}
onPress={() => {
bottomSheetModalRef.current?.close();
router.navigate("/login?logout=true");
}}
>
<FontAwesomeIcon icon={faRightFromBracket} size={22} />
<ThemedText style={{ paddingLeft: 16 }}>Logout</ThemedText>
Expand Down
37 changes: 23 additions & 14 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { GestureHandlerRootView } from "react-native-gesture-handler";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import type { AppStateStatus } from "react-native";
import { AppState, Platform } from "react-native";
import { LoginWebViewProvider } from "@/hooks/useInruptLogin";

// Prevent the splash screen from auto-hiding before asset loading is complete.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Expand Down Expand Up @@ -79,22 +80,30 @@ export default function RootLayout() {
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<SessionProvider>
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen
name="scan-qr"
options={{
<LoginWebViewProvider>
<Stack
screenOptions={{
headerShown: false,
// Set the presentation mode to modal for our modal route.
// presentation: "fullScreenModal",
animation: "slide_from_bottom",
animationDuration: 200,
}}
/>
</Stack>
>
<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>
</LoginWebViewProvider>
</SessionProvider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
Expand Down
43 changes: 14 additions & 29 deletions app/login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ jest.mock("expo-constants", () => ({
appOwnership: "expo",
}));

// Jest chokes on importing native SVG.
jest.mock("@/assets/images/future_co.svg", () => {
return jest.fn();
});

// Mock the react-native RCTNetworking module
jest.mock("react-native/Libraries/Network/RCTNetworking", () => ({
default: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
clearCookies: jest.fn((callback) => callback(true)),
},
}));

describe("Snapshot testing the login screen", () => {
it("renders the login screen when unauthenticated", async () => {
// Mocks start...
Expand Down Expand Up @@ -83,33 +97,4 @@ describe("Snapshot testing the login screen", () => {
) as jest.Mocked<typeof ExpoRouter>;
expect(mockedRedirect).not.toHaveBeenCalledWith();
});

it("redirects to the home screen when authenticated", async () => {
// Mocks start...
const { useSession: mockedUseSession } = jest.requireMock(
"@/hooks/session"
) as jest.Mocked<typeof SessionHooks>;
const [mockedSignIn, mockedSignOut, mockedSession] = [
jest.fn(),
jest.fn(),
"some-session-id",
];
mockedUseSession.mockReturnValueOnce({
signIn: mockedSignIn,
signOut: mockedSignOut,
session: mockedSession,
});
const { Redirect: mockedRedirect } = jest.requireMock(
"expo-router"
) as jest.Mocked<typeof ExpoRouter>;
// This checks that the Redirect component is returned.
mockedRedirect.mockReturnValue("Dummy return" as unknown as null);
// ... mocks end.

render(<LoginScreen />);
// The login button should *not* be mounted
await expect(() => screen.findByTestId("login-button")).rejects.toThrow();
// Check that what we get back here is the result of the redirect.
expect(screen.toJSON()).toBe("Dummy return");
});
});
98 changes: 26 additions & 72 deletions app/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,39 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Modal,
SafeAreaView,
} from "react-native";
import { Redirect } from "expo-router";
import React, { useEffect } from "react";
import { View, StyleSheet } from "react-native";
import Constants from "expo-constants";
import { SvgXml } from "react-native-svg";
import type { WebViewNavigation } from "react-native-webview";
import { WebView } from "react-native-webview";
import { useSession } from "@/hooks/session";
import CustomButton from "@/components/Button";
import { ThemedText } from "@/components/ThemedText";
import { clearWebViewIOSCache } from "react-native-webview-ios-cache-clear";
import LogoSvg from "../assets/images/future_co.svg";
import Logo from "@/assets/images/future_co.svg";
import { useLoginWebView } from "@/hooks/useInruptLogin";
import { useLocalSearchParams } from "expo-router";
import { useSession } from "@/hooks/session";

const isRunningInExpoGo = Constants.appOwnership === "expo";

// Despite what TS says, this works, so we can keep this as is
// for now.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const Logo = () => <SvgXml width="200" height="200" xml={LogoSvg} />;

const LoginScreen = () => {
const [showWebView, setShowWebView] = useState(false);
const { signIn, session } = useSession();
const { showLoginPage, requestLogout } = useLoginWebView();
const { logout } = useLocalSearchParams();
const { session } = useSession();

useEffect(() => {
if (session && logout) {
requestLogout();
}
}, [logout, session, requestLogout]);

useEffect(() => {
clearWebViewIOSCache();
// eslint-disable-next-line @typescript-eslint/no-var-requires,global-require
const RCTNetworking = require("react-native/Libraries/Network/RCTNetworking");
RCTNetworking.default.clearCookies((result: never) => {
console.log("clearCookies", result);
});

if (!isRunningInExpoGo) {
clearWebViewIOSCache();
import("@react-native-cookies/cookies")
.then((CookieManager) =>
CookieManager.default
Expand All @@ -58,27 +56,16 @@ const LoginScreen = () => {
}
}, []);

if (session) {
return <Redirect href="/home" />;
}
const handleLoginPress = () => {
setShowWebView(true);
};

const handleNavigationStateChange = async (navState: WebViewNavigation) => {
const { url } = navState;

const isLoginSuccess = url.includes("/login/success");

if (isLoginSuccess) {
signIn();
if (!logout) {
showLoginPage();
}
};

return (
<View style={styles.container}>
<View style={styles.logoContainer}>
<Logo />
<Logo width="200" height="200" />
<ThemedText
style={[styles.header, { paddingTop: 16 }]}
fontWeight="bold"
Expand All @@ -95,26 +82,7 @@ const LoginScreen = () => {
variant="primary"
customStyle={{ paddingHorizontal: 74 }}
testID="login-button"
></CustomButton>

<Modal visible={showWebView} animationType="slide">
<SafeAreaView style={{ flex: 1, backgroundColor: "transparent" }}>
<WebView
source={{ uri: process.env.EXPO_PUBLIC_LOGIN_URL || "" }}
onNavigationStateChange={handleNavigationStateChange}
incognito={false}
domStorageEnabled={false}
sharedCookiesEnabled={true}
thirdPartyCookiesEnabled={true}
/>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setShowWebView(false)}
>
<Text style={styles.closeButtonText}>Close</Text>
</TouchableOpacity>
</SafeAreaView>
</Modal>
/>
</View>
);
};
Expand All @@ -126,20 +94,6 @@ const styles = StyleSheet.create({
alignItems: "center",
backgroundColor: "#fff",
},

closeButton: {
position: "absolute",
top: 40,
right: 20,
backgroundColor: "#fff",
padding: 10,
borderRadius: 5,
elevation: 2,
},
closeButtonText: {
fontSize: 16,
color: "#007BFF",
},
logoContainer: {
marginBottom: 40,
justifyContent: "center",
Expand Down
Loading

0 comments on commit f93815a

Please sign in to comment.