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",
|
"@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"
|
||||||
},
|
},
|
||||||
|
@ -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();
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user