clear more tickets
@@ -6,7 +6,7 @@ import * as SplashScreen from "expo-splash-screen";
|
|||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import "moment/locale/fr";
|
import "moment/locale/fr";
|
||||||
import { Platform } from "react-native";
|
import { Platform, Text, TextInput } from "react-native";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import React, {
|
import React, {
|
||||||
setGlobal,
|
setGlobal,
|
||||||
@@ -45,6 +45,18 @@ console.reportErrorsAsExceptions = false;
|
|||||||
|
|
||||||
moment.locale("fr");
|
moment.locale("fr");
|
||||||
|
|
||||||
|
// Keep typography stable regardless of the user's system font scaling on mobile
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
if (Text.defaultProps == null) {
|
||||||
|
Text.defaultProps = {};
|
||||||
|
}
|
||||||
|
if (TextInput.defaultProps == null) {
|
||||||
|
TextInput.defaultProps = {};
|
||||||
|
}
|
||||||
|
Text.defaultProps.allowFontScaling = false;
|
||||||
|
TextInput.defaultProps.allowFontScaling = false;
|
||||||
|
}
|
||||||
|
|
||||||
setGlobal(initialGlobalState);
|
setGlobal(initialGlobalState);
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash.png",
|
"image": "./assets/splash.png",
|
||||||
"resizeMode": "cover",
|
"resizeMode": "cover",
|
||||||
"backgroundColor": "#0A0D12"
|
"backgroundColor": "#000"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"googleServicesFile": "./config/GoogleService-Info.plist",
|
"googleServicesFile": "./config/GoogleService-Info.plist",
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"softwareKeyboardLayoutMode": "resize",
|
"softwareKeyboardLayoutMode": "resize",
|
||||||
"googleServicesFile": "./config/google-services.json",
|
"googleServicesFile": "./config/google-services.json",
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/foreground.png",
|
||||||
"backgroundColor": "#0A0D12"
|
"backgroundImage": "./assets/background.png"
|
||||||
},
|
},
|
||||||
"package": "com.omedis.musicland",
|
"package": "com.omedis.musicland",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 681 KiB After Width: | Height: | Size: 677 KiB |
@@ -67,6 +67,25 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
const evaluateShouldClose = useCallback(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
|
||||||
|
if (!video || hasClosedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.ended) {
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = video.duration - video.currentTime;
|
||||||
|
|
||||||
|
if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}, [handleClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
@@ -103,44 +122,22 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
|||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) {
|
if (!visible || !uri) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasClosedRef.current = false;
|
hasClosedRef.current = false;
|
||||||
|
let rafId;
|
||||||
|
|
||||||
|
const attemptPlay = () => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
|
|
||||||
if (!video || !uri) {
|
if (!video) {
|
||||||
return undefined;
|
rafId = requestAnimationFrame(attemptPlay);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldClose = () => {
|
|
||||||
if (hasClosedRef.current) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (video.ended) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const remaining = video.duration - video.currentTime;
|
|
||||||
return Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tryHandleClose = () => {
|
|
||||||
if (shouldClose()) {
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleEnded = tryHandleClose;
|
|
||||||
const handleTimeUpdate = tryHandleClose;
|
|
||||||
const handlePause = tryHandleClose;
|
|
||||||
video.addEventListener("ended", handleEnded);
|
|
||||||
video.addEventListener("timeupdate", handleTimeUpdate);
|
|
||||||
video.addEventListener("pause", handlePause);
|
|
||||||
const pollId = setInterval(tryHandleClose, CLOSE_POLL_INTERVAL_MS);
|
|
||||||
|
|
||||||
video.currentTime = 0;
|
video.currentTime = 0;
|
||||||
const attemptPlay = () => {
|
|
||||||
const result = video.play();
|
const result = video.play();
|
||||||
|
|
||||||
if (result?.catch) {
|
if (result?.catch) {
|
||||||
@@ -153,15 +150,17 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
attemptPlay();
|
attemptPlay();
|
||||||
|
const pollId = setInterval(evaluateShouldClose, CLOSE_POLL_INTERVAL_MS);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
video.pause();
|
if (rafId) {
|
||||||
video.removeEventListener("ended", handleEnded);
|
cancelAnimationFrame(rafId);
|
||||||
video.removeEventListener("timeupdate", handleTimeUpdate);
|
}
|
||||||
video.removeEventListener("pause", handlePause);
|
|
||||||
clearInterval(pollId);
|
clearInterval(pollId);
|
||||||
|
const video = videoRef.current;
|
||||||
|
video?.pause();
|
||||||
};
|
};
|
||||||
}, [handleClose, muted, uri, visible]);
|
}, [evaluateShouldClose, muted, uri, visible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
@@ -194,6 +193,10 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
|||||||
loop={false}
|
loop={false}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
controls={false}
|
controls={false}
|
||||||
|
onEnded={handleClose}
|
||||||
|
onPause={evaluateShouldClose}
|
||||||
|
onTimeUpdate={evaluateShouldClose}
|
||||||
|
onError={handleClose}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Pressable onPress={handleClose} style={closeButtonStyle}>
|
<Pressable onPress={handleClose} style={closeButtonStyle}>
|
||||||
|
|||||||
@@ -43,7 +43,119 @@ const formatCurrency = (amount, currency = "eur") => {
|
|||||||
return `${normalized.toFixed(2)} ${upperCurrency}`;
|
return `${normalized.toFixed(2)} ${upperCurrency}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CoinPackCard({ pack, selected, onSelect }) {
|
const FALLBACK_BASE_PRICE_PER_COIN = 12; // ~0,12€ par jeton (valeur indicatif pour l'affichage)
|
||||||
|
const FALLBACK_DISCOUNT_STEPS = [0, 12, 18, 26, 32];
|
||||||
|
|
||||||
|
const computeCoinPackPricing = (packs = []) => {
|
||||||
|
if (!Array.isArray(packs) || !packs.length) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const packsWithPrice = packs
|
||||||
|
.filter((pack) => pack?.productId)
|
||||||
|
.map((pack) => {
|
||||||
|
const hasValidPrice =
|
||||||
|
typeof pack.unitAmount === "number" &&
|
||||||
|
typeof pack.coinAmount === "number" &&
|
||||||
|
pack.coinAmount > 0;
|
||||||
|
return {
|
||||||
|
...pack,
|
||||||
|
hasValidPrice,
|
||||||
|
pricePerCoin: hasValidPrice
|
||||||
|
? pack.unitAmount / pack.coinAmount
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const basePack = packsWithPrice.reduce((current, pack) => {
|
||||||
|
if (!pack.hasValidPrice) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
if (!current) {
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof pack.coinAmount === "number" &&
|
||||||
|
typeof current.coinAmount === "number" &&
|
||||||
|
pack.coinAmount < current.coinAmount
|
||||||
|
) {
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
const basePricePerCoin =
|
||||||
|
basePack?.pricePerCoin && isFinite(basePack.pricePerCoin)
|
||||||
|
? basePack.pricePerCoin
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const bestDiscountPack = packsWithPrice.reduce((best, pack) => {
|
||||||
|
if (!pack.hasValidPrice || !basePricePerCoin) {
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
const discount =
|
||||||
|
((basePricePerCoin - pack.pricePerCoin) / basePricePerCoin) * 100;
|
||||||
|
if (!best || discount > best.discount) {
|
||||||
|
return { productId: pack.productId, discount };
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
const fallbackBase = basePricePerCoin || FALLBACK_BASE_PRICE_PER_COIN;
|
||||||
|
const fallbackBestId =
|
||||||
|
!bestDiscountPack && packs.length
|
||||||
|
? packs[packs.length - 1]?.productId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return packs.reduce((acc, pack, index) => {
|
||||||
|
if (!pack?.productId) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPack = packsWithPrice.find(
|
||||||
|
(item) => item.productId === pack.productId,
|
||||||
|
);
|
||||||
|
const computedPricePerCoin = currentPack?.pricePerCoin;
|
||||||
|
const fallbackDiscount =
|
||||||
|
FALLBACK_DISCOUNT_STEPS[
|
||||||
|
Math.min(index, FALLBACK_DISCOUNT_STEPS.length - 1)
|
||||||
|
] || 0;
|
||||||
|
const pricePerCoin =
|
||||||
|
typeof computedPricePerCoin === "number" && isFinite(computedPricePerCoin)
|
||||||
|
? computedPricePerCoin
|
||||||
|
: fallbackBase * (1 - fallbackDiscount / 100);
|
||||||
|
|
||||||
|
const baseReference = fallbackBase || 1;
|
||||||
|
const rawDiscount =
|
||||||
|
baseReference && pricePerCoin
|
||||||
|
? ((baseReference - pricePerCoin) / baseReference) * 100
|
||||||
|
: 0;
|
||||||
|
const discountPercent = Math.max(
|
||||||
|
0,
|
||||||
|
Number.isFinite(rawDiscount) ? Math.round(rawDiscount) : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isBasePack =
|
||||||
|
(basePack && basePack.productId === pack.productId) ||
|
||||||
|
(!basePack && index === 0);
|
||||||
|
|
||||||
|
acc[pack.productId] = {
|
||||||
|
currency: pack.currency,
|
||||||
|
pricePerCoin,
|
||||||
|
formattedPricePerCoin: formatCurrency(pricePerCoin, pack.currency),
|
||||||
|
discountPercent,
|
||||||
|
isBasePack,
|
||||||
|
isBestValue:
|
||||||
|
(bestDiscountPack &&
|
||||||
|
bestDiscountPack.productId === pack.productId) ||
|
||||||
|
(!bestDiscountPack && fallbackBestId === pack.productId),
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
function CoinPackCard({ pack, selected, pricingDetail, onSelect }) {
|
||||||
const handleSelect = React.useCallback(() => {
|
const handleSelect = React.useCallback(() => {
|
||||||
if (typeof onSelect !== "function" || !pack?.productId) {
|
if (typeof onSelect !== "function" || !pack?.productId) {
|
||||||
return;
|
return;
|
||||||
@@ -52,6 +164,13 @@ function CoinPackCard({ pack, selected, onSelect }) {
|
|||||||
}, [onSelect, pack?.productId]);
|
}, [onSelect, pack?.productId]);
|
||||||
|
|
||||||
const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency);
|
const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency);
|
||||||
|
const perCoinPrice = pricingDetail?.formattedPricePerCoin;
|
||||||
|
const discountPercent = pricingDetail?.discountPercent;
|
||||||
|
const discountLabel = pricingDetail?.isBasePack
|
||||||
|
? "Pack de base (référence)"
|
||||||
|
: typeof discountPercent === "number"
|
||||||
|
? `-${discountPercent}% vs pack de base`
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -73,13 +192,54 @@ function CoinPackCard({ pack, selected, onSelect }) {
|
|||||||
iconSize={26}
|
iconSize={26}
|
||||||
/>
|
/>
|
||||||
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
|
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
|
||||||
|
{pricingDetail?.isBestValue ? (
|
||||||
|
<View style={styles.tagBestValue}>
|
||||||
|
<Text style={styles.tagBestValueText}>Meilleure offre</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{pack?.description ? (
|
{pack?.description ? (
|
||||||
<Text style={styles.packDescription}>{pack.description}</Text>
|
<Text style={styles.packDescription}>{pack.description}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
<View style={styles.priceBlock}>
|
||||||
{formattedPrice ? (
|
{formattedPrice ? (
|
||||||
<Text style={styles.packPrice}>{formattedPrice}</Text>
|
<Text style={styles.packPrice}>{formattedPrice}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Text style={styles.pricePerCoin} numberOfLines={1}>
|
||||||
|
{perCoinPrice
|
||||||
|
? `≈ ${perCoinPrice} / jeton`
|
||||||
|
: "Valeurs indicatives / jeton"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{discountLabel ? (
|
||||||
|
<View style={styles.discountRow}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.discountBadge,
|
||||||
|
pricingDetail?.isBestValue && styles.discountBadgeBest,
|
||||||
|
pricingDetail?.isBasePack && styles.discountBadgeBase,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.discountText,
|
||||||
|
pricingDetail?.isBestValue && styles.discountTextBest,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{discountLabel}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{!pricingDetail?.isBasePack && typeof discountPercent === "number" ? (
|
||||||
|
<Text style={styles.discountHelper}>
|
||||||
|
Économie estimée vs pack de base
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.discountHelperMuted}>
|
||||||
|
Référence prix/jeton
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
@@ -98,6 +258,10 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
refreshCatalog,
|
refreshCatalog,
|
||||||
createCoinPackCheckout,
|
createCoinPackCheckout,
|
||||||
} = useStripe();
|
} = useStripe();
|
||||||
|
const packPricingById = React.useMemo(
|
||||||
|
() => computeCoinPackPricing(coinPacks),
|
||||||
|
[coinPacks],
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!coinPacks.length) {
|
if (!coinPacks.length) {
|
||||||
@@ -200,6 +364,7 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
key={pack.productId}
|
key={pack.productId}
|
||||||
pack={pack}
|
pack={pack}
|
||||||
selected={selectedPackId === pack.productId}
|
selected={selectedPackId === pack.productId}
|
||||||
|
pricingDetail={packPricingById[pack.productId]}
|
||||||
onSelect={setSelectedPackId}
|
onSelect={setSelectedPackId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -236,6 +401,12 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
|
|
||||||
<View style={styles.content}>{renderContent()}</View>
|
<View style={styles.content}>{renderContent()}</View>
|
||||||
|
|
||||||
|
<Text style={styles.indicativeNote}>
|
||||||
|
Tarifs indicatifs : les prix et quantités de jetons sont amenés à
|
||||||
|
évoluer. Les remises sont affichées vs le pack de base pour mieux
|
||||||
|
valoriser les offres volumineuses.
|
||||||
|
</Text>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title={isProcessing ? "Redirection..." : "Acheter ce pack"}
|
title={isProcessing ? "Redirection..." : "Acheter ce pack"}
|
||||||
@@ -393,12 +564,74 @@ const styles = StyleSheet.create({
|
|||||||
color: "rgba(255, 255, 255, 0.72)",
|
color: "rgba(255, 255, 255, 0.72)",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
|
priceBlock: {
|
||||||
|
gap: 2,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
packPrice: {
|
packPrice: {
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
|
pricePerCoin: {
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
fontSize: 13,
|
||||||
|
color: "rgba(255, 255, 255, 0.7)",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
discountRow: {
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
discountBadge: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.12)",
|
||||||
|
},
|
||||||
|
discountBadgeBase: {
|
||||||
|
borderColor: Palette.primary,
|
||||||
|
backgroundColor: Palette.transparentPrimary,
|
||||||
|
},
|
||||||
|
discountBadgeBest: {
|
||||||
|
borderColor: Palette.green,
|
||||||
|
backgroundColor: Palette.transparentGreen,
|
||||||
|
},
|
||||||
|
discountText: {
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
fontSize: 13,
|
||||||
|
color: Palette.white,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
discountTextBest: {
|
||||||
|
color: Palette.white,
|
||||||
|
},
|
||||||
|
discountHelper: {
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "rgba(255, 255, 255, 0.72)",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
discountHelperMuted: {
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "rgba(255, 255, 255, 0.56)",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
tagBestValue: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: Palette.transparentGreen,
|
||||||
|
},
|
||||||
|
tagBestValueText: {
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
width: "85%",
|
width: "85%",
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
@@ -411,4 +644,11 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
indicativeNote: {
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "rgba(255, 255, 255, 0.62)",
|
||||||
|
textAlign: "center",
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const BottomTabScreen = () => {
|
|||||||
name={Routes.HomeStack}
|
name={Routes.HomeStack}
|
||||||
component={HomeStack}
|
component={HomeStack}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: "Créer",
|
tabBarLabel: "Menu",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarShowLabel: true,
|
tabBarShowLabel: true,
|
||||||
tabBarIcon: ({ focused }) => renderIcon(tabs.addTab, focused),
|
tabBarIcon: ({ focused }) => renderIcon(tabs.addTab, focused),
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ const STAGE_CARD_CONTENT = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const CLUB_CARD_IMAGE = icons.clubIcon;
|
|
||||||
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
|
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
|
||||||
"trialing",
|
"trialing",
|
||||||
"active",
|
"active",
|
||||||
@@ -114,6 +113,7 @@ const Home = ({ navigation, route }) => {
|
|||||||
videos,
|
videos,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { setTooltip } = useMinuit();
|
const { setTooltip } = useMinuit();
|
||||||
|
const [videoUrl, setVideoUrl] = useState(null);
|
||||||
|
|
||||||
const hasActiveSubscription = useMemo(() => {
|
const hasActiveSubscription = useMemo(() => {
|
||||||
if (!currentUserData) {
|
if (!currentUserData) {
|
||||||
@@ -208,7 +208,6 @@ const Home = ({ navigation, route }) => {
|
|||||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||||
const [menuProject, setMenuProject] = useState(null);
|
const [menuProject, setMenuProject] = useState(null);
|
||||||
const menuAnchorRef = useRef(null);
|
const menuAnchorRef = useRef(null);
|
||||||
const [isIntroVideoVisible, setIsIntroVideoVisible] = useState(false);
|
|
||||||
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
|
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
|
||||||
|
|
||||||
const handleSelectProject = useCallback(
|
const handleSelectProject = useCallback(
|
||||||
@@ -404,18 +403,17 @@ const Home = ({ navigation, route }) => {
|
|||||||
}, [hasLocalAdventureFlag, persistAdventureStarted, setTooltip]);
|
}, [hasLocalAdventureFlag, persistAdventureStarted, setTooltip]);
|
||||||
|
|
||||||
const handleStartVisit = useCallback(() => {
|
const handleStartVisit = useCallback(() => {
|
||||||
setIsIntroVideoVisible(true);
|
setVideoUrl(isWeb ? videos?.landingWeb : videos?.landing);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleIntroVideoClose = useCallback(() => {
|
const handleIntroVideoClose = useCallback(() => {
|
||||||
setIsIntroVideoVisible(false);
|
setVideoUrl(null);
|
||||||
markAdventureStarted();
|
markAdventureStarted();
|
||||||
}, [markAdventureStarted]);
|
}, [markAdventureStarted]);
|
||||||
|
|
||||||
const adventureStarted =
|
const adventureStarted =
|
||||||
hasLocalAdventureFlag || !!currentUserData?.adventureStarted;
|
hasLocalAdventureFlag || !!currentUserData?.adventureStarted;
|
||||||
|
|
||||||
const videoUrl = isWeb ? videos?.landingWeb : videos?.landing;
|
|
||||||
const homeBackgroundImage = background.bgTrans;
|
const homeBackgroundImage = background.bgTrans;
|
||||||
const landingBackgroundImage = homeBackgroundImage;
|
const landingBackgroundImage = homeBackgroundImage;
|
||||||
|
|
||||||
@@ -523,10 +521,7 @@ const Home = ({ navigation, route }) => {
|
|||||||
iconPosition="left"
|
iconPosition="left"
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<ShareBtn
|
<ShareBtn style={styles.mobileShareButton} label="Partager" />
|
||||||
style={styles.mobileShareButton}
|
|
||||||
iconOnly
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -590,7 +585,6 @@ const Home = ({ navigation, route }) => {
|
|||||||
{stageCardsList}
|
{stageCardsList}
|
||||||
|
|
||||||
<ClubCard
|
<ClubCard
|
||||||
image={CLUB_CARD_IMAGE}
|
|
||||||
onPress={handleClubPress}
|
onPress={handleClubPress}
|
||||||
hasActiveSubscription={hasActiveSubscription}
|
hasActiveSubscription={hasActiveSubscription}
|
||||||
/>
|
/>
|
||||||
@@ -654,7 +648,6 @@ const Home = ({ navigation, route }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
url={videoUrl}
|
|
||||||
title="Commencer l'aventure MusicLand"
|
title="Commencer l'aventure MusicLand"
|
||||||
onPress={handleStartVisit}
|
onPress={handleStartVisit}
|
||||||
/>
|
/>
|
||||||
@@ -662,7 +655,7 @@ const Home = ({ navigation, route }) => {
|
|||||||
</Page>
|
</Page>
|
||||||
<FullscreenIntroVideo
|
<FullscreenIntroVideo
|
||||||
url={videoUrl}
|
url={videoUrl}
|
||||||
visible={isIntroVideoVisible}
|
visible={!!videoUrl}
|
||||||
onClose={handleIntroVideoClose}
|
onClose={handleIntroVideoClose}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const StageCard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const isMobileVariant = variant === "mobile";
|
const isMobileVariant = variant === "mobile";
|
||||||
const isImageOnLeft = !isMobileVariant && imagePosition !== "right";
|
const isImageOnLeft = !isMobileVariant && imagePosition !== "right";
|
||||||
const isTextRight = !isMobileVariant && textAlign === "right";
|
|
||||||
|
|
||||||
const basePressableStyle =
|
const basePressableStyle =
|
||||||
variant === "web"
|
variant === "web"
|
||||||
@@ -72,7 +71,7 @@ const StageCard = ({
|
|||||||
{
|
{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 190,
|
height: 190,
|
||||||
minHeight: 190,
|
minHeight: 210,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
},
|
},
|
||||||
isMobileVariant
|
isMobileVariant
|
||||||
@@ -96,8 +95,6 @@ const StageCard = ({
|
|||||||
},
|
},
|
||||||
isMobileVariant
|
isMobileVariant
|
||||||
? { justifyContent: "center" }
|
? { justifyContent: "center" }
|
||||||
: isTextRight
|
|
||||||
? { justifyContent: "flex-end" }
|
|
||||||
: { justifyContent: "flex-start" },
|
: { justifyContent: "flex-start" },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -109,11 +106,7 @@ const StageCard = ({
|
|||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
isMobileVariant
|
isMobileVariant ? { textAlign: "center" } : { textAlign: "left" },
|
||||||
? { textAlign: "center" }
|
|
||||||
: isTextRight
|
|
||||||
? { textAlign: "right" }
|
|
||||||
: { textAlign: "left" },
|
|
||||||
]}
|
]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
@@ -153,9 +146,6 @@ const StageCard = ({
|
|||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
borderBottomLeftRadius: 20,
|
borderBottomLeftRadius: 20,
|
||||||
}),
|
}),
|
||||||
...(isTextRight
|
|
||||||
? { alignItems: "flex-end" }
|
|
||||||
: { alignItems: "flex-start" }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const textBlock = (
|
const textBlock = (
|
||||||
@@ -177,12 +167,8 @@ const StageCard = ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
|
right: 10,
|
||||||
},
|
},
|
||||||
isMobileVariant
|
|
||||||
? { right: 10 }
|
|
||||||
: isTextRight
|
|
||||||
? { left: 10 }
|
|
||||||
: { right: 10 },
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FontAwesome name="lock" size={18} color={Palette.primary} />
|
<FontAwesome name="lock" size={18} color={Palette.primary} />
|
||||||
@@ -193,9 +179,7 @@ const StageCard = ({
|
|||||||
style={[
|
style={[
|
||||||
isMobileVariant
|
isMobileVariant
|
||||||
? { textAlign: "center" }
|
? { textAlign: "center" }
|
||||||
: isTextRight
|
: { textAlign: "left", marginRight: 10 },
|
||||||
? { textAlign: "right" }
|
|
||||||
: { textAlign: "left" },
|
|
||||||
{
|
{
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Routes } from "../navigation/Routes";
|
|||||||
import { useUser } from "../providers/UserDataProvider";
|
import { useUser } from "../providers/UserDataProvider";
|
||||||
import { Palette } from "../styles";
|
import { Palette } from "../styles";
|
||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
|
import { isMobile } from "../hooks/useLayoutType";
|
||||||
|
|
||||||
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
||||||
|
|
||||||
@@ -103,13 +104,19 @@ export default function LandingPage() {
|
|||||||
title="Présentation de Musicland"
|
title="Présentation de Musicland"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
setVideoUrl(
|
setVideoUrl(
|
||||||
Platform.OS === "web" ? video?.landingWeb : video?.landing
|
Platform.OS === "web"
|
||||||
|
? video?.landingWeb
|
||||||
|
: video?.landing,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
containerStyle={styles.actionButton}
|
containerStyle={styles.actionButton}
|
||||||
/>
|
/>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Je me lance dans l'aventure"
|
title={
|
||||||
|
Platform.OS === "web"
|
||||||
|
? "Je me lance dans l'aventure"
|
||||||
|
: "Commencer"
|
||||||
|
}
|
||||||
onPress={handleLaunchAdventure}
|
onPress={handleLaunchAdventure}
|
||||||
containerStyle={styles.actionButton}
|
containerStyle={styles.actionButton}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|||||||
import GradientButton from "../components/GradientButton";
|
import GradientButton from "../components/GradientButton";
|
||||||
import CreditAmount from "../components/CreditAmount";
|
import CreditAmount from "../components/CreditAmount";
|
||||||
import Page from "../layouts/Page";
|
import Page from "../layouts/Page";
|
||||||
import { background, subBadges } from "../assets";
|
import { background, icons, subBadges } from "../assets";
|
||||||
import { Palette, gutters } from "../styles";
|
import { Palette, gutters } from "../styles";
|
||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
import { isWeb } from "../hooks/useLayoutType";
|
import { isWeb } from "../hooks/useLayoutType";
|
||||||
import { useStripe } from "../providers/StripeProvider";
|
import { useStripe } from "../providers/StripeProvider";
|
||||||
|
import { goBack } from "../navigation/NavigationService";
|
||||||
|
|
||||||
const formatCurrency = (amount, currency = "eur") => {
|
const formatCurrency = (amount, currency = "eur") => {
|
||||||
if (typeof amount !== "number") {
|
if (typeof amount !== "number") {
|
||||||
@@ -489,6 +490,9 @@ export default function Payments() {
|
|||||||
}),
|
}),
|
||||||
[mobileActionSafePadding]
|
[mobileActionSafePadding]
|
||||||
);
|
);
|
||||||
|
const handleBackPress = React.useCallback(() => {
|
||||||
|
goBack();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderHeaderSection = React.useCallback(() => {
|
const renderHeaderSection = React.useCallback(() => {
|
||||||
return (
|
return (
|
||||||
@@ -615,6 +619,25 @@ export default function Payments() {
|
|||||||
isActionDisabled,
|
isActionDisabled,
|
||||||
mobileActionSafePadding,
|
mobileActionSafePadding,
|
||||||
]);
|
]);
|
||||||
|
const renderWebBackButton = React.useCallback(() => {
|
||||||
|
if (isMobile) return null;
|
||||||
|
return (
|
||||||
|
<View style={styles.webBackContainer}>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={handleBackPress}
|
||||||
|
style={styles.webBackButton}
|
||||||
|
>
|
||||||
|
<ExpoImage
|
||||||
|
source={icons.chevronDown}
|
||||||
|
contentFit="contain"
|
||||||
|
style={styles.webBackIcon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.webBackText}>Retour</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [handleBackPress, isMobile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View style={styles.root}>
|
||||||
@@ -629,7 +652,8 @@ export default function Payments() {
|
|||||||
isMobile && styles.pageContentMobile,
|
isMobile && styles.pageContentMobile,
|
||||||
]}
|
]}
|
||||||
backgroundColor={PAGE_BACKGROUND_COLOR}
|
backgroundColor={PAGE_BACKGROUND_COLOR}
|
||||||
topStickyContent={isMobile ? renderMobileTopSticky : undefined}
|
hideBackButton={!isMobile}
|
||||||
|
topStickyContent={isMobile ? renderMobileTopSticky : renderWebBackButton}
|
||||||
>
|
>
|
||||||
<View style={[styles.inner, isMobile && styles.mobileInner]}>
|
<View style={[styles.inner, isMobile && styles.mobileInner]}>
|
||||||
<ExpoImage
|
<ExpoImage
|
||||||
@@ -771,6 +795,32 @@ const styles = StyleSheet.create({
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
webBackContainer: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
webBackButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 7,
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Palette.ultraLightWhite,
|
||||||
|
backgroundColor: Palette.ultraLightWhite,
|
||||||
|
},
|
||||||
|
webBackIcon: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
transform: [{ rotate: "90deg" }],
|
||||||
|
},
|
||||||
|
webBackText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
fontFamily: FONT_FAMILY.InterBold,
|
fontFamily: FONT_FAMILY.InterBold,
|
||||||
fontSize: isWeb ? 32 : 24,
|
fontSize: isWeb ? 32 : 24,
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
currentUserData?.stripeSubscription?.id ||
|
currentUserData?.stripeSubscription?.id ||
|
||||||
currentUserData?.stripeSubscription?.subscriptionId ||
|
currentUserData?.stripeSubscription?.subscriptionId ||
|
||||||
null;
|
null;
|
||||||
|
const localSubscription = currentUserData?.stripeSubscription || null;
|
||||||
|
|
||||||
const triggerRefresh = useCallback(() => {
|
const triggerRefresh = useCallback(() => {
|
||||||
setRefreshToken((value) => value + 1);
|
setRefreshToken((value) => value + 1);
|
||||||
@@ -219,8 +220,9 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
}, [stripeCustomerId, localSubscriptionId, refreshToken]);
|
}, [stripeCustomerId, localSubscriptionId, refreshToken]);
|
||||||
|
|
||||||
const subscriptionInfo = useMemo(() => {
|
const subscriptionInfo = useMemo(() => {
|
||||||
const rawSubscription =
|
const subscriptionSources = [remoteSubscription, localSubscription].filter(
|
||||||
remoteSubscription || currentUserData?.stripeSubscription || null;
|
Boolean,
|
||||||
|
);
|
||||||
|
|
||||||
const pickString = (value) => {
|
const pickString = (value) => {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
@@ -230,40 +232,71 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
return trimmed ? trimmed : null;
|
return trimmed ? trimmed : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pickFromSources = (resolver) => {
|
||||||
|
for (const source of subscriptionSources) {
|
||||||
|
if (!source) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = resolver(source);
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickDateFromSources = (...resolvers) => {
|
||||||
|
for (const source of subscriptionSources) {
|
||||||
|
if (!source) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const resolveValue of resolvers) {
|
||||||
|
const date = toDate(resolveValue(source));
|
||||||
|
if (date) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const statusSource =
|
const statusSource =
|
||||||
pickString(currentUserData?.stripeSubscriptionStatus) ||
|
pickString(currentUserData?.stripeSubscriptionStatus) ||
|
||||||
pickString(remoteSubscription?.status) ||
|
pickString(pickFromSources((source) => source?.status)) ||
|
||||||
pickString(currentUserData?.stripeSubscription?.status) ||
|
pickString(pickFromSources((source) => source?.stripeSubscriptionStatus)) ||
|
||||||
pickString(
|
pickString(pickFromSources((source) => source?.metadata?.status)) ||
|
||||||
currentUserData?.stripeSubscription?.stripeSubscriptionStatus,
|
|
||||||
) ||
|
|
||||||
pickString(rawSubscription?.status) ||
|
|
||||||
null;
|
null;
|
||||||
|
|
||||||
const status = statusSource ? statusSource.toLowerCase() : null;
|
const status = statusSource ? statusSource.toLowerCase() : null;
|
||||||
|
|
||||||
const cancelAtPeriodEnd =
|
const cancelAtPeriodEnd = subscriptionSources.some(
|
||||||
remoteSubscription?.cancelAtPeriodEnd === true ||
|
(source) =>
|
||||||
rawSubscription?.cancelAtPeriodEnd === true ||
|
source?.cancelAtPeriodEnd === true ||
|
||||||
rawSubscription?.cancel_at_period_end === true ||
|
source?.cancel_at_period_end === true,
|
||||||
false;
|
);
|
||||||
|
|
||||||
const resolveLevelSource = () => {
|
const resolveLevelSource = () => {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
pickString(remoteSubscription?.level),
|
pickString(pickFromSources((source) => source?.level)),
|
||||||
pickString(currentUserData?.premiumLevel),
|
pickString(currentUserData?.premiumLevel),
|
||||||
pickString(rawSubscription?.metadata?.level),
|
pickString(pickFromSources((source) => source?.metadata?.level)),
|
||||||
pickString(rawSubscription?.metadata?.subscriptionLevel),
|
pickString(
|
||||||
|
pickFromSources((source) => source?.metadata?.subscriptionLevel),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
return candidates.find(Boolean) || null;
|
return candidates.find(Boolean) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolvePeriodSource = () => {
|
const resolvePeriodSource = () => {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
pickString(remoteSubscription?.billingPeriod),
|
pickString(pickFromSources((source) => source?.billingPeriod)),
|
||||||
pickString(currentUserData?.premiumBillingPeriod),
|
pickString(currentUserData?.premiumBillingPeriod),
|
||||||
pickString(rawSubscription?.metadata?.billingPeriod),
|
pickString(pickFromSources((source) => source?.metadata?.billingPeriod)),
|
||||||
pickString(rawSubscription?.metadata?.subscriptionBillingPeriod),
|
pickString(
|
||||||
|
pickFromSources(
|
||||||
|
(source) => source?.metadata?.subscriptionBillingPeriod,
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
return candidates.find(Boolean) || null;
|
return candidates.find(Boolean) || null;
|
||||||
};
|
};
|
||||||
@@ -292,14 +325,15 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
? planLabelParts.join(" · ")
|
? planLabelParts.join(" · ")
|
||||||
: "Abonnement Musicland";
|
: "Abonnement Musicland";
|
||||||
|
|
||||||
const currentPeriodEndDate =
|
const currentPeriodEndDate = pickDateFromSources(
|
||||||
toDate(remoteSubscription?.currentPeriodEnd) ||
|
(source) => source?.currentPeriodEnd,
|
||||||
toDate(rawSubscription?.currentPeriodEnd) ||
|
(source) => source?.current_period_end,
|
||||||
toDate(rawSubscription?.current_period_end);
|
);
|
||||||
const createdAtDate =
|
const createdAtDate = pickDateFromSources(
|
||||||
toDate(remoteSubscription?.created) ||
|
(source) => source?.created,
|
||||||
toDate(rawSubscription?.createdAt) ||
|
(source) => source?.createdAt,
|
||||||
toDate(rawSubscription?.created_at);
|
(source) => source?.created_at,
|
||||||
|
);
|
||||||
|
|
||||||
const statusLabelBase =
|
const statusLabelBase =
|
||||||
STATUS_LABELS[status] ||
|
STATUS_LABELS[status] ||
|
||||||
@@ -307,7 +341,13 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null;
|
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null;
|
||||||
const statusColors = getStatusColors(status);
|
const statusColors = getStatusColors(status);
|
||||||
|
|
||||||
const hasAnySubscription = Boolean(rawSubscription?.id);
|
const subscriptionId =
|
||||||
|
pickString(pickFromSources((source) => source?.id)) ||
|
||||||
|
pickString(pickFromSources((source) => source?.subscriptionId)) ||
|
||||||
|
pickString(currentUserData?.stripeSubscription?.subscriptionId) ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
const hasAnySubscription = Boolean(subscriptionId);
|
||||||
const hasActiveSubscription =
|
const hasActiveSubscription =
|
||||||
hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status);
|
hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status);
|
||||||
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd;
|
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd;
|
||||||
@@ -317,10 +357,7 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
: "—";
|
: "—";
|
||||||
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null;
|
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null;
|
||||||
|
|
||||||
let coinsPerMonth =
|
let coinsPerMonth = pickFromSources((source) => source?.coinsPerMonth);
|
||||||
typeof remoteSubscription?.coinsPerMonth === "number"
|
|
||||||
? remoteSubscription.coinsPerMonth
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
coinsPerMonth === null &&
|
coinsPerMonth === null &&
|
||||||
@@ -395,17 +432,13 @@ const ManageSubscription = ({ navigation }) => {
|
|||||||
createdAtLabel,
|
createdAtLabel,
|
||||||
helperMessage: helperMessage || null,
|
helperMessage: helperMessage || null,
|
||||||
level: level || null,
|
level: level || null,
|
||||||
subscriptionId:
|
subscriptionId,
|
||||||
remoteSubscription?.id ||
|
|
||||||
rawSubscription?.id ||
|
|
||||||
currentUserData?.stripeSubscription?.subscriptionId ||
|
|
||||||
null,
|
|
||||||
coinsPerMonth: normalizedCoins,
|
coinsPerMonth: normalizedCoins,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
nextGrantDate: resolvedNextGrantDate,
|
nextGrantDate: resolvedNextGrantDate,
|
||||||
nextGrantLabel,
|
nextGrantLabel,
|
||||||
};
|
};
|
||||||
}, [currentUserData, remoteSubscription]);
|
}, [currentUserData, localSubscription, remoteSubscription]);
|
||||||
|
|
||||||
const handleOpenPlans = useCallback(() => {
|
const handleOpenPlans = useCallback(() => {
|
||||||
const params =
|
const params =
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import * as AppleAuthentication from "expo-apple-authentication";
|
import * as AppleAuthentication from "expo-apple-authentication";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Modal,
|
|
||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||||
import { BlurView } from "expo-blur";
|
|
||||||
import Svg, { Path } from "react-native-svg";
|
import Svg, { Path } from "react-native-svg";
|
||||||
import { background } from "../assets";
|
import { background } from "../assets";
|
||||||
import BorderGradientButton from "../components/BorderGradientButton";
|
import BorderGradientButton from "../components/BorderGradientButton";
|
||||||
import { Input } from "../components/Input";
|
import { Input } from "../components/Input";
|
||||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||||
import COUNTRIES from "../constants/countries";
|
|
||||||
import firebase from "../config/firebase";
|
import firebase from "../config/firebase";
|
||||||
import { isWeb } from "../hooks/useLayoutType";
|
import { isWeb } from "../hooks/useLayoutType";
|
||||||
import useSocialAuth from "../hooks/useSocialAuth";
|
import useSocialAuth from "../hooks/useSocialAuth";
|
||||||
@@ -35,11 +30,6 @@ const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
|||||||
const Register = () => {
|
const Register = () => {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
|
||||||
const [city, setCity] = useState("");
|
|
||||||
const [selectedCountry, setSelectedCountry] = useState(null);
|
|
||||||
const [countrySearch, setCountrySearch] = useState("");
|
|
||||||
const [isCountryModalVisible, setIsCountryModalVisible] = useState(false);
|
|
||||||
const [preferredLanguage, setPreferredLanguage] = useState(null);
|
const [preferredLanguage, setPreferredLanguage] = useState(null);
|
||||||
|
|
||||||
const afterSocialAuth = useCallback(async () => {
|
const afterSocialAuth = useCallback(async () => {
|
||||||
@@ -71,33 +61,7 @@ const Register = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredCountries = useMemo(() => {
|
const isFormValid = email.trim().length > 0 && firstName.trim().length > 0;
|
||||||
const query = countrySearch.trim().toLowerCase();
|
|
||||||
if (!query) {
|
|
||||||
return COUNTRIES;
|
|
||||||
}
|
|
||||||
return COUNTRIES.filter((country) =>
|
|
||||||
country.name.toLowerCase().includes(query),
|
|
||||||
);
|
|
||||||
}, [countrySearch]);
|
|
||||||
|
|
||||||
const isFormValid =
|
|
||||||
email.trim().length > 0 &&
|
|
||||||
firstName.trim().length > 0 &&
|
|
||||||
lastName.trim().length > 0 &&
|
|
||||||
city.trim().length > 0 &&
|
|
||||||
selectedCountry;
|
|
||||||
|
|
||||||
const handleSelectCountry = (country) => {
|
|
||||||
setSelectedCountry(country);
|
|
||||||
setIsCountryModalVisible(false);
|
|
||||||
setCountrySearch("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
setIsCountryModalVisible(false);
|
|
||||||
setCountrySearch("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFormContent = () => (
|
const renderFormContent = () => (
|
||||||
<View style={styles.formContent}>
|
<View style={styles.formContent}>
|
||||||
@@ -111,7 +75,6 @@ const Register = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={{ gap: 16 }}>
|
<View style={{ gap: 16 }}>
|
||||||
<View style={{ gap: 12 }}>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Prénom"
|
placeholder="Prénom"
|
||||||
label="Prénom"
|
label="Prénom"
|
||||||
@@ -119,50 +82,6 @@ const Register = () => {
|
|||||||
value={firstName}
|
value={firstName}
|
||||||
setValue={setFirstName}
|
setValue={setFirstName}
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
placeholder="Nom"
|
|
||||||
label="Nom"
|
|
||||||
isBlur
|
|
||||||
value={lastName}
|
|
||||||
setValue={setLastName}
|
|
||||||
/>
|
|
||||||
<View style={styles.countryField}>
|
|
||||||
<Text style={styles.inputLabel}>Pays</Text>
|
|
||||||
<BlurView
|
|
||||||
tint="dark"
|
|
||||||
style={[
|
|
||||||
styles.countryBlurWrapper,
|
|
||||||
selectedCountry ? styles.countryButtonFilled : null,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.countryButton,
|
|
||||||
pressed ? styles.countryButtonPressed : null,
|
|
||||||
]}
|
|
||||||
onPress={() => setIsCountryModalVisible(true)}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.countryButtonText,
|
|
||||||
!selectedCountry && styles.countryButtonPlaceholder,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{selectedCountry
|
|
||||||
? selectedCountry.name
|
|
||||||
: "Sélectionne ton pays"}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</BlurView>
|
|
||||||
</View>
|
|
||||||
<Input
|
|
||||||
placeholder="Ville"
|
|
||||||
label="Ville"
|
|
||||||
isBlur
|
|
||||||
value={city}
|
|
||||||
setValue={setCity}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Adresse mail"
|
placeholder="Adresse mail"
|
||||||
label="Adresse mail"
|
label="Adresse mail"
|
||||||
@@ -182,9 +101,6 @@ const Register = () => {
|
|||||||
navigate(Routes.CreatePassword, {
|
navigate(Routes.CreatePassword, {
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
firstName: firstName.trim(),
|
firstName: firstName.trim(),
|
||||||
lastName: lastName.trim(),
|
|
||||||
city: city.trim(),
|
|
||||||
country: selectedCountry,
|
|
||||||
preferredLanguage,
|
preferredLanguage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -239,10 +155,6 @@ const Register = () => {
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
{renderFormContent()}
|
{renderFormContent()}
|
||||||
</KeyboardAwareScrollView>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
)}
|
|
||||||
</ItemContainer>
|
|
||||||
<View style={styles.bottomLoginPrompt}>
|
<View style={styles.bottomLoginPrompt}>
|
||||||
<Text style={styles.bottomFooterText}>
|
<Text style={styles.bottomFooterText}>
|
||||||
Tu as déjà un compte ?{" "}
|
Tu as déjà un compte ?{" "}
|
||||||
@@ -254,48 +166,11 @@ const Register = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</KeyboardAwareScrollView>
|
||||||
<Modal
|
</KeyboardAvoidingView>
|
||||||
transparent
|
|
||||||
visible={isCountryModalVisible}
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={handleCloseModal}
|
|
||||||
>
|
|
||||||
<View style={styles.modalOverlay}>
|
|
||||||
<View style={styles.modalContainer}>
|
|
||||||
<View style={styles.modalHeader}>
|
|
||||||
<Text style={styles.modalTitle}>Sélectionne ton pays</Text>
|
|
||||||
<Pressable onPress={handleCloseModal}>
|
|
||||||
<Text style={styles.closeText}>Fermer</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<TextInput
|
|
||||||
value={countrySearch}
|
|
||||||
onChangeText={setCountrySearch}
|
|
||||||
placeholder="Rechercher un pays"
|
|
||||||
placeholderTextColor={Palette.gray}
|
|
||||||
style={styles.searchInput}
|
|
||||||
/>
|
|
||||||
<FlatList
|
|
||||||
data={filteredCountries}
|
|
||||||
keyExtractor={(item) => item.code}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<Pressable
|
|
||||||
style={styles.countryItem}
|
|
||||||
onPress={() => handleSelectCountry(item)}
|
|
||||||
>
|
|
||||||
<Text style={styles.countryItemText}>{item.name}</Text>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
)}
|
||||||
keyboardShouldPersistTaps="handled"
|
</ItemContainer>
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
ListEmptyComponent={
|
|
||||||
<Text style={styles.emptyListText}>Aucun pays trouvé</Text>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -413,36 +288,6 @@ const styles = StyleSheet.create({
|
|||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
},
|
},
|
||||||
countryBlurWrapper: {
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: Palette.glass,
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
countryButton: {
|
|
||||||
minHeight: 50,
|
|
||||||
justifyContent: "center",
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
countryButtonPressed: {
|
|
||||||
opacity: 0.85,
|
|
||||||
},
|
|
||||||
countryButtonText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
},
|
|
||||||
countryButtonPlaceholder: {
|
|
||||||
color: Palette.gray,
|
|
||||||
},
|
|
||||||
countryField: {
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
inputLabel: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
|
||||||
},
|
|
||||||
footer: {
|
footer: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
@@ -542,61 +387,4 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
modalOverlay: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: Palette.transparentBlack,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: 16,
|
|
||||||
},
|
|
||||||
modalContainer: {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 420,
|
|
||||||
maxHeight: "80%",
|
|
||||||
backgroundColor: Palette.ultraLightBlack,
|
|
||||||
borderRadius: 20,
|
|
||||||
padding: 16,
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
modalHeader: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
||||||
},
|
|
||||||
closeText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
},
|
|
||||||
searchInput: {
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: Palette.ultraLightWhite,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 10,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
},
|
|
||||||
countryItem: {
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: Palette.ultraLightWhite,
|
|
||||||
},
|
|
||||||
countryItemText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
},
|
|
||||||
emptyListText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: Palette.gray,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
textAlign: "center",
|
|
||||||
paddingVertical: 20,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import ListSelection from "../../components/ListSelection/ListSelection";
|
|||||||
const SECTION_INSTRUCTIONS = {
|
const SECTION_INSTRUCTIONS = {
|
||||||
BASE: "Choisis ta base",
|
BASE: "Choisis ta base",
|
||||||
SENSIBILITE: "Choisis ta sensibilité",
|
SENSIBILITE: "Choisis ta sensibilité",
|
||||||
TECHNIQUE: "Choisis ta technique (Facultatif)",
|
TECHNIQUE: "Choisis ta technique",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const OPTIONAL_SECTION_CATEGORIES = new Set(["SENSIBILITE", "TECHNIQUE"]);
|
||||||
|
|
||||||
const normalizeCategory = (value) => {
|
const normalizeCategory = (value) => {
|
||||||
if (typeof value !== "string") return "";
|
if (typeof value !== "string") return "";
|
||||||
return value
|
return value
|
||||||
@@ -76,6 +78,10 @@ const CustomizeVoice = ({
|
|||||||
const subtitle =
|
const subtitle =
|
||||||
SECTION_INSTRUCTIONS[normalizedCategory] ||
|
SECTION_INSTRUCTIONS[normalizedCategory] ||
|
||||||
"Choisis la voix pour ta chanson";
|
"Choisis la voix pour ta chanson";
|
||||||
|
const isOptionalSection = OPTIONAL_SECTION_CATEGORIES.has(normalizedCategory);
|
||||||
|
const title = `Personnalise la voix que tu veux pour ta chanson${
|
||||||
|
isOptionalSection ? " (Facultatif)" : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
const voiceObject = useMemo(
|
const voiceObject = useMemo(
|
||||||
() => selectionToObject(selected),
|
() => selectionToObject(selected),
|
||||||
@@ -105,7 +111,7 @@ const CustomizeVoice = ({
|
|||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, gap: 10, paddingTop: 16 }}>
|
<View style={{ flex: 1, gap: 10, paddingTop: 16 }}>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
title="Personnalise la voix que tu veux pour ta chanson"
|
title={title}
|
||||||
subTitle={subtitle}
|
subTitle={subtitle}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -396,7 +396,6 @@ const Lyrics = ({ navigation }) => {
|
|||||||
<MusicLandHeader
|
<MusicLandHeader
|
||||||
onPressBack={() => navigate(Routes.Home)}
|
onPressBack={() => navigate(Routes.Home)}
|
||||||
progress={95}
|
progress={95}
|
||||||
logo={icons.musicLandWriting}
|
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const SongStructure = ({
|
|||||||
selected: selectedProp,
|
selected: selectedProp,
|
||||||
setSelected: setSelectedProp,
|
setSelected: setSelectedProp,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [containerLayout, setContainerLayout] = useState(null);
|
||||||
const [internalSelected, setInternalSelected] = useState(null);
|
const [internalSelected, setInternalSelected] = useState(null);
|
||||||
const selected = selectedProp ?? internalSelected;
|
const selected = selectedProp ?? internalSelected;
|
||||||
const setSelected = setSelectedProp ?? setInternalSelected;
|
const setSelected = setSelectedProp ?? setInternalSelected;
|
||||||
@@ -23,7 +24,11 @@ const SongStructure = ({
|
|||||||
title="Comment veux-tu structurer ta chanson ?"
|
title="Comment veux-tu structurer ta chanson ?"
|
||||||
subTitle="Sélectionne une structure."
|
subTitle="Sélectionne une structure."
|
||||||
/>
|
/>
|
||||||
<ItemContainer>
|
<View
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
|
||||||
|
>
|
||||||
|
<ItemContainer height={containerLayout?.height}>
|
||||||
<ListSelection
|
<ListSelection
|
||||||
options={SONG_STRUCTURE}
|
options={SONG_STRUCTURE}
|
||||||
variant="simple"
|
variant="simple"
|
||||||
@@ -35,6 +40,7 @@ const SongStructure = ({
|
|||||||
/>
|
/>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const SongStyle = ({
|
|||||||
const [previousOtherStyle, setPreviousOtherStyle] = useState(
|
const [previousOtherStyle, setPreviousOtherStyle] = useState(
|
||||||
normalizedOtherStyle
|
normalizedOtherStyle
|
||||||
);
|
);
|
||||||
|
const [containerLayout, setContainerLayout] = useState(null);
|
||||||
|
|
||||||
const selected = selectedProp ?? internalSelected;
|
const selected = selectedProp ?? internalSelected;
|
||||||
const setSelectedBase = setSelectedProp ?? setInternalSelected;
|
const setSelectedBase = setSelectedProp ?? setInternalSelected;
|
||||||
@@ -97,12 +98,16 @@ const SongStyle = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={{ flex: 1, gap: 16, marginTop: 16 }}>
|
<View style={{ flex: 1, gap: 16, marginTop: 16 }}>
|
||||||
<View style={{ gap: 10 }}>
|
<View style={{ flex: 1, gap: 10 }}>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
title={`Quel est le style de ta chanson ?`}
|
title={`Quel est le style de ta chanson ?`}
|
||||||
subTitle="Choisis l'ambiance émotions que tu veux faire passer."
|
subTitle="Choisis l'ambiance émotions que tu veux faire passer."
|
||||||
/>
|
/>
|
||||||
<ItemContainer>
|
<View
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
|
||||||
|
>
|
||||||
|
<ItemContainer height={containerLayout?.height}>
|
||||||
<ListSelection
|
<ListSelection
|
||||||
options={songStyleOptions}
|
options={songStyleOptions}
|
||||||
variant="titleDescription"
|
variant="titleDescription"
|
||||||
@@ -113,6 +118,7 @@ const SongStyle = ({
|
|||||||
/>
|
/>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
{selected === OTHER_STYLE_OPTION ? (
|
{selected === OTHER_STYLE_OPTION ? (
|
||||||
<View style={styles.otherStyleSummary}>
|
<View style={styles.otherStyleSummary}>
|
||||||
<View style={{ flex: 1, gap: 4 }}>
|
<View style={{ flex: 1, gap: 4 }}>
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ const WritingLyrics = () => {
|
|||||||
const [videoUrl, setVideoUrl] = useState(null);
|
const [videoUrl, setVideoUrl] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVideoUrl(isWeb ? videos.celineWeb : videos?.celine);
|
// setVideoUrl(isWeb ? videos.celineWeb : videos?.celine);
|
||||||
|
setVideoUrl(videos?.test);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startWriting = React.useCallback(async () => {
|
const startWriting = React.useCallback(async () => {
|
||||||
|
|||||||