feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+64 -67
View File
@@ -1,87 +1,84 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useState } from "react";
import { Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { checkIfEmailIsValid } from "../../actions/signupActions";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import firebase, { usersRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import EditInput from "./components/EditInput";
import { BlurView } from 'expo-blur'
import React, { useEffect, useState } from 'react'
import { Platform, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { checkIfEmailIsValid } from '../../actions/signupActions'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import firebase, { usersRef } from '../../config/firebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import EditInput from './components/EditInput'
const ChangeEmailAddress = () => {
const [email, setEmail] = useState("");
const [originalEmail, setOriginalEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [, setTooltip] = useGlobal("_tooltip");
const [currentUserData] = useGlobal("currentUserData");
const [loading, setLoading] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
const [email, setEmail] = useState('')
const [originalEmail, setOriginalEmail] = useState('')
const [emailError, setEmailError] = useState('')
const [, setTooltip] = useGlobal('_tooltip')
const [currentUserData] = useGlobal('currentUserData')
const [loading, setLoading] = useState(false)
const [currentPassword, setCurrentPassword] = useState('')
useEffect(() => {
const authEmail = firebase.auth().currentUser?.email || "";
const profileEmail = (currentUserData?.email || authEmail || "").trim();
setEmail((prev) => (prev ? prev : profileEmail));
setOriginalEmail(profileEmail);
}, [currentUserData?.email]);
const authEmail = firebase.auth().currentUser?.email || ''
const profileEmail = (currentUserData?.email || authEmail || '').trim()
setEmail((prev) => (prev ? prev : profileEmail))
setOriginalEmail(profileEmail)
}, [currentUserData?.email])
const onChangeEmail = (val) => {
setEmail(val);
const trimmed = (val || "").trim();
setEmail(val)
const trimmed = (val || '').trim()
if (trimmed.length === 0) {
setEmailError("");
return;
setEmailError('')
return
}
if (!checkIfEmailIsValid({ email: trimmed })) {
setEmailError("Adresse email invalide");
setEmailError('Adresse email invalide')
} else {
setEmailError("");
setEmailError('')
}
};
}
const onSave = async () => {
const val = (email || "").trim();
if (!val) return;
const val = (email || '').trim()
if (!val) return
try {
setLoading(true);
const user = firebase.auth().currentUser;
const uid = user?.uid;
if (!user || !uid) return;
setLoading(true)
const user = firebase.auth().currentUser
const uid = user?.uid
if (!user || !uid) return
// Reauthenticate user with current password before sensitive update
try {
const credential = firebase.auth.EmailAuthProvider.credential(
user.email,
currentPassword
);
await user.reauthenticateWithCredential(credential);
const credential = firebase.auth.EmailAuthProvider.credential(user.email, currentPassword)
await user.reauthenticateWithCredential(credential)
} catch (reauthErr) {
throw {
code: "auth/wrong-password",
message: "Mot de passe incorrect.",
};
code: 'auth/wrong-password',
message: 'Mot de passe incorrect.',
}
}
await user.updateEmail(val);
await usersRef.doc(uid).set({ email: val }, { merge: true });
setTooltip({ type: "success", text: "Email mis à jour" });
goBack();
await user.updateEmail(val)
await usersRef.doc(uid).set({ email: val }, { merge: true })
setTooltip({ type: 'success', text: 'Email mis à jour' })
goBack()
} catch (e) {
console.log("ChangeEmailAddress error", e?.message);
console.log('ChangeEmailAddress error', e?.message)
const msg =
e?.code === "auth/requires-recent-login"
e?.code === 'auth/requires-recent-login'
? "Veuillez vous reconnecter pour changer l'adresse email"
: e?.message || "Mise à jour impossible";
setTooltip({ type: "error", text: msg });
: e?.message || 'Mise à jour impossible'
setTooltip({ type: 'error', text: msg })
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
headerType="NAVIGATE"
@@ -91,17 +88,17 @@ const ChangeEmailAddress = () => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingHorizontal: 20,
paddingVertical: 23,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
}}
// experimentalBlurMethod={
@@ -138,10 +135,10 @@ const ChangeEmailAddress = () => {
</BlurView>
</View>
<GradientButton
title={loading ? "Chargement..." : "Enregistrer les modifications"}
title={loading ? 'Chargement...' : 'Enregistrer les modifications'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onSave}
disabled={
@@ -153,7 +150,7 @@ const ChangeEmailAddress = () => {
}
/>
</Page>
);
};
)
}
export default ChangeEmailAddress;
export default ChangeEmailAddress
+44 -47
View File
@@ -1,50 +1,47 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import { Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import firebase from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import EditInput from "./components/EditInput";
import { BlurView } from 'expo-blur'
import React, { useState } from 'react'
import { Platform, Pressable, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import firebase from '../../config/firebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import EditInput from './components/EditInput'
const ChangePassword = () => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [loading, setLoading] = useState(false)
const [, setTooltip] = useGlobal('_tooltip')
const onSave = async () => {
const oldVal = (oldPassword || "").trim();
const newVal = (newPassword || "").trim();
if (!oldVal || !newVal) return;
const oldVal = (oldPassword || '').trim()
const newVal = (newPassword || '').trim()
if (!oldVal || !newVal) return
try {
setLoading(true);
const user = firebase.auth().currentUser;
if (!user?.email) return;
setLoading(true)
const user = firebase.auth().currentUser
if (!user?.email) return
const credential = firebase.auth.EmailAuthProvider.credential(
user.email,
oldVal
);
await user.reauthenticateWithCredential(credential);
await user.updatePassword(newVal);
setTooltip({ type: "success", text: "Mot de passe mis à jour" });
navigate(Routes.Settings);
const credential = firebase.auth.EmailAuthProvider.credential(user.email, oldVal)
await user.reauthenticateWithCredential(credential)
await user.updatePassword(newVal)
setTooltip({ type: 'success', text: 'Mot de passe mis à jour' })
navigate(Routes.Settings)
} catch (e) {
console.log("ChangePassword error", e?.message);
const msg = e?.message || "Mise à jour impossible";
setTooltip({ type: "error", text: msg });
console.log('ChangePassword error', e?.message)
const msg = e?.message || 'Mise à jour impossible'
setTooltip({ type: 'error', text: msg })
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
headerType="NAVIGATE"
@@ -54,17 +51,17 @@ const ChangePassword = () => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingHorizontal: 20,
paddingVertical: 16,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
gap: 4,
}}
@@ -100,16 +97,16 @@ const ChangePassword = () => {
</BlurView>
</View>
<GradientButton
title={loading ? "Chargement..." : "Enregistrer les modifications"}
title={loading ? 'Chargement...' : 'Enregistrer les modifications'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onSave}
disabled={loading || !oldPassword?.trim() || !newPassword?.trim()}
/>
</Page>
);
};
)
}
export default ChangePassword;
export default ChangePassword
+75 -83
View File
@@ -1,55 +1,52 @@
import { BlurView } from "expo-blur";
import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useState } from "react";
import { Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import ProfilePicture from "../../components/ProfilePicture";
import firebase, { serverTimestamp, usersRef } from "../../config/firebase";
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import EditInput from "./components/EditInput";
import { BlurView } from 'expo-blur'
import * as ImagePicker from 'expo-image-picker'
import React, { useEffect, useState } from 'react'
import { Platform, Pressable, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import ProfilePicture from '../../components/ProfilePicture'
import firebase, { serverTimestamp, usersRef } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import EditInput from './components/EditInput'
const EditProfile = () => {
const [currentUserData] = useGlobal("currentUserData");
const [, setTooltip] = useGlobal("_tooltip");
const { currentUID } = useUser();
const [pseudo, setPseudo] = useState("");
const [loading, setLoading] = useState(false);
const [updatingPhoto, setUpdatingPhoto] = useState(false);
const [currentUserData] = useGlobal('currentUserData')
const [, setTooltip] = useGlobal('_tooltip')
const { currentUID } = useUser()
const [pseudo, setPseudo] = useState('')
const [loading, setLoading] = useState(false)
const [updatingPhoto, setUpdatingPhoto] = useState(false)
useEffect(() => {
if (currentUserData?.userName && !pseudo) {
setPseudo(currentUserData.userName);
setPseudo(currentUserData.userName)
}
}, [currentUserData?.userName]);
}, [currentUserData?.userName])
const onSave = async () => {
const raw = pseudo || "";
const val = raw.trim();
if (!val) return;
const raw = pseudo || ''
const val = raw.trim()
if (!val) return
try {
setLoading(true);
const uid = firebase.auth().currentUser?.uid;
if (!uid) return;
setLoading(true)
const uid = firebase.auth().currentUser?.uid
if (!uid) return
// Check uniqueness (case-insensitive)
const lower = val.toLowerCase();
const snap = await usersRef
.where("userNameLower", "==", lower)
.limit(1)
.get();
const lower = val.toLowerCase()
const snap = await usersRef.where('userNameLower', '==', lower).limit(1).get()
if (!snap.empty && snap.docs[0].id !== uid) {
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
return;
setTooltip({ text: 'Ce pseudo est déjà pris', type: 'error' })
return
}
await usersRef.doc(uid).set(
@@ -59,43 +56,43 @@ const EditProfile = () => {
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
)
setTooltip({ text: "Pseudo mis à jour", type: "success" });
goBack();
setTooltip({ text: 'Pseudo mis à jour', type: 'success' })
goBack()
} catch (e) {
console.log("EditProfile onSave error", e?.message);
console.log('EditProfile onSave error', e?.message)
setTooltip({
text: e?.message || "Mise à jour impossible",
type: "error",
});
text: e?.message || 'Mise à jour impossible',
type: 'error',
})
} finally {
setLoading(false);
setLoading(false)
}
};
}
const onChangePicture = async () => {
try {
setUpdatingPhoto(true);
const uid = currentUID;
if (!uid) return;
setUpdatingPhoto(true)
const uid = currentUID
if (!uid) return
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 4],
quality: 1,
});
})
const pickedUri = result?.assets?.[0]?.uri || null;
if (!pickedUri) return;
const pickedUri = result?.assets?.[0]?.uri || null
if (!pickedUri) return
const { resultURI = null } = await uploadFileToFirebase({
uri: pickedUri,
path: `users/${uid}/profilePicture.png`,
});
})
if (!resultURI) throw new Error("Téléversement de l'image impossible");
if (!resultURI) throw new Error("Téléversement de l'image impossible")
await usersRef.doc(uid).set(
{
@@ -103,22 +100,22 @@ const EditProfile = () => {
updatedAt: serverTimestamp(),
},
{ merge: true }
);
)
await firebase.auth().currentUser.updateProfile({
photoURL: resultURI,
});
})
setTooltip({ text: "Photo de profil mise à jour", type: "success" });
setTooltip({ text: 'Photo de profil mise à jour', type: 'success' })
} catch (e) {
setTooltip({
text: e?.message || "Erreur changement photo de profil",
type: "error",
});
text: e?.message || 'Erreur changement photo de profil',
type: 'error',
})
} finally {
setUpdatingPhoto(false);
setUpdatingPhoto(false)
}
};
}
return (
<Page
headerType="NAVIGATE"
@@ -127,29 +124,29 @@ const EditProfile = () => {
paddingBottom: gutters * 2,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingVertical: 14,
backgroundColor: Palette.glass,
paddingHorizontal: 20,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
gap: 24,
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Pressable style={{ alignItems: "center" }} onPress={onChangePicture}>
<Pressable style={{ alignItems: 'center' }} onPress={onChangePicture}>
<ProfilePicture
uri={currentUserData?.profilePictureURL || null}
size={108}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
<Text
style={{
@@ -158,34 +155,29 @@ const EditProfile = () => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{updatingPhoto ? "Chargement..." : "Modifier la photo"}
{updatingPhoto ? 'Chargement...' : 'Modifier la photo'}
</Text>
</Pressable>
<EditInput
label="Pseudo"
placeholder="Pseudo"
value={pseudo}
setValue={setPseudo}
/>
<EditInput label="Pseudo" placeholder="Pseudo" value={pseudo} setValue={setPseudo} />
<GradientButton
title={loading ? "Chargement..." : "Enregistrer les modifications"}
title={loading ? 'Chargement...' : 'Enregistrer les modifications'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onSave}
disabled={
loading ||
updatingPhoto ||
!pseudo?.trim() ||
pseudo?.trim() === (currentUserData?.userName || "")
pseudo?.trim() === (currentUserData?.userName || '')
}
/>
</BlurView>
</View>
</Page>
);
};
)
}
export default EditProfile;
export default EditProfile
+51 -69
View File
@@ -1,25 +1,25 @@
import React, { useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { useUser } from "../../providers/UserDataProvider";
import { usersRef } from "../../config/firebase";
import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId";
import useDataFromRef from "../../hooks/useDataFromRef";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { navigate } from "../../navigation/NavigationService";
import { getArtistDisplayName } from "../../utils/artistName";
import ProfilePicture from "../../components/ProfilePicture";
import React, { useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import Page from '../../layouts/Page'
import { background } from '../../assets'
import { Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { useUser } from '../../providers/UserDataProvider'
import { usersRef } from '../../config/firebase'
import useDataFromArrayId from 'react-native-minuit/src/hooks/useDataFromArrayId'
import useDataFromRef from '../../hooks/useDataFromRef'
import { Routes } from '../../navigation'
import { useRoute } from '@react-navigation/native'
import { navigate } from '../../navigation/NavigationService'
import { getArtistDisplayName } from '../../utils/artistName'
import ProfilePicture from '../../components/ProfilePicture'
const Follows = () => {
const { params } = useRoute();
const initial = params?.selected || "Abonnés"; // "Abonnés" | "Abonnements"
const targetUserId = params?.userId || null;
const [selected, setSelected] = useState(initial);
const { currentUID, currentUserData } = useUser();
const { params } = useRoute()
const initial = params?.selected || 'Abonnés' // "Abonnés" | "Abonnements"
const targetUserId = params?.userId || null
const [selected, setSelected] = useState(initial)
const { currentUID, currentUserData } = useUser()
// Abonnés (followers): docs for IDs in currentUserData.followedBy
// If viewing another user's profile, subscribe to that user's doc
@@ -29,48 +29,39 @@ const Follows = () => {
listener: true,
condition: !!targetUserId,
refreshArray: [targetUserId],
});
})
const followerIds = useMemo(() => {
if (targetUserId) {
return Array.isArray(targetUserDoc?.followedBy)
? targetUserDoc.followedBy
: [];
return Array.isArray(targetUserDoc?.followedBy) ? targetUserDoc.followedBy : []
}
return Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy
: [];
return Array.isArray(currentUserData?.followedBy) ? currentUserData.followedBy : []
}, [
targetUserId,
targetUserDoc?.followedBy?.length || 0,
currentUserData?.followedBy?.length || 0,
]);
const { data: followers = [], loading: followersLoading } =
useDataFromArrayId({
ref: usersRef,
arrayId: followerIds,
condition: followerIds.length > 0,
refreshArray: [followerIds.length],
});
])
const { data: followers = [], loading: followersLoading } = useDataFromArrayId({
ref: usersRef,
arrayId: followerIds,
condition: followerIds.length > 0,
refreshArray: [followerIds.length],
})
// Abonnements (following): users whose followedBy includes currentUID
const { data: following = [], loading: followingLoading } = useDataFromRef({
ref:
targetUserId || currentUID
? usersRef.where(
"followedBy",
"array-contains",
targetUserId || currentUID,
)
? usersRef.where('followedBy', 'array-contains', targetUserId || currentUID)
: null,
simpleRef: false,
listener: true,
condition: !!(targetUserId || currentUID),
refreshArray: [targetUserId || currentUID],
});
})
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<View style={{ flex: 1, alignItems: 'center', paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
@@ -82,10 +73,10 @@ const Follows = () => {
{text}
</Text>
</View>
);
)
const UserRow = ({ user }) => {
const displayName = getArtistDisplayName(user, "Utilisateur");
const displayName = getArtistDisplayName(user, 'Utilisateur')
return (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
@@ -94,7 +85,7 @@ const Follows = () => {
<ProfilePicture
uri={user?.profilePictureURL || null}
size={60}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
<Text
style={{
@@ -106,36 +97,31 @@ const Follows = () => {
{displayName}
</Text>
</Pressable>
);
};
)
}
const list = selected === "Abonnés" ? followers : following;
const loading = selected === "Abonnés" ? followersLoading : followingLoading;
const list = selected === 'Abonnés' ? followers : following
const loading = selected === 'Abonnés' ? followersLoading : followingLoading
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.profileBG}
title={"Abonnés et abonnements"}
title={'Abonnés et abonnements'}
contentContainerStyle={{ paddingBottom: 20 }}
>
<View style={{ gap: 12 }}>
<View style={{ ...Style.containerRow, gap: 6 }}>
{["Abonnés", "Abonnements"].map((item) => (
<Pressable
key={item}
onPress={() => setSelected(item)}
style={{ flex: 1 }}
>
{['Abonnés', 'Abonnements'].map((item) => (
<Pressable key={item} onPress={() => setSelected(item)} style={{ flex: 1 }}>
<View
style={{
height: 30,
borderRadius: 100,
overflow: "hidden",
backgroundColor:
selected === item ? "#FFFFFF22" : Palette.glass,
alignItems: "center",
justifyContent: "center",
overflow: 'hidden',
backgroundColor: selected === item ? '#FFFFFF22' : Palette.glass,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
@@ -155,16 +141,12 @@ const Follows = () => {
{Array.isArray(list) && list.length > 0 ? (
list.map((u) => <UserRow key={u.id} user={u} />)
) : !loading ? (
<EmptyText
text={
selected === "Abonnés" ? "Aucun abonné" : "Aucun abonnement"
}
/>
<EmptyText text={selected === 'Abonnés' ? 'Aucun abonné' : 'Aucun abonnement'} />
) : null}
</View>
</View>
</Page>
);
};
)
}
export default Follows;
export default Follows
+18 -18
View File
@@ -1,18 +1,18 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import { Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import AppCheckbox from "../../components/AppCheckbox";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { BlurView } from 'expo-blur'
import React, { useState } from 'react'
import { Platform, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import AppCheckbox from '../../components/AppCheckbox'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
const LANGUAGE = ["Anglais", "Français", "Chinois", "Japonais", "Coreen"];
const LANGUAGE = ['Anglais', 'Français', 'Chinois', 'Japonais', 'Coreen']
const Language = () => {
const [selected, setSelected] = useState("Français");
const [selected, setSelected] = useState('Français')
return (
<Page
@@ -23,19 +23,19 @@ const Language = () => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingVertical: 10,
paddingHorizontal: 20,
backgroundColor: Palette.glass,
gap: 10,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -63,7 +63,7 @@ const Language = () => {
</BlurView>
</View>
</Page>
);
};
)
}
export default Language;
export default Language
+275 -347
View File
@@ -1,431 +1,394 @@
import { useFocusEffect } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from "react";
import { Alert, Platform, StyleSheet, Text, View } from "react-native";
import BorderGradientButton from "../../components/BorderGradientButton";
import CreditAmount from "../../components/CreditAmount";
import { background } from "../../assets";
import { getFunctionsClient } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation/Routes";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { formatDate, toDate } from "../../utils/dateFormatting";
import { Image as ExpoImage } from "expo-image";
import { isWeb } from "../../hooks/useLayoutType";
import ClubAdvantagesCard from "./components/ClubAdvantagesCard";
import { useFocusEffect } from '@react-navigation/native'
import React, { useCallback, useMemo, useState } from 'react'
import { Alert, Platform, StyleSheet, Text, View } from 'react-native'
import BorderGradientButton from '../../components/BorderGradientButton'
import CreditAmount from '../../components/CreditAmount'
import { background } from '../../assets'
import { getFunctionsClient } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation/Routes'
import { useUserData } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { formatDate, toDate } from '../../utils/dateFormatting'
import { Image as ExpoImage } from 'expo-image'
import { isWeb } from '../../hooks/useLayoutType'
import ClubAdvantagesCard from './components/ClubAdvantagesCard'
const FUNCTIONS_REGION = "europe-west1";
const FUNCTIONS_REGION = 'europe-west1'
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
"trialing",
"active",
"past_due",
"unpaid",
]);
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['trialing', 'active', 'past_due', 'unpaid'])
const STATUS_LABELS = {
trialing: "Période d'essai",
active: "Actif",
past_due: "Paiement en attente",
unpaid: "Impaye",
canceled: "Annulé",
incomplete: "Incomplet",
incomplete_expired: "Expiré",
paused: "En pause",
};
active: 'Actif',
past_due: 'Paiement en attente',
unpaid: 'Impaye',
canceled: 'Annulé',
incomplete: 'Incomplet',
incomplete_expired: 'Expiré',
paused: 'En pause',
}
const PLAN_LABELS = {
starter: "Starter",
pro: "Pro",
premium: "Premium",
};
starter: 'Starter',
pro: 'Pro',
premium: 'Premium',
}
const PERIOD_LABELS = {
monthly: "Mensuel",
annual: "Annuel",
};
monthly: 'Mensuel',
annual: 'Annuel',
}
const PAGE_BACKGROUND_COLOR = "#252438";
const PAGE_BACKGROUND_COLOR = '#252438'
const getStatusColors = (status) => {
switch (status) {
case "trialing":
case "active":
case 'trialing':
case 'active':
return {
text: Palette.green,
background: Palette.transparentGreen,
};
case "past_due":
case "unpaid":
}
case 'past_due':
case 'unpaid':
return {
text: Palette.orange,
background: Palette.transparentOrange,
};
case "canceled":
case "incomplete_expired":
}
case 'canceled':
case 'incomplete_expired':
return {
text: Palette.red,
background: Palette.transparentRed,
};
}
default:
return {
text: Palette.grayMid,
background: Palette.ultraLightWhite,
};
}
}
};
}
const capitalize = (value) => {
if (typeof value !== "string" || !value) {
return null;
if (typeof value !== 'string' || !value) {
return null
}
return value.charAt(0).toUpperCase() + value.slice(1);
};
return value.charAt(0).toUpperCase() + value.slice(1)
}
const SCHEDULE_COMPARISON_HOUR = 2;
const SCHEDULE_COMPARISON_MINUTE = 30;
const SCHEDULE_DISPLAY_HOUR = 3;
const SCHEDULE_DISPLAY_MINUTE = 30;
const SCHEDULE_COMPARISON_HOUR = 2
const SCHEDULE_COMPARISON_MINUTE = 30
const SCHEDULE_DISPLAY_HOUR = 3
const SCHEDULE_DISPLAY_MINUTE = 30
const addMonthsSafe = (date, months = 1) => {
if (!(date instanceof Date) || !Number.isFinite(months)) {
return null;
return null
}
const result = new Date(date.getTime());
const initialDay = result.getDate();
result.setMonth(result.getMonth() + months);
const result = new Date(date.getTime())
const initialDay = result.getDate()
result.setMonth(result.getMonth() + months)
if (result.getDate() !== initialDay) {
result.setDate(0);
result.setDate(0)
}
return result;
};
return result
}
const addDaysSafe = (date, days = 1) => {
if (!(date instanceof Date) || !Number.isFinite(days)) {
return null;
return null
}
const result = new Date(date.getTime());
result.setDate(result.getDate() + days);
return result;
};
const result = new Date(date.getTime())
result.setDate(result.getDate() + days)
return result
}
const alignToScheduleTime = (date) => {
if (!(date instanceof Date)) {
return null;
return null
}
const aligned = new Date(date.getTime());
aligned.setHours(SCHEDULE_DISPLAY_HOUR, SCHEDULE_DISPLAY_MINUTE, 0, 0);
return aligned;
};
const aligned = new Date(date.getTime())
aligned.setHours(SCHEDULE_DISPLAY_HOUR, SCHEDULE_DISPLAY_MINUTE, 0, 0)
return aligned
}
const computeFirstAnnualGrantFromCreation = (creationDate) => {
if (!(creationDate instanceof Date)) {
return null;
return null
}
const cutoff = new Date(creationDate.getTime());
cutoff.setHours(SCHEDULE_COMPARISON_HOUR, SCHEDULE_COMPARISON_MINUTE, 0, 0);
const cutoff = new Date(creationDate.getTime())
cutoff.setHours(SCHEDULE_COMPARISON_HOUR, SCHEDULE_COMPARISON_MINUTE, 0, 0)
let base = null;
let base = null
if (creationDate <= cutoff) {
base = addMonthsSafe(creationDate, 1);
base = addMonthsSafe(creationDate, 1)
} else {
base = addDaysSafe(creationDate, 1);
base = addDaysSafe(creationDate, 1)
}
if (!base) {
return null;
return null
}
return alignToScheduleTime(base);
};
return alignToScheduleTime(base)
}
const ManageSubscription = ({ navigation }) => {
const { currentUserData } = useUserData() || {};
const [isCancelling, setIsCancelling] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const [remoteSubscription, setRemoteSubscription] = useState(null);
const [remoteError, setRemoteError] = useState(null);
const [isFetchingRemote, setIsFetchingRemote] = useState(false);
const [refreshToken, setRefreshToken] = useState(0);
const { currentUserData } = useUserData() || {}
const [isCancelling, setIsCancelling] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const [successMessage, setSuccessMessage] = useState(null)
const [remoteSubscription, setRemoteSubscription] = useState(null)
const [remoteError, setRemoteError] = useState(null)
const [isFetchingRemote, setIsFetchingRemote] = useState(false)
const [refreshToken, setRefreshToken] = useState(0)
const stripeCustomerId = currentUserData?.stripeCustomerId || null;
const stripeCustomerId = currentUserData?.stripeCustomerId || null
const localSubscriptionId =
currentUserData?.stripeSubscription?.id ||
currentUserData?.stripeSubscription?.subscriptionId ||
null;
const localSubscription = currentUserData?.stripeSubscription || null;
null
const localSubscription = currentUserData?.stripeSubscription || null
const triggerRefresh = useCallback(() => {
setRefreshToken((value) => value + 1);
setIsFetchingRemote(true);
}, []);
setRefreshToken((value) => value + 1)
setIsFetchingRemote(true)
}, [])
useFocusEffect(
useCallback(() => {
triggerRefresh();
}, [triggerRefresh]),
);
triggerRefresh()
}, [triggerRefresh])
)
React.useEffect(() => {
let isMounted = true;
let isMounted = true
const run = async () => {
const hasLookupContext =
Boolean(stripeCustomerId) || Boolean(localSubscriptionId);
const hasLookupContext = Boolean(stripeCustomerId) || Boolean(localSubscriptionId)
if (!hasLookupContext) {
if (isMounted) {
setRemoteSubscription(null);
setRemoteError(null);
setIsFetchingRemote(false);
setRemoteSubscription(null)
setRemoteError(null)
setIsFetchingRemote(false)
}
return;
return
}
setIsFetchingRemote(true);
setRemoteError(null);
setIsFetchingRemote(true)
setRemoteError(null)
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-getActiveSubscription",
);
const { data } = await callable();
'subscription-getActiveSubscription'
)
const { data } = await callable()
if (!isMounted) {
return;
return
}
setRemoteSubscription(data?.subscription || null);
setRemoteSubscription(data?.subscription || null)
} catch (error) {
console.warn(
"[ManageSubscription] fetch subscription error",
error?.message || error,
);
console.warn('[ManageSubscription] fetch subscription error', error?.message || error)
if (isMounted) {
const message =
error?.message ||
"Impossible de mettre à jour les informations d'abonnement.";
setRemoteError(message);
error?.message || "Impossible de mettre à jour les informations d'abonnement."
setRemoteError(message)
}
} finally {
if (isMounted) {
setIsFetchingRemote(false);
setIsFetchingRemote(false)
}
}
};
}
run();
run()
return () => {
isMounted = false;
};
}, [stripeCustomerId, localSubscriptionId, refreshToken]);
isMounted = false
}
}, [stripeCustomerId, localSubscriptionId, refreshToken])
const subscriptionInfo = useMemo(() => {
const subscriptionSources = [remoteSubscription, localSubscription].filter(
Boolean,
);
const subscriptionSources = [remoteSubscription, localSubscription].filter(Boolean)
const pickString = (value) => {
if (typeof value !== "string") {
return null;
if (typeof value !== 'string') {
return null
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
};
const trimmed = value.trim()
return trimmed ? trimmed : null
}
const pickFromSources = (resolver) => {
for (const source of subscriptionSources) {
if (!source) {
continue;
continue
}
const value = resolver(source);
const value = resolver(source)
if (value !== undefined && value !== null) {
return value;
return value
}
}
return null;
};
return null
}
const pickDateFromSources = (...resolvers) => {
for (const source of subscriptionSources) {
if (!source) {
continue;
continue
}
for (const resolveValue of resolvers) {
const date = toDate(resolveValue(source));
const date = toDate(resolveValue(source))
if (date) {
return date;
return date
}
}
}
return null;
};
return null
}
const statusSource =
pickString(currentUserData?.stripeSubscriptionStatus) ||
pickString(pickFromSources((source) => source?.status)) ||
pickString(
pickFromSources((source) => source?.stripeSubscriptionStatus),
) ||
pickString(pickFromSources((source) => source?.stripeSubscriptionStatus)) ||
pickString(pickFromSources((source) => source?.metadata?.status)) ||
null;
null
const status = statusSource ? statusSource.toLowerCase() : null;
const status = statusSource ? statusSource.toLowerCase() : null
const cancelAtPeriodEnd = subscriptionSources.some(
(source) =>
source?.cancelAtPeriodEnd === true ||
source?.cancel_at_period_end === true,
);
(source) => source?.cancelAtPeriodEnd === true || source?.cancel_at_period_end === true
)
const resolveLevelSource = () => {
const candidates = [
pickString(pickFromSources((source) => source?.level)),
pickString(currentUserData?.premiumLevel),
pickString(pickFromSources((source) => source?.metadata?.level)),
pickString(
pickFromSources((source) => source?.metadata?.subscriptionLevel),
),
];
return candidates.find(Boolean) || null;
};
pickString(pickFromSources((source) => source?.metadata?.subscriptionLevel)),
]
return candidates.find(Boolean) || null
}
const resolvePeriodSource = () => {
const candidates = [
pickString(pickFromSources((source) => source?.billingPeriod)),
pickString(currentUserData?.premiumBillingPeriod),
pickString(
pickFromSources((source) => source?.metadata?.billingPeriod),
),
pickString(
pickFromSources(
(source) => source?.metadata?.subscriptionBillingPeriod,
),
),
];
return candidates.find(Boolean) || null;
};
pickString(pickFromSources((source) => source?.metadata?.billingPeriod)),
pickString(pickFromSources((source) => source?.metadata?.subscriptionBillingPeriod)),
]
return candidates.find(Boolean) || null
}
const levelSource = resolveLevelSource();
const periodSource = resolvePeriodSource();
const levelSource = resolveLevelSource()
const periodSource = resolvePeriodSource()
const level = levelSource ? levelSource.toLowerCase() : null;
const billingPeriod = periodSource ? periodSource.toLowerCase() : null;
const level = levelSource ? levelSource.toLowerCase() : null
const billingPeriod = periodSource ? periodSource.toLowerCase() : null
const isAnnual = billingPeriod === "annual";
const isAnnual = billingPeriod === 'annual'
const planLabelParts = [];
const planLabelParts = []
if (PLAN_LABELS[level]) {
planLabelParts.push(PLAN_LABELS[level]);
planLabelParts.push(PLAN_LABELS[level])
} else if (level) {
planLabelParts.push(capitalize(level));
planLabelParts.push(capitalize(level))
}
if (PERIOD_LABELS[billingPeriod]) {
planLabelParts.push(PERIOD_LABELS[billingPeriod]);
planLabelParts.push(PERIOD_LABELS[billingPeriod])
} else if (billingPeriod) {
planLabelParts.push(capitalize(billingPeriod));
planLabelParts.push(capitalize(billingPeriod))
}
const planLabel =
planLabelParts.length > 0
? planLabelParts.join(" · ")
: "Abonnement Musicland";
planLabelParts.length > 0 ? planLabelParts.join(' · ') : 'Abonnement Musicland'
const currentPeriodEndDate = pickDateFromSources(
(source) => source?.currentPeriodEnd,
(source) => source?.current_period_end,
);
(source) => source?.current_period_end
)
const createdAtDate = pickDateFromSources(
(source) => source?.created,
(source) => source?.createdAt,
(source) => source?.created_at,
);
(source) => source?.created_at
)
const statusLabelBase =
STATUS_LABELS[status] ||
(status ? status.replace(/_/g, " ").toLowerCase() : null);
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null;
const statusColors = getStatusColors(status);
STATUS_LABELS[status] || (status ? status.replace(/_/g, ' ').toLowerCase() : null)
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null
const statusColors = getStatusColors(status)
const subscriptionId =
pickString(pickFromSources((source) => source?.id)) ||
pickString(pickFromSources((source) => source?.subscriptionId)) ||
pickString(currentUserData?.stripeSubscription?.subscriptionId) ||
null;
null
const hasAnySubscription = Boolean(subscriptionId);
const hasActiveSubscription =
hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status);
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd;
const hasAnySubscription = Boolean(subscriptionId)
const hasActiveSubscription = hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status)
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd
const periodEndLabel = currentPeriodEndDate
? formatDate(currentPeriodEndDate)
: "—";
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null;
const periodEndLabel = currentPeriodEndDate ? formatDate(currentPeriodEndDate) : '—'
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null
let coinsPerMonth = pickFromSources((source) => source?.coinsPerMonth);
let coinsPerMonth = pickFromSources((source) => source?.coinsPerMonth)
if (
coinsPerMonth === null &&
typeof currentUserData?.subscriptionCoinsPerMonth === "number"
) {
const userCoins = currentUserData.subscriptionCoinsPerMonth;
coinsPerMonth = Number.isFinite(userCoins) ? userCoins : null;
if (coinsPerMonth === null && typeof currentUserData?.subscriptionCoinsPerMonth === 'number') {
const userCoins = currentUserData.subscriptionCoinsPerMonth
coinsPerMonth = Number.isFinite(userCoins) ? userCoins : null
}
const normalizedCoins =
typeof coinsPerMonth === "number" && Number.isFinite(coinsPerMonth)
typeof coinsPerMonth === 'number' && Number.isFinite(coinsPerMonth)
? Math.round(coinsPerMonth)
: null;
: null
const grantStrategy =
typeof currentUserData?.subscriptionGrantStrategy === "string"
typeof currentUserData?.subscriptionGrantStrategy === 'string'
? currentUserData.subscriptionGrantStrategy
: null;
const isUpfrontGrant = grantStrategy === "upfront";
: null
const isUpfrontGrant = grantStrategy === 'upfront'
const nextGrantTimestamp = isUpfrontGrant
? null
: currentUserData?.subscriptionNextGrantAt ||
currentUserData?.subscriptionGrantNextAt ||
null;
const nextGrantRawDate = toDate(nextGrantTimestamp);
const now = new Date();
: currentUserData?.subscriptionNextGrantAt || currentUserData?.subscriptionGrantNextAt || null
const nextGrantRawDate = toDate(nextGrantTimestamp)
const now = new Date()
const alignedStoredNextGrant = alignToScheduleTime(nextGrantRawDate);
const alignedStoredNextGrant = alignToScheduleTime(nextGrantRawDate)
const futureStoredNextGrant =
alignedStoredNextGrant &&
alignedStoredNextGrant.getTime() >= now.getTime()
alignedStoredNextGrant && alignedStoredNextGrant.getTime() >= now.getTime()
? alignedStoredNextGrant
: null;
: null
const fallbackInitialGrant =
!isUpfrontGrant && isAnnual && createdAtDate
? computeFirstAnnualGrantFromCreation(createdAtDate)
: null;
: null
const futureFallbackGrant =
fallbackInitialGrant && fallbackInitialGrant.getTime() >= now.getTime()
? fallbackInitialGrant
: null;
: null
let resolvedNextGrantDate =
futureStoredNextGrant || futureFallbackGrant || null;
let resolvedNextGrantDate = futureStoredNextGrant || futureFallbackGrant || null
if (futureStoredNextGrant && futureFallbackGrant) {
resolvedNextGrantDate =
futureFallbackGrant.getTime() < futureStoredNextGrant.getTime()
? futureFallbackGrant
: futureStoredNextGrant;
: futureStoredNextGrant
}
const nextGrantLabel =
resolvedNextGrantDate && isAnnual && !isUpfrontGrant
? formatDate(resolvedNextGrantDate)
: null;
: null
const helperMessages = [];
const helperMessages = []
if (!hasActiveSubscription && hasAnySubscription) {
helperMessages.push(
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment.",
);
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment."
)
}
const helperMessage = helperMessages.join("\n");
const helperMessage = helperMessages.join('\n')
return {
hasAnySubscription,
@@ -436,8 +399,7 @@ const ManageSubscription = ({ navigation }) => {
statusLabel,
statusColors,
planLabel,
billingPeriodLabel:
PERIOD_LABELS[billingPeriod] || capitalize(billingPeriod),
billingPeriodLabel: PERIOD_LABELS[billingPeriod] || capitalize(billingPeriod),
periodEndLabel,
createdAtLabel,
helperMessage: helperMessage || null,
@@ -447,105 +409,97 @@ const ManageSubscription = ({ navigation }) => {
isAnnual,
nextGrantDate: resolvedNextGrantDate,
nextGrantLabel,
};
}, [currentUserData, localSubscription, remoteSubscription]);
}
}, [currentUserData, localSubscription, remoteSubscription])
const handleOpenPlans = useCallback(() => {
const params =
subscriptionInfo.level && typeof subscriptionInfo.level === "string"
subscriptionInfo.level && typeof subscriptionInfo.level === 'string'
? { pack: subscriptionInfo.level }
: undefined;
navigation.navigate(Routes.Payments, params);
}, [navigation, subscriptionInfo.level]);
: undefined
navigation.navigate(Routes.Payments, params)
}, [navigation, subscriptionInfo.level])
const performCancellation = useCallback(async () => {
setIsCancelling(true);
setErrorMessage(null);
setSuccessMessage(null);
setIsCancelling(true)
setErrorMessage(null)
setSuccessMessage(null)
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-cancelActiveSubscription",
);
'subscription-cancelActiveSubscription'
)
const payload = subscriptionInfo.subscriptionId
? { subscriptionId: subscriptionInfo.subscriptionId }
: {};
const { data } = await callable(payload);
: {}
const { data } = await callable(payload)
if (data?.alreadyCanceled) {
setSuccessMessage(
"Ton abonnement est déjà en cours d'annulation. L'accès premium restera actif jusqu'à la fin de la période en cours.",
);
"Ton abonnement est déjà en cours d'annulation. L'accès premium restera actif jusqu'à la fin de la période en cours."
)
} else if (data?.cancelAtPeriodEnd) {
setSuccessMessage(
"Ton abonnement sera résilié à la fin de la période en cours.",
);
setSuccessMessage('Ton abonnement sera résilié à la fin de la période en cours.')
} else {
setSuccessMessage(
"La demande d'annulation a été prise en compte. Vérifie ton abonnement dans quelques instants.",
);
"La demande d'annulation a été prise en compte. Vérifie ton abonnement dans quelques instants."
)
}
triggerRefresh();
triggerRefresh()
} catch (error) {
console.warn(
"[ManageSubscription] cancel subscription error",
error?.message || error,
);
const message =
error?.message || "Impossible d'annuler l'abonnement pour le moment.";
setErrorMessage(message);
console.warn('[ManageSubscription] cancel subscription error', error?.message || error)
const message = error?.message || "Impossible d'annuler l'abonnement pour le moment."
setErrorMessage(message)
} finally {
setIsCancelling(false);
setIsCancelling(false)
}
}, [subscriptionInfo.subscriptionId, triggerRefresh]);
}, [subscriptionInfo.subscriptionId, triggerRefresh])
const handleCancel = useCallback(() => {
if (!subscriptionInfo.canCancel || isCancelling) {
return;
return
}
const confirm = () => {
performCancellation();
};
performCancellation()
}
if (Platform.OS === "web" && typeof window !== "undefined") {
if (Platform.OS === 'web' && typeof window !== 'undefined') {
const confirmed = window.confirm(
"Confirmer l'annulation ? Ton accès premium restera actif jusqu'à la fin de la période en cours.",
);
"Confirmer l'annulation ? Ton accès premium restera actif jusqu'à la fin de la période en cours."
)
if (confirmed) {
confirm();
confirm()
}
return;
return
}
Alert.alert(
"Confirmer l'annulation",
"Ton accès premium restera actif jusqu'à la fin de la période en cours.",
[
{ text: "Conserver mon abonnement", style: "cancel" },
{ text: 'Conserver mon abonnement', style: 'cancel' },
{
text: "Annuler l'abonnement",
style: "destructive",
style: 'destructive',
onPress: confirm,
},
],
);
}, [isCancelling, performCancellation, subscriptionInfo.canCancel]);
]
)
}, [isCancelling, performCancellation, subscriptionInfo.canCancel])
const cancelButtonTitle = subscriptionInfo.cancelAtPeriodEnd
? "Annulation programmée"
? 'Annulation programmée'
: isCancelling
? "Annulation..."
: "Annuler l'abonnement";
? 'Annulation...'
: "Annuler l'abonnement"
const cancelTitleColor = subscriptionInfo.cancelAtPeriodEnd
? Palette.grayMid
: Palette.red;
const cancelTitleColor = subscriptionInfo.cancelAtPeriodEnd ? Palette.grayMid : Palette.red
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120;
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80;
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80
return (
<Page
@@ -562,15 +516,15 @@ const ManageSubscription = ({ navigation }) => {
width: HOME_BACKGROUND_STYLE_WIDTH,
height: HOME_BACKGROUND_STYLE_HEIGHT,
borderRadius: 22,
overflow: "hidden",
position: "absolute",
top: isWeb ? "45%" : "50%",
left: "50%",
overflow: 'hidden',
position: 'absolute',
top: isWeb ? '45%' : '50%',
left: '50%',
transform: [
{ translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
],
pointerEvents: "none",
pointerEvents: 'none',
zIndex: 0,
}}
/>
@@ -587,16 +541,12 @@ const ManageSubscription = ({ navigation }) => {
style={[
styles.statusBadge,
{
backgroundColor:
subscriptionInfo.statusColors.background,
backgroundColor: subscriptionInfo.statusColors.background,
},
]}
>
<Text
style={[
styles.statusText,
{ color: subscriptionInfo.statusColors.text },
]}
style={[styles.statusText, { color: subscriptionInfo.statusColors.text }]}
>
{subscriptionInfo.statusLabel}
</Text>
@@ -608,14 +558,12 @@ const ManageSubscription = ({ navigation }) => {
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Cycle de facturation</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.billingPeriodLabel || "—"}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.billingPeriodLabel || '—'}</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Crédits mensuels</Text>
{typeof subscriptionInfo.coinsPerMonth === "number" ? (
{typeof subscriptionInfo.coinsPerMonth === 'number' ? (
<CreditAmount
value={subscriptionInfo.coinsPerMonth}
textStyle={styles.detailValue}
@@ -629,29 +577,21 @@ const ManageSubscription = ({ navigation }) => {
{subscriptionInfo.isAnnual && subscriptionInfo.nextGrantLabel ? (
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Prochain versement</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.nextGrantLabel}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.nextGrantLabel}</Text>
</View>
) : null}
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>
{subscriptionInfo.cancelAtPeriodEnd
? "Fin d'accès"
: "Prochain renouvellement"}
</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.periodEndLabel}
{subscriptionInfo.cancelAtPeriodEnd ? "Fin d'accès" : 'Prochain renouvellement'}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.periodEndLabel}</Text>
</View>
{subscriptionInfo.createdAtLabel ? (
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Abonné depuis</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.createdAtLabel}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.createdAtLabel}</Text>
</View>
) : null}
@@ -662,16 +602,11 @@ const ManageSubscription = ({ navigation }) => {
) : null}
{subscriptionInfo.helperMessage ? (
<Text style={styles.helperText}>
{subscriptionInfo.helperMessage}
</Text>
<Text style={styles.helperText}>{subscriptionInfo.helperMessage}</Text>
) : null}
{subscriptionInfo.isAnnual &&
typeof subscriptionInfo.coinsPerMonth === "number" ? (
{subscriptionInfo.isAnnual && typeof subscriptionInfo.coinsPerMonth === 'number' ? (
<View style={styles.helperInlineRow}>
<Text style={styles.helperText}>
Tes pièces sont versées chaque mois (
</Text>
<Text style={styles.helperText}>Tes pièces sont versées chaque mois (</Text>
<CreditAmount
value={subscriptionInfo.coinsPerMonth}
textStyle={styles.helperText}
@@ -682,15 +617,9 @@ const ManageSubscription = ({ navigation }) => {
) : null}
</View>
{successMessage ? (
<Text style={styles.successMessage}>{successMessage}</Text>
) : null}
{errorMessage ? (
<Text style={styles.errorMessage}>{errorMessage}</Text>
) : null}
{remoteError ? (
<Text style={styles.errorMessage}>{remoteError}</Text>
) : null}
{successMessage ? <Text style={styles.successMessage}>{successMessage}</Text> : null}
{errorMessage ? <Text style={styles.errorMessage}>{errorMessage}</Text> : null}
{remoteError ? <Text style={styles.errorMessage}>{remoteError}</Text> : null}
<BorderGradientButton
title="Changer d'abonnement"
@@ -716,8 +645,7 @@ const ManageSubscription = ({ navigation }) => {
<View style={styles.card}>
<Text style={styles.cardTitle}>Aucun abonnement actif</Text>
<Text style={styles.infoText}>
Souscris à lune de nos offres pour profiter des fonctionnalités
premium de Musicland.
Souscris à lune de nos offres pour profiter des fonctionnalités premium de Musicland.
</Text>
<BorderGradientButton
title="Découvrir les offres"
@@ -729,8 +657,8 @@ const ManageSubscription = ({ navigation }) => {
{/*<ClubAdvantagesCard isDev style={styles.clubCardSpacing} />*/}
</View>
</Page>
);
};
)
}
const styles = StyleSheet.create({
container: {
@@ -743,9 +671,9 @@ const styles = StyleSheet.create({
card: {
padding: gutters * 1.5,
borderRadius: 24,
backgroundColor: "rgba(255, 255, 255, 0.04)",
backgroundColor: 'rgba(255, 255, 255, 0.04)',
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.08)",
borderColor: 'rgba(255, 255, 255, 0.08)',
gap: gutters * 0.75,
},
cardTitle: {
@@ -754,9 +682,9 @@ const styles = StyleSheet.create({
color: Palette.white,
},
detailRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: gutters,
},
detailLabel: {
@@ -770,7 +698,7 @@ const styles = StyleSheet.create({
fontSize: 15,
color: Palette.white,
flexShrink: 0,
textAlign: "right",
textAlign: 'right',
},
statusBadge: {
borderRadius: 999,
@@ -789,9 +717,9 @@ const styles = StyleSheet.create({
color: Palette.grayMid,
},
helperInlineRow: {
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 4,
},
successMessage: {
@@ -822,6 +750,6 @@ const styles = StyleSheet.create({
clubCardSpacing: {
marginTop: gutters,
},
});
})
export default ManageSubscription;
export default ManageSubscription
+41 -42
View File
@@ -1,56 +1,56 @@
/* eslint-disable react/display-name */
import { BlurView } from "expo-blur";
import { useState } from "react";
import { Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import React, { useEffect, useGlobal } from "reactn";
import { background } from "../../assets";
import Switch from "../../components/Switch";
import firebase, { usersRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { gutters, Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { BlurView } from 'expo-blur'
import { useState } from 'react'
import { Platform, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import React, { useEffect, useGlobal } from 'reactn'
import { background } from '../../assets'
import Switch from '../../components/Switch'
import firebase, { usersRef } from '../../config/firebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { gutters, Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
export default (props) => {
const [currentUID] = useGlobal("currentUID");
const [currentUserData] = useGlobal("currentUserData");
const [, setTooltip] = useGlobal("_tooltip");
const [currentUID] = useGlobal('currentUID')
const [currentUserData] = useGlobal('currentUserData')
const [, setTooltip] = useGlobal('_tooltip')
const [isPushSwitch, setIsPushSwitch] = useState(false);
const [isEmailSwitch, setIsEmailSwitch] = useState(false);
const [isPushSwitch, setIsPushSwitch] = useState(false)
const [isEmailSwitch, setIsEmailSwitch] = useState(false)
useEffect(() => {
setIsPushSwitch(!!currentUserData?.notifications);
}, [currentUserData?.notifications]);
setIsPushSwitch(!!currentUserData?.notifications)
}, [currentUserData?.notifications])
useEffect(() => {
setIsEmailSwitch(!!currentUserData?.emailNotifications);
}, [currentUserData?.emailNotifications]);
setIsEmailSwitch(!!currentUserData?.emailNotifications)
}, [currentUserData?.emailNotifications])
const updateNotificationPreference = async (next, field, setState) => {
try {
setState(next);
const uid = currentUID || firebase.auth().currentUser?.uid;
setState(next)
const uid = currentUID || firebase.auth().currentUser?.uid
if (!uid) {
throw new Error("Utilisateur non connecté");
throw new Error('Utilisateur non connecté')
}
await usersRef.doc(uid).set({ [field]: next }, { merge: true });
setTooltip({ type: "success", text: "Préférence enregistrée" });
await usersRef.doc(uid).set({ [field]: next }, { merge: true })
setTooltip({ type: 'success', text: 'Préférence enregistrée' })
} catch (e) {
setState(!next);
setState(!next)
setTooltip({
type: "error",
text: e?.message || "Enregistrement impossible",
});
type: 'error',
text: e?.message || 'Enregistrement impossible',
})
}
};
}
const onTogglePush = (next) =>
updateNotificationPreference(next, "notifications", setIsPushSwitch);
updateNotificationPreference(next, 'notifications', setIsPushSwitch)
const onToggleEmail = (next) =>
updateNotificationPreference(next, "emailNotifications", setIsEmailSwitch);
updateNotificationPreference(next, 'emailNotifications', setIsEmailSwitch)
return (
<Page
@@ -61,18 +61,18 @@ export default (props) => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
gap: 12,
paddingVertical: 15,
paddingHorizontal: 20,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
}}
// experimentalBlurMethod={
@@ -98,8 +98,8 @@ export default (props) => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Nous tencourageons à activer les notifications pour découvrir les
derniers classements, les nouveaux sons et les meilleurs playbacks.
Nous tencourageons à activer les notifications pour découvrir les derniers classements,
les nouveaux sons et les meilleurs playbacks.
</Text>
<View style={Style.containerSpaceBetween}>
<Text
@@ -120,11 +120,10 @@ export default (props) => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Reste informé en recevant nos nouveautés et rappels directement dans
ta boîte mail.
Reste informé en recevant nos nouveautés et rappels directement dans ta boîte mail.
</Text>
</BlurView>
</View>
</Page>
);
};
)
}
+112 -141
View File
@@ -1,210 +1,181 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from 'react-native'
import firebase from "../../config/firebase";
import { background } from "../../assets";
import Page from "../../layouts/Page";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreditAmount from "../../components/CreditAmount";
import firebase from '../../config/firebase'
import { background } from '../../assets'
import Page from '../../layouts/Page'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import CreditAmount from '../../components/CreditAmount'
const ORDER_TYPE_LABELS = {
GIFT: "Crédit offert",
COINS: "Achat de coins",
SONG: "Génération de musique",
SUBSCRIPTION: "Abonnement",
};
GIFT: 'Crédit offert',
COINS: 'Achat de coins',
SONG: 'Génération de musique',
SUBSCRIPTION: 'Abonnement',
}
const formatDateParts = (date) => {
if (!date) {
return {
dateLabel: "En attente de confirmation",
timeLabel: "",
};
dateLabel: 'En attente de confirmation',
timeLabel: '',
}
}
try {
const dateLabel = date.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
year: "numeric",
});
const timeLabel = date.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
});
return { dateLabel, timeLabel };
const dateLabel = date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: 'short',
year: 'numeric',
})
const timeLabel = date.toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit',
})
return { dateLabel, timeLabel }
} catch (_error) {
return {
dateLabel: date.toString(),
timeLabel: "",
};
timeLabel: '',
}
}
};
}
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || "Opération";
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || 'Opération'
const shortenIdentifier = (value, visible = 6) => {
if (typeof value !== "string") return null;
if (value.length <= visible + 2) return value;
return `${value.slice(0, visible)}`;
};
if (typeof value !== 'string') return null
if (value.length <= visible + 2) return value
return `${value.slice(0, visible)}`
}
const OrderHistory = () => {
const { currentUID } = useUser() || {};
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { currentUID } = useUser() || {}
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
if (!currentUID) {
setOrders([]);
setLoading(false);
return;
setOrders([])
setLoading(false)
return
}
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const unsubscribe = firebase
.firestore()
.collection("orders")
.where("userId", "==", currentUID)
.orderBy("createdAt", "desc")
.collection('orders')
.where('userId', '==', currentUID)
.orderBy('createdAt', 'desc')
.onSnapshot(
(snapshot) => {
const nextOrders = snapshot.docs.map((doc) => {
const rawData = doc.data() || {};
const rawData = doc.data() || {}
const createdAt =
typeof rawData.createdAt?.toDate === "function"
? rawData.createdAt.toDate()
: null;
typeof rawData.createdAt?.toDate === 'function' ? rawData.createdAt.toDate() : null
return {
id: doc.id,
...rawData,
createdAt,
};
});
}
})
setOrders(nextOrders);
setLoading(false);
setOrders(nextOrders)
setLoading(false)
},
(firestoreError) => {
console.warn(
"[OrderHistory] Unable to load orders",
firestoreError?.message || firestoreError,
);
setError(
firestoreError?.message ||
"Impossible de récupérer l'historique des commandes.",
);
setLoading(false);
},
);
'[OrderHistory] Unable to load orders',
firestoreError?.message || firestoreError
)
setError(firestoreError?.message || "Impossible de récupérer l'historique des commandes.")
setLoading(false)
}
)
return () => {
if (typeof unsubscribe === "function") {
unsubscribe();
if (typeof unsubscribe === 'function') {
unsubscribe()
}
};
}, [currentUID]);
}
}, [currentUID])
const renderOrder = useCallback(({ item }) => {
const amountValue =
typeof item.amount === "number" && Number.isFinite(item.amount)
? item.amount
: 0;
const isPositive = amountValue > 0;
typeof item.amount === 'number' && Number.isFinite(item.amount) ? item.amount : 0
const isPositive = amountValue > 0
const amountTextStyle = [
styles.orderAmount,
isPositive ? styles.amountPositive : styles.amountNegative,
];
]
const { dateLabel } = formatDateParts(item.createdAt);
const { dateLabel } = formatDateParts(item.createdAt)
let reasonLabel = mapOrderType(item.type);
let reasonLabel = mapOrderType(item.type)
if (item.type === "GIFT") {
const reason = item.metadata?.reason;
if (reason === "WELCOME_BONUS") {
reasonLabel = "Crédit de bienvenue";
} else if (typeof reason === "string" && reason.trim()) {
reasonLabel = reason.trim();
if (item.type === 'GIFT') {
const reason = item.metadata?.reason
if (reason === 'WELCOME_BONUS') {
reasonLabel = 'Crédit de bienvenue'
} else if (typeof reason === 'string' && reason.trim()) {
reasonLabel = reason.trim()
}
} else if (item.type === "SONG") {
reasonLabel = "Génération de musique";
} else if (item.type === "COINS") {
} else if (item.type === 'SONG') {
reasonLabel = 'Génération de musique'
} else if (item.type === 'COINS') {
if (item.metadata?.coinPackKey) {
const shortenedPack = shortenIdentifier(
item.metadata.coinPackKey,
10,
);
reasonLabel = `Pack ${shortenedPack || item.metadata.coinPackKey}`;
} else if (
typeof item.metadata?.source === "string" &&
item.metadata.source.trim()
) {
const source = item.metadata.source.trim();
reasonLabel =
source === "STRIPE_CHECKOUT" ? "Recharge Stripe" : source;
const shortenedPack = shortenIdentifier(item.metadata.coinPackKey, 10)
reasonLabel = `Pack ${shortenedPack || item.metadata.coinPackKey}`
} else if (typeof item.metadata?.source === 'string' && item.metadata.source.trim()) {
const source = item.metadata.source.trim()
reasonLabel = source === 'STRIPE_CHECKOUT' ? 'Recharge Stripe' : source
} else {
reasonLabel = "Rechargement de coins";
reasonLabel = 'Rechargement de coins'
}
} else if (item.type === "SUBSCRIPTION") {
} else if (item.type === 'SUBSCRIPTION') {
const rawPeriod =
typeof item.metadata?.billingPeriod === "string"
typeof item.metadata?.billingPeriod === 'string'
? item.metadata.billingPeriod.toLowerCase()
: null;
: null
reasonLabel =
rawPeriod === "annual"
? "Abonnement annuel"
: rawPeriod === "monthly"
? "Abonnement mensuel"
: "Abonnement";
rawPeriod === 'annual'
? 'Abonnement annuel'
: rawPeriod === 'monthly'
? 'Abonnement mensuel'
: 'Abonnement'
}
return (
<View style={styles.orderCard}>
<CreditAmount
value={amountValue}
showPlus
textStyle={amountTextStyle}
iconSize={18}
/>
<CreditAmount value={amountValue} showPlus textStyle={amountTextStyle} iconSize={18} />
<Text style={styles.orderReason}>{reasonLabel}</Text>
<Text style={styles.orderTimestamp}>{dateLabel}</Text>
</View>
);
}, []);
)
}, [])
const keyExtractor = useCallback((item) => item.id, []);
const keyExtractor = useCallback((item) => item.id, [])
const listEmptyComponent = useMemo(() => {
if (loading) {
return null;
return null
}
if (!currentUID) {
return (
<Text style={styles.emptyText}>
Connecte-toi pour consulter l'historique de tes commandes.
</Text>
);
)
}
if (error) {
return <Text style={styles.errorText}>{error}</Text>;
return <Text style={styles.errorText}>{error}</Text>
}
return (
<Text style={styles.emptyText}>
Aucune opération de coins pour le moment.
</Text>
);
}, [loading, currentUID, error]);
return <Text style={styles.emptyText}>Aucune opération de coins pour le moment.</Text>
}, [loading, currentUID, error])
return (
<Page
@@ -213,7 +184,7 @@ const OrderHistory = () => {
backgroundImg={background.profileBG}
contentContainerStyle={{ paddingBottom: gutters * 2 }}
containerStyle={{
backgroundColor: "rgba(0,0,0,0.4)",
backgroundColor: 'rgba(0,0,0,0.4)',
}}
>
<View style={styles.container}>
@@ -236,8 +207,8 @@ const OrderHistory = () => {
)}
</View>
</Page>
);
};
)
}
const styles = StyleSheet.create({
container: {
@@ -247,22 +218,22 @@ const styles = StyleSheet.create({
},
loaderContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
listContent: {
paddingBottom: gutters * 2,
},
emptyListContent: {
flexGrow: 1,
justifyContent: "center",
justifyContent: 'center',
},
orderCard: {
padding: 16,
borderRadius: 16,
backgroundColor: Palette.ultraLightWhite,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.08)",
borderColor: 'rgba(255, 255, 255, 0.08)',
gap: 6,
},
orderAmount: {
@@ -286,17 +257,17 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterMedium,
},
emptyText: {
textAlign: "center",
textAlign: 'center',
color: Palette.grayMid,
fontSize: 15,
fontFamily: FONT_FAMILY.InterRegular,
},
errorText: {
textAlign: "center",
textAlign: 'center',
color: Palette.red,
fontSize: 15,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
export default OrderHistory;
export default OrderHistory
+192 -242
View File
@@ -1,7 +1,7 @@
import { useRoute } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useMemo, useState } from "react";
import { useRoute } from '@react-navigation/native'
import { BlurView } from 'expo-blur'
import * as ImagePicker from 'expo-image-picker'
import React, { useEffect, useMemo, useState } from 'react'
import {
ActivityIndicator,
FlatList,
@@ -11,42 +11,38 @@ import {
StyleSheet,
Text,
View,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { Feather } from "@expo/vector-icons";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, icons } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import MoreMenu from "../../components/MoreMenu";
import MobileCoinBadge from "../../components/MobileCoinBadge";
import PressableScale from "../../components/PressableScale";
import ProfilePicture from "../../components/ProfilePicture";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import firebase, {
projectsRef,
serverTimestamp,
usersRef,
} from "../../config/firebase";
import loaderMessages from "../../config/loaderMessages";
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import useLayoutType from "../../hooks/useLayoutType";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate, push, goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { gutters, size } from "../../styles/Style";
import { getArtistDisplayName } from "../../utils/artistName";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import MusicCard from "../Library/components/MusicCard";
} from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { Feather } from '@expo/vector-icons'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background, icons } from '../../assets'
import BorderGradient from '../../components/BorderGradient/BorderGradient'
import MoreMenu from '../../components/MoreMenu'
import MobileCoinBadge from '../../components/MobileCoinBadge'
import PressableScale from '../../components/PressableScale'
import ProfilePicture from '../../components/ProfilePicture'
import ShareBtn from '../../components/ShareBtn/ShareBtn'
import firebase, { projectsRef, serverTimestamp, usersRef } from '../../config/firebase'
import loaderMessages from '../../config/loaderMessages'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import useLayoutType from '../../hooks/useLayoutType'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate, push, goBack } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import Style, { gutters, size } from '../../styles/Style'
import { getArtistDisplayName } from '../../utils/artistName'
import { getProjectLikes, LIKE_TARGET } from '../../utils/likes'
import MusicCard from '../Library/components/MusicCard'
const Profile = () => {
const { params } = useRoute();
const [selected, setSelected] = useState("Chansons");
const { isWeb } = useLayoutType();
const { params } = useRoute()
const [selected, setSelected] = useState('Chansons')
const { isWeb } = useLayoutType()
const {
userProjects: selfProjects,
userPlaybacks: selfPlaybacks,
@@ -55,72 +51,65 @@ const Profile = () => {
getUserByUid,
followUser,
unfollowUser,
} = useUser();
const [currentUID] = useGlobal("currentUID");
const targetUserId = params?.userId || currentUID || null;
const isSelf = !!currentUID && targetUserId === currentUID;
const [, setTooltip] = useGlobal("_tooltip");
const [userData, setUserData] = useState(null);
const [isFollowing, setIsFollowing] = useState(false);
const [followers, setFollowers] = useState(0);
const [following, setFollowing] = useState(0);
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [updatingPhoto, setUpdatingPhoto] = useState(false);
const [webUploadMessage, setWebUploadMessage] = useState("");
} = useUser()
const [currentUID] = useGlobal('currentUID')
const targetUserId = params?.userId || currentUID || null
const isSelf = !!currentUID && targetUserId === currentUID
const [, setTooltip] = useGlobal('_tooltip')
const [userData, setUserData] = useState(null)
const [isFollowing, setIsFollowing] = useState(false)
const [followers, setFollowers] = useState(0)
const [following, setFollowing] = useState(0)
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const [updatingPhoto, setUpdatingPhoto] = useState(false)
const [webUploadMessage, setWebUploadMessage] = useState('')
const selfDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, "MusicLand"),
[currentUserData],
);
const targetDisplayName = useMemo(
() => getArtistDisplayName(userData, "MusicLand"),
[userData],
);
() => getArtistDisplayName(currentUserData, 'MusicLand'),
[currentUserData]
)
const targetDisplayName = useMemo(() => getArtistDisplayName(userData, 'MusicLand'), [userData])
const profileTitle = useMemo(
() => getArtistDisplayName(isSelf ? currentUserData : userData, "Profil"),
[currentUserData, isSelf, userData],
);
const navigateToMusicDetails = useNavigateToMusicDetails();
() => getArtistDisplayName(isSelf ? currentUserData : userData, 'Profil'),
[currentUserData, isSelf, userData]
)
const navigateToMusicDetails = useNavigateToMusicDetails()
const onPressMenu = (item) => {
setSelected(item);
};
setSelected(item)
}
const handleOpenSettings = () => {
navigate(Routes.Settings);
};
navigate(Routes.Settings)
}
// Chargement des données utilisateur si on consulte un autre profil
useEffect(() => {
const fetch = async () => {
if (!targetUserId) return;
if (!targetUserId) return
if (isSelf) {
setUserData(currentUserData || null);
setUserData(currentUserData || null)
setFollowers(
Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy.length
: 0,
);
return;
Array.isArray(currentUserData?.followedBy) ? currentUserData.followedBy.length : 0
)
return
}
const data = await getUserByUid(targetUserId);
setUserData(data || null);
const list = Array.isArray(data?.followedBy) ? data.followedBy : [];
setFollowers(list.length);
setIsFollowing(currentUID ? list.includes(currentUID) : false);
};
fetch();
}, [targetUserId, isSelf, currentUserData?.followedBy?.length || 0]);
const data = await getUserByUid(targetUserId)
setUserData(data || null)
const list = Array.isArray(data?.followedBy) ? data.followedBy : []
setFollowers(list.length)
setIsFollowing(currentUID ? list.includes(currentUID) : false)
}
fetch()
}, [targetUserId, isSelf, currentUserData?.followedBy?.length || 0])
// Live following count pour l'utilisateur consulté (autre que soi)
const followingQueryRef = useMemo(() => {
try {
return targetUserId
? usersRef.where("followedBy", "array-contains", targetUserId)
: null;
return targetUserId ? usersRef.where('followedBy', 'array-contains', targetUserId) : null
} catch (e) {
return null;
return null
}
}, [targetUserId]);
}, [targetUserId])
useDataFromRef({
ref: followingQueryRef,
@@ -129,47 +118,47 @@ const Profile = () => {
condition: !!followingQueryRef && !isSelf,
refreshArray: [targetUserId],
onUpdate: (list) => setFollowing(Array.isArray(list) ? list.length : 0),
});
})
const handleFollowUser = async () => {
if (!currentUID || !targetUserId || isSelf) return;
if (!currentUID || !targetUserId || isSelf) return
if (isFollowing) {
await unfollowUser(targetUserId);
setIsFollowing(false);
setFollowers((v) => Math.max(0, (v || 0) - 1));
await unfollowUser(targetUserId)
setIsFollowing(false)
setFollowers((v) => Math.max(0, (v || 0) - 1))
} else {
await followUser(targetUserId);
setIsFollowing(true);
setFollowers((v) => (v || 0) + 1);
await followUser(targetUserId)
setIsFollowing(true)
setFollowers((v) => (v || 0) + 1)
}
};
}
const onChangeProfilePicture = async (message = "") => {
if (!isSelf || updatingPhoto) return;
const onChangeProfilePicture = async (message = '') => {
if (!isSelf || updatingPhoto) return
try {
setUpdatingPhoto(true);
setWebUploadMessage(isWeb && message ? message : "");
const uid = currentUID;
if (!uid) return;
setUpdatingPhoto(true)
setWebUploadMessage(isWeb && message ? message : '')
const uid = currentUID
if (!uid) return
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 4],
quality: 1,
});
})
const pickedUri = result?.assets?.[0]?.uri || null;
if (!pickedUri) return;
const pickedUri = result?.assets?.[0]?.uri || null
if (!pickedUri) return
const { resultURI = null } = await uploadFileToFirebase({
uri: pickedUri,
path: `users/${uid}/profilePicture.png`,
});
})
if (!resultURI) {
throw new Error("Téléversement de l'image impossible");
throw new Error("Téléversement de l'image impossible")
}
await usersRef.doc(uid).set(
@@ -177,32 +166,28 @@ const Profile = () => {
profilePictureURL: resultURI,
updatedAt: serverTimestamp(),
},
{ merge: true },
);
{ merge: true }
)
const authUser = firebase.auth().currentUser;
const authUser = firebase.auth().currentUser
if (authUser) {
await authUser.updateProfile({ photoURL: resultURI });
await authUser.updateProfile({ photoURL: resultURI })
}
setTooltip({ text: "Photo de profil mise à jour", type: "success" });
setTooltip({ text: 'Photo de profil mise à jour', type: 'success' })
} catch (e) {
setTooltip({
text: e?.message || "Erreur changement photo de profil",
type: "error",
});
text: e?.message || 'Erreur changement photo de profil',
type: 'error',
})
} finally {
setUpdatingPhoto(false);
setWebUploadMessage("");
setUpdatingPhoto(false)
setWebUploadMessage('')
}
};
}
// Composant factorisé pour afficher la liste de musiques
const MusicListSection = ({
data,
emptyText,
likeTarget = LIKE_TARGET.SONG,
}) => (
const MusicListSection = ({ data, emptyText, likeTarget = LIKE_TARGET.SONG }) => (
<ScrollView style={{ flex: 1, marginBottom: 70 }}>
{Array.isArray(data) && data.length > 0 ? (
<FlatList
@@ -214,7 +199,7 @@ const Profile = () => {
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
subtitle={isSelf ? selfDisplayName : targetDisplayName}
imageUri={item?.coverUrl || null}
projectId={item?.id}
@@ -227,21 +212,16 @@ const Profile = () => {
project: item,
queueProjects: data,
queueSource: {
id: isSelf ? "profile-self" : "profile-other",
type: "collection",
name: isSelf
? "Mes musiques"
: targetDisplayName || "Profil",
id: isSelf ? 'profile-self' : 'profile-other',
type: 'collection',
name: isSelf ? 'Mes musiques' : targetDisplayName || 'Profil',
},
})
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuPosition(posTop);
setShowMenu(
(prev) =>
!prev || posTop?.top !== (menuPosition?.top ?? null),
);
setSelectedProjectId(item.id)
setMenuPosition(posTop)
setShowMenu((prev) => !prev || posTop?.top !== (menuPosition?.top ?? null))
}}
/>
)}
@@ -260,26 +240,25 @@ const Profile = () => {
isSelf
? [
{
label: "Supprimer",
label: 'Supprimer',
onPress: () =>
SheetManager.show("Delete", {
SheetManager.show('Delete', {
payload: {
title: "Supprimer le projet",
message:
"Cette action supprimera définitivement ce projet.",
title: 'Supprimer le projet',
message: 'Cette action supprimera définitivement ce projet.',
onConfirm: async () => {
try {
if (!selectedProjectId) return;
await projectsRef.doc(selectedProjectId).delete();
if (!selectedProjectId) return
await projectsRef.doc(selectedProjectId).delete()
setTooltip({
type: "success",
text: "Projet supprimé",
});
type: 'success',
text: 'Projet supprimé',
})
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
}
},
},
@@ -290,7 +269,7 @@ const Profile = () => {
}
/>
</ScrollView>
);
)
const EmptyText = ({ text }) => (
<View style={{ flex: 1, ...Style.containerCenter, paddingTop: 20 }}>
@@ -305,19 +284,17 @@ const Profile = () => {
{text}
</Text>
</View>
);
)
// Données projets/profils selon si c'est soi ou un autre utilisateur
const projectsQueryRef = useMemo(() => {
try {
if (!targetUserId || isSelf) return null;
return projectsRef
.where("userId", "==", targetUserId)
.orderBy("updatedAt", "desc");
if (!targetUserId || isSelf) return null
return projectsRef.where('userId', '==', targetUserId).orderBy('updatedAt', 'desc')
} catch (e) {
return null;
return null
}
}, [targetUserId, isSelf]);
}, [targetUserId, isSelf])
const { data: otherUserProjects = [] } = useDataFromRef({
ref: projectsQueryRef,
@@ -325,26 +302,26 @@ const Profile = () => {
listener: true,
condition: !!projectsQueryRef,
refreshArray: [targetUserId],
});
})
const displayedProjects = isSelf ? selfProjects : otherUserProjects;
const displayedPlaybacksSource = isSelf ? selfPlaybacks : otherUserProjects;
const displayedProjects = isSelf ? selfProjects : otherUserProjects
const displayedPlaybacksSource = isSelf ? selfPlaybacks : otherUserProjects
const displayedPlaybacks = Array.isArray(displayedPlaybacksSource)
? displayedPlaybacksSource.filter((project) => project?.playbackUrl)
: [];
const showHeader = isWeb;
const showBackButton = !params?.noBack && !isWeb;
: []
const showHeader = isWeb
const showBackButton = !params?.noBack && !isWeb
return (
<Page
backgroundImg={isWeb ? background.profileWebBG : background.profileWebBG}
headerType={showHeader ? "NAVIGATE" : "NONE"}
headerType={showHeader ? 'NAVIGATE' : 'NONE'}
hideBackButton={params?.noBack}
contentContainerStyle={{
paddingBottom: gutters * 2,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
paddingTop: showHeader ? undefined : 0,
}}
>
@@ -352,16 +329,8 @@ const Profile = () => {
<View style={styles.mobileTopRow}>
<View style={styles.mobileTopLeft}>
{showBackButton ? (
<Pressable
onPress={goBack}
hitSlop={16}
style={styles.backButton}
>
<Image
source={icons.chevronDown}
style={styles.backIcon}
resizeMode="contain"
/>
<Pressable onPress={goBack} hitSlop={16} style={styles.backButton}>
<Image source={icons.chevronDown} style={styles.backIcon} resizeMode="contain" />
</Pressable>
) : null}
<MobileCoinBadge />
@@ -370,7 +339,7 @@ const Profile = () => {
</View>
) : null}
<View style={{ flex: 1, marginTop: responsiveHeight(2), gap: 20 }}>
<View style={{ borderRadius: 20, overflow: "hidden" }}>
<View style={{ borderRadius: 20, overflow: 'hidden' }}>
<BlurView
intensity={20}
style={{
@@ -382,7 +351,7 @@ const Profile = () => {
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ alignItems: "center" }}>
<View style={{ alignItems: 'center' }}>
{isSelf && (
<PressableScale
onPress={handleOpenSettings}
@@ -400,16 +369,12 @@ const Profile = () => {
: userData?.profilePictureURL || null
}
size={108}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
{isSelf && (
<Pressable
style={styles.editAvatarButton}
onPress={() =>
onChangeProfilePicture(
loaderMessages.profilePhotoUploadWeb,
)
}
onPress={() => onChangeProfilePicture(loaderMessages.profilePhotoUploadWeb)}
disabled={updatingPhoto}
hitSlop={10}
>
@@ -448,42 +413,36 @@ const Profile = () => {
paddingHorizontal: isWeb && 30,
}}
>
<View style={{ flex: 1, alignItems: "center" }}>
<View style={{ flex: 1, alignItems: 'center' }}>
<Text style={styles.value}>
{Array.isArray(displayedProjects)
? displayedProjects.length
: 0}
{Array.isArray(displayedProjects) ? displayedProjects.length : 0}
</Text>
<Text style={styles.label}>projets</Text>
</View>
<Pressable
style={{ flex: 1, alignItems: "center" }}
style={{ flex: 1, alignItems: 'center' }}
onPress={() =>
push(Routes.Follows, {
selected: "Abonnés",
selected: 'Abonnés',
...(isSelf ? {} : { userId: targetUserId }),
})
}
>
<Text style={styles.value}>
{isSelf
? currentUserData?.followedBy?.length || 0
: followers}
{isSelf ? currentUserData?.followedBy?.length || 0 : followers}
</Text>
<Text style={styles.label}>abonnés</Text>
</Pressable>
<Pressable
style={{ flex: 1, alignItems: "center" }}
style={{ flex: 1, alignItems: 'center' }}
onPress={() =>
push(Routes.Follows, {
selected: "Abonnements",
selected: 'Abonnements',
...(isSelf ? {} : { userId: targetUserId }),
})
}
>
<Text style={styles.value}>
{isSelf ? followingCount : following}
</Text>
<Text style={styles.value}>{isSelf ? followingCount : following}</Text>
<Text style={styles.label}>abonnements</Text>
</Pressable>
</View>
@@ -493,12 +452,12 @@ const Profile = () => {
style={{
height: 36,
borderRadius: 100,
overflow: "hidden",
backgroundColor: "#FFFFFF22",
alignItems: "center",
justifyContent: "center",
width: "80%",
alignSelf: "center",
overflow: 'hidden',
backgroundColor: '#FFFFFF22',
alignItems: 'center',
justifyContent: 'center',
width: '80%',
alignSelf: 'center',
marginTop: 18,
}}
>
@@ -509,7 +468,7 @@ const Profile = () => {
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{isFollowing ? "Suivi(e)" : "Suivre"}
{isFollowing ? 'Suivi(e)' : 'Suivre'}
</Text>
</View>
</Pressable>
@@ -523,18 +482,12 @@ const Profile = () => {
}}
>
{/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
{["Chansons", "Playback"].map((item, index) => (
<Pressable
key={index}
onPress={() => onPressMenu(item)}
style={{ flex: 1 }}
>
{['Chansons', 'Playback'].map((item, index) => (
<Pressable key={index} onPress={() => onPressMenu(item)} style={{ flex: 1 }}>
<BorderGradient
gradientProps={{
colors:
selected === item
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran],
selected === item ? ['#FFFFFF00', '#FFFFFF'] : [Palette.tran, Palette.tran],
locations: [0, 1],
start: { x: 0, y: 0 },
end: { x: 1, y: 0 },
@@ -555,42 +508,39 @@ const Profile = () => {
</Pressable>
))}
</View>
{selected === "Playback" && (
{selected === 'Playback' && (
<MusicListSection
data={displayedPlaybacks}
emptyText="Aucun playback pour le moment"
likeTarget={LIKE_TARGET.PLAYBACK}
/>
)}
{selected === "Chansons" && (
<MusicListSection
data={displayedProjects}
emptyText="Aucune chanson pour le moment"
/>
{selected === 'Chansons' && (
<MusicListSection data={displayedProjects} emptyText="Aucune chanson pour le moment" />
)}
{selected === "Clips" && (
{selected === 'Clips' && (
<View style={{ flex: 1 }}>
<EmptyText text={"Aucun clip pour le moment"} />
<EmptyText text={'Aucun clip pour le moment'} />
</View>
)}
</View>
</Page>
);
};
)
}
export default Profile;
export default Profile
const styles = StyleSheet.create({
mobileTopRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
mobileTopLeft: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flexShrink: 1,
},
@@ -604,7 +554,7 @@ const styles = StyleSheet.create({
backIcon: {
...Style.iconSmall,
...Style.mirrorHorizontal,
transform: [{ rotate: "90deg" }],
transform: [{ rotate: '90deg' }],
},
value: {
fontSize: 16,
@@ -625,31 +575,31 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderRadius: 10,
height: 33,
position: "absolute",
position: 'absolute',
zIndex: 1,
width: "100%",
width: '100%',
},
blurContainer: {
height: 33,
zIndex: -1,
borderRadius: 10,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
},
blurView: {
width: "100%",
height: "100%",
width: '100%',
height: '100%',
paddingHorizontal: 10,
...Style.containerCenter,
},
avatarWrapper: {
position: "relative",
position: 'relative',
...size({ size: 108 }),
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
settingsButton: {
position: "absolute",
position: 'absolute',
top: -6,
right: -6,
width: 36,
@@ -662,7 +612,7 @@ const styles = StyleSheet.create({
...Style.containerCenter,
},
editAvatarButton: {
position: "absolute",
position: 'absolute',
bottom: 4,
right: 4,
},
@@ -684,7 +634,7 @@ const styles = StyleSheet.create({
color: Palette.white,
fontSize: 12,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
opacity: 0.85,
},
});
})
+21 -28
View File
@@ -1,23 +1,23 @@
import { View, Text, Pressable, Image } from "react-native";
import React, { useState } from "react";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { icons, img } from "../../assets";
import Style, { gutters, size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { goBack } from "../../navigation/NavigationService";
import { SheetManager } from "react-native-actions-sheet";
import { View, Text, Pressable, Image } from 'react-native'
import React, { useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { icons, img } from '../../assets'
import Style, { gutters, size } from '../../styles/Style'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { goBack } from '../../navigation/NavigationService'
import { SheetManager } from 'react-native-actions-sheet'
const Reels = () => {
const { top } = useSafeAreaInsets();
const [isFav, setIsFav] = useState(false);
const { top } = useSafeAreaInsets()
const [isFav, setIsFav] = useState(false)
return (
<View style={{ flex: 1 }}>
<View
style={{
...Style.containerRow,
position: "absolute",
position: 'absolute',
top: top + 10,
zIndex: 1,
paddingHorizontal: gutters,
@@ -29,7 +29,7 @@ const Reels = () => {
source={icons.chevronDown}
style={{
...size({ size: 15 }),
transform: [{ rotate: "90deg" }],
transform: [{ rotate: '90deg' }],
}}
resizeMode="contain"
/>
@@ -44,19 +44,16 @@ const Reels = () => {
>
Mon Profile
</Text>
<View style={{ flex: 1, alignItems: "flex-end" }}>
<Pressable onPress={() => SheetManager.show("DeletePlayback")}>
<View style={{ flex: 1, alignItems: 'flex-end' }}>
<Pressable onPress={() => SheetManager.show('DeletePlayback')}>
<Image source={icons.trash} />
</Pressable>
</View>
</View>
<Image
source={img.placeholder3}
style={{ width: "100%", height: "100%" }}
/>
<Image source={img.placeholder3} style={{ width: '100%', height: '100%' }} />
<View
style={{
position: "absolute",
position: 'absolute',
zIndex: 1,
bottom: gutters * 2,
right: 33,
@@ -71,15 +68,11 @@ const Reels = () => {
/>
</Pressable>
<Pressable>
<Image
source={icons.share}
style={size({ size: 26 })}
resizeMode="contain"
/>
<Image source={icons.share} style={size({ size: 26 })} resizeMode="contain" />
</Pressable>
</View>
</View>
);
};
)
}
export default Reels;
export default Reels
+38 -42
View File
@@ -1,68 +1,68 @@
import React from "react";
import { View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import BlurItemButton from "../../components/BlurItemButton";
import BorderGradientButton from "../../components/BorderGradientButton";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import React from 'react'
import { View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import BlurItemButton from '../../components/BlurItemButton'
import BorderGradientButton from '../../components/BorderGradientButton'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUserData } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
const SETTINGS = [
{
title: "Adresse mail",
title: 'Adresse mail',
action: () => {
navigate(Routes.ChangeEmailAddress);
navigate(Routes.ChangeEmailAddress)
},
},
{
title: "Modifier mon mot de passe",
title: 'Modifier mon mot de passe',
action: () => {
navigate(Routes.ChangePassword);
navigate(Routes.ChangePassword)
},
},
{
title: "Notifications",
title: 'Notifications',
action: () => {
navigate(Routes.Notifications);
navigate(Routes.Notifications)
},
},
{
title: "Langue",
title: 'Langue',
action: () => {
navigate(Routes.Language);
navigate(Routes.Language)
},
},
{
title: "Historique de commandes",
title: 'Historique de commandes',
action: () => {
navigate(Routes.OrderHistory);
navigate(Routes.OrderHistory)
},
},
{
title: "Gérer mon abonnement",
title: 'Gérer mon abonnement',
action: () => {
navigate(Routes.ManageSubscription);
navigate(Routes.ManageSubscription)
},
},
];
]
const Settings = () => {
const { onSignOut } = useUserData();
const { onSignOut } = useUserData()
const handleSignOut = async () => {
try {
await onSignOut();
await onSignOut()
} catch (e) {
console.log("Sign out error", e?.message);
console.log('Sign out error', e?.message)
} finally {
navigate(Routes.Login);
navigate(Routes.Login)
}
};
}
return (
<Page
headerType="NAVIGATION"
@@ -72,32 +72,28 @@ const Settings = () => {
paddingBottom: gutters * 2,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<View style={{ flex: 1, gap: 10 }}>
{SETTINGS.map((item, index) => (
<BlurItemButton
key={index}
title={item.title}
onPress={item.action}
/>
<BlurItemButton key={index} title={item.title} onPress={item.action} />
))}
</View>
<View style={{ width: "80%", gap: 5, alignSelf: "center" }}>
<View style={{ width: '80%', gap: 5, alignSelf: 'center' }}>
<BorderGradientButton title="Déconnexion" onPress={handleSignOut} />
<BorderGradientButton
title="Supprimer mon profil"
titleStyle={{
color: Palette.red,
}}
onPress={() => SheetManager.show("DeleteAccount")}
onPress={() => SheetManager.show('DeleteAccount')}
/>
</View>
</View>
</Page>
);
};
)
}
export default Settings;
export default Settings
@@ -1,66 +1,62 @@
import React from "react";
import { StyleSheet, Switch, Text, View } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { Image as ExpoImage } from "expo-image";
import { useNavigation } from "@react-navigation/native";
import React from 'react'
import { StyleSheet, Switch, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons'
import { Image as ExpoImage } from 'expo-image'
import { useNavigation } from '@react-navigation/native'
import ClubCard from "../../Home/components/ClubCard";
import { gutters, Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { Routes } from "../../../navigation/Routes";
import { useUserData } from "../../../providers/UserDataProvider";
import { icons } from "../../../assets";
import { isWeb } from "../../../hooks/useLayoutType";
import ClubCard from '../../Home/components/ClubCard'
import { gutters, Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { Routes } from '../../../navigation/Routes'
import { useUserData } from '../../../providers/UserDataProvider'
import { icons } from '../../../assets'
import { isWeb } from '../../../hooks/useLayoutType'
const ICON_BASE_ACCENT = "#A96BFF";
const ICON_BASE_ACCENT = '#A96BFF'
const CLUB_ADVANTAGES = [
{
title: "Gagne des crédits avec ton abonnement",
description: "Des crédits premium tous les mois pour créer plus.",
iconType: "image",
title: 'Gagne des crédits avec ton abonnement',
description: 'Des crédits premium tous les mois pour créer plus.',
iconType: 'image',
icon: icons.coin,
accent: ICON_BASE_ACCENT,
},
{
title: "Gagne le double de cashprice grace à ton adésion au club",
description: "Cashprice doublé pour les membres du Club Musicland.",
iconType: "vector",
iconName: "chart-line",
accent: "#F8C24D",
title: 'Gagne le double de cashprice grace à ton adésion au club',
description: 'Cashprice doublé pour les membres du Club Musicland.',
iconType: 'vector',
iconName: 'chart-line',
accent: '#F8C24D',
},
];
]
const ClubAdvantagesCard = ({ style, isDev = false }) => {
const navigation = useNavigation();
const { hasActiveSubscription } = useUserData() || {};
const [useWebLayout, setUseWebLayout] = React.useState(isDev ? isWeb : false);
const advantages = CLUB_ADVANTAGES;
const navigation = useNavigation()
const { hasActiveSubscription } = useUserData() || {}
const [useWebLayout, setUseWebLayout] = React.useState(isDev ? isWeb : false)
const advantages = CLUB_ADVANTAGES
React.useEffect(() => {
if (!isDev) {
setUseWebLayout(false);
setUseWebLayout(false)
}
}, [isDev]);
}, [isDev])
const title = hasActiveSubscription
? "Tu profites déjà du Club Musicland"
: "Rejoins le Club Musicland";
? 'Tu profites déjà du Club Musicland'
: 'Rejoins le Club Musicland'
const subtitle = hasActiveSubscription
? "Grâce à ton abonnement, tu bénéficies de tous ces avantages."
: "Découvre ce que tu gagnes en passant au club premium.";
? 'Grâce à ton abonnement, tu bénéficies de tous ces avantages.'
: 'Découvre ce que tu gagnes en passant au club premium.'
const handleOpenPlans = () => {
navigation.navigate(Routes.Payments);
};
navigation.navigate(Routes.Payments)
}
return (
<View
style={[
styles.card,
hasActiveSubscription ? styles.cardActive : styles.cardInactive,
style,
]}
style={[styles.card, hasActiveSubscription ? styles.cardActive : styles.cardInactive, style]}
>
{/* {isDev ? (
<View style={styles.testSwitches}>
@@ -93,17 +89,15 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
]}
>
{advantages.map((advantage) => {
const accent = advantage.accent || Palette.white;
const iconType = advantage.iconType || "image";
const accent = advantage.accent || Palette.white
const iconType = advantage.iconType || 'image'
return (
<View
key={advantage.title}
style={[
styles.advantageCard,
useWebLayout
? styles.advantageCardWeb
: styles.advantageCardMobile,
useWebLayout ? styles.advantageCardWeb : styles.advantageCardMobile,
]}
>
{hasActiveSubscription ? (
@@ -113,9 +107,7 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
color={Palette.green}
style={[
styles.advantageCheck,
useWebLayout
? styles.advantageCheckWeb
: styles.advantageCheckMobile,
useWebLayout ? styles.advantageCheckWeb : styles.advantageCheckMobile,
]}
/>
) : null}
@@ -129,12 +121,8 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
},
]}
>
{iconType === "vector" ? (
<MaterialCommunityIcons
name={advantage.iconName}
size={24}
color={accent}
/>
{iconType === 'vector' ? (
<MaterialCommunityIcons name={advantage.iconName} size={24} color={accent} />
) : (
<ExpoImage
source={advantage.icon}
@@ -147,29 +135,18 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
style={[
styles.advantageContent,
useWebLayout && styles.advantageContentWeb,
hasActiveSubscription &&
!useWebLayout && { paddingRight: 10 },
hasActiveSubscription && !useWebLayout && { paddingRight: 10 },
]}
>
<Text
style={[
styles.advantageTitle,
useWebLayout && styles.advantageTextCenter,
]}
>
<Text style={[styles.advantageTitle, useWebLayout && styles.advantageTextCenter]}>
{advantage.title}
</Text>
<Text
style={[
styles.advantageText,
useWebLayout && styles.advantageTextCenter,
]}
>
<Text style={[styles.advantageText, useWebLayout && styles.advantageTextCenter]}>
{advantage.description}
</Text>
</View>
</View>
);
)
})}
</View>
@@ -179,29 +156,29 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
hasActiveSubscription={hasActiveSubscription}
/>
</View>
);
};
)
}
const styles = StyleSheet.create({
card: {
paddingHorizontal: gutters * 1.25,
paddingVertical: gutters,
borderRadius: 24,
backgroundColor: "#252438",
backgroundColor: '#252438',
borderWidth: 1,
gap: gutters,
borderColor: "white",
borderColor: 'white',
},
cardInactive: {
borderColor: "rgba(255, 255, 255, 0.08)",
borderColor: 'rgba(255, 255, 255, 0.08)',
},
cardActive: {
borderColor: Palette.green,
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: gutters,
},
testSwitches: {
@@ -210,90 +187,90 @@ const styles = StyleSheet.create({
padding: gutters * 0.9,
borderRadius: 14,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.12)",
borderColor: 'rgba(255, 255, 255, 0.12)',
},
switchRow: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: gutters * 0.5,
justifyContent: "space-between",
justifyContent: 'space-between',
},
switchLabel: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 11,
color: Palette.grayMid,
textTransform: "uppercase",
textTransform: 'uppercase',
letterSpacing: 0.6,
},
titleBlock: {
flex: 1,
gap: gutters * 0.3,
alignItems: "center",
alignItems: 'center',
},
title: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
subtitle: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
color: "rgba(255, 255, 255, 0.82)",
color: 'rgba(255, 255, 255, 0.82)',
lineHeight: 18,
textAlign: "center",
textAlign: 'center',
},
advantagesList: {
gap: gutters * 0.75,
},
advantagesListWeb: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
advantagesListMobile: {
flexDirection: "column",
flexDirection: 'column',
},
advantageCard: {
flexDirection: "row",
alignItems: "flex-start",
flexDirection: 'row',
alignItems: 'flex-start',
gap: gutters * 0.65,
backgroundColor: "rgba(255, 255, 255, 0.08)",
backgroundColor: 'rgba(255, 255, 255, 0.08)',
borderRadius: 16,
borderWidth: 0,
padding: gutters,
shadowColor: "#000",
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.2,
shadowRadius: 10,
elevation: 2,
},
advantageCardWeb: {
flexBasis: "31%",
maxWidth: "32%",
flexBasis: '31%',
maxWidth: '32%',
minWidth: 200,
flexGrow: 1,
flexShrink: 1,
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-start",
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
gap: gutters * 0.5,
position: "relative",
position: 'relative',
},
advantageCardMobile: {
width: "100%",
position: "relative",
width: '100%',
position: 'relative',
},
advantageIcon: {
width: 42,
height: 42,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
},
advantageIconWeb: {
alignSelf: "center",
alignSelf: 'center',
marginBottom: gutters * 0.15,
},
advantageIconImage: {
@@ -305,7 +282,7 @@ const styles = StyleSheet.create({
gap: gutters * 0.2,
},
advantageContentWeb: {
alignItems: "center",
alignItems: 'center',
gap: gutters * 0.4,
},
advantageTitle: {
@@ -317,23 +294,23 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
lineHeight: 18,
color: "rgba(255, 255, 255, 0.85)",
color: 'rgba(255, 255, 255, 0.85)',
},
advantageTextCenter: {
textAlign: "center",
textAlign: 'center',
},
advantageCheck: {
position: "absolute",
position: 'absolute',
},
advantageCheckWeb: {
top: 10,
right: 10,
},
advantageCheckMobile: {
top: "50%",
top: '50%',
right: 12,
transform: [{ translateY: -10 }],
},
});
})
export default ClubAdvantagesCard;
export default ClubAdvantagesCard
+19 -19
View File
@@ -1,19 +1,19 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import { Platform, Pressable, Text, TextInput, View } from "react-native";
import EyeSlashSVG from "../../../assets/UI/EyeSlashSVG";
import EyeSVG from "../../../assets/UI/EyeSVG";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { BlurView } from 'expo-blur'
import React, { useState } from 'react'
import { Platform, Pressable, Text, TextInput, View } from 'react-native'
import EyeSlashSVG from '../../../assets/UI/EyeSlashSVG'
import EyeSVG from '../../../assets/UI/EyeSVG'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
const EditInput = ({
label = "",
placeholder = "",
type = "default",
value = "",
label = '',
placeholder = '',
type = 'default',
value = '',
setValue = () => {},
}) => {
const [showPassword, setShowPassword] = useState(false);
const [showPassword, setShowPassword] = useState(false)
return (
<View style={{ gap: 4 }}>
@@ -42,12 +42,12 @@ const EditInput = ({
</Text>
</View>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingHorizontal: 12,
borderRadius: 12,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
height: 52,
...Style.containerRow,
}}
@@ -67,18 +67,18 @@ const EditInput = ({
keyboardType={type}
value={value}
onChangeText={setValue}
{...(type === "password" && {
{...(type === 'password' && {
secureTextEntry: !showPassword,
})}
/>
{type === "password" && (
{type === 'password' && (
<Pressable onPress={() => setShowPassword(!showPassword)}>
{showPassword ? <EyeSVG /> : <EyeSlashSVG />}
</Pressable>
)}
</BlurView>
</View>
);
};
)
}
export default EditInput;
export default EditInput