Merge pull request 'Login + Profile -- UI' (#109) from loewy/login-ui into master

Reviewed-on: railbird/railbird-mobile#109
Reviewed-by: Kat Huang <kkathuang@gmail.com>
This commit is contained in:
loewy 2024-02-12 11:22:03 -07:00
commit 2c5a843835
10 changed files with 234 additions and 71 deletions

View File

@ -73,6 +73,7 @@
"@types/react-native-svg-charts": "^5.0.16", "@types/react-native-svg-charts": "^5.0.16",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"metro-react-native-babel-preset": "^0.77.0", "metro-react-native-babel-preset": "^0.77.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },

View File

@ -30,6 +30,24 @@ export const confirmCode = async (
} }
}; };
export const createOrSignInUser = async (
email: string,
password: string,
isSignUp: boolean,
) => {
try {
if (isSignUp) {
await auth().createUserWithEmailAndPassword(email, password);
} else {
await auth().signInWithEmailAndPassword(email, password);
}
} catch (err) {
console.log(err);
// TODO: #107 -- Correct error handling
Alert.alert(err.message);
}
};
export const onAuthStateChanged = ( export const onAuthStateChanged = (
// TODO: eslint not detecting ts? // TODO: eslint not detecting ts?
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@ -38,16 +56,6 @@ export const onAuthStateChanged = (
return auth().onAuthStateChanged(callback); return auth().onAuthStateChanged(callback);
}; };
export const currentUser = () => auth().currentUser;
export const getCurrentUserToken = async (): Promise<string | null> => {
const user = auth().currentUser;
if (user) {
return await user.getIdToken();
}
return null;
};
export const handleSignOut = (): Promise<void> => { export const handleSignOut = (): Promise<void> => {
return auth().signOut(); return auth().signOut();
}; };

View File

@ -1,6 +1,5 @@
import { import {
confirmCode, confirmCode,
currentUser,
handleSignInWithPhoneNumber, handleSignInWithPhoneNumber,
handleSignOut, handleSignOut,
onAuthStateChanged, onAuthStateChanged,
@ -8,7 +7,6 @@ import {
export { export {
confirmCode, confirmCode,
currentUser,
handleSignInWithPhoneNumber, handleSignInWithPhoneNumber,
handleSignOut, handleSignOut,
onAuthStateChanged, onAuthStateChanged,

View File

@ -1,6 +1,12 @@
import { DEV_USER_ID } from "@env"; import { DEV_USER_ID } from "@env";
import { FirebaseAuthTypes } from "@react-native-firebase/auth"; import { FirebaseAuthTypes } from "@react-native-firebase/auth";
import React, { createContext, useContext, useEffect, useState } from "react"; import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { handleSignOut, onAuthStateChanged } from "../auth"; import { handleSignOut, onAuthStateChanged } from "../auth";
import { useAuthHeader } from "../graphql/client"; import { useAuthHeader } from "../graphql/client";
@ -24,16 +30,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true); // this is for a LoadingContext (auth, app reloads, foreground etc) const [isLoading, setIsLoading] = useState<boolean>(true); // this is for a LoadingContext (auth, app reloads, foreground etc)
const _completeAuthentication = ( const _completeAuthentication = useCallback(
(
user: FirebaseAuthTypes.User, user: FirebaseAuthTypes.User,
token: string, token: string,
isLoggedIn: boolean, isLoggedIn: boolean,
tokenType: "user_id" | "Authorization" = "Authorization",
) => { ) => {
setAuthHeader({ key: "Authorization", value: token }); setAuthHeader({ key: tokenType, value: token });
setContextUser(user); setContextUser(user);
setIsLoggedIn(isLoggedIn); setIsLoggedIn(isLoggedIn);
setIsLoading(false); setIsLoading(false);
}; },
[setAuthHeader],
);
const authStateChangeCallback = async (user: FirebaseAuthTypes.User) => { const authStateChangeCallback = async (user: FirebaseAuthTypes.User) => {
if (user) { if (user) {
@ -61,13 +71,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const setAuthAsync = async () => { const setAuthAsync = async () => {
if (DEV_USER_ID) { if (DEV_USER_ID) {
console.log("Setting fake authorization user to: ", DEV_USER_ID); console.log("Setting fake authorization user to: ", DEV_USER_ID);
setAuthHeader({ key: "user_id", value: DEV_USER_ID }); _completeAuthentication(null, DEV_USER_ID, true, "user_id");
setIsLoggedIn(true);
setIsLoading(false);
} }
}; };
setAuthAsync(); setAuthAsync();
}, [setAuthHeader]); }, [_completeAuthentication]);
const logOut = async () => { const logOut = async () => {
await handleSignOut(); await handleSignOut();

View File

@ -2,8 +2,10 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Image } from "react-native"; import { Image } from "react-native";
import CameraScreen from "../component/video/camera"; import CameraScreen from "../component/video/camera";
import Profile from "../screens/profile";
import Session from "../screens/session"; import Session from "../screens/session";
import RecordScreen from "../screens/video-stack/record"; import RecordScreen from "../screens/video-stack/record";
import { tabIconColors } from "../styles";
// TODO: add ts support for assets folder to use imports // TODO: add ts support for assets folder to use imports
const Icon = require("../assets/favicon.png"); const Icon = require("../assets/favicon.png");
@ -15,6 +17,7 @@ const RecordStack = createNativeStackNavigator();
const tabIcons = { const tabIcons = {
Session: <Image source={Icon} style={{ width: 20, height: 20 }} />, Session: <Image source={Icon} style={{ width: 20, height: 20 }} />,
VideoStack: <Image source={Icon} style={{ width: 20, height: 20 }} />, VideoStack: <Image source={Icon} style={{ width: 20, height: 20 }} />,
Profile: <Image source={Icon} style={{ width: 20, height: 20 }} />,
}; };
function VideoTabStack() { function VideoTabStack() {
@ -39,8 +42,8 @@ export default function Tabs(): React.JSX.Element {
<Tab.Navigator <Tab.Navigator
screenOptions={({ route }) => ({ screenOptions={({ route }) => ({
headerShown: false, headerShown: false,
tabBarActiveTintColor: "tomato", tabBarActiveTintColor: tabIconColors.selected,
tabBarInactiveTintColor: "gray", tabBarInactiveTintColor: tabIconColors.default,
tabBarIcon: () => { tabBarIcon: () => {
return tabIcons[route.name]; return tabIcons[route.name];
}, },
@ -56,6 +59,11 @@ export default function Tabs(): React.JSX.Element {
component={VideoTabStack} component={VideoTabStack}
options={{ tabBarLabel: "Record" }} options={{ tabBarLabel: "Record" }}
/> />
<Tab.Screen
name="Profile"
component={Profile}
options={{ tabBarLabel: "Profile" }}
/>
</Tab.Navigator> </Tab.Navigator>
); );
} }

View File

@ -1,58 +1,75 @@
// Login.tsx
import { FirebaseAuthTypes } from "@react-native-firebase/auth"; import { FirebaseAuthTypes } from "@react-native-firebase/auth";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
Button, Button,
Keyboard, Keyboard,
StyleSheet,
Switch,
Text,
TextInput, TextInput,
TouchableOpacity,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
} from "react-native"; } from "react-native";
import { confirmCode, handleSignInWithPhoneNumber } from "../auth"; import { confirmCode, handleSignInWithPhoneNumber } from "../auth";
import { createOrSignInUser } from "../auth/firebase-auth";
import { colors } from "../styles";
export default function Login() { export default function Login() {
const [isEmailLogin, setIsEmailLogin] = useState(false);
const toggleSwitch = () => setIsEmailLogin((previousState) => !previousState);
// Phone number
const [phoneNumber, setPhoneNumber] = useState<string>(""); const [phoneNumber, setPhoneNumber] = useState<string>("");
const [code, setCode] = useState<string>(""); const [code, setCode] = useState<string>("");
const [confirm, setConfirm] = const [confirm, setConfirm] =
useState<FirebaseAuthTypes.ConfirmationResult | null>(null); useState<FirebaseAuthTypes.ConfirmationResult | null>(null);
// Email
const [isSignUp, setIsSignUp] = useState<boolean>(false);
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const toggleSignUp = () => setIsSignUp(!isSignUp);
return ( return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}> <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}> <View style={styles.container}>
<View style={styles.toggleRow}>
<Text style={styles.toggleText}>Use email</Text>
<Switch
trackColor={{ false: colors.darkGrey, true: colors.lightGrey }}
thumbColor={isEmailLogin ? colors.buttonBlue : colors.panelWhite}
onValueChange={toggleSwitch}
value={isEmailLogin}
/>
</View>
{!isEmailLogin ? (
<>
<TextInput <TextInput
style={{ style={styles.input}
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Phone" placeholder="Phone"
textContentType="telephoneNumber" textContentType="telephoneNumber"
keyboardType="phone-pad" keyboardType="phone-pad"
autoCapitalize="none" autoCapitalize="none"
value={phoneNumber} value={phoneNumber}
onChangeText={(value) => setPhoneNumber(value)} onChangeText={setPhoneNumber}
/> />
{confirm && ( {confirm && (
<TextInput <TextInput
style={{ style={styles.input}
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Code" placeholder="Code"
keyboardType="number-pad" keyboardType="number-pad"
textContentType="oneTimeCode" textContentType="oneTimeCode"
autoCapitalize="none" autoCapitalize="none"
value={code} value={code}
onChangeText={(value) => setCode(value)} onChangeText={setCode}
/> />
)} )}
<View style={styles.buttonContainer}>
<Button <Button
title={!confirm ? "Receive code" : "Confirm code"} title={!confirm ? "Send code" : "Confirm code"}
onPress={() => onPress={() =>
!confirm !confirm
? handleSignInWithPhoneNumber(phoneNumber).then(setConfirm) ? handleSignInWithPhoneNumber(phoneNumber).then(setConfirm)
@ -60,6 +77,79 @@ export default function Login() {
} }
/> />
</View> </View>
</>
) : (
<>
<TextInput
style={styles.input}
placeholder="Email"
textContentType="emailAddress"
keyboardType="email-address"
autoCapitalize="none"
value={email}
onChangeText={setEmail}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
textContentType="password"
autoCapitalize="none"
value={password}
onChangeText={setPassword}
/>
<TouchableOpacity onPress={toggleSignUp}>
<Text style={styles.linkText}>
{isSignUp
? "Already have an account? Sign In"
: "Don't have an account? Sign Up"}
</Text>
</TouchableOpacity>
<View style={styles.buttonContainer}>
<Button
title={isSignUp ? "SignUp" : "Sign in"}
onPress={() => createOrSignInUser(email, password, isSignUp)}
/>
</View>
</>
)}
</View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#fff",
},
input: {
width: "80%",
height: 50,
borderWidth: 1,
borderColor: "black",
borderRadius: 25,
marginBottom: 20,
padding: 15,
fontSize: 18,
},
toggleRow: {
flexDirection: "row",
alignItems: "center",
marginBottom: 20,
},
toggleText: { marginRight: 20, fontSize: 16 },
linkText: {
fontSize: 16,
color: "#007AFF",
paddingVertical: 10,
textAlign: "center",
},
buttonContainer: {
width: "80%",
borderRadius: 25,
},
});

41
src/screens/profile.tsx Normal file
View File

@ -0,0 +1,41 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import SignOutButton from "../component/buttons/sign-out";
import { useAuth } from "../context";
// Profile Mock - can be used for session summary screen using a query handler component
// Sign out button only functional when NOT using dev env
export default function ProfileScreen() {
const { user } = useAuth();
return (
<View style={styles.container}>
{user && (
<>
<View style={styles.userInfo}>
<Text>Display name: {user?.displayName ?? "No username set"}</Text>
<Text>Phone number/Email: {user?.phoneNumber ?? user?.email}</Text>
</View>
<View style={styles.signOutButton}>
<SignOutButton />
</View>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "space-between",
},
userInfo: {
marginTop: 10,
paddingHorizontal: 10,
},
signOutButton: {
paddingBottom: "5%",
paddingHorizontal: "25%",
},
});

View File

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { StyleSheet, View } from "react-native"; import { StyleSheet, View } from "react-native";
import { graph_data_two_measures } from "../../test/mock/charts/mock-data"; import { graph_data_two_measures } from "../../test/mock/charts/mock-data";
import SignOutButton from "../component/buttons/sign-out";
import BarGraph from "../component/charts/bar-graph/bar-graph"; import BarGraph from "../component/charts/bar-graph/bar-graph";
// Session Mock - can be used for session summary screen using a query handler component // Session Mock - can be used for session summary screen using a query handler component
@ -10,7 +9,6 @@ export default function SessionScreen() {
return ( return (
<View style={StyleSheet.absoluteFill}> <View style={StyleSheet.absoluteFill}>
<BarGraph data={graph_data_two_measures} /> <BarGraph data={graph_data_two_measures} />
<SignOutButton />
</View> </View>
); );
} }

View File

@ -4,6 +4,7 @@
export const colors = { export const colors = {
bgBlack: "#121212", bgBlack: "#121212",
lightGrey: "#BFC2C8", lightGrey: "#BFC2C8",
darkGrey: "#767577",
themeBrown: "#D9AA84", themeBrown: "#D9AA84",
panelWhite: "#F2FBFE", panelWhite: "#F2FBFE",
tournamentBlue: "#50a6c2", tournamentBlue: "#50a6c2",
@ -12,6 +13,11 @@ export const colors = {
textWhite: "#ffffff", textWhite: "#ffffff",
}; };
export const tabIconColors = {
default: "#1D1B20",
selected: "#598EBB",
};
export const shadows = { export const shadows = {
standard: { standard: {
shadowColor: "#000000", shadowColor: "#000000",

View File

@ -9509,6 +9509,11 @@ prettier-plugin-organize-imports@^3.2.4:
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e" resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e"
integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog== integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==
prettier@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
pretty-bytes@5.6.0: pretty-bytes@5.6.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"