clear more tickets

This commit is contained in:
Thomas Demirdjian
2025-11-20 17:44:09 +01:00
parent 70c94c97b4
commit 83ced20dcf
24 changed files with 506 additions and 378 deletions
+13 -1
View File
@@ -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();
+3 -3
View File
@@ -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": [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 159 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

After

Width:  |  Height:  |  Size: 677 KiB

+40 -37
View File
@@ -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 video = videoRef.current;
if (!video || !uri) {
return undefined;
}
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;
const attemptPlay = () => { const attemptPlay = () => {
const video = videoRef.current;
if (!video) {
rafId = requestAnimationFrame(attemptPlay);
return;
}
video.currentTime = 0;
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}>
+243 -3
View File
@@ -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,12 +192,53 @@ 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}
{formattedPrice ? ( <View style={styles.priceBlock}>
<Text style={styles.packPrice}>{formattedPrice}</Text> {formattedPrice ? (
<Text style={styles.packPrice}>{formattedPrice}</Text>
) : 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} ) : 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,
},
}); });
+1 -1
View File
@@ -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),
+5 -12
View File
@@ -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}
/> />
</> </>
+5 -21
View File
@@ -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,9 +95,7 @@ const StageCard = ({
}, },
isMobileVariant isMobileVariant
? { justifyContent: "center" } ? { justifyContent: "center" }
: isTextRight : { justifyContent: "flex-start" },
? { justifyContent: "flex-end" }
: { justifyContent: "flex-start" },
]} ]}
> >
<Text <Text
@@ -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,
+9 -2
View File
@@ -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}
/> />
+52 -2
View File
@@ -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,
+71 -38
View File
@@ -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 =
+20 -232
View File
@@ -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,58 +75,13 @@ 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" isBlur
isBlur 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,63 +155,22 @@ const Register = () => {
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
{renderFormContent()} {renderFormContent()}
<View style={styles.bottomLoginPrompt}>
<Text style={styles.bottomFooterText}>
Tu as déjà un compte ?{" "}
<Text
style={styles.bottomFooterLink}
onPress={() => navigate(Routes.Login)}
>
Se connecter
</Text>
</Text>
</View>
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
)} )}
</ItemContainer> </ItemContainer>
<View style={styles.bottomLoginPrompt}>
<Text style={styles.bottomFooterText}>
Tu as déjà un compte ?{" "}
<Text
style={styles.bottomFooterLink}
onPress={() => navigate(Routes.Login)}
>
Se connecter
</Text>
</Text>
</View>
</View> </View>
<Modal
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"
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<Text style={styles.emptyListText}>Aucun pays trouvé</Text>
}
/>
</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,
},
}); });
+8 -2
View File
@@ -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
-1
View File
@@ -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={{
+17 -11
View File
@@ -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,17 +24,22 @@ 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
<ListSelection style={{ flex: 1 }}
options={SONG_STRUCTURE} onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
variant="simple" >
selected={selected} <ItemContainer height={containerLayout?.height}>
setSelected={setSelected} <ListSelection
contentContainerStyle={styles.contentContainer} options={SONG_STRUCTURE}
itemContainerStyle={styles.itemContainer} variant="simple"
itemTextStyle={styles.itemText} selected={selected}
/> setSelected={setSelected}
</ItemContainer> contentContainerStyle={styles.contentContainer}
itemContainerStyle={styles.itemContainer}
itemTextStyle={styles.itemText}
/>
</ItemContainer>
</View>
</View> </View>
); );
}; };
+17 -11
View File
@@ -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,21 +98,26 @@ 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
<ListSelection style={{ flex: 1 }}
options={songStyleOptions} onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
variant="titleDescription" >
selected={selected} <ItemContainer height={containerLayout?.height}>
setSelected={handleStyleSelect} <ListSelection
contentContainerStyle={styles.contentContainer} options={songStyleOptions}
itemContainerStyle={styles.itemContainer} variant="titleDescription"
/> selected={selected}
</ItemContainer> setSelected={handleStyleSelect}
contentContainerStyle={styles.contentContainer}
itemContainerStyle={styles.itemContainer}
/>
</ItemContainer>
</View>
</View> </View>
{selected === OTHER_STYLE_OPTION ? ( {selected === OTHER_STYLE_OPTION ? (
<View style={styles.otherStyleSummary}> <View style={styles.otherStyleSummary}>
+2 -1
View File
@@ -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 () => {