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:
commit
2c5a843835
@ -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"
|
||||
},
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {
|
||||
confirmCode,
|
||||
currentUser,
|
||||
handleSignInWithPhoneNumber,
|
||||
handleSignOut,
|
||||
onAuthStateChanged,
|
||||
@ -8,7 +7,6 @@ import {
|
||||
|
||||
export {
|
||||
confirmCode,
|
||||
currentUser,
|
||||
handleSignInWithPhoneNumber,
|
||||
handleSignOut,
|
||||
onAuthStateChanged,
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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
41
src/screens/profile.tsx
Normal 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%",
|
||||
},
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user