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",
"eslint-config-prettier": "^9.1.0",
"metro-react-native-babel-preset": "^0.77.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"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 = (
// TODO: eslint not detecting ts?
// eslint-disable-next-line no-unused-vars
@ -38,16 +56,6 @@ export const onAuthStateChanged = (
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> => {
return auth().signOut();
};

View File

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

View File

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

View File

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

View File

@ -1,65 +1,155 @@
// Login.tsx
import { FirebaseAuthTypes } from "@react-native-firebase/auth";
import React, { useState } from "react";
import {
Button,
Keyboard,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from "react-native";
import { confirmCode, handleSignInWithPhoneNumber } from "../auth";
import { createOrSignInUser } from "../auth/firebase-auth";
import { colors } from "../styles";
export default function Login() {
const [isEmailLogin, setIsEmailLogin] = useState(false);
const toggleSwitch = () => setIsEmailLogin((previousState) => !previousState);
// Phone number
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [code, setCode] = useState<string>("");
const [confirm, setConfirm] =
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 (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<TextInput
style={{
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Phone"
textContentType="telephoneNumber"
keyboardType="phone-pad"
autoCapitalize="none"
value={phoneNumber}
onChangeText={(value) => setPhoneNumber(value)}
/>
{confirm && (
<TextInput
style={{
width: "50%",
height: 30,
borderWidth: 1,
borderColor: "black",
marginBottom: 20,
}}
placeholder="Code"
keyboardType="number-pad"
textContentType="oneTimeCode"
autoCapitalize="none"
value={code}
onChangeText={(value) => setCode(value)}
<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
style={styles.input}
placeholder="Phone"
textContentType="telephoneNumber"
keyboardType="phone-pad"
autoCapitalize="none"
value={phoneNumber}
onChangeText={setPhoneNumber}
/>
{confirm && (
<TextInput
style={styles.input}
placeholder="Code"
keyboardType="number-pad"
textContentType="oneTimeCode"
autoCapitalize="none"
value={code}
onChangeText={setCode}
/>
)}
<View style={styles.buttonContainer}>
<Button
title={!confirm ? "Send code" : "Confirm code"}
onPress={() =>
!confirm
? handleSignInWithPhoneNumber(phoneNumber).then(setConfirm)
: confirm && confirmCode(confirm, code)
}
/>
</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>
</>
)}
<Button
title={!confirm ? "Receive code" : "Confirm code"}
onPress={() =>
!confirm
? handleSignInWithPhoneNumber(phoneNumber).then(setConfirm)
: confirm && confirmCode(confirm, code)
}
/>
</View>
</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 { StyleSheet, View } from "react-native";
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";
// Session Mock - can be used for session summary screen using a query handler component
@ -10,7 +9,6 @@ export default function SessionScreen() {
return (
<View style={StyleSheet.absoluteFill}>
<BarGraph data={graph_data_two_measures} />
<SignOutButton />
</View>
);
}

View File

@ -4,6 +4,7 @@
export const colors = {
bgBlack: "#121212",
lightGrey: "#BFC2C8",
darkGrey: "#767577",
themeBrown: "#D9AA84",
panelWhite: "#F2FBFE",
tournamentBlue: "#50a6c2",
@ -12,6 +13,11 @@ export const colors = {
textWhite: "#ffffff",
};
export const tabIconColors = {
default: "#1D1B20",
selected: "#598EBB",
};
export const shadows = {
standard: {
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"
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:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"