feat: fixes and formatter
This commit is contained in:
@@ -1,22 +1,20 @@
|
||||
import { Motion } from "@legendapp/motion";
|
||||
import React, { createContext, useRef, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Motion } from '@legendapp/motion'
|
||||
import React, { createContext, useRef, useState } from 'react'
|
||||
import { TouchableOpacity, View } from 'react-native'
|
||||
|
||||
import { Palette } from "../styles";
|
||||
import { mainBorderRadius } from "../styles/Style";
|
||||
import { Palette } from '../styles'
|
||||
import { mainBorderRadius } from '../styles/Style'
|
||||
|
||||
export const BottomSheetContext = createContext();
|
||||
export const BottomSheetContext = createContext()
|
||||
|
||||
export default ({ children }) => {
|
||||
const [showSheet, setShowSheet] = useState(false);
|
||||
const [sheetContent, setSheetContent] = useState(null);
|
||||
const [showSheet, setShowSheet] = useState(false)
|
||||
const [sheetContent, setSheetContent] = useState(null)
|
||||
|
||||
const bottomSheetRef = useRef();
|
||||
const bottomSheetRef = useRef()
|
||||
|
||||
return (
|
||||
<BottomSheetContext.Provider
|
||||
value={{ showSheet, setShowSheet, setSheetContent }}
|
||||
>
|
||||
<BottomSheetContext.Provider value={{ showSheet, setShowSheet, setSheetContent }}>
|
||||
{children}
|
||||
|
||||
{showSheet && (
|
||||
@@ -25,42 +23,42 @@ export default ({ children }) => {
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
style={{
|
||||
position: "fixed",
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
flex: 1,
|
||||
zIndex: 1000000,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowSheet(false)}
|
||||
ref={bottomSheetRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "70%",
|
||||
height: "auto",
|
||||
width: '70%',
|
||||
height: 'auto',
|
||||
maxWidth: 500,
|
||||
maxHeight: "60vh",
|
||||
maxHeight: '60vh',
|
||||
minHeight: 400,
|
||||
alignSelf: "center",
|
||||
alignSelf: 'center',
|
||||
backgroundColor: Palette.lightPurple,
|
||||
position: "absolute",
|
||||
overflow: "scroll",
|
||||
position: 'absolute',
|
||||
overflow: 'scroll',
|
||||
borderRadius: mainBorderRadius,
|
||||
}}
|
||||
>
|
||||
@@ -69,5 +67,5 @@ export default ({ children }) => {
|
||||
</Motion.View>
|
||||
)}
|
||||
</BottomSheetContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { useDataFromRef } from "react-native-minuit/src/hooks";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
||||
import { createContext, useContext, useGlobal } from "reactn";
|
||||
import { useDataFromRef } from 'react-native-minuit/src/hooks'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
|
||||
import { createContext, useContext, useGlobal } from 'reactn'
|
||||
|
||||
import { usersRef } from "../config/firebase";
|
||||
import { usersRef } from '../config/firebase'
|
||||
|
||||
export const UserDataContext = createContext();
|
||||
export const UserDataContext = createContext()
|
||||
|
||||
export default ({ children }) => {
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
const { setIsLoading, setTooltip } = useMinuit()
|
||||
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentUserData, setCurrentUserData] = useGlobal("currentUserData");
|
||||
const [currentUserRoles] = useGlobal("currentUserRoles");
|
||||
const [currentUID] = useGlobal('currentUID')
|
||||
const [currentUserData, setCurrentUserData] = useGlobal('currentUserData')
|
||||
const [currentUserRoles] = useGlobal('currentUserRoles')
|
||||
|
||||
useDataFromRef({
|
||||
ref: currentUID ? usersRef.doc(currentUID) : null,
|
||||
@@ -22,23 +22,21 @@ export default ({ children }) => {
|
||||
refreshArray: [currentUID],
|
||||
onUpdate: (data) => {
|
||||
if (data) {
|
||||
setCurrentUserData(data);
|
||||
setCurrentUserData(data)
|
||||
} else {
|
||||
setCurrentUserData(null);
|
||||
setCurrentUserData(null)
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<UserDataContext.Provider value={{}}>{children}</UserDataContext.Provider>
|
||||
);
|
||||
};
|
||||
return <UserDataContext.Provider value={{}}>{children}</UserDataContext.Provider>
|
||||
}
|
||||
|
||||
export const useUserData = () => {
|
||||
return useContext(UserDataContext);
|
||||
};
|
||||
return useContext(UserDataContext)
|
||||
}
|
||||
|
||||
// Convenience alias returning the same context, as requested
|
||||
export const useUser = () => {
|
||||
return useContext(UserDataContext);
|
||||
};
|
||||
return useContext(UserDataContext)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ActivityIndicator, Text, View } from "react-native";
|
||||
import React, { useGlobal } from "reactn";
|
||||
import { ActivityIndicator, Text, View } from 'react-native'
|
||||
import React, { useGlobal } from 'reactn'
|
||||
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import useLayoutType from '../hooks/useLayoutType'
|
||||
import { Palette } from '../styles'
|
||||
import { FONT_FAMILY } from '../styles/Fonts'
|
||||
export default ({ children }) => {
|
||||
const [isGlobalLoading] = useGlobal("_isLoading");
|
||||
const [loadingMessage] = useGlobal("_loadingMessage");
|
||||
const [isGlobalLoading] = useGlobal('_isLoading')
|
||||
const [loadingMessage] = useGlobal('_loadingMessage')
|
||||
|
||||
const { isDesktopWeb = false, isMobileWeb = false } = useLayoutType();
|
||||
const { isDesktopWeb = false, isMobileWeb = false } = useLayoutType()
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
@@ -18,8 +18,8 @@ export default ({ children }) => {
|
||||
style={[
|
||||
{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -28,40 +28,33 @@ export default ({ children }) => {
|
||||
backgroundColor: Palette.transparentBlack,
|
||||
},
|
||||
isDesktopWeb
|
||||
? { position: "fixed" }
|
||||
? { position: 'fixed' }
|
||||
: isMobileWeb
|
||||
? { position: "fixed" }
|
||||
: { position: "absolute" },
|
||||
? { position: 'fixed' }
|
||||
: { position: 'absolute' },
|
||||
]}
|
||||
>
|
||||
<LoaderIndicator message={loadingMessage ?? undefined} />
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const LoaderIndicator = ({
|
||||
message,
|
||||
defaultMessage = "Chargement...",
|
||||
defaultMessage = 'Chargement...',
|
||||
messageStyle = null,
|
||||
indicatorProps = {},
|
||||
}) => {
|
||||
const {
|
||||
size = "large",
|
||||
color = Palette.primary,
|
||||
...restIndicatorProps
|
||||
} = indicatorProps;
|
||||
const { size = 'large', color = Palette.primary, ...restIndicatorProps } = indicatorProps
|
||||
|
||||
const resolvedMessage =
|
||||
message === undefined ? defaultMessage : message;
|
||||
const resolvedMessage = message === undefined ? defaultMessage : message
|
||||
const shouldShowMessage =
|
||||
resolvedMessage !== null &&
|
||||
resolvedMessage !== undefined &&
|
||||
String(resolvedMessage).length > 0;
|
||||
resolvedMessage !== null && resolvedMessage !== undefined && String(resolvedMessage).length > 0
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: "center", gap: 10 }}>
|
||||
<View style={{ alignItems: 'center', gap: 10 }}>
|
||||
<ActivityIndicator size={size} color={color} {...restIndicatorProps} />
|
||||
{shouldShowMessage ? (
|
||||
<Text
|
||||
@@ -69,7 +62,7 @@ export const LoaderIndicator = ({
|
||||
{
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
},
|
||||
messageStyle,
|
||||
]}
|
||||
@@ -78,5 +71,5 @@ export const LoaderIndicator = ({
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,41 @@
|
||||
import Constants from "expo-constants";
|
||||
import * as Device from "expo-device";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { AppState, Platform } from "react-native";
|
||||
import { useGlobal } from "reactn";
|
||||
import Constants from 'expo-constants'
|
||||
import * as Device from 'expo-device'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AppState, Platform } from 'react-native'
|
||||
import { useGlobal } from 'reactn'
|
||||
import firebase, {
|
||||
arrayUnion,
|
||||
notificationsRef,
|
||||
serverTimestamp,
|
||||
usersRef,
|
||||
} from "../config/firebase";
|
||||
import useDataFromRef from "../hooks/useDataFromRef";
|
||||
} from '../config/firebase'
|
||||
import useDataFromRef from '../hooks/useDataFromRef'
|
||||
|
||||
export const NotificationContext = createContext(null);
|
||||
export const NotificationContext = createContext(null)
|
||||
|
||||
export default function NotificationProvider({ children }) {
|
||||
const [user] = useGlobal("currentUserData");
|
||||
const [uid] = useGlobal("currentUID");
|
||||
const [user] = useGlobal('currentUserData')
|
||||
const [uid] = useGlobal('currentUID')
|
||||
|
||||
const [pendingData, setPendingData] = useGlobal("pendingData");
|
||||
const [notifInit, setNotifInit] = useState(false);
|
||||
const [allowNotifications, setAllowNotifications] = useState(false);
|
||||
const [pendingData, setPendingData] = useGlobal('pendingData')
|
||||
const [notifInit, setNotifInit] = useState(false)
|
||||
const [allowNotifications, setAllowNotifications] = useState(false)
|
||||
|
||||
const notificationListener = useRef();
|
||||
const responseListener = useRef();
|
||||
const notificationListener = useRef()
|
||||
const responseListener = useRef()
|
||||
|
||||
const notificationsQuery = useMemo(() => {
|
||||
if (!uid) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return notificationsRef
|
||||
.where("receiver", "==", uid)
|
||||
.orderBy("time", "desc");
|
||||
return notificationsRef.where('receiver', '==', uid).orderBy('time', 'desc')
|
||||
} catch (error) {
|
||||
console.log("notificationsQuery error:", error);
|
||||
return null;
|
||||
console.log('notificationsQuery error:', error)
|
||||
return null
|
||||
}
|
||||
}, [uid]);
|
||||
}, [uid])
|
||||
|
||||
const {
|
||||
data: notifications = [],
|
||||
@@ -56,57 +47,57 @@ export default function NotificationProvider({ children }) {
|
||||
refreshArray: [uid],
|
||||
listener: true,
|
||||
condition: !!notificationsQuery,
|
||||
});
|
||||
})
|
||||
|
||||
const unreadCount = useMemo(() => {
|
||||
try {
|
||||
return notifications.filter((notif) => !notif?.read).length;
|
||||
return notifications.filter((notif) => !notif?.read).length
|
||||
} catch (_error) {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
}, [notifications]);
|
||||
}, [notifications])
|
||||
|
||||
const clearBadgeCount = useCallback(async () => {
|
||||
if (Platform.OS === "web") {
|
||||
return;
|
||||
if (Platform.OS === 'web') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await Notifications.setBadgeCountAsync(0);
|
||||
await Notifications.setBadgeCountAsync(0)
|
||||
} catch (error) {
|
||||
console.log("clearBadgeCount error:", error);
|
||||
console.log('clearBadgeCount error:', error)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const markNotificationAsRead = useCallback(async (notificationId) => {
|
||||
try {
|
||||
if (!notificationId) return;
|
||||
if (!notificationId) return
|
||||
await notificationsRef.doc(notificationId).set(
|
||||
{
|
||||
read: true,
|
||||
readAt: serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
console.log("markNotificationAsRead error:", error);
|
||||
console.log('markNotificationAsRead error:', error)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const markAllNotificationsAsRead = useCallback(async () => {
|
||||
try {
|
||||
if (!notifications?.length) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const unread = notifications.filter((notif) => !notif?.read);
|
||||
const unread = notifications.filter((notif) => !notif?.read)
|
||||
if (!unread.length) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const batch = firebase.firestore().batch();
|
||||
const batch = firebase.firestore().batch()
|
||||
unread.forEach((notif) => {
|
||||
if (!notif?.id) return;
|
||||
if (!notif?.id) return
|
||||
batch.set(
|
||||
notificationsRef.doc(notif.id),
|
||||
{
|
||||
@@ -114,122 +105,114 @@ export default function NotificationProvider({ children }) {
|
||||
readAt: serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
});
|
||||
await batch.commit();
|
||||
)
|
||||
})
|
||||
await batch.commit()
|
||||
} catch (error) {
|
||||
console.log("markAllNotificationsAsRead error:", error);
|
||||
console.log('markAllNotificationsAsRead error:', error)
|
||||
}
|
||||
}, [notifications]);
|
||||
}, [notifications])
|
||||
|
||||
useEffect(() => {
|
||||
if (!uid || !user || notifInit) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
registerForPushNotificationsAsync();
|
||||
}, [user, uid, notifInit]);
|
||||
registerForPushNotificationsAsync()
|
||||
}, [user, uid, notifInit])
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "web") {
|
||||
return;
|
||||
if (Platform.OS === 'web') {
|
||||
return
|
||||
}
|
||||
|
||||
clearBadgeCount();
|
||||
clearBadgeCount()
|
||||
|
||||
const handleAppStateChange = (state) => {
|
||||
if (state === "active") {
|
||||
clearBadgeCount();
|
||||
if (state === 'active') {
|
||||
clearBadgeCount()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const subscription = AppState.addEventListener(
|
||||
"change",
|
||||
handleAppStateChange
|
||||
);
|
||||
const subscription = AppState.addEventListener('change', handleAppStateChange)
|
||||
|
||||
return () => {
|
||||
if (subscription?.remove) {
|
||||
subscription.remove();
|
||||
subscription.remove()
|
||||
} else {
|
||||
AppState.removeEventListener("change", handleAppStateChange);
|
||||
AppState.removeEventListener('change', handleAppStateChange)
|
||||
}
|
||||
};
|
||||
}, [clearBadgeCount]);
|
||||
}
|
||||
}, [clearBadgeCount])
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowNotifications) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
notificationListener.current =
|
||||
Notifications.addNotificationReceivedListener(async (notification) => {
|
||||
console.log("Notification received", notification);
|
||||
await Notifications.scheduleNotificationAsync(notification?.request);
|
||||
});
|
||||
notificationListener.current = Notifications.addNotificationReceivedListener(
|
||||
async (notification) => {
|
||||
console.log('Notification received', notification)
|
||||
await Notifications.scheduleNotificationAsync(notification?.request)
|
||||
}
|
||||
)
|
||||
|
||||
responseListener.current =
|
||||
Notifications.addNotificationResponseReceivedListener(
|
||||
async (response) => {
|
||||
console.log("Notification response", response);
|
||||
if (
|
||||
response?.actionIdentifier ===
|
||||
"expo.modules.notifications.actions.DEFAULT"
|
||||
) {
|
||||
const { data } = response.notification.request.content;
|
||||
if (data) {
|
||||
console.log("Notification data", data);
|
||||
await setPendingData(data);
|
||||
}
|
||||
responseListener.current = Notifications.addNotificationResponseReceivedListener(
|
||||
async (response) => {
|
||||
console.log('Notification response', response)
|
||||
if (response?.actionIdentifier === 'expo.modules.notifications.actions.DEFAULT') {
|
||||
const { data } = response.notification.request.content
|
||||
if (data) {
|
||||
console.log('Notification data', data)
|
||||
await setPendingData(data)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
Notifications.removeNotificationSubscription(
|
||||
notificationListener.current
|
||||
);
|
||||
Notifications.removeNotificationSubscription(responseListener.current);
|
||||
};
|
||||
}, [allowNotifications, setPendingData]);
|
||||
Notifications.removeNotificationSubscription(notificationListener.current)
|
||||
Notifications.removeNotificationSubscription(responseListener.current)
|
||||
}
|
||||
}, [allowNotifications, setPendingData])
|
||||
|
||||
async function registerForPushNotificationsAsync() {
|
||||
try {
|
||||
const isWeb = Platform.OS === "web";
|
||||
const isWeb = Platform.OS === 'web'
|
||||
if (!Device.isDevice && !isWeb) {
|
||||
console.log("Must use physical device for Push Notifications");
|
||||
setAllowNotifications(false);
|
||||
setNotifInit(true);
|
||||
return;
|
||||
console.log('Must use physical device for Push Notifications')
|
||||
setAllowNotifications(false)
|
||||
setNotifInit(true)
|
||||
return
|
||||
}
|
||||
|
||||
let { status } = await Notifications.getPermissionsAsync();
|
||||
if (status !== "granted") {
|
||||
const request = await Notifications.requestPermissionsAsync();
|
||||
status = request.status;
|
||||
let { status } = await Notifications.getPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
const request = await Notifications.requestPermissionsAsync()
|
||||
status = request.status
|
||||
}
|
||||
|
||||
if (status !== "granted") {
|
||||
console.log("Notification permissions denied");
|
||||
setAllowNotifications(false);
|
||||
return;
|
||||
if (status !== 'granted') {
|
||||
console.log('Notification permissions denied')
|
||||
setAllowNotifications(false)
|
||||
return
|
||||
}
|
||||
|
||||
setAllowNotifications(true);
|
||||
setAllowNotifications(true)
|
||||
|
||||
const projectId =
|
||||
Constants.expoConfig?.extra?.eas?.projectId ||
|
||||
Constants.manifest2?.extra?.eas?.projectId ||
|
||||
Constants.manifest?.extra?.eas?.projectId ||
|
||||
"";
|
||||
''
|
||||
|
||||
const pushToken = (
|
||||
await Notifications.getExpoPushTokenAsync({
|
||||
projectId,
|
||||
})
|
||||
)?.data;
|
||||
)?.data
|
||||
|
||||
if (!pushToken) {
|
||||
console.log("No push token retrieved");
|
||||
return;
|
||||
console.log('No push token retrieved')
|
||||
return
|
||||
}
|
||||
|
||||
if (!!user && pushToken) {
|
||||
@@ -238,12 +221,12 @@ export default function NotificationProvider({ children }) {
|
||||
pushTokens: arrayUnion(pushToken),
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e)
|
||||
} finally {
|
||||
setNotifInit(true);
|
||||
setNotifInit(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,5 +246,5 @@ export default function NotificationProvider({ children }) {
|
||||
>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+312
-362
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,16 @@
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import React from "react";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
|
||||
import React from 'react'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
|
||||
import { SheetProvider } from "react-native-actions-sheet";
|
||||
import NotificationProvider from "./NotificationProvider";
|
||||
import PlayerProvider from "./PlayerProvider";
|
||||
import SplashAnimationProvider from "./SplashAnimationProvider";
|
||||
import StripeProvider from "./StripeProvider";
|
||||
import UniversalLinkProvider from "./UniversalLinkProvider";
|
||||
import UserDataProvider from "./UserDataProvider";
|
||||
import WebViewProvider from "./WebViewProvider";
|
||||
import { SheetProvider } from 'react-native-actions-sheet'
|
||||
import NotificationProvider from './NotificationProvider'
|
||||
import PlayerProvider from './PlayerProvider'
|
||||
import SplashAnimationProvider from './SplashAnimationProvider'
|
||||
import StripeProvider from './StripeProvider'
|
||||
import UniversalLinkProvider from './UniversalLinkProvider'
|
||||
import UserDataProvider from './UserDataProvider'
|
||||
import WebViewProvider from './WebViewProvider'
|
||||
|
||||
const SharedProviders = ({ children }) => {
|
||||
const providers = [
|
||||
@@ -25,7 +25,7 @@ const SharedProviders = ({ children }) => {
|
||||
[SheetProvider, {}],
|
||||
[PlayerProvider, {}],
|
||||
[NotificationProvider, {}],
|
||||
];
|
||||
]
|
||||
|
||||
// Dynamically nest the providers using reduce
|
||||
const Combined = providers.reduceRight(
|
||||
@@ -34,12 +34,12 @@ const SharedProviders = ({ children }) => {
|
||||
<Provider {...props}>
|
||||
<AccumulatedChildren>{children}</AccumulatedChildren>
|
||||
</Provider>
|
||||
);
|
||||
)
|
||||
},
|
||||
({ children }) => <>{children}</>
|
||||
);
|
||||
)
|
||||
|
||||
return <Combined>{children}</Combined>;
|
||||
};
|
||||
return <Combined>{children}</Combined>
|
||||
}
|
||||
|
||||
export default SharedProviders;
|
||||
export default SharedProviders
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useState, useEffect, createContext, useRef } from "react";
|
||||
import { StyleSheet, Animated } from "react-native";
|
||||
import { Palette, Style } from "../styles";
|
||||
import { Image } from "expo-image";
|
||||
import { useState, useEffect, createContext, useRef } from 'react'
|
||||
import { StyleSheet, Animated } from 'react-native'
|
||||
import { Palette, Style } from '../styles'
|
||||
import { Image } from 'expo-image'
|
||||
|
||||
export const SplashAnimationContext = createContext();
|
||||
export const SplashAnimationContext = createContext()
|
||||
|
||||
const SplashAnimationProvider = ({ children }) => {
|
||||
const [isFullyLoaded, setIsFullyLoaded] = useState(false);
|
||||
const [showOverlay, setShowOverlay] = useState(true);
|
||||
const [isFullyLoaded, setIsFullyLoaded] = useState(false)
|
||||
const [showOverlay, setShowOverlay] = useState(true)
|
||||
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current
|
||||
|
||||
useEffect(() => {
|
||||
if (isFullyLoaded) {
|
||||
@@ -18,15 +18,13 @@ const SplashAnimationProvider = ({ children }) => {
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setShowOverlay(false);
|
||||
});
|
||||
setShowOverlay(false)
|
||||
})
|
||||
}
|
||||
}, [isFullyLoaded]);
|
||||
}, [isFullyLoaded])
|
||||
|
||||
return (
|
||||
<SplashAnimationContext.Provider
|
||||
value={{ isFullyLoaded, setIsFullyLoaded }}
|
||||
>
|
||||
<SplashAnimationContext.Provider value={{ isFullyLoaded, setIsFullyLoaded }}>
|
||||
{children}
|
||||
|
||||
{showOverlay && (
|
||||
@@ -40,8 +38,8 @@ const SplashAnimationProvider = ({ children }) => {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("../../assets/splash.png")}
|
||||
contentFit={"contain"}
|
||||
source={require('../../assets/splash.png')}
|
||||
contentFit={'contain'}
|
||||
style={{
|
||||
width: 250,
|
||||
height: 250,
|
||||
@@ -50,7 +48,7 @@ const SplashAnimationProvider = ({ children }) => {
|
||||
</Animated.View>
|
||||
)}
|
||||
</SplashAnimationContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default SplashAnimationProvider;
|
||||
export default SplashAnimationProvider
|
||||
|
||||
+186
-243
@@ -1,386 +1,340 @@
|
||||
import React from "react";
|
||||
import { Linking, Platform, StyleSheet, View } from "react-native";
|
||||
import {
|
||||
EmbeddedCheckout,
|
||||
EmbeddedCheckoutProvider,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import Overlay from "../components/Overlay";
|
||||
import Button from "../components/Button";
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
import { mainBorderRadius } from "../styles/Style";
|
||||
import {
|
||||
STRIPE_PUBLISHABLE_KEY_LIVE,
|
||||
STRIPE_PUBLISHABLE_KEY_TEST,
|
||||
} from "../data/keys";
|
||||
import { getFunctionsClient } from "../config/firebase";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import { useUserData } from "./UserDataProvider";
|
||||
import { formatDate, toDate } from "../utils/dateFormatting";
|
||||
import React from 'react'
|
||||
import { Linking, Platform, StyleSheet, View } from 'react-native'
|
||||
import { EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import Overlay from '../components/Overlay'
|
||||
import Button from '../components/Button'
|
||||
import useLayoutType from '../hooks/useLayoutType'
|
||||
import { mainBorderRadius } from '../styles/Style'
|
||||
import { STRIPE_PUBLISHABLE_KEY_LIVE, STRIPE_PUBLISHABLE_KEY_TEST } from '../data/keys'
|
||||
import { getFunctionsClient } from '../config/firebase'
|
||||
import { isWeb } from '../hooks/useLayoutType'
|
||||
import { useUserData } from './UserDataProvider'
|
||||
import { formatDate, toDate } from '../utils/dateFormatting'
|
||||
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
const FUNCTIONS_REGION = 'europe-west1'
|
||||
const STRIPE_SUCCESS_URL =
|
||||
"https://dashboard.stripe.com/test/billing/starter-guide/checkout-success";
|
||||
const STRIPE_CANCEL_URL = STRIPE_SUCCESS_URL;
|
||||
'https://dashboard.stripe.com/test/billing/starter-guide/checkout-success'
|
||||
const STRIPE_CANCEL_URL = STRIPE_SUCCESS_URL
|
||||
|
||||
const isStripeTesting = true;
|
||||
const isStripeTesting = true
|
||||
const rawStripePublishableKey = isStripeTesting
|
||||
? STRIPE_PUBLISHABLE_KEY_TEST
|
||||
: STRIPE_PUBLISHABLE_KEY_LIVE;
|
||||
: STRIPE_PUBLISHABLE_KEY_LIVE
|
||||
const stripePublishableKey =
|
||||
typeof rawStripePublishableKey === "string"
|
||||
? rawStripePublishableKey.trim()
|
||||
: "";
|
||||
const stripePromise = stripePublishableKey
|
||||
? loadStripe(stripePublishableKey)
|
||||
: null;
|
||||
typeof rawStripePublishableKey === 'string' ? rawStripePublishableKey.trim() : ''
|
||||
const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : null
|
||||
|
||||
if (!stripePublishableKey) {
|
||||
console.warn(
|
||||
"[StripeProvider] Aucune clé publique Stripe fournie, le checkout intégré sera désactivé.",
|
||||
);
|
||||
'[StripeProvider] Aucune clé publique Stripe fournie, le checkout intégré sera désactivé.'
|
||||
)
|
||||
}
|
||||
|
||||
const isSafariBrowser = () => {
|
||||
if (
|
||||
typeof navigator !== "object" ||
|
||||
typeof navigator.userAgent !== "string"
|
||||
) {
|
||||
return false;
|
||||
if (typeof navigator !== 'object' || typeof navigator.userAgent !== 'string') {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
/safari/i.test(navigator.userAgent) &&
|
||||
!/(chrome|crios|android|fxios|edgios|opr|ucbrowser)/i.test(
|
||||
navigator.userAgent,
|
||||
)
|
||||
);
|
||||
};
|
||||
!/(chrome|crios|android|fxios|edgios|opr|ucbrowser)/i.test(navigator.userAgent)
|
||||
)
|
||||
}
|
||||
|
||||
const openSafariCheckoutWindow = () => {
|
||||
if (!isWeb || typeof window === "undefined") {
|
||||
return null;
|
||||
if (!isWeb || typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
if (!isSafariBrowser()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const placeholder = window.open("", "_blank", "noopener,noreferrer");
|
||||
const placeholder = window.open('', '_blank', 'noopener,noreferrer')
|
||||
if (!placeholder) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
try {
|
||||
placeholder.document.write(
|
||||
"<p style='font-family: sans-serif; color: #111; padding: 16px;'>Chargement du paiement Stripe...</p>",
|
||||
);
|
||||
placeholder.document.title = "Stripe Checkout";
|
||||
"<p style='font-family: sans-serif; color: #111; padding: 16px;'>Chargement du paiement Stripe...</p>"
|
||||
)
|
||||
placeholder.document.title = 'Stripe Checkout'
|
||||
} catch {
|
||||
// Ignore failures when the window gets reused or is cross-domain.
|
||||
}
|
||||
placeholder.focus?.();
|
||||
return placeholder;
|
||||
placeholder.focus?.()
|
||||
return placeholder
|
||||
} catch (error) {
|
||||
console.warn("[StripeProvider] Safari window pre-open failed", error);
|
||||
return null;
|
||||
console.warn('[StripeProvider] Safari window pre-open failed', error)
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const StripeContext = React.createContext(null);
|
||||
export const StripeContext = React.createContext(null)
|
||||
|
||||
export const useStripe = () => {
|
||||
const context = React.useContext(StripeContext);
|
||||
const context = React.useContext(StripeContext)
|
||||
if (!context) {
|
||||
throw new Error("useStripe must be used within a StripeProvider");
|
||||
throw new Error('useStripe must be used within a StripeProvider')
|
||||
}
|
||||
return context;
|
||||
};
|
||||
return context
|
||||
}
|
||||
|
||||
const StripeProvider = ({ children }) => {
|
||||
const { isMobile } = useLayoutType();
|
||||
const { currentUserData } = useUserData() || {};
|
||||
const canUseEmbeddedCheckout = isWeb && Boolean(stripePromise);
|
||||
const { isMobile } = useLayoutType()
|
||||
const { currentUserData } = useUserData() || {}
|
||||
const canUseEmbeddedCheckout = isWeb && Boolean(stripePromise)
|
||||
|
||||
const [clientSecret, setClientSecret] = React.useState(null);
|
||||
const [clientSecret, setClientSecret] = React.useState(null)
|
||||
const [subscriptions, setSubscriptions] = React.useState({
|
||||
monthly: [],
|
||||
annual: [],
|
||||
});
|
||||
const [coinPacks, setCoinPacks] = React.useState([]);
|
||||
const [isCatalogLoading, setIsCatalogLoading] = React.useState(true);
|
||||
const [catalogError, setCatalogError] = React.useState(null);
|
||||
const [activeSubscription, setActiveSubscription] = React.useState(null);
|
||||
const [isActiveSubscriptionLoading, setIsActiveSubscriptionLoading] =
|
||||
React.useState(false);
|
||||
const [activeSubscriptionError, setActiveSubscriptionError] =
|
||||
React.useState(null);
|
||||
})
|
||||
const [coinPacks, setCoinPacks] = React.useState([])
|
||||
const [isCatalogLoading, setIsCatalogLoading] = React.useState(true)
|
||||
const [catalogError, setCatalogError] = React.useState(null)
|
||||
const [activeSubscription, setActiveSubscription] = React.useState(null)
|
||||
const [isActiveSubscriptionLoading, setIsActiveSubscriptionLoading] = React.useState(false)
|
||||
const [activeSubscriptionError, setActiveSubscriptionError] = React.useState(null)
|
||||
|
||||
const stripeCustomerId = currentUserData?.stripeCustomerId || null;
|
||||
const stripeCustomerId = currentUserData?.stripeCustomerId || null
|
||||
const localSubscriptionId =
|
||||
currentUserData?.stripeSubscription?.id ||
|
||||
currentUserData?.stripeSubscription?.subscriptionId ||
|
||||
null;
|
||||
null
|
||||
|
||||
const closeEmbeddedCheckout = React.useCallback(() => {
|
||||
setClientSecret(null);
|
||||
}, []);
|
||||
setClientSecret(null)
|
||||
}, [])
|
||||
|
||||
const redirectToCheckout = React.useCallback(async (checkoutUrl = {}) => {
|
||||
if (!checkoutUrl) {
|
||||
throw new Error("Session Stripe introuvable.");
|
||||
throw new Error('Session Stripe introuvable.')
|
||||
}
|
||||
|
||||
if (isWeb) {
|
||||
throw new Error(
|
||||
"Impossible d'ouvrir le paiement Stripe sans checkout intégré.",
|
||||
);
|
||||
throw new Error("Impossible d'ouvrir le paiement Stripe sans checkout intégré.")
|
||||
}
|
||||
|
||||
const isMobileApp = Platform.OS === "ios" || Platform.OS === "android";
|
||||
const isMobileApp = Platform.OS === 'ios' || Platform.OS === 'android'
|
||||
|
||||
if (isMobileApp) {
|
||||
try {
|
||||
await WebBrowser.openBrowserAsync(checkoutUrl, {
|
||||
enableDefaultShareMenu: false,
|
||||
dismissButtonStyle: "close",
|
||||
presentationStyle:
|
||||
WebBrowser?.WebBrowserPresentationStyle?.PAGE_SHEET,
|
||||
});
|
||||
return;
|
||||
dismissButtonStyle: 'close',
|
||||
presentationStyle: WebBrowser?.WebBrowserPresentationStyle?.PAGE_SHEET,
|
||||
})
|
||||
return
|
||||
} catch (webBrowserError) {
|
||||
console.warn(
|
||||
"[StripeProvider] WebBrowser checkout fallback",
|
||||
webBrowserError,
|
||||
);
|
||||
console.warn('[StripeProvider] WebBrowser checkout fallback', webBrowserError)
|
||||
}
|
||||
}
|
||||
|
||||
const canOpen = await Linking.canOpenURL(checkoutUrl);
|
||||
const canOpen = await Linking.canOpenURL(checkoutUrl)
|
||||
if (!canOpen) {
|
||||
throw new Error("Impossible d'ouvrir l'URL de paiement.");
|
||||
throw new Error("Impossible d'ouvrir l'URL de paiement.")
|
||||
}
|
||||
await Linking.openURL(checkoutUrl);
|
||||
}, []);
|
||||
await Linking.openURL(checkoutUrl)
|
||||
}, [])
|
||||
|
||||
const runCheckoutSession = React.useCallback(
|
||||
async ({ callableName, payload, logTag, useEmbeddedFlow = false }) => {
|
||||
if (useEmbeddedFlow && !canUseEmbeddedCheckout) {
|
||||
throw new Error(
|
||||
"Le paiement intégré Stripe est indisponible sur cette plateforme.",
|
||||
);
|
||||
throw new Error('Le paiement intégré Stripe est indisponible sur cette plateforme.')
|
||||
}
|
||||
|
||||
const wantsEmbeddedCheckout = useEmbeddedFlow && canUseEmbeddedCheckout;
|
||||
const safariWindow = wantsEmbeddedCheckout
|
||||
? null
|
||||
: openSafariCheckoutWindow();
|
||||
const wantsEmbeddedCheckout = useEmbeddedFlow && canUseEmbeddedCheckout
|
||||
const safariWindow = wantsEmbeddedCheckout ? null : openSafariCheckoutWindow()
|
||||
try {
|
||||
const callable =
|
||||
getFunctionsClient(FUNCTIONS_REGION).httpsCallable(callableName);
|
||||
const { data } = await callable(payload);
|
||||
const checkoutUrl = data?.url;
|
||||
const clientSecret = data?.client_secret || data?.clientSecret;
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(callableName)
|
||||
const { data } = await callable(payload)
|
||||
const checkoutUrl = data?.url
|
||||
const clientSecret = data?.client_secret || data?.clientSecret
|
||||
|
||||
if (wantsEmbeddedCheckout) {
|
||||
if (!clientSecret) {
|
||||
throw new Error("Session Stripe introuvable.");
|
||||
throw new Error('Session Stripe introuvable.')
|
||||
}
|
||||
setClientSecret(clientSecret);
|
||||
return;
|
||||
setClientSecret(clientSecret)
|
||||
return
|
||||
}
|
||||
|
||||
if (!checkoutUrl) {
|
||||
throw new Error("Session Stripe introuvable.");
|
||||
throw new Error('Session Stripe introuvable.')
|
||||
}
|
||||
|
||||
console.log("[StripeProvider] Stripe checkout URL", checkoutUrl);
|
||||
await redirectToCheckout(checkoutUrl, { safariWindow });
|
||||
console.log('[StripeProvider] Stripe checkout URL', checkoutUrl)
|
||||
await redirectToCheckout(checkoutUrl, { safariWindow })
|
||||
} catch (error) {
|
||||
if (safariWindow && !safariWindow.closed) {
|
||||
safariWindow.close();
|
||||
safariWindow.close()
|
||||
}
|
||||
console.error(`[StripeProvider] ${logTag}`, error);
|
||||
console.error(`[StripeProvider] ${logTag}`, error)
|
||||
throw new Error(
|
||||
error?.message ||
|
||||
"Une erreur est survenue lors de la création de la session Stripe.",
|
||||
);
|
||||
error?.message || 'Une erreur est survenue lors de la création de la session Stripe.'
|
||||
)
|
||||
}
|
||||
},
|
||||
[canUseEmbeddedCheckout, redirectToCheckout, setClientSecret],
|
||||
);
|
||||
[canUseEmbeddedCheckout, redirectToCheckout, setClientSecret]
|
||||
)
|
||||
|
||||
const createSubscriptionCheckout = React.useCallback(
|
||||
async (priceId) => {
|
||||
if (!priceId) {
|
||||
throw new Error("Aucun abonnement sélectionné.");
|
||||
throw new Error('Aucun abonnement sélectionné.')
|
||||
}
|
||||
if (isWeb && !canUseEmbeddedCheckout) {
|
||||
console.error(
|
||||
"[StripeProvider] checkout blocked (missing embedded support)",
|
||||
{
|
||||
hasStripeKey: Boolean(stripePublishableKey),
|
||||
isClientSecretReady: false,
|
||||
},
|
||||
);
|
||||
console.error('[StripeProvider] checkout blocked (missing embedded support)', {
|
||||
hasStripeKey: Boolean(stripePublishableKey),
|
||||
isClientSecretReady: false,
|
||||
})
|
||||
throw new Error(
|
||||
"Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).",
|
||||
);
|
||||
'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).'
|
||||
)
|
||||
}
|
||||
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout;
|
||||
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout
|
||||
await runCheckoutSession({
|
||||
callableName: "subscription-createSubscriptionCheckoutSession",
|
||||
callableName: 'subscription-createSubscriptionCheckoutSession',
|
||||
payload: {
|
||||
priceId,
|
||||
returnUrls: {
|
||||
successUrl: STRIPE_SUCCESS_URL,
|
||||
cancelUrl: STRIPE_CANCEL_URL,
|
||||
},
|
||||
uiMode: shouldUseEmbeddedCheckout ? "embedded" : "hosted",
|
||||
uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted',
|
||||
},
|
||||
logTag: "subscription checkout error",
|
||||
logTag: 'subscription checkout error',
|
||||
useEmbeddedFlow: shouldUseEmbeddedCheckout,
|
||||
});
|
||||
})
|
||||
},
|
||||
[canUseEmbeddedCheckout, runCheckoutSession],
|
||||
);
|
||||
[canUseEmbeddedCheckout, runCheckoutSession]
|
||||
)
|
||||
|
||||
const createCoinPackCheckout = React.useCallback(
|
||||
async (productId) => {
|
||||
if (!productId) {
|
||||
throw new Error("Aucun pack sélectionné.");
|
||||
throw new Error('Aucun pack sélectionné.')
|
||||
}
|
||||
if (isWeb && !canUseEmbeddedCheckout) {
|
||||
console.error(
|
||||
"[StripeProvider] checkout blocked (missing embedded support)",
|
||||
{
|
||||
hasStripeKey: Boolean(stripePublishableKey),
|
||||
isClientSecretReady: false,
|
||||
},
|
||||
);
|
||||
console.error('[StripeProvider] checkout blocked (missing embedded support)', {
|
||||
hasStripeKey: Boolean(stripePublishableKey),
|
||||
isClientSecretReady: false,
|
||||
})
|
||||
throw new Error(
|
||||
"Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).",
|
||||
);
|
||||
'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).'
|
||||
)
|
||||
}
|
||||
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout;
|
||||
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout
|
||||
await runCheckoutSession({
|
||||
callableName: "subscription-createCoinPackCheckoutSession",
|
||||
callableName: 'subscription-createCoinPackCheckoutSession',
|
||||
payload: {
|
||||
productId,
|
||||
returnUrls: {
|
||||
successUrl: STRIPE_SUCCESS_URL,
|
||||
cancelUrl: STRIPE_CANCEL_URL,
|
||||
},
|
||||
uiMode: shouldUseEmbeddedCheckout ? "embedded" : "hosted",
|
||||
uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted',
|
||||
},
|
||||
logTag: "coin pack checkout error",
|
||||
logTag: 'coin pack checkout error',
|
||||
useEmbeddedFlow: shouldUseEmbeddedCheckout,
|
||||
});
|
||||
})
|
||||
},
|
||||
[canUseEmbeddedCheckout, runCheckoutSession],
|
||||
);
|
||||
[canUseEmbeddedCheckout, runCheckoutSession]
|
||||
)
|
||||
|
||||
const fetchActiveSubscription = React.useCallback(async () => {
|
||||
const hasLookupContext =
|
||||
Boolean(stripeCustomerId) || Boolean(localSubscriptionId);
|
||||
const hasLookupContext = Boolean(stripeCustomerId) || Boolean(localSubscriptionId)
|
||||
if (!hasLookupContext) {
|
||||
setActiveSubscription(null);
|
||||
setActiveSubscriptionError(null);
|
||||
setIsActiveSubscriptionLoading(false);
|
||||
return;
|
||||
setActiveSubscription(null)
|
||||
setActiveSubscriptionError(null)
|
||||
setIsActiveSubscriptionLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsActiveSubscriptionLoading(true);
|
||||
setActiveSubscriptionError(null);
|
||||
setIsActiveSubscriptionLoading(true)
|
||||
setActiveSubscriptionError(null)
|
||||
try {
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||||
"subscription-getActiveSubscription",
|
||||
);
|
||||
const { data } = await callable();
|
||||
setActiveSubscription(data?.subscription || null);
|
||||
'subscription-getActiveSubscription'
|
||||
)
|
||||
const { data } = await callable()
|
||||
setActiveSubscription(data?.subscription || null)
|
||||
} catch (error) {
|
||||
console.error("[StripeProvider] getActiveSubscription error", error);
|
||||
console.error('[StripeProvider] getActiveSubscription error', error)
|
||||
setActiveSubscriptionError(
|
||||
error?.message ||
|
||||
"Impossible de mettre à jour les informations d'abonnement.",
|
||||
);
|
||||
setActiveSubscription(null);
|
||||
error?.message || "Impossible de mettre à jour les informations d'abonnement."
|
||||
)
|
||||
setActiveSubscription(null)
|
||||
} finally {
|
||||
setIsActiveSubscriptionLoading(false);
|
||||
setIsActiveSubscriptionLoading(false)
|
||||
}
|
||||
}, [stripeCustomerId, localSubscriptionId]);
|
||||
}, [stripeCustomerId, localSubscriptionId])
|
||||
|
||||
const fetchStripeCatalog = React.useCallback(async () => {
|
||||
setIsCatalogLoading(true);
|
||||
setCatalogError(null);
|
||||
const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
|
||||
let lastError = null;
|
||||
setIsCatalogLoading(true)
|
||||
setCatalogError(null)
|
||||
const functionsClient = getFunctionsClient(FUNCTIONS_REGION)
|
||||
let lastError = null
|
||||
|
||||
try {
|
||||
const callable = functionsClient.httpsCallable(
|
||||
"subscription-listSubscriptionPlans",
|
||||
);
|
||||
const { data } = await callable();
|
||||
const nextPlans = data?.plans || {};
|
||||
const callable = functionsClient.httpsCallable('subscription-listSubscriptionPlans')
|
||||
const { data } = await callable()
|
||||
const nextPlans = data?.plans || {}
|
||||
setSubscriptions({
|
||||
monthly: Array.isArray(nextPlans?.monthly) ? nextPlans.monthly : [],
|
||||
annual: Array.isArray(nextPlans?.annual) ? nextPlans.annual : [],
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error("[StripeProvider] listSubscriptionPlans error", error);
|
||||
lastError = error
|
||||
console.error('[StripeProvider] listSubscriptionPlans error', error)
|
||||
}
|
||||
|
||||
try {
|
||||
const callable = functionsClient.httpsCallable(
|
||||
"subscription-listCoinPacks",
|
||||
);
|
||||
const { data } = await callable();
|
||||
setCoinPacks(Array.isArray(data?.packs) ? data.packs : []);
|
||||
const callable = functionsClient.httpsCallable('subscription-listCoinPacks')
|
||||
const { data } = await callable()
|
||||
setCoinPacks(Array.isArray(data?.packs) ? data.packs : [])
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error("[StripeProvider] listCoinPacks error", error);
|
||||
lastError = error
|
||||
console.error('[StripeProvider] listCoinPacks error', error)
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
setCatalogError(
|
||||
lastError?.message ||
|
||||
"Impossible de récupérer les informations Stripe pour le moment.",
|
||||
);
|
||||
lastError?.message || 'Impossible de récupérer les informations Stripe pour le moment.'
|
||||
)
|
||||
} else {
|
||||
setCatalogError(null);
|
||||
setCatalogError(null)
|
||||
}
|
||||
setIsCatalogLoading(false);
|
||||
}, []);
|
||||
setIsCatalogLoading(false)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchStripeCatalog();
|
||||
}, [fetchStripeCatalog]);
|
||||
fetchStripeCatalog()
|
||||
}, [fetchStripeCatalog])
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchActiveSubscription();
|
||||
}, [fetchActiveSubscription]);
|
||||
fetchActiveSubscription()
|
||||
}, [fetchActiveSubscription])
|
||||
|
||||
const activeSubscriptionInfo = React.useMemo(() => {
|
||||
const subscription = activeSubscription || null;
|
||||
const subscription = activeSubscription || null
|
||||
const nextRenewalDate =
|
||||
toDate(subscription?.currentPeriodEnd) ||
|
||||
toDate(subscription?.current_period_end) ||
|
||||
null;
|
||||
toDate(subscription?.currentPeriodEnd) || toDate(subscription?.current_period_end) || null
|
||||
const subscribedSinceDate =
|
||||
toDate(subscription?.created) ||
|
||||
toDate(subscription?.createdAt) ||
|
||||
toDate(subscription?.created_at) ||
|
||||
null;
|
||||
null
|
||||
|
||||
return {
|
||||
subscription,
|
||||
nextRenewalDate,
|
||||
nextRenewalLabel: nextRenewalDate ? formatDate(nextRenewalDate) : null,
|
||||
subscribedSinceDate,
|
||||
subscribedSinceLabel: subscribedSinceDate
|
||||
? formatDate(subscribedSinceDate)
|
||||
: null,
|
||||
};
|
||||
}, [activeSubscription]);
|
||||
subscribedSinceLabel: subscribedSinceDate ? formatDate(subscribedSinceDate) : null,
|
||||
}
|
||||
}, [activeSubscription])
|
||||
|
||||
const providerValue = React.useMemo(
|
||||
() => ({
|
||||
@@ -413,64 +367,53 @@ const StripeProvider = ({ children }) => {
|
||||
isActiveSubscriptionLoading,
|
||||
activeSubscriptionError,
|
||||
fetchActiveSubscription,
|
||||
],
|
||||
);
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<StripeContext.Provider value={providerValue}>
|
||||
{children}
|
||||
<Overlay
|
||||
isVisible={clientSecret !== null}
|
||||
setIsVisible={closeEmbeddedCheckout}
|
||||
>
|
||||
<Overlay isVisible={clientSecret !== null} setIsVisible={closeEmbeddedCheckout}>
|
||||
<View style={[StyleSheet.absoluteFillObject, styles.overlayContent]}>
|
||||
<View
|
||||
style={[
|
||||
styles.embeddedWrapper,
|
||||
{
|
||||
width: isMobile ? "99%" : "95%",
|
||||
height: isMobile ? "92vh" : "96vh",
|
||||
maxWidth: isMobile ? "100%" : "1400px",
|
||||
width: isMobile ? '99%' : '95%',
|
||||
height: isMobile ? '92vh' : '96vh',
|
||||
maxWidth: isMobile ? '100%' : '1400px',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{clientSecret && stripePromise ? (
|
||||
<EmbeddedCheckoutProvider
|
||||
stripe={stripePromise}
|
||||
options={{ clientSecret }}
|
||||
>
|
||||
<EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
|
||||
<EmbeddedCheckout
|
||||
onComplete={() => {
|
||||
closeEmbeddedCheckout();
|
||||
fetchActiveSubscription();
|
||||
closeEmbeddedCheckout()
|
||||
fetchActiveSubscription()
|
||||
}}
|
||||
/>
|
||||
</EmbeddedCheckoutProvider>
|
||||
) : null}
|
||||
</View>
|
||||
<Button
|
||||
type="secondary"
|
||||
isAbsoluteBottom
|
||||
text="Fermer"
|
||||
onPress={closeEmbeddedCheckout}
|
||||
/>
|
||||
<Button type="secondary" isAbsoluteBottom text="Fermer" onPress={closeEmbeddedCheckout} />
|
||||
</View>
|
||||
</Overlay>
|
||||
</StripeContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlayContent: {
|
||||
position: "absolute",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: 'absolute',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
embeddedWrapper: {
|
||||
alignSelf: "center",
|
||||
alignSelf: 'center',
|
||||
borderRadius: mainBorderRadius,
|
||||
overflow: "hidden",
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default StripeProvider;
|
||||
export default StripeProvider
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import React, { useEffect, useGlobal } from "reactn";
|
||||
import { Text } from "react-native";
|
||||
import { Motion } from "@legendapp/motion";
|
||||
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";
|
||||
import { Fonts, Palette } from '../styles'
|
||||
import { responsiveHeight } from '../actions/responsiveSizes'
|
||||
|
||||
function TooltipProvider({ children }) {
|
||||
const [tooltip, setTooltip] = useGlobal("_tooltip");
|
||||
const [tooltip, setTooltip] = useGlobal('_tooltip')
|
||||
|
||||
useEffect(() => {
|
||||
const resetTimeout = setTimeout(async () => {
|
||||
setTooltip(null);
|
||||
}, 2500);
|
||||
setTooltip(null)
|
||||
}, 2500)
|
||||
|
||||
return () => clearTimeout(resetTimeout);
|
||||
}, [tooltip]);
|
||||
return () => clearTimeout(resetTimeout)
|
||||
}, [tooltip])
|
||||
|
||||
function getBackgroundColor() {
|
||||
if (tooltip?.type === "error") {
|
||||
return Palette.red;
|
||||
if (tooltip?.type === 'error') {
|
||||
return Palette.red
|
||||
}
|
||||
return Palette.primary;
|
||||
return Palette.primary
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -29,13 +29,13 @@ function TooltipProvider({ children }) {
|
||||
<Motion.View
|
||||
animate={{ top: tooltip ? responsiveHeight(5) : -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
type: 'spring',
|
||||
damping: 20,
|
||||
stiffness: 400,
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignSelf: "center",
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
backgroundColor: getBackgroundColor(),
|
||||
padding: 10,
|
||||
paddingHorizontal: 20,
|
||||
@@ -45,11 +45,11 @@ function TooltipProvider({ children }) {
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
...Fonts({
|
||||
type: "default",
|
||||
type: 'default',
|
||||
color: Palette.white,
|
||||
style: { textAlign: "center" },
|
||||
style: { textAlign: 'center' },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
@@ -57,7 +57,7 @@ function TooltipProvider({ children }) {
|
||||
</Text>
|
||||
</Motion.View>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default TooltipProvider;
|
||||
export default TooltipProvider
|
||||
|
||||
@@ -1,293 +1,286 @@
|
||||
import * as Linking from "expo-linking";
|
||||
import { useContext, useEffect, useRef, useState } from "reactn";
|
||||
import { appleAppStoreUrl } from "../data";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import { Routes } from "../navigation";
|
||||
import { navigate, navigateToTask } from "../navigation/NavigationService";
|
||||
import { SplashAnimationContext } from "./SplashAnimationProvider";
|
||||
import { UserDataContext } from "./UserDataProvider";
|
||||
import * as Linking from 'expo-linking'
|
||||
import { useContext, useEffect, useRef, useState } from 'reactn'
|
||||
import { appleAppStoreUrl } from '../data'
|
||||
import { isWeb } from '../hooks/useLayoutType'
|
||||
import { Routes } from '../navigation'
|
||||
import { navigate, navigateToTask } from '../navigation/NavigationService'
|
||||
import { SplashAnimationContext } from './SplashAnimationProvider'
|
||||
import { UserDataContext } from './UserDataProvider'
|
||||
|
||||
const APP_SCHEME = "musicland";
|
||||
const APP_PATH_PREFIX = "app";
|
||||
const APP_STORE_FALLBACK_URL = appleAppStoreUrl;
|
||||
const APP_SCHEME = 'musicland'
|
||||
const APP_PATH_PREFIX = 'app'
|
||||
const APP_STORE_FALLBACK_URL = appleAppStoreUrl
|
||||
|
||||
const normalizePath = (rawPath = "") => {
|
||||
if (!rawPath) return "";
|
||||
const trimmed = rawPath.replace(/^\//, "");
|
||||
const normalizePath = (rawPath = '') => {
|
||||
if (!rawPath) return ''
|
||||
const trimmed = rawPath.replace(/^\//, '')
|
||||
if (trimmed.startsWith(`${APP_PATH_PREFIX}/`)) {
|
||||
return trimmed.slice(APP_PATH_PREFIX.length + 1);
|
||||
return trimmed.slice(APP_PATH_PREFIX.length + 1)
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const buildAppSchemeUrl = (rawPath, queryParams = {}) => {
|
||||
const path = normalizePath(rawPath);
|
||||
const fullPath = path ? `${APP_PATH_PREFIX}/${path}` : APP_PATH_PREFIX;
|
||||
const queryString = new URLSearchParams(queryParams ?? {}).toString();
|
||||
return `${APP_SCHEME}://${fullPath}${queryString ? `?${queryString}` : ""}`;
|
||||
};
|
||||
const path = normalizePath(rawPath)
|
||||
const fullPath = path ? `${APP_PATH_PREFIX}/${path}` : APP_PATH_PREFIX
|
||||
const queryString = new URLSearchParams(queryParams ?? {}).toString()
|
||||
return `${APP_SCHEME}://${fullPath}${queryString ? `?${queryString}` : ''}`
|
||||
}
|
||||
|
||||
const UniversalLinkProvider = ({ children }) => {
|
||||
const { currentUID } = useContext(UserDataContext);
|
||||
const { isFullyLoaded } = useContext(SplashAnimationContext);
|
||||
const [pendingNavigation, setPendingNavigation] = useState(null);
|
||||
const lastUrlRef = useRef(null);
|
||||
const { currentUID } = useContext(UserDataContext)
|
||||
const { isFullyLoaded } = useContext(SplashAnimationContext)
|
||||
const [pendingNavigation, setPendingNavigation] = useState(null)
|
||||
const lastUrlRef = useRef(null)
|
||||
|
||||
const handleLinkRedirect = async (queryParams = {}) => {
|
||||
const schemeParam = queryParams?.scheme;
|
||||
const fallbackParam = queryParams?.fallback;
|
||||
const schemeParam = queryParams?.scheme
|
||||
const fallbackParam = queryParams?.fallback
|
||||
|
||||
const scheme =
|
||||
typeof schemeParam === "string" && schemeParam.trim().length > 0
|
||||
? schemeParam.trim()
|
||||
: null;
|
||||
typeof schemeParam === 'string' && schemeParam.trim().length > 0 ? schemeParam.trim() : null
|
||||
const fallback =
|
||||
typeof fallbackParam === "string" && fallbackParam.trim().length > 0
|
||||
typeof fallbackParam === 'string' && fallbackParam.trim().length > 0
|
||||
? fallbackParam.trim()
|
||||
: null;
|
||||
: null
|
||||
|
||||
if (isWeb) {
|
||||
if (scheme) {
|
||||
let fallbackTimer = null;
|
||||
let fallbackTimer = null
|
||||
if (fallback) {
|
||||
fallbackTimer = window.setTimeout(() => {
|
||||
try {
|
||||
window.location.href = fallback;
|
||||
window.location.href = fallback
|
||||
} catch (error) {
|
||||
console.error("link.redirect.fallback.error", error);
|
||||
console.error('link.redirect.fallback.error', error)
|
||||
}
|
||||
}, 1500);
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
try {
|
||||
window.location.href = scheme;
|
||||
window.location.href = scheme
|
||||
} catch (error) {
|
||||
console.error("link.redirect.scheme.error", error);
|
||||
if (fallbackTimer) window.clearTimeout(fallbackTimer);
|
||||
console.error('link.redirect.scheme.error', error)
|
||||
if (fallbackTimer) window.clearTimeout(fallbackTimer)
|
||||
if (fallback) {
|
||||
try {
|
||||
window.location.href = fallback;
|
||||
window.location.href = fallback
|
||||
} catch (fallbackError) {
|
||||
console.error("link.redirect.fallback.error", fallbackError);
|
||||
console.error('link.redirect.fallback.error', fallbackError)
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
try {
|
||||
window.location.href = fallback;
|
||||
window.location.href = fallback
|
||||
} catch (error) {
|
||||
console.error("link.redirect.fallback.error", error);
|
||||
console.error('link.redirect.fallback.error', error)
|
||||
}
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!scheme && fallback) {
|
||||
try {
|
||||
await Linking.openURL(fallback);
|
||||
await Linking.openURL(fallback)
|
||||
} catch (error) {
|
||||
console.error("link.redirect.native.fallback.error", error);
|
||||
console.error('link.redirect.native.fallback.error', error)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!scheme) return;
|
||||
if (!scheme) return
|
||||
|
||||
try {
|
||||
const canOpen = await Linking.canOpenURL(scheme);
|
||||
const canOpen = await Linking.canOpenURL(scheme)
|
||||
if (canOpen) {
|
||||
await Linking.openURL(scheme);
|
||||
await Linking.openURL(scheme)
|
||||
} else if (fallback) {
|
||||
await Linking.openURL(fallback);
|
||||
await Linking.openURL(fallback)
|
||||
} else {
|
||||
await Linking.openURL(APP_STORE_FALLBACK_URL);
|
||||
await Linking.openURL(APP_STORE_FALLBACK_URL)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("link.redirect.native.scheme.error", error);
|
||||
console.error('link.redirect.native.scheme.error', error)
|
||||
try {
|
||||
if (fallback) {
|
||||
await Linking.openURL(fallback);
|
||||
await Linking.openURL(fallback)
|
||||
} else {
|
||||
await Linking.openURL(APP_STORE_FALLBACK_URL);
|
||||
await Linking.openURL(APP_STORE_FALLBACK_URL)
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error("link.redirect.native.fallback.error", fallbackError);
|
||||
console.error('link.redirect.native.fallback.error', fallbackError)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleDeepLink = async (event) => {
|
||||
// console.log("event", event);
|
||||
const incomingUrl = event?.url || "";
|
||||
if (!incomingUrl) return;
|
||||
const incomingUrl = event?.url || ''
|
||||
if (!incomingUrl) return
|
||||
|
||||
// Prevent handling the exact same URL multiple times
|
||||
if (lastUrlRef.current === incomingUrl) return;
|
||||
lastUrlRef.current = incomingUrl;
|
||||
if (lastUrlRef.current === incomingUrl) return
|
||||
lastUrlRef.current = incomingUrl
|
||||
|
||||
const { path = "", queryParams = {} } = Linking.parse(incomingUrl) || {};
|
||||
const { path = '', queryParams = {} } = Linking.parse(incomingUrl) || {}
|
||||
// console.log("path", path);
|
||||
// console.log("queryParams", queryParams);
|
||||
const appSchemeURL = buildAppSchemeUrl(path, queryParams);
|
||||
const appSchemeURL = buildAppSchemeUrl(path, queryParams)
|
||||
|
||||
if (isWeb) {
|
||||
const userAgent =
|
||||
navigator.userAgent || navigator.vendor || window.opera;
|
||||
const userAgent = navigator.userAgent || navigator.vendor || window.opera
|
||||
const isIOSBrowser =
|
||||
/iPad|iPhone|iPod/.test(userAgent) ||
|
||||
(/Macintosh/.test(userAgent) &&
|
||||
navigator.maxTouchPoints &&
|
||||
navigator.maxTouchPoints > 2);
|
||||
(/Macintosh/.test(userAgent) && navigator.maxTouchPoints && navigator.maxTouchPoints > 2)
|
||||
if (isIOSBrowser) {
|
||||
try {
|
||||
const canOpen = await Linking.canOpenURL(appSchemeURL);
|
||||
const canOpen = await Linking.canOpenURL(appSchemeURL)
|
||||
if (canOpen) {
|
||||
window.location.href = appSchemeURL;
|
||||
window.location.href = appSchemeURL
|
||||
} else {
|
||||
window.location.href = APP_STORE_FALLBACK_URL;
|
||||
window.location.href = APP_STORE_FALLBACK_URL
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Redirection error:", error);
|
||||
handleParams(path, queryParams);
|
||||
console.error('Redirection error:', error)
|
||||
handleParams(path, queryParams)
|
||||
}
|
||||
} else {
|
||||
handleParams(path, queryParams);
|
||||
handleParams(path, queryParams)
|
||||
}
|
||||
} else {
|
||||
handleParams(path, queryParams);
|
||||
handleParams(path, queryParams)
|
||||
}
|
||||
};
|
||||
}
|
||||
const init = async () => {
|
||||
try {
|
||||
const initialUrl = await Linking.getInitialURL();
|
||||
const initialUrl = await Linking.getInitialURL()
|
||||
if (initialUrl) {
|
||||
// Handle initial URL once
|
||||
handleDeepLink({ url: initialUrl });
|
||||
handleDeepLink({ url: initialUrl })
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Subscribe to future app-open deep links
|
||||
const sub = Linking.addEventListener("url", handleDeepLink);
|
||||
const sub = Linking.addEventListener('url', handleDeepLink)
|
||||
return () => {
|
||||
try {
|
||||
sub?.remove?.();
|
||||
sub?.remove?.()
|
||||
} catch (e) {
|
||||
// Fallback for older expo-linking versions
|
||||
Linking.removeEventListener?.("url", handleDeepLink);
|
||||
Linking.removeEventListener?.('url', handleDeepLink)
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cleanup;
|
||||
let cleanup
|
||||
init().then((c) => {
|
||||
cleanup = c;
|
||||
});
|
||||
return () => cleanup?.();
|
||||
}, []);
|
||||
cleanup = c
|
||||
})
|
||||
return () => cleanup?.()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (!pendingNavigation || !isFullyLoaded) return;
|
||||
if (!pendingNavigation || !isFullyLoaded) return
|
||||
|
||||
if (pendingNavigation.type === "resetPassword") {
|
||||
navigate(Routes.ResetPassword, pendingNavigation.params);
|
||||
setPendingNavigation(null);
|
||||
return;
|
||||
if (pendingNavigation.type === 'resetPassword') {
|
||||
navigate(Routes.ResetPassword, pendingNavigation.params)
|
||||
setPendingNavigation(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingNavigation.type === "task") {
|
||||
if (!currentUID) return;
|
||||
navigateToTask(pendingNavigation.params);
|
||||
setPendingNavigation(null);
|
||||
return;
|
||||
if (pendingNavigation.type === 'task') {
|
||||
if (!currentUID) return
|
||||
navigateToTask(pendingNavigation.params)
|
||||
setPendingNavigation(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingNavigation.type === "music") {
|
||||
navigate(Routes.MusicDetails, pendingNavigation.params);
|
||||
setPendingNavigation(null);
|
||||
return;
|
||||
if (pendingNavigation.type === 'music') {
|
||||
navigate(Routes.MusicDetails, pendingNavigation.params)
|
||||
setPendingNavigation(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingNavigation.type === "playback") {
|
||||
navigate(Routes.Playbacks, pendingNavigation.params);
|
||||
setPendingNavigation(null);
|
||||
if (pendingNavigation.type === 'playback') {
|
||||
navigate(Routes.Playbacks, pendingNavigation.params)
|
||||
setPendingNavigation(null)
|
||||
}
|
||||
}, [pendingNavigation, currentUID, isFullyLoaded]);
|
||||
}, [pendingNavigation, currentUID, isFullyLoaded])
|
||||
const handleParams = (path, queryParams = {}) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
const normalizedPath = normalizePath(path)
|
||||
|
||||
const modeParam = queryParams?.mode;
|
||||
const resetMode =
|
||||
typeof modeParam === "string" && modeParam.toLowerCase() === "resetpassword";
|
||||
const oobCodeParam = queryParams?.oobCode;
|
||||
const modeParam = queryParams?.mode
|
||||
const resetMode = typeof modeParam === 'string' && modeParam.toLowerCase() === 'resetpassword'
|
||||
const oobCodeParam = queryParams?.oobCode
|
||||
|
||||
if (resetMode && typeof oobCodeParam === "string" && oobCodeParam.trim()) {
|
||||
if (resetMode && typeof oobCodeParam === 'string' && oobCodeParam.trim()) {
|
||||
setPendingNavigation({
|
||||
type: "resetPassword",
|
||||
type: 'resetPassword',
|
||||
params: {
|
||||
oobCode: oobCodeParam.trim(),
|
||||
continueUrl:
|
||||
typeof queryParams?.continueUrl === "string"
|
||||
typeof queryParams?.continueUrl === 'string'
|
||||
? queryParams.continueUrl
|
||||
: typeof queryParams?.continueURL === "string"
|
||||
: typeof queryParams?.continueURL === 'string'
|
||||
? queryParams.continueURL
|
||||
: null,
|
||||
lang:
|
||||
typeof queryParams?.lang === "string"
|
||||
typeof queryParams?.lang === 'string'
|
||||
? queryParams.lang
|
||||
: typeof queryParams?.language === "string"
|
||||
: typeof queryParams?.language === 'string'
|
||||
? queryParams.language
|
||||
: null,
|
||||
email:
|
||||
typeof queryParams?.email === "string" ? queryParams.email : null,
|
||||
email: typeof queryParams?.email === 'string' ? queryParams.email : null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizedPath) return;
|
||||
if (!normalizedPath) return
|
||||
|
||||
if (normalizedPath.toLowerCase() === "link") {
|
||||
handleLinkRedirect(queryParams);
|
||||
return;
|
||||
if (normalizedPath.toLowerCase() === 'link') {
|
||||
handleLinkRedirect(queryParams)
|
||||
return
|
||||
}
|
||||
|
||||
const [resource, identifier] = normalizedPath.split("/").filter(Boolean);
|
||||
if (!resource || !identifier) return;
|
||||
const [resource, identifier] = normalizedPath.split('/').filter(Boolean)
|
||||
if (!resource || !identifier) return
|
||||
|
||||
const resourceKey = resource.toLowerCase();
|
||||
const resourceKey = resource.toLowerCase()
|
||||
|
||||
if (resourceKey === "tasks" || resourceKey === "task") {
|
||||
if (resourceKey === 'tasks' || resourceKey === 'task') {
|
||||
setPendingNavigation({
|
||||
type: "task",
|
||||
type: 'task',
|
||||
params: { taskID: identifier, ...queryParams },
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (resourceKey === "music" || resourceKey === "musics") {
|
||||
if (resourceKey === 'music' || resourceKey === 'musics') {
|
||||
setPendingNavigation({
|
||||
type: "music",
|
||||
type: 'music',
|
||||
params: { projectId: identifier, ...queryParams },
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (resourceKey === "playback" || resourceKey === "playbacks") {
|
||||
if (resourceKey === 'playback' || resourceKey === 'playbacks') {
|
||||
setPendingNavigation({
|
||||
type: "playback",
|
||||
type: 'playback',
|
||||
params: {
|
||||
projectId: identifier,
|
||||
playbackId: identifier,
|
||||
focusId: identifier,
|
||||
...queryParams,
|
||||
},
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
};
|
||||
return children;
|
||||
};
|
||||
export default UniversalLinkProvider;
|
||||
}
|
||||
return children
|
||||
}
|
||||
export default UniversalLinkProvider
|
||||
|
||||
+211
-248
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { useDataFromRef } from "react-native-minuit/src/hooks";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
||||
import { createContext, useContext, useGlobal } from "reactn";
|
||||
import { useDataFromRef } from 'react-native-minuit/src/hooks'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
|
||||
import { createContext, useContext, useGlobal } from 'reactn'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { checkIfEmailIsValid } from "../actions/signupActions";
|
||||
import { showPremiumRequiredAlert } from "../components/Alert";
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { checkIfEmailIsValid } from '../actions/signupActions'
|
||||
import { showPremiumRequiredAlert } from '../components/Alert'
|
||||
import firebase, {
|
||||
arrayRemove,
|
||||
arrayUnion,
|
||||
@@ -13,247 +13,223 @@ import firebase, {
|
||||
projectsRef,
|
||||
usersRef,
|
||||
videosRef,
|
||||
} from "../config/firebase";
|
||||
import { getUserPreferredArtistName } from "../utils/artistName";
|
||||
import { getLikeFieldPath, LIKE_TARGET } from "../utils/likes";
|
||||
import { ensureAuthenticated } from "../utils/authRedirect";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import useUserLikedProjects from "../hooks/useUserLikedProjects";
|
||||
} from '../config/firebase'
|
||||
import { getUserPreferredArtistName } from '../utils/artistName'
|
||||
import { getLikeFieldPath, LIKE_TARGET } from '../utils/likes'
|
||||
import { ensureAuthenticated } from '../utils/authRedirect'
|
||||
import { isWeb } from '../hooks/useLayoutType'
|
||||
import useUserLikedProjects from '../hooks/useUserLikedProjects'
|
||||
|
||||
const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK);
|
||||
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
|
||||
"trialing",
|
||||
"active",
|
||||
"past_due",
|
||||
"unpaid",
|
||||
]);
|
||||
const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK)
|
||||
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['trialing', 'active', 'past_due', 'unpaid'])
|
||||
|
||||
export const UserDataContext = createContext();
|
||||
export const UserDataContext = createContext()
|
||||
|
||||
export default ({ children }) => {
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
const { setIsLoading, setTooltip } = useMinuit()
|
||||
|
||||
const [pendingUserData, setPendingUserData] = useState({
|
||||
createdAt: new Date(),
|
||||
});
|
||||
})
|
||||
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentUserData, setCurrentUserData] = useGlobal("currentUserData");
|
||||
const [currentUserRoles] = useGlobal("currentUserRoles");
|
||||
const [currentUID] = useGlobal('currentUID')
|
||||
const [currentUserData, setCurrentUserData] = useGlobal('currentUserData')
|
||||
const [currentUserRoles] = useGlobal('currentUserRoles')
|
||||
// Current user document (live)
|
||||
const { data: currentUserDoc = null } = useDataFromRef({
|
||||
ref: currentUID ? usersRef.doc(currentUID) : null,
|
||||
updateGlobalState: "currentUserData",
|
||||
updateGlobalState: 'currentUserData',
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
})
|
||||
|
||||
// Subscribe to user's projects (musics)
|
||||
const { data: userProjects = [], loading: userProjectsLoading = true } =
|
||||
const { data: userProjects = [], loading: userProjectsLoading = true } = useDataFromRef({
|
||||
ref: currentUID
|
||||
? projectsRef.where('userId', '==', currentUID).orderBy('updatedAt', 'desc')
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
})
|
||||
|
||||
const userPlaybacks = Array.isArray(userProjects)
|
||||
? userProjects.filter((project) => project.playbackUrl)
|
||||
: []
|
||||
// Subscribe to user's liked projects (both new and legacy storage)
|
||||
const { projects: userLikedProjects = [], loading: userLikedProjectsLoading = true } =
|
||||
useUserLikedProjects()
|
||||
const toTimestamp = useCallback((value) => {
|
||||
if (!value) return 0
|
||||
if (typeof value.toDate === 'function') {
|
||||
return value.toDate().getTime()
|
||||
}
|
||||
if (typeof value.seconds === 'number') {
|
||||
return value.seconds * 1000
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
return 0
|
||||
}, [])
|
||||
const sortByPlaybackLikeDate = useCallback(
|
||||
(list) => {
|
||||
if (!Array.isArray(list)) return []
|
||||
const getLikeTimestamp = (project) => {
|
||||
const likedAt = project?.likes?.playbackLikedAt?.[currentUID]
|
||||
const likedAtTs = likedAt ? toTimestamp(likedAt) : 0
|
||||
if (likedAtTs > 0) return likedAtTs
|
||||
return toTimestamp(project?.updatedAt)
|
||||
}
|
||||
return [...list].sort((a, b) => getLikeTimestamp(b) - getLikeTimestamp(a))
|
||||
},
|
||||
[currentUID, toTimestamp]
|
||||
)
|
||||
const { data: userLikedPlaybacks = [], loading: userLikedPlaybacksLoading = true } =
|
||||
useDataFromRef({
|
||||
ref: currentUID
|
||||
? projectsRef
|
||||
.where("userId", "==", currentUID)
|
||||
.orderBy("updatedAt", "desc")
|
||||
? projectsRef.where(PLAYBACK_LIKES_FIELD, 'array-contains', currentUID)
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
|
||||
const userPlaybacks = Array.isArray(userProjects)
|
||||
? userProjects.filter((project) => project.playbackUrl)
|
||||
: [];
|
||||
// Subscribe to user's liked projects (both new and legacy storage)
|
||||
const {
|
||||
projects: userLikedProjects = [],
|
||||
loading: userLikedProjectsLoading = true,
|
||||
} = useUserLikedProjects();
|
||||
const toTimestamp = useCallback((value) => {
|
||||
if (!value) return 0;
|
||||
if (typeof value.toDate === "function") {
|
||||
return value.toDate().getTime();
|
||||
}
|
||||
if (typeof value.seconds === "number") {
|
||||
return value.seconds * 1000;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
return 0;
|
||||
}, []);
|
||||
const sortByPlaybackLikeDate = useCallback(
|
||||
(list) => {
|
||||
if (!Array.isArray(list)) return [];
|
||||
const getLikeTimestamp = (project) => {
|
||||
const likedAt = project?.likes?.playbackLikedAt?.[currentUID];
|
||||
const likedAtTs = likedAt ? toTimestamp(likedAt) : 0;
|
||||
if (likedAtTs > 0) return likedAtTs;
|
||||
return toTimestamp(project?.updatedAt);
|
||||
};
|
||||
return [...list].sort(
|
||||
(a, b) => getLikeTimestamp(b) - getLikeTimestamp(a),
|
||||
);
|
||||
},
|
||||
[currentUID, toTimestamp],
|
||||
);
|
||||
const {
|
||||
data: userLikedPlaybacks = [],
|
||||
loading: userLikedPlaybacksLoading = true,
|
||||
} = useDataFromRef({
|
||||
ref: currentUID
|
||||
? projectsRef.where(PLAYBACK_LIKES_FIELD, "array-contains", currentUID)
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
format: sortByPlaybackLikeDate,
|
||||
});
|
||||
format: sortByPlaybackLikeDate,
|
||||
})
|
||||
// Subscribe to user's playlists
|
||||
const { data: userPlaylists = [] } = useDataFromRef({
|
||||
ref: currentUID ? playlistsRef.where("createdBy", "==", currentUID) : null,
|
||||
ref: currentUID ? playlistsRef.where('createdBy', '==', currentUID) : null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
})
|
||||
|
||||
// Live list of users that the current user is following ("abonnements")
|
||||
const { data: userFollowing = [] } = useDataFromRef({
|
||||
ref: currentUID
|
||||
? usersRef.where("followedBy", "array-contains", currentUID)
|
||||
: null,
|
||||
ref: currentUID ? usersRef.where('followedBy', 'array-contains', currentUID) : null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
})
|
||||
|
||||
const updatePendingUserData = (newData) => {
|
||||
setPendingUserData((prevData) => ({
|
||||
...prevData,
|
||||
...newData,
|
||||
}));
|
||||
};
|
||||
}))
|
||||
}
|
||||
|
||||
// Selected project state and helpers
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
||||
const { data: selectedProject = null, setData: setSelectedProject } =
|
||||
useDataFromRef({
|
||||
ref: selectedProjectId ? projectsRef.doc(selectedProjectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!selectedProjectId,
|
||||
refreshArray: [selectedProjectId],
|
||||
});
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(null)
|
||||
const { data: selectedProject = null, setData: setSelectedProject } = useDataFromRef({
|
||||
ref: selectedProjectId ? projectsRef.doc(selectedProjectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!selectedProjectId,
|
||||
refreshArray: [selectedProjectId],
|
||||
})
|
||||
|
||||
const persistSelectedProjectId = useCallback(
|
||||
async (projectId) => {
|
||||
if (!currentUID) return;
|
||||
if (!currentUID) return
|
||||
try {
|
||||
await usersRef.doc(currentUID).set(
|
||||
{
|
||||
selectedProjectId: projectId || null,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
console.log("update selected project");
|
||||
{ merge: true }
|
||||
)
|
||||
console.log('update selected project')
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"UserDataProvider: unable to persist selectedProjectId",
|
||||
error?.message || error,
|
||||
);
|
||||
'UserDataProvider: unable to persist selectedProjectId',
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
},
|
||||
[currentUID],
|
||||
);
|
||||
[currentUID]
|
||||
)
|
||||
|
||||
const selectProject = useCallback(
|
||||
(projectId) => {
|
||||
const safeId = projectId || null;
|
||||
setSelectedProjectId(safeId);
|
||||
const safeId = projectId || null
|
||||
setSelectedProjectId(safeId)
|
||||
if (!safeId) {
|
||||
setSelectedProject(null);
|
||||
setSelectedProject(null)
|
||||
}
|
||||
|
||||
if (currentUID) {
|
||||
persistSelectedProjectId(safeId).catch((error) => {
|
||||
console.log(
|
||||
"UserDataProvider: persist selectedProjectId failed",
|
||||
error?.message || error,
|
||||
);
|
||||
});
|
||||
console.log('UserDataProvider: persist selectedProjectId failed', error?.message || error)
|
||||
})
|
||||
}
|
||||
},
|
||||
[currentUID, persistSelectedProjectId, setSelectedProject],
|
||||
);
|
||||
[currentUID, persistSelectedProjectId, setSelectedProject]
|
||||
)
|
||||
|
||||
const resetSelectedProject = useCallback(() => {
|
||||
selectProject(null);
|
||||
}, [selectProject]);
|
||||
selectProject(null)
|
||||
}, [selectProject])
|
||||
|
||||
// Ancienne variante de création de projet supprimée pour éviter les doublons.
|
||||
|
||||
const updateProjectData = async (partial = {}, options = { merge: true }) => {
|
||||
if (!selectedProjectId) return;
|
||||
if (!selectedProjectId) return
|
||||
try {
|
||||
await projectsRef.doc(selectedProjectId).set(
|
||||
{
|
||||
...partial,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
options,
|
||||
);
|
||||
options
|
||||
)
|
||||
} catch (e) {
|
||||
console.log("updateProjectData error", e?.message);
|
||||
throw e;
|
||||
console.log('updateProjectData error', e?.message)
|
||||
throw e
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const createNewProject = async ({ hasLyrics = false } = {}) => {
|
||||
if (!ensureAuthenticated(currentUID)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const existingProjectsCount = Array.isArray(userProjects)
|
||||
? userProjects.length
|
||||
: 0;
|
||||
const hasUnlimitedCreation = true;
|
||||
const existingProjectsCount = Array.isArray(userProjects) ? userProjects.length : 0
|
||||
const hasUnlimitedCreation = true
|
||||
|
||||
// currentUserDoc?.isPremium; mettre en place en prod
|
||||
if (!hasUnlimitedCreation && existingProjectsCount >= 1) {
|
||||
showPremiumRequiredAlert();
|
||||
return null;
|
||||
showPremiumRequiredAlert()
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
const artistDisplayName = getUserPreferredArtistName(
|
||||
currentUserDoc || currentUserData || {},
|
||||
);
|
||||
await setIsLoading(true)
|
||||
const artistDisplayName = getUserPreferredArtistName(currentUserDoc || currentUserData || {})
|
||||
const payload = {
|
||||
userId: currentUID || null,
|
||||
userName: artistDisplayName,
|
||||
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
};
|
||||
const { id } = await projectsRef.add(payload);
|
||||
selectProject(id);
|
||||
return id;
|
||||
}
|
||||
const { id } = await projectsRef.add(payload)
|
||||
selectProject(id)
|
||||
return id
|
||||
} catch (e) {
|
||||
console.log("createNewProject error", e?.message);
|
||||
throw e;
|
||||
console.log('createNewProject error', e?.message)
|
||||
throw e
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
await setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
const followUser = async (userId) => {
|
||||
try {
|
||||
if (!ensureAuthenticated(currentUID)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (userId) {
|
||||
await usersRef.doc(userId).set(
|
||||
@@ -261,20 +237,20 @@ export default ({ children }) => {
|
||||
followedBy: arrayUnion(currentUID),
|
||||
lastFollowersUpdateAt: new Date(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
setTooltip({ type: "success", text: "Abonnement mis à jour" });
|
||||
{ merge: true }
|
||||
)
|
||||
setTooltip({ type: 'success', text: 'Abonnement mis à jour' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setTooltip({ type: "error", text: e?.message || "Action impossible" });
|
||||
console.log(e)
|
||||
setTooltip({ type: 'error', text: e?.message || 'Action impossible' })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const unfollowUser = async (userId) => {
|
||||
try {
|
||||
if (!ensureAuthenticated(currentUID)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (userId) {
|
||||
await usersRef.doc(userId).set(
|
||||
@@ -282,44 +258,44 @@ export default ({ children }) => {
|
||||
followedBy: arrayRemove(currentUID),
|
||||
lastFollowersUpdateAt: new Date(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Vous ne suivez plus cet utilisateur",
|
||||
});
|
||||
type: 'success',
|
||||
text: 'Vous ne suivez plus cet utilisateur',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setTooltip({ type: "error", text: e?.message || "Action impossible" });
|
||||
console.log(e)
|
||||
setTooltip({ type: 'error', text: e?.message || 'Action impossible' })
|
||||
}
|
||||
};
|
||||
}
|
||||
const getUserByUid = async (uid) => {
|
||||
try {
|
||||
const userDoc = await usersRef.doc(uid).get();
|
||||
const userDoc = await usersRef.doc(uid).get()
|
||||
return userDoc.exists
|
||||
? {
|
||||
id: userDoc.id,
|
||||
...userDoc.data(),
|
||||
}
|
||||
: null;
|
||||
id: userDoc.id,
|
||||
...userDoc.data(),
|
||||
}
|
||||
: null
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
console.log(e)
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const updateUserData = async ({ data, shouldSetTooltip = true }) => {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
await setIsLoading(true)
|
||||
|
||||
let dynamicUID = currentUID || null;
|
||||
let dynamicUID = currentUID || null
|
||||
|
||||
let cleanData = {
|
||||
...currentUserData,
|
||||
...data,
|
||||
lastUpdateTimestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
// if (cleanData?.firstName?.length > 2 && cleanData?.lastName?.length > 2) {
|
||||
// cleanData = {
|
||||
@@ -332,7 +308,7 @@ export default ({ children }) => {
|
||||
// }
|
||||
|
||||
if (!checkIfEmailIsValid({ email: cleanData?.email })) {
|
||||
throw new Error("Veuillez renseigner un email valide");
|
||||
throw new Error('Veuillez renseigner un email valide')
|
||||
}
|
||||
|
||||
// let cleanPhoneNumber = formatPhoneNumber({
|
||||
@@ -346,146 +322,133 @@ export default ({ children }) => {
|
||||
// }
|
||||
|
||||
if (!dynamicUID) {
|
||||
const { email, password } = cleanData;
|
||||
const { email, password } = cleanData
|
||||
|
||||
const { user = null } = await firebase
|
||||
.auth()
|
||||
.createUserWithEmailAndPassword(email, password);
|
||||
.createUserWithEmailAndPassword(email, password)
|
||||
|
||||
dynamicUID = user.uid;
|
||||
dynamicUID = user.uid
|
||||
}
|
||||
|
||||
delete cleanData.password;
|
||||
delete cleanData.password
|
||||
|
||||
cleanData = {
|
||||
...cleanData,
|
||||
userID: dynamicUID,
|
||||
};
|
||||
|
||||
if (!cleanData?.createdAt) {
|
||||
cleanData.createdAt = firebase.firestore.FieldValue.serverTimestamp();
|
||||
}
|
||||
|
||||
await usersRef.doc(dynamicUID).set(cleanData, { merge: true });
|
||||
if (!cleanData?.createdAt) {
|
||||
cleanData.createdAt = firebase.firestore.FieldValue.serverTimestamp()
|
||||
}
|
||||
|
||||
await usersRef.doc(dynamicUID).set(cleanData, { merge: true })
|
||||
|
||||
if (shouldSetTooltip) {
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Données enregistrées avec succès",
|
||||
});
|
||||
type: 'success',
|
||||
text: 'Données enregistrées avec succès',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e)
|
||||
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: "Erreur lors de la mise à jour des données utilisateur",
|
||||
});
|
||||
type: 'error',
|
||||
text: 'Erreur lors de la mise à jour des données utilisateur',
|
||||
})
|
||||
|
||||
throw e;
|
||||
throw e
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
await setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onSignOut = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setIsLoading(true)
|
||||
|
||||
await firebase.auth().signOut();
|
||||
await firebase.auth().signOut()
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const isSuperAdmin = currentUserRoles.some((role) => role === "SUPERADMIN");
|
||||
const isSuperAdmin = currentUserRoles.some((role) => role === 'SUPERADMIN')
|
||||
|
||||
useEffect(() => {
|
||||
const remoteSelectedId = currentUserDoc?.selectedProjectId;
|
||||
const remoteSelectedId = currentUserDoc?.selectedProjectId
|
||||
if (!remoteSelectedId) {
|
||||
if (remoteSelectedId === null && selectedProjectId !== null) {
|
||||
setSelectedProjectId(null);
|
||||
setSelectedProject(null);
|
||||
setSelectedProjectId(null)
|
||||
setSelectedProject(null)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (remoteSelectedId !== selectedProjectId) {
|
||||
setSelectedProjectId(remoteSelectedId);
|
||||
setSelectedProjectId(remoteSelectedId)
|
||||
}
|
||||
}, [
|
||||
currentUserDoc?.selectedProjectId,
|
||||
selectedProjectId,
|
||||
setSelectedProject,
|
||||
]);
|
||||
}, [currentUserDoc?.selectedProjectId, selectedProjectId, setSelectedProject])
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(userProjects) || userProjects.length === 0) return;
|
||||
if (selectedProjectId) return;
|
||||
if (!Array.isArray(userProjects) || userProjects.length === 0) return
|
||||
if (selectedProjectId) return
|
||||
|
||||
const remoteSelectedId = currentUserDoc?.selectedProjectId;
|
||||
if (remoteSelectedId !== undefined) return;
|
||||
const remoteSelectedId = currentUserDoc?.selectedProjectId
|
||||
if (remoteSelectedId !== undefined) return
|
||||
|
||||
const fallbackProjectId = userProjects[0]?.id;
|
||||
if (!fallbackProjectId) return;
|
||||
const fallbackProjectId = userProjects[0]?.id
|
||||
if (!fallbackProjectId) return
|
||||
|
||||
selectProject(fallbackProjectId);
|
||||
}, [
|
||||
currentUserDoc?.selectedProjectId,
|
||||
selectProject,
|
||||
selectedProjectId,
|
||||
userProjects,
|
||||
]);
|
||||
selectProject(fallbackProjectId)
|
||||
}, [currentUserDoc?.selectedProjectId, selectProject, selectedProjectId, userProjects])
|
||||
|
||||
const { data: videos } = useDataFromRef({
|
||||
ref: videosRef.doc("fr"),
|
||||
ref: videosRef.doc('fr'),
|
||||
simpleRef: true,
|
||||
});
|
||||
})
|
||||
|
||||
const hasActiveSubscription = useMemo(() => {
|
||||
const pickStatus = (value) => {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed.toLowerCase() : null;
|
||||
};
|
||||
if (typeof value !== 'string') return null
|
||||
const trimmed = value.trim()
|
||||
return trimmed ? trimmed.toLowerCase() : null
|
||||
}
|
||||
|
||||
const statusCandidates = [
|
||||
pickStatus(currentUserDoc?.stripeSubscriptionStatus),
|
||||
pickStatus(currentUserDoc?.stripeSubscription?.status),
|
||||
pickStatus(currentUserDoc?.stripeSubscription?.stripeSubscriptionStatus),
|
||||
pickStatus(currentUserDoc?.stripeSubscription?.metadata?.status),
|
||||
].filter(Boolean);
|
||||
].filter(Boolean)
|
||||
|
||||
if (
|
||||
statusCandidates.some((status) =>
|
||||
ACTIVE_SUBSCRIPTION_STATUSES.has(status),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
if (statusCandidates.some((status) => ACTIVE_SUBSCRIPTION_STATUSES.has(status))) {
|
||||
return true
|
||||
}
|
||||
|
||||
const premiumUntil = currentUserDoc?.premiumUntil;
|
||||
const premiumUntil = currentUserDoc?.premiumUntil
|
||||
if (premiumUntil) {
|
||||
const asDate =
|
||||
premiumUntil?.toDate?.() ||
|
||||
(premiumUntil?.seconds ? new Date(premiumUntil.seconds * 1000) : null);
|
||||
(premiumUntil?.seconds ? new Date(premiumUntil.seconds * 1000) : null)
|
||||
if (asDate && asDate > new Date()) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (currentUserDoc?.premium?.active) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
const hasPremiumLevel =
|
||||
typeof currentUserDoc?.premiumLevel === "string" &&
|
||||
currentUserDoc.premiumLevel.trim().length > 0;
|
||||
typeof currentUserDoc?.premiumLevel === 'string' &&
|
||||
currentUserDoc.premiumLevel.trim().length > 0
|
||||
|
||||
return hasPremiumLevel;
|
||||
}, [currentUserDoc]);
|
||||
return hasPremiumLevel
|
||||
}, [currentUserDoc])
|
||||
|
||||
return (
|
||||
<UserDataContext.Provider
|
||||
@@ -530,14 +493,14 @@ export default ({ children }) => {
|
||||
>
|
||||
{children}
|
||||
</UserDataContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const useUserData = () => {
|
||||
return useContext(UserDataContext);
|
||||
};
|
||||
return useContext(UserDataContext)
|
||||
}
|
||||
|
||||
// Convenience alias returning the same context, as requested
|
||||
export const useUser = () => {
|
||||
return useContext(UserDataContext);
|
||||
};
|
||||
return useContext(UserDataContext)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,41 @@
|
||||
import React, { useRef, useEffect, useMemo, createContext } from "reactn";
|
||||
import React, { useRef, useEffect, useMemo, createContext } from 'reactn'
|
||||
|
||||
import BottomSheetContainer from "../components/BottomSheetContainer";
|
||||
import { WebView } from "../components/WebView";
|
||||
import BottomSheetContainer from '../components/BottomSheetContainer'
|
||||
import { WebView } from '../components/WebView'
|
||||
|
||||
import { Palette } from "../styles";
|
||||
import { useContext } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { Palette } from '../styles'
|
||||
import { useContext } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
export const WebViewContext = createContext();
|
||||
export const WebViewContext = createContext()
|
||||
|
||||
export default ({ children }) => {
|
||||
const [webViewUrl, setWebViewUrl] = React.useState(Platform.OS == "ios" ? null : undefined);
|
||||
const [webViewUrl, setWebViewUrl] = React.useState(Platform.OS == 'ios' ? null : undefined)
|
||||
|
||||
const snapPoints = useMemo(() => ["70%", "90%"], []);
|
||||
const bottomSheetRef = useRef(null);
|
||||
const snapPoints = useMemo(() => ['70%', '90%'], [])
|
||||
const bottomSheetRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (webViewUrl) {
|
||||
bottomSheetRef.current?.expand();
|
||||
bottomSheetRef.current?.expand()
|
||||
} else {
|
||||
bottomSheetRef.current?.close();
|
||||
bottomSheetRef.current?.close()
|
||||
}
|
||||
}, [webViewUrl]);
|
||||
}, [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
|
||||
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 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;
|
||||
? `https://docs.google.com/gview?embedded=true&url=${encodeURIComponent(webViewUrl)}`
|
||||
: webViewUrl
|
||||
|
||||
return (
|
||||
<WebViewContext.Provider value={{ webViewUrl, setWebViewUrl }}>
|
||||
@@ -50,7 +46,7 @@ export default ({ children }) => {
|
||||
snapPoints={snapPoints}
|
||||
onChange={(index) => {
|
||||
if (index === -1) {
|
||||
setWebViewUrl(Platform.OS == "ios" ? null : undefined);
|
||||
setWebViewUrl(Platform.OS == 'ios' ? null : undefined)
|
||||
}
|
||||
}}
|
||||
index={-1}
|
||||
@@ -67,7 +63,7 @@ export default ({ children }) => {
|
||||
/>
|
||||
</BottomSheetContainer>
|
||||
</WebViewContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const useWebView = () => useContext(WebViewContext);
|
||||
export const useWebView = () => useContext(WebViewContext)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import SharedProviders from "./SharedProviders";
|
||||
import React from 'react'
|
||||
import SharedProviders from './SharedProviders'
|
||||
|
||||
export default ({ children }) => {
|
||||
return (
|
||||
// <ShakeReportProvider projectID="musicland" defaultEmail="user@email.com">
|
||||
<SharedProviders>{children}</SharedProviders>
|
||||
// </ShakeReportProvider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import LoadingProvider from "./LoadingProvider";
|
||||
import TooltipProvider from "./TooltipProvider";
|
||||
import LoadingProvider from './LoadingProvider'
|
||||
import TooltipProvider from './TooltipProvider'
|
||||
|
||||
import SharedProviders from "./SharedProviders";
|
||||
import SharedProviders from './SharedProviders'
|
||||
|
||||
export default ({ children }) => {
|
||||
return (
|
||||
@@ -12,5 +12,5 @@ export default ({ children }) => {
|
||||
<SharedProviders>{children}</SharedProviders>
|
||||
</TooltipProvider>
|
||||
</LoadingProvider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user