This commit is contained in:
Philip Cesar Garay
2025-07-31 21:25:33 +08:00
parent 5cfbc81e3a
commit 84fc719a10
307 changed files with 46470 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
import React, { useState, useRef, createContext } from "react";
import { View, TouchableOpacity } from "react-native";
import { Motion } from "@legendapp/motion";
import { mainBorderRadius } from "../styles/Style";
import { Palette } from "../styles";
export const BottomSheetContext = createContext();
export default ({ children }) => {
const [showSheet, setShowSheet] = useState(false);
const [sheetContent, setSheetContent] = useState(null);
const bottomSheetRef = useRef();
return (
<BottomSheetContext.Provider
value={{ showSheet, setShowSheet, setSheetContent }}
>
{children}
{showSheet && (
<Motion.View
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
style={{
position: "fixed",
right: 0,
top: 0,
left: 0,
bottom: 0,
flex: 1,
zIndex: 1000000,
justifyContent: "center",
alignItems: "center",
}}
>
<TouchableOpacity
onPress={() => setShowSheet(false)}
ref={bottomSheetRef}
style={{
position: "absolute",
right: 0,
top: 0,
left: 0,
bottom: 0,
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
}}
/>
<View
style={{
width: "70%",
height: "auto",
maxWidth: 500,
maxHeight: "60vh",
minHeight: 400,
alignSelf: "center",
backgroundColor: Palette.lightPurple,
position: "absolute",
overflow: "scroll",
borderRadius: mainBorderRadius,
}}
>
{sheetContent}
</View>
</Motion.View>
)}
</BottomSheetContext.Provider>
);
};
+46
View File
@@ -0,0 +1,46 @@
import React, { useGlobal } from "reactn";
import { ActivityIndicator, View } from "react-native";
import { Palette } from "../styles";
import useLayoutType from "../hooks/useLayoutType";
export default ({ children }) => {
const [isGlobalLoading] = useGlobal("_isLoading");
const { isDesktopWeb = false, isMobileWeb = false } = useLayoutType();
return (
<>
{children}
{isGlobalLoading && (
<View
style={[
{
flex: 1,
justifyContent: "center",
alignItems: "center",
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9999,
backgroundColor: Palette.transparentBlack,
},
isDesktopWeb
? { position: "fixed" }
: isMobileWeb
? { position: "fixed" }
: { position: "absolute" },
]}
>
<LoaderIndicator />
</View>
)}
</>
);
};
export const LoaderIndicator = () => {
return <ActivityIndicator size={"large"} color={Palette.primary} />;
};
+155
View File
@@ -0,0 +1,155 @@
import {
createContext,
useContext,
useEffect,
useGlobal,
setGlobal,
} from "reactn";
import messaging from "@react-native-firebase/messaging";
import notifee, { EventType } from "@notifee/react-native";
import { CommonActions } from "@react-navigation/core";
import { useAppState } from "@react-native-community/hooks";
import { Linking } from "react-native";
import { dispatch } from "../navigation/NavigationService";
import { Routes } from "../navigation";
import firebase, { usersRef } from "../config/firebase";
import alert from "../components/Alert";
export const NotificationContext = createContext();
export async function displayNotification(data) {
const { title, message, picture = null, video = null } = data || {};
let ios = {
categoryId: "default",
};
if (picture) {
ios.attachments = [{ url: picture }];
}
if (video) {
ios.attachments = [
{
url: video,
thumbnailTime: data?.thumbnailTime
? parseInt(data?.thumbnailTime, 10)
: 0,
},
];
}
await notifee.displayNotification({
title,
body: message,
ios,
android: {
channelId: "default",
},
data,
});
await notifee.incrementBadgeCount();
}
export async function triggerEvent({ type, detail }) {
const { notification } = detail;
if (type === EventType.PRESS) {
const { data = {} } = notification || {};
const { projectID = null, taskID = null, messageID = null } = data || {};
if (projectID) {
setGlobal({ currentProjectID: projectID });
if (taskID) {
dispatch(
CommonActions.navigate({
name: Routes.TaskDetails,
params: { taskID, messageID },
})
);
}
}
}
}
export default ({ children }) => {
const [currentUID] = useGlobal("currentUID");
const currentAppState = useAppState();
useEffect(() => {
if (currentUID) {
updateFCMToken({ uid: currentUID });
}
}, [currentUID]);
useEffect(() => {
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
console.log("Message handled in the foreground!", remoteMessage);
const { data } = remoteMessage;
await displayNotification(data);
});
const foregroundSubscription = notifee.onForegroundEvent(triggerEvent);
return () => {
unsubscribe();
foregroundSubscription();
};
}, []);
useEffect(() => {
if (currentAppState === "active") {
notifee.setBadgeCount(0);
}
}, [currentAppState]);
const updateFCMToken = async ({ uid }) => {
try {
const authResponse = await messaging().requestPermission();
const enabled = authResponse === messaging.AuthorizationStatus.AUTHORIZED;
if (enabled) {
const fcmToken = await messaging().getToken();
if (fcmToken) {
await usersRef.doc(uid).update({
fcmTokens: firebase.firestore.FieldValue.arrayUnion(fcmToken),
});
}
} else {
alert(
"Activez les notifications",
"Afin de rester informé de l'avancée de votre projet, il est important d'activer les notifications.",
[
{
text: "Annuler",
onPress: () => {},
style: "cancel",
},
{
text: "Paramètres",
onPress: async () => {
Linking.openSettings();
},
style: "confirm",
},
]
);
}
} catch (error) {
console.log("error", error);
}
};
return (
<NotificationContext.Provider value={{}}>
{children}
</NotificationContext.Provider>
);
};
export const useNotification = () => {
return useContext(NotificationContext);
};
+56
View File
@@ -0,0 +1,56 @@
import React, { useContext, createContext, useEffect, useState } from "react";
import firebase from "../config/firebase";
import { useUserData } from "./UserDataProvider";
export const PremiumContext = createContext();
export default ({ children }) => {
const { currentUID } = useUserData();
const [subscriptions, setSubscriptions] = useState(null);
const subscriptionType = subscriptions?.length > 0 ? "PREMIUM" : "FREE";
const isPaying = subscriptionType !== "FREE";
useEffect(() => {
if (currentUID) {
getPremiumStatus();
}
}, [currentUID]);
const getPremiumStatus = async () => {
try {
const { data } = await firebase
.functions()
.httpsCallable("payment-getPremiumStatus")({
userID: currentUID,
});
if (data?.subscriptions?.length) {
setSubscriptions(
(data.subscriptions || [])?.filter((sub) => sub.status === "active")
);
}
} catch (error) {
console.log(error);
}
};
return (
<PremiumContext.Provider
value={{
getPremiumStatus,
subscriptionType,
subscriptions,
isPaying,
}}
>
{children}
</PremiumContext.Provider>
);
};
export const usePremium = () => useContext(PremiumContext);
+39
View File
@@ -0,0 +1,39 @@
import React from "react";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { SafeAreaProvider } from "react-native-safe-area-context";
import UserDataProvider from "./UserDataProvider";
import WebViewProvider from "./WebViewProvider";
import SplashAnimationProvider from "./SplashAnimationProvider";
import UniversalLinkProvider from "./UniversalLinkProvider";
import PremiumProvider from "./PremiumProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
const SharedProviders = ({ children }) => {
const providers = [
[SafeAreaProvider, {}],
[ActionSheetProvider, {}],
[SplashAnimationProvider, {}],
[WebViewProvider, {}],
[UserDataProvider, {}],
[StripeEmbeddedProvider, {}],
[PremiumProvider, {}],
[UniversalLinkProvider, {}],
];
// Dynamically nest the providers using reduce
const Combined = providers.reduceRight(
(AccumulatedChildren, [Provider, props]) => {
return ({ children }) => (
<Provider {...props}>
<AccumulatedChildren>{children}</AccumulatedChildren>
</Provider>
);
},
({ children }) => <>{children}</>
);
return <Combined>{children}</Combined>;
};
export default SharedProviders;
+49
View File
@@ -0,0 +1,49 @@
import { useState, useEffect, createContext, useRef } from "react";
import { StyleSheet, Text, Animated } from "react-native";
import { Fonts, Palette, Style } from "../styles";
import { isWeb } from "../hooks/useLayoutType";
export const SplashAnimationContext = createContext();
const SplashAnimationProvider = ({ children }) => {
const [isFullyLoaded, setIsFullyLoaded] = useState(false);
const [showOverlay, setShowOverlay] = useState(true);
const fadeAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (isFullyLoaded) {
Animated.timing(fadeAnim, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}).start(() => {
setShowOverlay(false);
});
}
}, [isFullyLoaded]);
return (
<SplashAnimationContext.Provider
value={{ isFullyLoaded, setIsFullyLoaded }}
>
{children}
{showOverlay && (
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
flex: 1,
...Style.containerCenter,
backgroundColor: Palette.darkPurple,
opacity: fadeAnim,
}}
>
{isWeb && <Text style={Fonts({ type: "megaTitle" })}>m</Text>}
</Animated.View>
)}
</SplashAnimationContext.Provider>
);
};
export default SplashAnimationProvider;
+73
View File
@@ -0,0 +1,73 @@
import {
EmbeddedCheckoutProvider,
EmbeddedCheckout,
} from "@stripe/react-stripe-js";
import { useState, createContext } from "react";
import { View, StyleSheet } from "react-native";
import { loadStripe } from "@stripe/stripe-js";
import {
STRIPE_PUBLISHABLE_KEY_LIVE,
STRIPE_PUBLISHABLE_KEY_TEST,
} from "../data/keys";
import { mainBorderRadius } from "../styles/Style";
import Overlay from "../components/Overlay";
import Button from "../components/Button";
import useLayoutType from "../hooks/useLayoutType";
const isStripeTesting = false;
const stripePromise = loadStripe(
isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE
);
export const StripeEmbeddedContext = createContext();
export default ({ children }) => {
const { isMobile } = useLayoutType();
const [clientSecret, setClientSecret] = useState(null);
return (
<StripeEmbeddedContext.Provider value={{ setClientSecret }}>
{children}
<Overlay
isVisible={clientSecret !== null}
setIsVisible={() => setClientSecret(null)}
>
<View
style={{
position: "absolute",
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
alignSelf: "center",
width: isMobile ? "90%" : "80%",
height: "70vh",
borderRadius: mainBorderRadius,
overflow: "scroll",
}}
>
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</View>
<Button
type="secondary"
isAbsoluteBottom
text="Fermer"
onPress={() => setClientSecret(null)}
/>
</View>
</Overlay>
</StripeEmbeddedContext.Provider>
);
};
+63
View File
@@ -0,0 +1,63 @@
import React, { useEffect, useGlobal } from "reactn";
import { Text } from "react-native";
import { Motion } from "@legendapp/motion";
import { Fonts, Palette } from "../styles";
import { responsiveHeight } from "../actions/responsiveSizes";
function TooltipProvider({ children }) {
const [tooltip, setTooltip] = useGlobal("_tooltip");
useEffect(() => {
const resetTimeout = setTimeout(async () => {
setTooltip(null);
}, 2500);
return () => clearTimeout(resetTimeout);
}, [tooltip]);
function getBackgroundColor() {
if (tooltip?.type === "error") {
return Palette.red;
}
return Palette.primary;
}
return (
<>
{children}
<Motion.View
animate={{ top: tooltip ? responsiveHeight(5) : -50 }}
transition={{
type: "spring",
damping: 20,
stiffness: 400,
}}
style={{
position: "absolute",
alignSelf: "center",
backgroundColor: getBackgroundColor(),
padding: 10,
paddingHorizontal: 20,
borderRadius: 30,
}}
>
<Text
numberOfLines={2}
style={{
textAlign: "center",
...Fonts({
type: "default",
color: Palette.white,
style: { textAlign: "center" },
}),
}}
>
{tooltip?.text}
</Text>
</Motion.View>
</>
);
}
export default TooltipProvider;
+111
View File
@@ -0,0 +1,111 @@
import { useContext, useState, useEffect } from "reactn";
import * as Linking from "expo-linking";
import { UserDataContext } from "./UserDataProvider";
import { isWeb } from "../hooks/useLayoutType";
import { navigateToTask } from "../navigation/NavigationService";
import { SplashAnimationContext } from "./SplashAnimationProvider";
const appJson = require("../../app.json");
export const storeURL = {
apple: "apps.apple.com/app/id1661696886",
google: "play.google.com/store/apps",
};
const UniversalLinkProvider = ({ children }) => {
const { currentUID } = useContext(UserDataContext);
const { isFullyLoaded } = useContext(SplashAnimationContext);
const [tempTaskData, setTempTaskData] = useState(null);
useEffect(() => {
const handleDeepLink = async (event) => {
console.log("event", event);
const { path = "", queryParams } = Linking.parse(event.url);
console.log("path", path);
console.log("queryParams", queryParams);
const appSchemeURL = `${
appJson.expo.scheme
}://app/${path}?${new URLSearchParams(queryParams).toString()}`;
const appStoreURL = `itms-apps://${storeURL.apple}`;
if (isWeb) {
const userAgent =
navigator.userAgent || navigator.vendor || window.opera;
const isIOSBrowser =
/iPad|iPhone|iPod/.test(userAgent) ||
(/Macintosh/.test(userAgent) &&
navigator.maxTouchPoints &&
navigator.maxTouchPoints > 2);
if (isIOSBrowser) {
try {
const canOpen = await Linking.canOpenURL(appSchemeURL);
if (canOpen) {
window.location.href = appSchemeURL;
} else {
window.location.href = appStoreURL;
}
} catch (error) {
console.error("Redirection error:", error);
handleParams(path);
}
} else {
handleParams(path);
}
} else {
handleParams(path);
}
};
const initDeepLinkHandling = async () => {
const initialUrl = await Linking.getInitialURL();
if (initialUrl) {
console.log("Initial URL:", initialUrl);
handleDeepLink({ url: initialUrl });
}
Linking.addEventListener("url", handleDeepLink);
return () => {
Linking.removeEventListener("url", handleDeepLink);
};
};
initDeepLinkHandling();
}, []);
useEffect(() => {
if (tempTaskData && currentUID && isFullyLoaded) {
console.log("try to navigate to task");
navigateToTask(tempTaskData);
setTempTaskData(null);
}
}, [tempTaskData, currentUID, isFullyLoaded]);
const handleParams = (path) => {
console.log("path", path);
cleanURL();
};
const cleanURL = () => {
if (isWeb) {
const url = new URL(window.location);
console.log(url);
url.search = "";
window.history.replaceState({}, document.title, url.origin.toString());
}
};
return children;
};
export default UniversalLinkProvider;
+165
View File
@@ -0,0 +1,165 @@
import { useContext, createContext, useGlobal } from "reactn";
import { useDataFromRef } from "react-native-minuit/src/hooks";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import firebase, { usersRef } from "../config/firebase";
import { useState } from "react";
import { checkIfEmailIsValid } from "../actions/signupActions";
export const UserDataContext = createContext();
export default ({ children }) => {
const { setIsLoading, setTooltip } = useMinuit();
const [pendingUserData, setPendingUserData] = useState({
creationTimestamp: new Date(),
});
const [currentUID] = useGlobal("currentUID");
const [currentUserData, setCurrentUserData] = useGlobal("currentUserData");
const [currentUserRoles] = useGlobal("currentUserRoles");
useDataFromRef({
ref: currentUID ? usersRef.doc(currentUID) : null,
simpleRef: true,
listener: true,
condition: currentUID,
refreshArray: [currentUID],
onUpdate: (data) => {
if (data) {
setCurrentUserData(data);
} else {
setCurrentUserData(null);
}
},
});
const updatePendingUserData = (newData) => {
setPendingUserData((prevData) => ({
...prevData,
...newData,
}));
};
const updateUserData = async ({ data, shouldSetTooltip = true }) => {
try {
await setIsLoading(true);
let dynamicUID = currentUID || null;
let cleanData = {
...currentUserData,
...data,
lastUpdateTimestamp: new Date(),
};
// if (cleanData?.firstName?.length > 2 && cleanData?.lastName?.length > 2) {
// cleanData = {
// ...cleanData,
// firstName: capitalize(cleanData?.firstName?.toLowerCase()),
// lastName: capitalize(cleanData?.lastName?.toLowerCase()),
// };
// } else {
// throw new Error("Veuillez renseigner un prénom et un nom");
// }
if (!checkIfEmailIsValid({ email: cleanData?.email })) {
throw new Error("Veuillez renseigner un email valide");
}
// let cleanPhoneNumber = formatPhoneNumber({
// phoneNumber: cleanData.phoneNumber,
// });
// if (cleanPhoneNumber) {
// cleanData.phoneNumber = cleanPhoneNumber;
// } else {
// throw new Error("Veuillez renseigner un numéro de téléphone valide");
// }
if (!dynamicUID) {
const { email, password } = cleanData;
const { user = null } = await firebase
.auth()
.createUserWithEmailAndPassword(email, password);
dynamicUID = user.uid;
}
delete cleanData.password;
cleanData = {
...cleanData,
userID: dynamicUID,
};
if (!cleanData?.creationTimestamp) {
cleanData.creationTimestamp = new Date();
}
console.log("cleanData", cleanData);
await usersRef.doc(dynamicUID).set(cleanData, { merge: true });
if (shouldSetTooltip) {
setTooltip({
type: "success",
text: "Données enregistrées avec succès",
});
}
} catch (e) {
console.log(e);
setTooltip({
type: "error",
text: "Erreur lors de la mise à jour des données utilisateur",
});
throw e;
} finally {
await setIsLoading(false);
}
};
const onSignOut = async () => {
try {
setIsLoading(true);
await firebase.auth().signOut();
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
};
const isSuperAdmin = currentUserRoles.some((role) => role === "SUPERADMIN");
return (
<UserDataContext.Provider
value={{
isSuperAdmin,
currentUserRoles,
currentUID,
currentUserData,
setCurrentUserData,
onSignOut,
pendingUserData,
updatePendingUserData,
updateUserData,
}}
>
{children}
</UserDataContext.Provider>
);
};
export const useUserData = () => {
return useContext(UserDataContext);
};
+73
View File
@@ -0,0 +1,73 @@
import React, { useRef, useEffect, useMemo, createContext } from "reactn";
import BottomSheetContainer from "../components/BottomSheetContainer";
import { WebView } from "../components/WebView";
import { Palette } from "../styles";
import { useContext } from "react";
import { Platform } from "react-native";
export const WebViewContext = createContext();
export default ({ children }) => {
const [webViewUrl, setWebViewUrl] = React.useState(Platform.OS == "ios" ? null : undefined);
const snapPoints = useMemo(() => ["70%", "90%"], []);
const bottomSheetRef = useRef(null);
useEffect(() => {
if (webViewUrl) {
bottomSheetRef.current?.expand();
} else {
bottomSheetRef.current?.close();
}
}, [webViewUrl]);
const handleCloseWebView = (url) => {
if (url?.includes("minuit.starter")) {
setWebViewUrl(Platform.OS == "ios" ? null : undefined); // Réinitialiser l'URL
bottomSheetRef.current?.close(); // Fermer la BottomSheet
}
};
const docExtensionList = [".docx", ".doc", ".xlsx", ".xls", ".pptx", ".ppt"];
const isOfficeDoc = docExtensionList.some((ext) =>
webViewUrl?.toLowerCase()?.includes(ext)
);
const dynamicWebViewUrl = isOfficeDoc
? `https://docs.google.com/gview?embedded=true&url=${encodeURIComponent(
webViewUrl
)}`
: webViewUrl;
return (
<WebViewContext.Provider value={{ webViewUrl, setWebViewUrl }}>
{children}
<BottomSheetContainer
bottomSheetRef={bottomSheetRef}
snapPoints={snapPoints}
onChange={(index) => {
if (index === -1) {
setWebViewUrl(Platform.OS == "ios" ? null : undefined);
}
}}
index={-1}
enablePanDownToClose
>
<WebView
style={{
flex: 1,
backgroundColor: Palette.darkPurple,
borderWidth: 0,
}}
source={{ uri: dynamicWebViewUrl }}
onNavigationStateChange={(event) => handleCloseWebView(event.url)}
/>
</BottomSheetContainer>
</WebViewContext.Provider>
);
};
export const useWebView = () => useContext(WebViewContext);
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import { MinuitProvider } from "react-native-minuit";
import SharedProviders from "./SharedProviders";
import NotificationProvider from "./NotificationProvider";
import { Palette } from "../styles";
export default ({ children }) => {
return (
<MinuitProvider
projectID={"minuitapp"}
themeColors={{
primary: Palette.primary,
secondary: Palette.darkPurple,
destructive: "red",
}}
>
<SharedProviders>
<NotificationProvider>{children}</NotificationProvider>
</SharedProviders>
</MinuitProvider>
);
};
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import LoadingProvider from "./LoadingProvider";
import TooltipProvider from "./TooltipProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
import SharedProviders from "./SharedProviders";
export default ({ children }) => {
return (
<LoadingProvider>
<TooltipProvider>
<StripeEmbeddedProvider>
<SharedProviders>{children}</SharedProviders>
</StripeEmbeddedProvider>
</TooltipProvider>
</LoadingProvider>
);
};