Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/wallet 424 wallet login #39

Merged
merged 10 commits into from
Sep 6, 2024
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