feat: fixes and formatter
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 à l’une de nos offres pour profiter des fonctionnalités
|
||||
premium de Musicland.
|
||||
Souscris à l’une 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
|
||||
|
||||
@@ -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 t’encourageons à activer les notifications pour découvrir les
|
||||
derniers classements, les nouveaux sons et les meilleurs playbacks.
|
||||
Nous t’encourageons à 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
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user