From 31700febd48adfa5b9e4cce62e9c60c954bf472f Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Mon, 2 Sep 2024 21:20:10 +0700 Subject: [PATCH 1/7] Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed --- api/apiRequest.ts | 2 +- app.config.ts | 6 ++ app/(tabs)/profile.tsx | 12 +-- app/_layout.tsx | 31 ++++--- app/login.test.tsx | 14 +++ app/login.tsx | 98 ++++++-------------- components/login/LoginWebViewModal.tsx | 123 +++++++++++++++++++++++++ hooks/session.tsx | 5 - hooks/useInruptLogin.tsx | 80 ++++++++++++++++ metro.config.js | 2 + 10 files changed, 272 insertions(+), 101 deletions(-) create mode 100644 components/login/LoginWebViewModal.tsx create mode 100644 hooks/useInruptLogin.tsx diff --git a/api/apiRequest.ts b/api/apiRequest.ts index 1d5501b..d4ecee0 100644 --- a/api/apiRequest.ts +++ b/api/apiRequest.ts @@ -48,7 +48,7 @@ export const makeApiRequest = async ( ); if (response.status === 401) { - router.navigate("/profile?forceLogout=true"); + router.navigate("/login?logout=true"); throw new Error(`Unauthorized: ${response.status}`); } if (!response.ok) { diff --git a/app.config.ts b/app.config.ts index a77eeed..d6ddc60 100644 --- a/app.config.ts +++ b/app.config.ts @@ -63,6 +63,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, }, ], + [ + "expo-dev-launcher", + { + launchMode: "most-recent", + }, + ], "./plugins/withSigningConfig", ], experiments: { diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 3e3dd4d..99578c9 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -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"; @@ -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({ queryKey: ["userInfo"], enabled: false, @@ -40,12 +38,6 @@ export default function Profile() { const bottomSheetModalRef = useRef(null); const navigation = useNavigation(); - const { forceLogout } = useLocalSearchParams(); - useEffect(() => { - if (forceLogout) { - signOut(); - } - }, [forceLogout, signOut]); useEffect(() => { navigation.setOptions({ headerTitle: "", @@ -121,7 +113,7 @@ export default function Profile() { signOut()} + onPress={() => router.navigate("/login?logout=true")} > Logout diff --git a/app/_layout.tsx b/app/_layout.tsx index fcf9142..e13ced4 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -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 @@ -79,22 +80,24 @@ export default function RootLayout() { - - + - + > + + + diff --git a/app/login.test.tsx b/app/login.test.tsx index c06a015..c718620 100644 --- a/app/login.test.tsx +++ b/app/login.test.tsx @@ -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... diff --git a/app/login.tsx b/app/login.tsx index 6d73ffc..f2f4ba6 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -13,41 +13,33 @@ // 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 { Redirect, useLocalSearchParams } from "expo-router"; 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 { 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 = () => ; - const LoginScreen = () => { - const [showWebView, setShowWebView] = useState(false); - const { signIn, session } = useSession(); + const { showLoginPage, requestLogout } = useLoginWebView(); + const { logout } = useLocalSearchParams(); + const { session } = useSession(); + + useEffect(() => { + if (logout) { + requestLogout(); + } + }, [logout, requestLogout]); useEffect(() => { - clearWebViewIOSCache(); if (!isRunningInExpoGo) { + clearWebViewIOSCache(); import("@react-native-cookies/cookies") .then((CookieManager) => CookieManager.default @@ -55,30 +47,27 @@ const LoginScreen = () => { .then((success) => console.log("Cleared all cookies?", success)) ) .catch((error) => console.log("Failed to clear cookies", error)); + } else { + // 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 (session) { - return ; - } const handleLoginPress = () => { - setShowWebView(true); + showLoginPage(); }; - const handleNavigationStateChange = async (navState: WebViewNavigation) => { - const { url } = navState; - - const isLoginSuccess = url.includes("/login/success"); - - if (isLoginSuccess) { - signIn(); - } - }; + if (session) { + return ; + } return ( - + { customStyle={{ paddingHorizontal: 74 }} testID="login-button" > - - - - - setShowWebView(false)} - > - Close - - - ); }; @@ -126,20 +96,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", diff --git a/components/login/LoginWebViewModal.tsx b/components/login/LoginWebViewModal.tsx new file mode 100644 index 0000000..20ecc9d --- /dev/null +++ b/components/login/LoginWebViewModal.tsx @@ -0,0 +1,123 @@ +// +// Copyright Inrupt Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import React, { useEffect, useRef, useState } from "react"; +import type { WebViewNavigation } from "react-native-webview"; +import { WebView } from "react-native-webview"; +import { SafeAreaView, StyleSheet, Text, TouchableOpacity } from "react-native"; + +const HTML_BLANK = + ''; + +const LoginWebViewModal = ({ + visible = false, + onLoginSuccess = () => null, + onLogoutSuccess = () => null, + onClose = () => null, + requestMode = "blank", +}: { + visible: boolean; + onClose: () => void; + onLoginSuccess: () => void; + onLogoutSuccess: () => void; + requestMode: "login" | "logout" | "blank"; +}) => { + const BASE_URL = `${process.env.EXPO_PUBLIC_WALLET_API}/`; + const LOGIN_URL = process.env.EXPO_PUBLIC_LOGIN_URL || ""; + const LOGOUT_URL = `${BASE_URL}logout`; + + const webViewRef = useRef(null); + const [webUrl, setWebUrl] = useState(LOGIN_URL); + + useEffect(() => { + if (requestMode === "login") { + setWebUrl(LOGIN_URL); + } + if (requestMode === "logout") { + setWebUrl(LOGOUT_URL); + } + if (requestMode === "blank") { + setWebUrl(""); + } + if (visible) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + webViewRef.current!.reload(); + } + }, [visible, requestMode]); + + const handleNavigationStateChange = async (navState: WebViewNavigation) => { + const { url } = navState; + const isLoginSuccess = url.includes("/login/success"); + const isLogout = url === `${process.env.EXPO_PUBLIC_WALLET_API}/`; + + if (isLoginSuccess) { + onLoginSuccess(); + } + + if (isLogout) { + onLogoutSuccess(); + } + }; + + return ( + + + onClose()}> + Close + + + ); +}; + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + flex: 1, + backgroundColor: "transparent", + }, + + closeButton: { + position: "absolute", + top: 40, + right: 20, + backgroundColor: "#fff", + padding: 10, + borderRadius: 5, + elevation: 2, + }, + closeButtonText: { + fontSize: 16, + color: "#007BFF", + }, +}); + +export default LoginWebViewModal; diff --git a/hooks/session.tsx b/hooks/session.tsx index 93c2b84..8c3a2f6 100644 --- a/hooks/session.tsx +++ b/hooks/session.tsx @@ -15,7 +15,6 @@ // import { useStorageState } from "@/hooks/useStorageState"; import React from "react"; -import { logout } from "@/api/user"; import { SESSION_KEY } from "@/api/apiRequest"; const AuthContext = React.createContext<{ @@ -51,10 +50,6 @@ export function SessionProvider(props: React.PropsWithChildren) { }, signOut: async () => { setSession(null); - logout().catch((e) => - // eslint-disable-next-line no-console - console.log("Error during logout:", e) - ); }, session, }} diff --git a/hooks/useInruptLogin.tsx b/hooks/useInruptLogin.tsx new file mode 100644 index 0000000..70ea9d2 --- /dev/null +++ b/hooks/useInruptLogin.tsx @@ -0,0 +1,80 @@ +// +// Copyright Inrupt Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import React, { createContext, useState } from "react"; +import LoginWebViewModal from "@/components/login/LoginWebViewModal"; +import { useSession } from "@/hooks/session"; +import { router } from "expo-router"; + +const LoginWebViewContext = createContext<{ + showLoginPage: () => void; + requestLogout: () => void; +}>({ + showLoginPage: () => null, + requestLogout: () => null, +}); + +export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { + const [visible, setVisible] = useState(false); + const [modalRequestMode, setModalRequestMode] = useState< + "login" | "blank" | "logout" + >("blank"); + const { signIn, signOut } = useSession(); + + const handleLoginSuccess = () => { + setVisible(false); + signIn(); + router.replace("/home"); + setModalRequestMode("blank"); + }; + + const handleLogoutSuccess = () => { + setVisible(false); + signOut(); + router.replace("/login"); + setModalRequestMode("blank"); + }; + + const handleCloseModal = () => { + setVisible(false); + setModalRequestMode("blank"); + }; + + return ( + { + setVisible(true); + setModalRequestMode("login"); + }, + requestLogout: () => { + setVisible(false); + setModalRequestMode("logout"); + }, + }} + > + {children} + + + ); +}; + +export const useLoginWebView = () => React.useContext(LoginWebViewContext); diff --git a/metro.config.js b/metro.config.js index 6a417c4..1d329ca 100644 --- a/metro.config.js +++ b/metro.config.js @@ -14,6 +14,7 @@ // limitations under the License. // const { getDefaultConfig } = require("expo/metro-config"); +const exclusionList = require('metro-config/src/defaults/exclusionList'); module.exports = (() => { // eslint-disable-next-line no-undef @@ -29,6 +30,7 @@ module.exports = (() => { ...resolver, assetExts: resolver.assetExts.filter((ext) => ext !== "svg"), sourceExts: [...resolver.sourceExts, "svg"], + blacklistRE: exclusionList([/.*\.test\.tsx$/]), }; return config; From e6343950a3792e5cb562a4bda06a3401e5c8be50 Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Wed, 4 Sep 2024 13:39:13 +0700 Subject: [PATCH 2/7] Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed --- api/apiRequest.ts | 2 +- app/(tabs)/profile.tsx | 5 +- app/_layout.tsx | 6 ++ app/login.test.tsx | 29 ------ app/login.tsx | 28 +++--- components/login/LoginWebViewModal.tsx | 131 +++++++++++++------------ hooks/useInruptLogin.tsx | 17 +--- 7 files changed, 97 insertions(+), 121 deletions(-) diff --git a/api/apiRequest.ts b/api/apiRequest.ts index d4ecee0..e8f5a0e 100644 --- a/api/apiRequest.ts +++ b/api/apiRequest.ts @@ -48,7 +48,7 @@ export const makeApiRequest = async ( ); if (response.status === 401) { - router.navigate("/login?logout=true"); + router.replace("/login?logout=true"); throw new Error(`Unauthorized: ${response.status}`); } if (!response.ok) { diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 99578c9..948cffb 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -113,7 +113,10 @@ export default function Profile() { router.navigate("/login?logout=true")} + onPress={() => { + bottomSheetModalRef.current?.close(); + router.navigate("/login?logout=true"); + }} > Logout diff --git a/app/_layout.tsx b/app/_layout.tsx index e13ced4..987b7f8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -86,6 +86,12 @@ export default function RootLayout() { headerShown: false, }} > + { ) as jest.Mocked; 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; - 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; - // This checks that the Redirect component is returned. - mockedRedirect.mockReturnValue("Dummy return" as unknown as null); - // ... mocks end. - - render(); - // 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"); - }); }); diff --git a/app/login.tsx b/app/login.tsx index f2f4ba6..cc18bed 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -15,13 +15,13 @@ // import React, { useEffect } from "react"; import { View, StyleSheet } from "react-native"; -import { Redirect, useLocalSearchParams } from "expo-router"; import Constants from "expo-constants"; import CustomButton from "@/components/Button"; import { ThemedText } from "@/components/ThemedText"; import { clearWebViewIOSCache } from "react-native-webview-ios-cache-clear"; 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"; @@ -32,12 +32,18 @@ const LoginScreen = () => { const { session } = useSession(); useEffect(() => { - if (logout) { + if (session && logout) { requestLogout(); } - }, [logout, requestLogout]); + }, [logout, session]); useEffect(() => { + // 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") @@ -47,23 +53,15 @@ const LoginScreen = () => { .then((success) => console.log("Cleared all cookies?", success)) ) .catch((error) => console.log("Failed to clear cookies", error)); - } else { - // 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); - }); } }, []); const handleLoginPress = () => { - showLoginPage(); + if (!logout) { + showLoginPage(); + } }; - if (session) { - return ; - } - return ( @@ -84,7 +82,7 @@ const LoginScreen = () => { variant="primary" customStyle={{ paddingHorizontal: 74 }} testID="login-button" - > + /> ); }; diff --git a/components/login/LoginWebViewModal.tsx b/components/login/LoginWebViewModal.tsx index 20ecc9d..55ee841 100644 --- a/components/login/LoginWebViewModal.tsx +++ b/components/login/LoginWebViewModal.tsx @@ -13,88 +13,93 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import React, { useEffect, useRef, useState } from "react"; -import type { WebViewNavigation } from "react-native-webview"; -import { WebView } from "react-native-webview"; -import { SafeAreaView, StyleSheet, Text, TouchableOpacity } from "react-native"; +import React, { useRef } from "react"; +import { + SafeAreaView, + TouchableOpacity, + Text, + StyleSheet, + Modal, +} from "react-native"; +import { WebView, type WebViewNavigation } from "react-native-webview"; -const HTML_BLANK = - ''; - -const LoginWebViewModal = ({ - visible = false, - onLoginSuccess = () => null, - onLogoutSuccess = () => null, - onClose = () => null, - requestMode = "blank", -}: { - visible: boolean; +interface LoginWebViewModalProps { onClose: () => void; onLoginSuccess: () => void; onLogoutSuccess: () => void; requestMode: "login" | "logout" | "blank"; +} + +const LoginWebViewModal: React.FC = ({ + onClose = () => null, + onLoginSuccess = () => null, + onLogoutSuccess = () => null, + requestMode = "blank", }) => { - const BASE_URL = `${process.env.EXPO_PUBLIC_WALLET_API}/`; + const BASE_URL = process.env.EXPO_PUBLIC_WALLET_API || ""; const LOGIN_URL = process.env.EXPO_PUBLIC_LOGIN_URL || ""; - const LOGOUT_URL = `${BASE_URL}logout`; + const LOGOUT_URL = `${BASE_URL}/logout`; - const webViewRef = useRef(null); - const [webUrl, setWebUrl] = useState(LOGIN_URL); + const webViewRef = useRef(null); - useEffect(() => { - if (requestMode === "login") { - setWebUrl(LOGIN_URL); + const handleNavigationStateChange = ({ url }: WebViewNavigation) => { + if (url.includes("/login/success")) { + onLoginSuccess(); } + }; + + const handleLoadEnd = () => { if (requestMode === "logout") { - setWebUrl(LOGOUT_URL); - } - if (requestMode === "blank") { - setWebUrl(""); + onLogoutSuccess(); } - if (visible) { + }; + + const handleCloseModal = () => { + if (!webViewRef.current) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - webViewRef.current!.reload(); + webViewRef.current?.clearCache(true); } - }, [visible, requestMode]); - - const handleNavigationStateChange = async (navState: WebViewNavigation) => { - const { url } = navState; - const isLoginSuccess = url.includes("/login/success"); - const isLogout = url === `${process.env.EXPO_PUBLIC_WALLET_API}/`; + onClose(); + }; - if (isLoginSuccess) { - onLoginSuccess(); - } + if (requestMode === "logout") { + return ( + + + + ); + } - if (isLogout) { - onLogoutSuccess(); - } - }; + const isVisible = requestMode === "login"; return ( - - - onClose()}> - Close - - + + + + + Close + + + ); }; diff --git a/hooks/useInruptLogin.tsx b/hooks/useInruptLogin.tsx index 70ea9d2..dd47815 100644 --- a/hooks/useInruptLogin.tsx +++ b/hooks/useInruptLogin.tsx @@ -27,28 +27,24 @@ const LoginWebViewContext = createContext<{ }); export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { - const [visible, setVisible] = useState(false); const [modalRequestMode, setModalRequestMode] = useState< - "login" | "blank" | "logout" + "login" | "logout" | "blank" >("blank"); const { signIn, signOut } = useSession(); const handleLoginSuccess = () => { - setVisible(false); signIn(); + closeModal(); router.replace("/home"); - setModalRequestMode("blank"); }; const handleLogoutSuccess = () => { - setVisible(false); signOut(); + closeModal(); router.replace("/login"); - setModalRequestMode("blank"); }; - const handleCloseModal = () => { - setVisible(false); + const closeModal = () => { setModalRequestMode("blank"); }; @@ -56,11 +52,9 @@ export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { { - setVisible(true); setModalRequestMode("login"); }, requestLogout: () => { - setVisible(false); setModalRequestMode("logout"); }, }} @@ -68,10 +62,9 @@ export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { {children} ); From bafb7590e63966b429d0b760719dbf140b736681 Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Wed, 4 Sep 2024 13:41:04 +0700 Subject: [PATCH 3/7] Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed --- app.config.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app.config.ts b/app.config.ts index d6ddc60..a77eeed 100644 --- a/app.config.ts +++ b/app.config.ts @@ -63,12 +63,6 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, }, ], - [ - "expo-dev-launcher", - { - launchMode: "most-recent", - }, - ], "./plugins/withSigningConfig", ], experiments: { From 5fa9344cfd0eeebaf697ce5d0cf095c00554a8fb Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Mon, 2 Sep 2024 21:20:10 +0700 Subject: [PATCH 4/7] Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed --- api/apiRequest.ts | 2 +- app.config.ts | 6 ++ app/(tabs)/profile.tsx | 12 +-- app/_layout.tsx | 31 ++++--- app/login.test.tsx | 14 +++ app/login.tsx | 98 ++++++-------------- components/login/LoginWebViewModal.tsx | 123 +++++++++++++++++++++++++ hooks/session.tsx | 5 - hooks/useInruptLogin.tsx | 80 ++++++++++++++++ metro.config.js | 2 + 10 files changed, 272 insertions(+), 101 deletions(-) create mode 100644 components/login/LoginWebViewModal.tsx create mode 100644 hooks/useInruptLogin.tsx diff --git a/api/apiRequest.ts b/api/apiRequest.ts index 1d5501b..d4ecee0 100644 --- a/api/apiRequest.ts +++ b/api/apiRequest.ts @@ -48,7 +48,7 @@ export const makeApiRequest = async ( ); if (response.status === 401) { - router.navigate("/profile?forceLogout=true"); + router.navigate("/login?logout=true"); throw new Error(`Unauthorized: ${response.status}`); } if (!response.ok) { diff --git a/app.config.ts b/app.config.ts index a77eeed..d6ddc60 100644 --- a/app.config.ts +++ b/app.config.ts @@ -63,6 +63,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, }, ], + [ + "expo-dev-launcher", + { + launchMode: "most-recent", + }, + ], "./plugins/withSigningConfig", ], experiments: { diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 3e3dd4d..99578c9 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -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"; @@ -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({ queryKey: ["userInfo"], enabled: false, @@ -40,12 +38,6 @@ export default function Profile() { const bottomSheetModalRef = useRef(null); const navigation = useNavigation(); - const { forceLogout } = useLocalSearchParams(); - useEffect(() => { - if (forceLogout) { - signOut(); - } - }, [forceLogout, signOut]); useEffect(() => { navigation.setOptions({ headerTitle: "", @@ -121,7 +113,7 @@ export default function Profile() { signOut()} + onPress={() => router.navigate("/login?logout=true")} > Logout diff --git a/app/_layout.tsx b/app/_layout.tsx index fcf9142..e13ced4 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -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 @@ -79,22 +80,24 @@ export default function RootLayout() { - - + - + > + + + diff --git a/app/login.test.tsx b/app/login.test.tsx index c06a015..c718620 100644 --- a/app/login.test.tsx +++ b/app/login.test.tsx @@ -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... diff --git a/app/login.tsx b/app/login.tsx index 6d73ffc..f2f4ba6 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -13,41 +13,33 @@ // 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 { Redirect, useLocalSearchParams } from "expo-router"; 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 { 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 = () => ; - const LoginScreen = () => { - const [showWebView, setShowWebView] = useState(false); - const { signIn, session } = useSession(); + const { showLoginPage, requestLogout } = useLoginWebView(); + const { logout } = useLocalSearchParams(); + const { session } = useSession(); + + useEffect(() => { + if (logout) { + requestLogout(); + } + }, [logout, requestLogout]); useEffect(() => { - clearWebViewIOSCache(); if (!isRunningInExpoGo) { + clearWebViewIOSCache(); import("@react-native-cookies/cookies") .then((CookieManager) => CookieManager.default @@ -55,30 +47,27 @@ const LoginScreen = () => { .then((success) => console.log("Cleared all cookies?", success)) ) .catch((error) => console.log("Failed to clear cookies", error)); + } else { + // 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 (session) { - return ; - } const handleLoginPress = () => { - setShowWebView(true); + showLoginPage(); }; - const handleNavigationStateChange = async (navState: WebViewNavigation) => { - const { url } = navState; - - const isLoginSuccess = url.includes("/login/success"); - - if (isLoginSuccess) { - signIn(); - } - }; + if (session) { + return ; + } return ( - + { customStyle={{ paddingHorizontal: 74 }} testID="login-button" > - - - - - setShowWebView(false)} - > - Close - - - ); }; @@ -126,20 +96,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", diff --git a/components/login/LoginWebViewModal.tsx b/components/login/LoginWebViewModal.tsx new file mode 100644 index 0000000..20ecc9d --- /dev/null +++ b/components/login/LoginWebViewModal.tsx @@ -0,0 +1,123 @@ +// +// Copyright Inrupt Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import React, { useEffect, useRef, useState } from "react"; +import type { WebViewNavigation } from "react-native-webview"; +import { WebView } from "react-native-webview"; +import { SafeAreaView, StyleSheet, Text, TouchableOpacity } from "react-native"; + +const HTML_BLANK = + ''; + +const LoginWebViewModal = ({ + visible = false, + onLoginSuccess = () => null, + onLogoutSuccess = () => null, + onClose = () => null, + requestMode = "blank", +}: { + visible: boolean; + onClose: () => void; + onLoginSuccess: () => void; + onLogoutSuccess: () => void; + requestMode: "login" | "logout" | "blank"; +}) => { + const BASE_URL = `${process.env.EXPO_PUBLIC_WALLET_API}/`; + const LOGIN_URL = process.env.EXPO_PUBLIC_LOGIN_URL || ""; + const LOGOUT_URL = `${BASE_URL}logout`; + + const webViewRef = useRef(null); + const [webUrl, setWebUrl] = useState(LOGIN_URL); + + useEffect(() => { + if (requestMode === "login") { + setWebUrl(LOGIN_URL); + } + if (requestMode === "logout") { + setWebUrl(LOGOUT_URL); + } + if (requestMode === "blank") { + setWebUrl(""); + } + if (visible) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + webViewRef.current!.reload(); + } + }, [visible, requestMode]); + + const handleNavigationStateChange = async (navState: WebViewNavigation) => { + const { url } = navState; + const isLoginSuccess = url.includes("/login/success"); + const isLogout = url === `${process.env.EXPO_PUBLIC_WALLET_API}/`; + + if (isLoginSuccess) { + onLoginSuccess(); + } + + if (isLogout) { + onLogoutSuccess(); + } + }; + + return ( + + + onClose()}> + Close + + + ); +}; + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + flex: 1, + backgroundColor: "transparent", + }, + + closeButton: { + position: "absolute", + top: 40, + right: 20, + backgroundColor: "#fff", + padding: 10, + borderRadius: 5, + elevation: 2, + }, + closeButtonText: { + fontSize: 16, + color: "#007BFF", + }, +}); + +export default LoginWebViewModal; diff --git a/hooks/session.tsx b/hooks/session.tsx index 93c2b84..8c3a2f6 100644 --- a/hooks/session.tsx +++ b/hooks/session.tsx @@ -15,7 +15,6 @@ // import { useStorageState } from "@/hooks/useStorageState"; import React from "react"; -import { logout } from "@/api/user"; import { SESSION_KEY } from "@/api/apiRequest"; const AuthContext = React.createContext<{ @@ -51,10 +50,6 @@ export function SessionProvider(props: React.PropsWithChildren) { }, signOut: async () => { setSession(null); - logout().catch((e) => - // eslint-disable-next-line no-console - console.log("Error during logout:", e) - ); }, session, }} diff --git a/hooks/useInruptLogin.tsx b/hooks/useInruptLogin.tsx new file mode 100644 index 0000000..70ea9d2 --- /dev/null +++ b/hooks/useInruptLogin.tsx @@ -0,0 +1,80 @@ +// +// Copyright Inrupt Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import React, { createContext, useState } from "react"; +import LoginWebViewModal from "@/components/login/LoginWebViewModal"; +import { useSession } from "@/hooks/session"; +import { router } from "expo-router"; + +const LoginWebViewContext = createContext<{ + showLoginPage: () => void; + requestLogout: () => void; +}>({ + showLoginPage: () => null, + requestLogout: () => null, +}); + +export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { + const [visible, setVisible] = useState(false); + const [modalRequestMode, setModalRequestMode] = useState< + "login" | "blank" | "logout" + >("blank"); + const { signIn, signOut } = useSession(); + + const handleLoginSuccess = () => { + setVisible(false); + signIn(); + router.replace("/home"); + setModalRequestMode("blank"); + }; + + const handleLogoutSuccess = () => { + setVisible(false); + signOut(); + router.replace("/login"); + setModalRequestMode("blank"); + }; + + const handleCloseModal = () => { + setVisible(false); + setModalRequestMode("blank"); + }; + + return ( + { + setVisible(true); + setModalRequestMode("login"); + }, + requestLogout: () => { + setVisible(false); + setModalRequestMode("logout"); + }, + }} + > + {children} + + + ); +}; + +export const useLoginWebView = () => React.useContext(LoginWebViewContext); diff --git a/metro.config.js b/metro.config.js index 6a417c4..1d329ca 100644 --- a/metro.config.js +++ b/metro.config.js @@ -14,6 +14,7 @@ // limitations under the License. // const { getDefaultConfig } = require("expo/metro-config"); +const exclusionList = require('metro-config/src/defaults/exclusionList'); module.exports = (() => { // eslint-disable-next-line no-undef @@ -29,6 +30,7 @@ module.exports = (() => { ...resolver, assetExts: resolver.assetExts.filter((ext) => ext !== "svg"), sourceExts: [...resolver.sourceExts, "svg"], + blacklistRE: exclusionList([/.*\.test\.tsx$/]), }; return config; From c534230f7487c483dfa5bcddc9c7242e647cd2cf Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Wed, 4 Sep 2024 13:39:13 +0700 Subject: [PATCH 5/7] Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed --- api/apiRequest.ts | 2 +- app/(tabs)/profile.tsx | 5 +- app/_layout.tsx | 6 ++ app/login.test.tsx | 29 ------ app/login.tsx | 28 +++--- components/login/LoginWebViewModal.tsx | 131 +++++++++++++------------ hooks/useInruptLogin.tsx | 17 +--- 7 files changed, 97 insertions(+), 121 deletions(-) diff --git a/api/apiRequest.ts b/api/apiRequest.ts index d4ecee0..e8f5a0e 100644 --- a/api/apiRequest.ts +++ b/api/apiRequest.ts @@ -48,7 +48,7 @@ export const makeApiRequest = async ( ); if (response.status === 401) { - router.navigate("/login?logout=true"); + router.replace("/login?logout=true"); throw new Error(`Unauthorized: ${response.status}`); } if (!response.ok) { diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 99578c9..948cffb 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -113,7 +113,10 @@ export default function Profile() { router.navigate("/login?logout=true")} + onPress={() => { + bottomSheetModalRef.current?.close(); + router.navigate("/login?logout=true"); + }} > Logout diff --git a/app/_layout.tsx b/app/_layout.tsx index e13ced4..987b7f8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -86,6 +86,12 @@ export default function RootLayout() { headerShown: false, }} > + { ) as jest.Mocked; 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; - 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; - // This checks that the Redirect component is returned. - mockedRedirect.mockReturnValue("Dummy return" as unknown as null); - // ... mocks end. - - render(); - // 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"); - }); }); diff --git a/app/login.tsx b/app/login.tsx index f2f4ba6..cc18bed 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -15,13 +15,13 @@ // import React, { useEffect } from "react"; import { View, StyleSheet } from "react-native"; -import { Redirect, useLocalSearchParams } from "expo-router"; import Constants from "expo-constants"; import CustomButton from "@/components/Button"; import { ThemedText } from "@/components/ThemedText"; import { clearWebViewIOSCache } from "react-native-webview-ios-cache-clear"; 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"; @@ -32,12 +32,18 @@ const LoginScreen = () => { const { session } = useSession(); useEffect(() => { - if (logout) { + if (session && logout) { requestLogout(); } - }, [logout, requestLogout]); + }, [logout, session]); useEffect(() => { + // 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") @@ -47,23 +53,15 @@ const LoginScreen = () => { .then((success) => console.log("Cleared all cookies?", success)) ) .catch((error) => console.log("Failed to clear cookies", error)); - } else { - // 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); - }); } }, []); const handleLoginPress = () => { - showLoginPage(); + if (!logout) { + showLoginPage(); + } }; - if (session) { - return ; - } - return ( @@ -84,7 +82,7 @@ const LoginScreen = () => { variant="primary" customStyle={{ paddingHorizontal: 74 }} testID="login-button" - > + /> ); }; diff --git a/components/login/LoginWebViewModal.tsx b/components/login/LoginWebViewModal.tsx index 20ecc9d..55ee841 100644 --- a/components/login/LoginWebViewModal.tsx +++ b/components/login/LoginWebViewModal.tsx @@ -13,88 +13,93 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import React, { useEffect, useRef, useState } from "react"; -import type { WebViewNavigation } from "react-native-webview"; -import { WebView } from "react-native-webview"; -import { SafeAreaView, StyleSheet, Text, TouchableOpacity } from "react-native"; +import React, { useRef } from "react"; +import { + SafeAreaView, + TouchableOpacity, + Text, + StyleSheet, + Modal, +} from "react-native"; +import { WebView, type WebViewNavigation } from "react-native-webview"; -const HTML_BLANK = - ''; - -const LoginWebViewModal = ({ - visible = false, - onLoginSuccess = () => null, - onLogoutSuccess = () => null, - onClose = () => null, - requestMode = "blank", -}: { - visible: boolean; +interface LoginWebViewModalProps { onClose: () => void; onLoginSuccess: () => void; onLogoutSuccess: () => void; requestMode: "login" | "logout" | "blank"; +} + +const LoginWebViewModal: React.FC = ({ + onClose = () => null, + onLoginSuccess = () => null, + onLogoutSuccess = () => null, + requestMode = "blank", }) => { - const BASE_URL = `${process.env.EXPO_PUBLIC_WALLET_API}/`; + const BASE_URL = process.env.EXPO_PUBLIC_WALLET_API || ""; const LOGIN_URL = process.env.EXPO_PUBLIC_LOGIN_URL || ""; - const LOGOUT_URL = `${BASE_URL}logout`; + const LOGOUT_URL = `${BASE_URL}/logout`; - const webViewRef = useRef(null); - const [webUrl, setWebUrl] = useState(LOGIN_URL); + const webViewRef = useRef(null); - useEffect(() => { - if (requestMode === "login") { - setWebUrl(LOGIN_URL); + const handleNavigationStateChange = ({ url }: WebViewNavigation) => { + if (url.includes("/login/success")) { + onLoginSuccess(); } + }; + + const handleLoadEnd = () => { if (requestMode === "logout") { - setWebUrl(LOGOUT_URL); - } - if (requestMode === "blank") { - setWebUrl(""); + onLogoutSuccess(); } - if (visible) { + }; + + const handleCloseModal = () => { + if (!webViewRef.current) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - webViewRef.current!.reload(); + webViewRef.current?.clearCache(true); } - }, [visible, requestMode]); - - const handleNavigationStateChange = async (navState: WebViewNavigation) => { - const { url } = navState; - const isLoginSuccess = url.includes("/login/success"); - const isLogout = url === `${process.env.EXPO_PUBLIC_WALLET_API}/`; + onClose(); + }; - if (isLoginSuccess) { - onLoginSuccess(); - } + if (requestMode === "logout") { + return ( + + + + ); + } - if (isLogout) { - onLogoutSuccess(); - } - }; + const isVisible = requestMode === "login"; return ( - - - onClose()}> - Close - - + + + + + Close + + + ); }; diff --git a/hooks/useInruptLogin.tsx b/hooks/useInruptLogin.tsx index 70ea9d2..dd47815 100644 --- a/hooks/useInruptLogin.tsx +++ b/hooks/useInruptLogin.tsx @@ -27,28 +27,24 @@ const LoginWebViewContext = createContext<{ }); export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { - const [visible, setVisible] = useState(false); const [modalRequestMode, setModalRequestMode] = useState< - "login" | "blank" | "logout" + "login" | "logout" | "blank" >("blank"); const { signIn, signOut } = useSession(); const handleLoginSuccess = () => { - setVisible(false); signIn(); + closeModal(); router.replace("/home"); - setModalRequestMode("blank"); }; const handleLogoutSuccess = () => { - setVisible(false); signOut(); + closeModal(); router.replace("/login"); - setModalRequestMode("blank"); }; - const handleCloseModal = () => { - setVisible(false); + const closeModal = () => { setModalRequestMode("blank"); }; @@ -56,11 +52,9 @@ export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { { - setVisible(true); setModalRequestMode("login"); }, requestLogout: () => { - setVisible(false); setModalRequestMode("logout"); }, }} @@ -68,10 +62,9 @@ export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { {children} ); From 85004e997368d661b116261162c2b815f118919e Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Wed, 4 Sep 2024 13:41:04 +0700 Subject: [PATCH 6/7] Support Clear Cookies on Expo, and useInruptLogin and LoginWebViewModal to make WebView do not unmounted when modal closed --- app.config.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app.config.ts b/app.config.ts index d6ddc60..a77eeed 100644 --- a/app.config.ts +++ b/app.config.ts @@ -63,12 +63,6 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, }, ], - [ - "expo-dev-launcher", - { - launchMode: "most-recent", - }, - ], "./plugins/withSigningConfig", ], experiments: { From b7e741bc1543ba63ff41f209c9fcad875f80353b Mon Sep 17 00:00:00 2001 From: "quan.vo" Date: Fri, 6 Sep 2024 13:09:03 +0700 Subject: [PATCH 7/7] Add requestLogout to deps array in useEffect --- app/login.tsx | 2 +- hooks/useInruptLogin.tsx | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/login.tsx b/app/login.tsx index cc18bed..f8592c5 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -35,7 +35,7 @@ const LoginScreen = () => { if (session && logout) { requestLogout(); } - }, [logout, session]); + }, [logout, session, requestLogout]); useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-var-requires,global-require diff --git a/hooks/useInruptLogin.tsx b/hooks/useInruptLogin.tsx index dd47815..64bf874 100644 --- a/hooks/useInruptLogin.tsx +++ b/hooks/useInruptLogin.tsx @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import React, { createContext, useState } from "react"; +import React, { createContext, useCallback, useState } from "react"; import LoginWebViewModal from "@/components/login/LoginWebViewModal"; import { useSession } from "@/hooks/session"; import { router } from "expo-router"; @@ -48,15 +48,19 @@ export const LoginWebViewProvider = ({ children }: React.PropsWithChildren) => { setModalRequestMode("blank"); }; + const showLoginPage = useCallback(() => { + setModalRequestMode("login"); + }, []); + + const requestLogout = useCallback(() => { + setModalRequestMode("logout"); + }, []); + return ( { - setModalRequestMode("login"); - }, - requestLogout: () => { - setModalRequestMode("logout"); - }, + showLoginPage, + requestLogout, }} > {children}