Files
musicland/src/providers/UserDataProvider.js
T
2025-12-03 16:11:44 +01:00

544 lines
15 KiB
JavaScript

/* 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 { useCallback, useEffect, useMemo, useState } from "react";
import { checkIfEmailIsValid } from "../actions/signupActions";
import { showPremiumRequiredAlert } from "../components/Alert";
import firebase, {
arrayRemove,
arrayUnion,
playlistsRef,
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";
const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK);
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
"trialing",
"active",
"past_due",
"unpaid",
]);
export const UserDataContext = createContext();
export default ({ children }) => {
const { setIsLoading, setTooltip } = useMinuit();
const [pendingUserData, setPendingUserData] = useState({
createdAt: new Date(),
});
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",
simpleRef: true,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
});
// Subscribe to user's projects (musics)
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(PLAYBACK_LIKES_FIELD, "array-contains", currentUID)
: null,
simpleRef: false,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
format: sortByPlaybackLikeDate,
});
// Subscribe to user's playlists
const { data: userPlaylists = [] } = useDataFromRef({
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,
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 persistSelectedProjectId = useCallback(
async (projectId) => {
if (!currentUID) return;
try {
await usersRef.doc(currentUID).set(
{
selectedProjectId: projectId || null,
},
{ merge: true },
);
console.log("update selected project");
} catch (error) {
console.log(
"UserDataProvider: unable to persist selectedProjectId",
error?.message || error,
);
}
},
[currentUID],
);
const selectProject = useCallback(
(projectId) => {
const safeId = projectId || null;
setSelectedProjectId(safeId);
if (!safeId) {
setSelectedProject(null);
}
if (currentUID) {
persistSelectedProjectId(safeId).catch((error) => {
console.log(
"UserDataProvider: persist selectedProjectId failed",
error?.message || error,
);
});
}
},
[currentUID, persistSelectedProjectId, setSelectedProject],
);
const resetSelectedProject = useCallback(() => {
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;
try {
await projectsRef.doc(selectedProjectId).set(
{
...partial,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
options,
);
} catch (e) {
console.log("updateProjectData error", e?.message);
throw e;
}
};
const createNewProject = async ({ hasLyrics = false } = {}) => {
if (!ensureAuthenticated(currentUID)) {
return null;
}
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;
}
try {
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;
} catch (e) {
console.log("createNewProject error", e?.message);
throw e;
} finally {
await setIsLoading(false);
}
};
const followUser = async (userId) => {
try {
if (!ensureAuthenticated(currentUID)) {
return;
}
if (userId) {
await usersRef.doc(userId).set(
{
followedBy: arrayUnion(currentUID),
lastFollowersUpdateAt: new Date(),
},
{ merge: true },
);
setTooltip({ type: "success", text: "Abonnement mis à jour" });
}
} catch (e) {
console.log(e);
setTooltip({ type: "error", text: e?.message || "Action impossible" });
}
};
const unfollowUser = async (userId) => {
try {
if (!ensureAuthenticated(currentUID)) {
return;
}
if (userId) {
await usersRef.doc(userId).set(
{
followedBy: arrayRemove(currentUID),
lastFollowersUpdateAt: new Date(),
},
{ merge: true },
);
setTooltip({
type: "success",
text: "Vous ne suivez plus cet utilisateur",
});
}
} catch (e) {
console.log(e);
setTooltip({ type: "error", text: e?.message || "Action impossible" });
}
};
const getUserByUid = async (uid) => {
try {
const userDoc = await usersRef.doc(uid).get();
return userDoc.exists
? {
id: userDoc.id,
...userDoc.data(),
}
: null;
} catch (e) {
console.log(e);
return null;
}
};
const updateUserData = async ({ data, shouldSetTooltip = true }) => {
try {
await setIsLoading(true);
let dynamicUID = currentUID || null;
let cleanData = {
...currentUserData,
...data,
lastUpdateTimestamp: new Date(),
};
// if (cleanData?.firstName?.length > 2 && cleanData?.lastName?.length > 2) {
// cleanData = {
// ...cleanData,
// firstName: capitalize(cleanData?.firstName?.toLowerCase()),
// lastName: capitalize(cleanData?.lastName?.toLowerCase()),
// };
// } else {
// throw new Error("Veuillez renseigner un prénom et un nom");
// }
if (!checkIfEmailIsValid({ email: cleanData?.email })) {
throw new Error("Veuillez renseigner un email valide");
}
// let cleanPhoneNumber = formatPhoneNumber({
// phoneNumber: cleanData.phoneNumber,
// });
// if (cleanPhoneNumber) {
// cleanData.phoneNumber = cleanPhoneNumber;
// } else {
// throw new Error("Veuillez renseigner un numéro de téléphone valide");
// }
if (!dynamicUID) {
const { email, password } = cleanData;
const { user = null } = await firebase
.auth()
.createUserWithEmailAndPassword(email, password);
dynamicUID = user.uid;
}
delete cleanData.password;
cleanData = {
...cleanData,
userID: dynamicUID,
};
if (!cleanData?.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",
});
}
} catch (e) {
console.log(e);
setTooltip({
type: "error",
text: "Erreur lors de la mise à jour des données utilisateur",
});
throw e;
} finally {
await setIsLoading(false);
}
};
const onSignOut = async () => {
try {
setIsLoading(true);
await firebase.auth().signOut();
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
};
const isSuperAdmin = currentUserRoles.some((role) => role === "SUPERADMIN");
useEffect(() => {
const remoteSelectedId = currentUserDoc?.selectedProjectId;
if (!remoteSelectedId) {
if (remoteSelectedId === null && selectedProjectId !== null) {
setSelectedProjectId(null);
setSelectedProject(null);
}
return;
}
if (remoteSelectedId !== selectedProjectId) {
setSelectedProjectId(remoteSelectedId);
}
}, [
currentUserDoc?.selectedProjectId,
selectedProjectId,
setSelectedProject,
]);
useEffect(() => {
if (!Array.isArray(userProjects) || userProjects.length === 0) return;
if (selectedProjectId) return;
const remoteSelectedId = currentUserDoc?.selectedProjectId;
if (remoteSelectedId !== undefined) return;
const fallbackProjectId = userProjects[0]?.id;
if (!fallbackProjectId) return;
selectProject(fallbackProjectId);
}, [
currentUserDoc?.selectedProjectId,
selectProject,
selectedProjectId,
userProjects,
]);
const { data: videos } = useDataFromRef({
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;
};
const statusCandidates = [
pickStatus(currentUserDoc?.stripeSubscriptionStatus),
pickStatus(currentUserDoc?.stripeSubscription?.status),
pickStatus(currentUserDoc?.stripeSubscription?.stripeSubscriptionStatus),
pickStatus(currentUserDoc?.stripeSubscription?.metadata?.status),
].filter(Boolean);
if (
statusCandidates.some((status) =>
ACTIVE_SUBSCRIPTION_STATUSES.has(status),
)
) {
return true;
}
const premiumUntil = currentUserDoc?.premiumUntil;
if (premiumUntil) {
const asDate =
premiumUntil?.toDate?.() ||
(premiumUntil?.seconds ? new Date(premiumUntil.seconds * 1000) : null);
if (asDate && asDate > new Date()) {
return true;
}
}
if (currentUserDoc?.premium?.active) {
return true;
}
const hasPremiumLevel =
typeof currentUserDoc?.premiumLevel === "string" &&
currentUserDoc.premiumLevel.trim().length > 0;
return hasPremiumLevel;
}, [currentUserDoc]);
return (
<UserDataContext.Provider
value={{
isSuperAdmin,
currentUserRoles,
currentUID,
currentUserData: currentUserDoc,
userProjects,
userProjectsLoading,
userPlaybacks,
userPlaybacksLoading: userProjectsLoading,
userLikedProjects,
userLikedProjectsLoading,
userLikedPlaybacks,
userLikedPlaybacksLoading,
userPlaylists,
selectedProjectId,
selectedProject,
followingCount: userFollowing?.length || 0,
setCurrentUserData,
getUserByUid,
onSignOut,
pendingUserData,
updatePendingUserData,
updateUserData,
followUser,
unfollowUser,
resetSelectedProject,
selectProject,
setSelectedProjectId: selectProject,
updateProjectData,
createNewProject,
videos,
hasActiveSubscription,
}}
>
{children}
</UserDataContext.Provider>
);
};
export const useUserData = () => {
return useContext(UserDataContext);
};
// Convenience alias returning the same context, as requested
export const useUser = () => {
return useContext(UserDataContext);
};