544 lines
15 KiB
JavaScript
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);
|
|
};
|