clear more tickets
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 681 KiB After Width: | Height: | Size: 677 KiB |
@@ -67,6 +67,25 @@ const FullscreenIntroVideo = ({ url, visible = true, 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(() => {
|
||||
let isMounted = true;
|
||||
|
||||
@@ -103,44 +122,22 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
if (!visible || !uri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 video = videoRef.current;
|
||||
|
||||
if (!video) {
|
||||
rafId = requestAnimationFrame(attemptPlay);
|
||||
return;
|
||||
}
|
||||
|
||||
video.currentTime = 0;
|
||||
const result = video.play();
|
||||
|
||||
if (result?.catch) {
|
||||
@@ -153,15 +150,17 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
};
|
||||
|
||||
attemptPlay();
|
||||
const pollId = setInterval(evaluateShouldClose, CLOSE_POLL_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
video.pause();
|
||||
video.removeEventListener("ended", handleEnded);
|
||||
video.removeEventListener("timeupdate", handleTimeUpdate);
|
||||
video.removeEventListener("pause", handlePause);
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
clearInterval(pollId);
|
||||
const video = videoRef.current;
|
||||
video?.pause();
|
||||
};
|
||||
}, [handleClose, muted, uri, visible]);
|
||||
}, [evaluateShouldClose, muted, uri, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
@@ -194,6 +193,10 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
loop={false}
|
||||
muted={muted}
|
||||
controls={false}
|
||||
onEnded={handleClose}
|
||||
onPause={evaluateShouldClose}
|
||||
onTimeUpdate={evaluateShouldClose}
|
||||
onError={handleClose}
|
||||
/>
|
||||
) : null}
|
||||
<Pressable onPress={handleClose} style={closeButtonStyle}>
|
||||
|
||||
@@ -43,7 +43,119 @@ const formatCurrency = (amount, currency = "eur") => {
|
||||
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(() => {
|
||||
if (typeof onSelect !== "function" || !pack?.productId) {
|
||||
return;
|
||||
@@ -52,6 +164,13 @@ function CoinPackCard({ pack, selected, onSelect }) {
|
||||
}, [onSelect, pack?.productId]);
|
||||
|
||||
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 (
|
||||
<Pressable
|
||||
@@ -73,12 +192,53 @@ function CoinPackCard({ pack, selected, onSelect }) {
|
||||
iconSize={26}
|
||||
/>
|
||||
{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>
|
||||
{pack?.description ? (
|
||||
<Text style={styles.packDescription}>{pack.description}</Text>
|
||||
) : null}
|
||||
{formattedPrice ? (
|
||||
<Text style={styles.packPrice}>{formattedPrice}</Text>
|
||||
<View style={styles.priceBlock}>
|
||||
{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}
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
@@ -98,6 +258,10 @@ const CoinPackModal = ({ visible, onClose }) => {
|
||||
refreshCatalog,
|
||||
createCoinPackCheckout,
|
||||
} = useStripe();
|
||||
const packPricingById = React.useMemo(
|
||||
() => computeCoinPackPricing(coinPacks),
|
||||
[coinPacks],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!coinPacks.length) {
|
||||
@@ -200,6 +364,7 @@ const CoinPackModal = ({ visible, onClose }) => {
|
||||
key={pack.productId}
|
||||
pack={pack}
|
||||
selected={selectedPackId === pack.productId}
|
||||
pricingDetail={packPricingById[pack.productId]}
|
||||
onSelect={setSelectedPackId}
|
||||
/>
|
||||
))}
|
||||
@@ -236,6 +401,12 @@ const CoinPackModal = ({ visible, onClose }) => {
|
||||
|
||||
<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}>
|
||||
<GradientButton
|
||||
title={isProcessing ? "Redirection..." : "Acheter ce pack"}
|
||||
@@ -393,12 +564,74 @@ const styles = StyleSheet.create({
|
||||
color: "rgba(255, 255, 255, 0.72)",
|
||||
textAlign: "center",
|
||||
},
|
||||
priceBlock: {
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
},
|
||||
packPrice: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 18,
|
||||
color: Palette.white,
|
||||
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: {
|
||||
width: "85%",
|
||||
alignSelf: "center",
|
||||
@@ -411,4 +644,11 @@ const styles = StyleSheet.create({
|
||||
textAlign: "center",
|
||||
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}
|
||||
component={HomeStack}
|
||||
options={{
|
||||
tabBarLabel: "Créer",
|
||||
tabBarLabel: "Menu",
|
||||
headerShown: false,
|
||||
tabBarShowLabel: true,
|
||||
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([
|
||||
"trialing",
|
||||
"active",
|
||||
@@ -114,6 +113,7 @@ const Home = ({ navigation, route }) => {
|
||||
videos,
|
||||
} = useUser();
|
||||
const { setTooltip } = useMinuit();
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
const hasActiveSubscription = useMemo(() => {
|
||||
if (!currentUserData) {
|
||||
@@ -208,7 +208,6 @@ const Home = ({ navigation, route }) => {
|
||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||
const [menuProject, setMenuProject] = useState(null);
|
||||
const menuAnchorRef = useRef(null);
|
||||
const [isIntroVideoVisible, setIsIntroVideoVisible] = useState(false);
|
||||
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
|
||||
|
||||
const handleSelectProject = useCallback(
|
||||
@@ -404,18 +403,17 @@ const Home = ({ navigation, route }) => {
|
||||
}, [hasLocalAdventureFlag, persistAdventureStarted, setTooltip]);
|
||||
|
||||
const handleStartVisit = useCallback(() => {
|
||||
setIsIntroVideoVisible(true);
|
||||
setVideoUrl(isWeb ? videos?.landingWeb : videos?.landing);
|
||||
}, []);
|
||||
|
||||
const handleIntroVideoClose = useCallback(() => {
|
||||
setIsIntroVideoVisible(false);
|
||||
setVideoUrl(null);
|
||||
markAdventureStarted();
|
||||
}, [markAdventureStarted]);
|
||||
|
||||
const adventureStarted =
|
||||
hasLocalAdventureFlag || !!currentUserData?.adventureStarted;
|
||||
|
||||
const videoUrl = isWeb ? videos?.landingWeb : videos?.landing;
|
||||
const homeBackgroundImage = background.bgTrans;
|
||||
const landingBackgroundImage = homeBackgroundImage;
|
||||
|
||||
@@ -523,10 +521,7 @@ const Home = ({ navigation, route }) => {
|
||||
iconPosition="left"
|
||||
/>
|
||||
</Pressable>
|
||||
<ShareBtn
|
||||
style={styles.mobileShareButton}
|
||||
iconOnly
|
||||
/>
|
||||
<ShareBtn style={styles.mobileShareButton} label="Partager" />
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
@@ -590,7 +585,6 @@ const Home = ({ navigation, route }) => {
|
||||
{stageCardsList}
|
||||
|
||||
<ClubCard
|
||||
image={CLUB_CARD_IMAGE}
|
||||
onPress={handleClubPress}
|
||||
hasActiveSubscription={hasActiveSubscription}
|
||||
/>
|
||||
@@ -654,7 +648,6 @@ const Home = ({ navigation, route }) => {
|
||||
}}
|
||||
>
|
||||
<GradientButton
|
||||
url={videoUrl}
|
||||
title="Commencer l'aventure MusicLand"
|
||||
onPress={handleStartVisit}
|
||||
/>
|
||||
@@ -662,7 +655,7 @@ const Home = ({ navigation, route }) => {
|
||||
</Page>
|
||||
<FullscreenIntroVideo
|
||||
url={videoUrl}
|
||||
visible={isIntroVideoVisible}
|
||||
visible={!!videoUrl}
|
||||
onClose={handleIntroVideoClose}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -19,7 +19,6 @@ const StageCard = ({
|
||||
}) => {
|
||||
const isMobileVariant = variant === "mobile";
|
||||
const isImageOnLeft = !isMobileVariant && imagePosition !== "right";
|
||||
const isTextRight = !isMobileVariant && textAlign === "right";
|
||||
|
||||
const basePressableStyle =
|
||||
variant === "web"
|
||||
@@ -72,7 +71,7 @@ const StageCard = ({
|
||||
{
|
||||
width: "100%",
|
||||
height: 190,
|
||||
minHeight: 190,
|
||||
minHeight: 210,
|
||||
borderRadius: 0,
|
||||
},
|
||||
isMobileVariant
|
||||
@@ -96,9 +95,7 @@ const StageCard = ({
|
||||
},
|
||||
isMobileVariant
|
||||
? { justifyContent: "center" }
|
||||
: isTextRight
|
||||
? { justifyContent: "flex-end" }
|
||||
: { justifyContent: "flex-start" },
|
||||
: { justifyContent: "flex-start" },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
@@ -109,11 +106,7 @@ const StageCard = ({
|
||||
color: Palette.white,
|
||||
flexShrink: 1,
|
||||
},
|
||||
isMobileVariant
|
||||
? { textAlign: "center" }
|
||||
: isTextRight
|
||||
? { textAlign: "right" }
|
||||
: { textAlign: "left" },
|
||||
isMobileVariant ? { textAlign: "center" } : { textAlign: "left" },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@@ -153,9 +146,6 @@ const StageCard = ({
|
||||
borderTopLeftRadius: 20,
|
||||
borderBottomLeftRadius: 20,
|
||||
}),
|
||||
...(isTextRight
|
||||
? { alignItems: "flex-end" }
|
||||
: { alignItems: "flex-start" }),
|
||||
};
|
||||
|
||||
const textBlock = (
|
||||
@@ -177,12 +167,8 @@ const StageCard = ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2,
|
||||
right: 10,
|
||||
},
|
||||
isMobileVariant
|
||||
? { right: 10 }
|
||||
: isTextRight
|
||||
? { left: 10 }
|
||||
: { right: 10 },
|
||||
]}
|
||||
>
|
||||
<FontAwesome name="lock" size={18} color={Palette.primary} />
|
||||
@@ -193,9 +179,7 @@ const StageCard = ({
|
||||
style={[
|
||||
isMobileVariant
|
||||
? { textAlign: "center" }
|
||||
: isTextRight
|
||||
? { textAlign: "right" }
|
||||
: { textAlign: "left" },
|
||||
: { textAlign: "left", marginRight: 10 },
|
||||
{
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 13,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Routes } from "../navigation/Routes";
|
||||
import { useUser } from "../providers/UserDataProvider";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { isMobile } from "../hooks/useLayoutType";
|
||||
|
||||
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
||||
|
||||
@@ -103,13 +104,19 @@ export default function LandingPage() {
|
||||
title="Présentation de Musicland"
|
||||
onPress={() =>
|
||||
setVideoUrl(
|
||||
Platform.OS === "web" ? video?.landingWeb : video?.landing
|
||||
Platform.OS === "web"
|
||||
? video?.landingWeb
|
||||
: video?.landing,
|
||||
)
|
||||
}
|
||||
containerStyle={styles.actionButton}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Je me lance dans l'aventure"
|
||||
title={
|
||||
Platform.OS === "web"
|
||||
? "Je me lance dans l'aventure"
|
||||
: "Commencer"
|
||||
}
|
||||
onPress={handleLaunchAdventure}
|
||||
containerStyle={styles.actionButton}
|
||||
/>
|
||||
|
||||
+52
-2
@@ -14,11 +14,12 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import CreditAmount from "../components/CreditAmount";
|
||||
import Page from "../layouts/Page";
|
||||
import { background, subBadges } from "../assets";
|
||||
import { background, icons, subBadges } from "../assets";
|
||||
import { Palette, gutters } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import { useStripe } from "../providers/StripeProvider";
|
||||
import { goBack } from "../navigation/NavigationService";
|
||||
|
||||
const formatCurrency = (amount, currency = "eur") => {
|
||||
if (typeof amount !== "number") {
|
||||
@@ -489,6 +490,9 @@ export default function Payments() {
|
||||
}),
|
||||
[mobileActionSafePadding]
|
||||
);
|
||||
const handleBackPress = React.useCallback(() => {
|
||||
goBack();
|
||||
}, []);
|
||||
|
||||
const renderHeaderSection = React.useCallback(() => {
|
||||
return (
|
||||
@@ -615,6 +619,25 @@ export default function Payments() {
|
||||
isActionDisabled,
|
||||
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 (
|
||||
<View style={styles.root}>
|
||||
@@ -629,7 +652,8 @@ export default function Payments() {
|
||||
isMobile && styles.pageContentMobile,
|
||||
]}
|
||||
backgroundColor={PAGE_BACKGROUND_COLOR}
|
||||
topStickyContent={isMobile ? renderMobileTopSticky : undefined}
|
||||
hideBackButton={!isMobile}
|
||||
topStickyContent={isMobile ? renderMobileTopSticky : renderWebBackButton}
|
||||
>
|
||||
<View style={[styles.inner, isMobile && styles.mobileInner]}>
|
||||
<ExpoImage
|
||||
@@ -771,6 +795,32 @@ const styles = StyleSheet.create({
|
||||
gap: 12,
|
||||
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: {
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
fontSize: isWeb ? 32 : 24,
|
||||
|
||||
@@ -154,6 +154,7 @@ const ManageSubscription = ({ navigation }) => {
|
||||
currentUserData?.stripeSubscription?.id ||
|
||||
currentUserData?.stripeSubscription?.subscriptionId ||
|
||||
null;
|
||||
const localSubscription = currentUserData?.stripeSubscription || null;
|
||||
|
||||
const triggerRefresh = useCallback(() => {
|
||||
setRefreshToken((value) => value + 1);
|
||||
@@ -219,8 +220,9 @@ const ManageSubscription = ({ navigation }) => {
|
||||
}, [stripeCustomerId, localSubscriptionId, refreshToken]);
|
||||
|
||||
const subscriptionInfo = useMemo(() => {
|
||||
const rawSubscription =
|
||||
remoteSubscription || currentUserData?.stripeSubscription || null;
|
||||
const subscriptionSources = [remoteSubscription, localSubscription].filter(
|
||||
Boolean,
|
||||
);
|
||||
|
||||
const pickString = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
@@ -230,40 +232,71 @@ const ManageSubscription = ({ navigation }) => {
|
||||
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 =
|
||||
pickString(currentUserData?.stripeSubscriptionStatus) ||
|
||||
pickString(remoteSubscription?.status) ||
|
||||
pickString(currentUserData?.stripeSubscription?.status) ||
|
||||
pickString(
|
||||
currentUserData?.stripeSubscription?.stripeSubscriptionStatus,
|
||||
) ||
|
||||
pickString(rawSubscription?.status) ||
|
||||
pickString(pickFromSources((source) => source?.status)) ||
|
||||
pickString(pickFromSources((source) => source?.stripeSubscriptionStatus)) ||
|
||||
pickString(pickFromSources((source) => source?.metadata?.status)) ||
|
||||
null;
|
||||
|
||||
const status = statusSource ? statusSource.toLowerCase() : null;
|
||||
|
||||
const cancelAtPeriodEnd =
|
||||
remoteSubscription?.cancelAtPeriodEnd === true ||
|
||||
rawSubscription?.cancelAtPeriodEnd === true ||
|
||||
rawSubscription?.cancel_at_period_end === true ||
|
||||
false;
|
||||
const cancelAtPeriodEnd = subscriptionSources.some(
|
||||
(source) =>
|
||||
source?.cancelAtPeriodEnd === true ||
|
||||
source?.cancel_at_period_end === true,
|
||||
);
|
||||
|
||||
const resolveLevelSource = () => {
|
||||
const candidates = [
|
||||
pickString(remoteSubscription?.level),
|
||||
pickString(pickFromSources((source) => source?.level)),
|
||||
pickString(currentUserData?.premiumLevel),
|
||||
pickString(rawSubscription?.metadata?.level),
|
||||
pickString(rawSubscription?.metadata?.subscriptionLevel),
|
||||
pickString(pickFromSources((source) => source?.metadata?.level)),
|
||||
pickString(
|
||||
pickFromSources((source) => source?.metadata?.subscriptionLevel),
|
||||
),
|
||||
];
|
||||
return candidates.find(Boolean) || null;
|
||||
};
|
||||
|
||||
const resolvePeriodSource = () => {
|
||||
const candidates = [
|
||||
pickString(remoteSubscription?.billingPeriod),
|
||||
pickString(pickFromSources((source) => source?.billingPeriod)),
|
||||
pickString(currentUserData?.premiumBillingPeriod),
|
||||
pickString(rawSubscription?.metadata?.billingPeriod),
|
||||
pickString(rawSubscription?.metadata?.subscriptionBillingPeriod),
|
||||
pickString(pickFromSources((source) => source?.metadata?.billingPeriod)),
|
||||
pickString(
|
||||
pickFromSources(
|
||||
(source) => source?.metadata?.subscriptionBillingPeriod,
|
||||
),
|
||||
),
|
||||
];
|
||||
return candidates.find(Boolean) || null;
|
||||
};
|
||||
@@ -292,14 +325,15 @@ const ManageSubscription = ({ navigation }) => {
|
||||
? planLabelParts.join(" · ")
|
||||
: "Abonnement Musicland";
|
||||
|
||||
const currentPeriodEndDate =
|
||||
toDate(remoteSubscription?.currentPeriodEnd) ||
|
||||
toDate(rawSubscription?.currentPeriodEnd) ||
|
||||
toDate(rawSubscription?.current_period_end);
|
||||
const createdAtDate =
|
||||
toDate(remoteSubscription?.created) ||
|
||||
toDate(rawSubscription?.createdAt) ||
|
||||
toDate(rawSubscription?.created_at);
|
||||
const currentPeriodEndDate = pickDateFromSources(
|
||||
(source) => source?.currentPeriodEnd,
|
||||
(source) => source?.current_period_end,
|
||||
);
|
||||
const createdAtDate = pickDateFromSources(
|
||||
(source) => source?.created,
|
||||
(source) => source?.createdAt,
|
||||
(source) => source?.created_at,
|
||||
);
|
||||
|
||||
const statusLabelBase =
|
||||
STATUS_LABELS[status] ||
|
||||
@@ -307,7 +341,13 @@ const ManageSubscription = ({ navigation }) => {
|
||||
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null;
|
||||
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 =
|
||||
hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status);
|
||||
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd;
|
||||
@@ -317,10 +357,7 @@ const ManageSubscription = ({ navigation }) => {
|
||||
: "—";
|
||||
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null;
|
||||
|
||||
let coinsPerMonth =
|
||||
typeof remoteSubscription?.coinsPerMonth === "number"
|
||||
? remoteSubscription.coinsPerMonth
|
||||
: null;
|
||||
let coinsPerMonth = pickFromSources((source) => source?.coinsPerMonth);
|
||||
|
||||
if (
|
||||
coinsPerMonth === null &&
|
||||
@@ -395,17 +432,13 @@ const ManageSubscription = ({ navigation }) => {
|
||||
createdAtLabel,
|
||||
helperMessage: helperMessage || null,
|
||||
level: level || null,
|
||||
subscriptionId:
|
||||
remoteSubscription?.id ||
|
||||
rawSubscription?.id ||
|
||||
currentUserData?.stripeSubscription?.subscriptionId ||
|
||||
null,
|
||||
subscriptionId,
|
||||
coinsPerMonth: normalizedCoins,
|
||||
isAnnual,
|
||||
nextGrantDate: resolvedNextGrantDate,
|
||||
nextGrantLabel,
|
||||
};
|
||||
}, [currentUserData, remoteSubscription]);
|
||||
}, [currentUserData, localSubscription, remoteSubscription]);
|
||||
|
||||
const handleOpenPlans = useCallback(() => {
|
||||
const params =
|
||||
|
||||
+20
-232
@@ -1,26 +1,21 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as AppleAuthentication from "expo-apple-authentication";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { BlurView } from "expo-blur";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
import { background } from "../assets";
|
||||
import BorderGradientButton from "../components/BorderGradientButton";
|
||||
import { Input } from "../components/Input";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import COUNTRIES from "../constants/countries";
|
||||
import firebase from "../config/firebase";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import useSocialAuth from "../hooks/useSocialAuth";
|
||||
@@ -35,11 +30,6 @@ const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
||||
const Register = () => {
|
||||
const [email, setEmail] = 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 afterSocialAuth = useCallback(async () => {
|
||||
@@ -71,33 +61,7 @@ const Register = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const filteredCountries = useMemo(() => {
|
||||
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 isFormValid = email.trim().length > 0 && firstName.trim().length > 0;
|
||||
|
||||
const renderFormContent = () => (
|
||||
<View style={styles.formContent}>
|
||||
@@ -111,58 +75,13 @@ const Register = () => {
|
||||
</Text>
|
||||
) : null}
|
||||
<View style={{ gap: 16 }}>
|
||||
<View style={{ gap: 12 }}>
|
||||
<Input
|
||||
placeholder="Prénom"
|
||||
label="Prénom"
|
||||
isBlur
|
||||
value={firstName}
|
||||
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
|
||||
placeholder="Prénom"
|
||||
label="Prénom"
|
||||
isBlur
|
||||
value={firstName}
|
||||
setValue={setFirstName}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Adresse mail"
|
||||
label="Adresse mail"
|
||||
@@ -182,9 +101,6 @@ const Register = () => {
|
||||
navigate(Routes.CreatePassword, {
|
||||
email: email.trim(),
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
city: city.trim(),
|
||||
country: selectedCountry,
|
||||
preferredLanguage,
|
||||
})
|
||||
}
|
||||
@@ -239,63 +155,22 @@ const Register = () => {
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{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>
|
||||
</KeyboardAvoidingView>
|
||||
)}
|
||||
</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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -413,36 +288,6 @@ const styles = StyleSheet.create({
|
||||
color: Palette.white,
|
||||
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: {
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
@@ -542,61 +387,4 @@ const styles = StyleSheet.create({
|
||||
alignItems: "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 = {
|
||||
BASE: "Choisis ta base",
|
||||
SENSIBILITE: "Choisis ta sensibilité",
|
||||
TECHNIQUE: "Choisis ta technique (Facultatif)",
|
||||
TECHNIQUE: "Choisis ta technique",
|
||||
};
|
||||
|
||||
const OPTIONAL_SECTION_CATEGORIES = new Set(["SENSIBILITE", "TECHNIQUE"]);
|
||||
|
||||
const normalizeCategory = (value) => {
|
||||
if (typeof value !== "string") return "";
|
||||
return value
|
||||
@@ -76,6 +78,10 @@ const CustomizeVoice = ({
|
||||
const subtitle =
|
||||
SECTION_INSTRUCTIONS[normalizedCategory] ||
|
||||
"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(
|
||||
() => selectionToObject(selected),
|
||||
@@ -105,7 +111,7 @@ const CustomizeVoice = ({
|
||||
return (
|
||||
<View style={{ flex: 1, gap: 10, paddingTop: 16 }}>
|
||||
<CreateLyricsHeader
|
||||
title="Personnalise la voix que tu veux pour ta chanson"
|
||||
title={title}
|
||||
subTitle={subtitle}
|
||||
/>
|
||||
<View
|
||||
|
||||
@@ -396,7 +396,6 @@ const Lyrics = ({ navigation }) => {
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigate(Routes.Home)}
|
||||
progress={95}
|
||||
logo={icons.musicLandWriting}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -11,6 +11,7 @@ const SongStructure = ({
|
||||
selected: selectedProp,
|
||||
setSelected: setSelectedProp,
|
||||
}) => {
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
const [internalSelected, setInternalSelected] = useState(null);
|
||||
const selected = selectedProp ?? internalSelected;
|
||||
const setSelected = setSelectedProp ?? setInternalSelected;
|
||||
@@ -23,17 +24,22 @@ const SongStructure = ({
|
||||
title="Comment veux-tu structurer ta chanson ?"
|
||||
subTitle="Sélectionne une structure."
|
||||
/>
|
||||
<ItemContainer>
|
||||
<ListSelection
|
||||
options={SONG_STRUCTURE}
|
||||
variant="simple"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
itemContainerStyle={styles.itemContainer}
|
||||
itemTextStyle={styles.itemText}
|
||||
/>
|
||||
</ItemContainer>
|
||||
<View
|
||||
style={{ flex: 1 }}
|
||||
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
|
||||
>
|
||||
<ItemContainer height={containerLayout?.height}>
|
||||
<ListSelection
|
||||
options={SONG_STRUCTURE}
|
||||
variant="simple"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
itemContainerStyle={styles.itemContainer}
|
||||
itemTextStyle={styles.itemText}
|
||||
/>
|
||||
</ItemContainer>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ const SongStyle = ({
|
||||
const [previousOtherStyle, setPreviousOtherStyle] = useState(
|
||||
normalizedOtherStyle
|
||||
);
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
|
||||
const selected = selectedProp ?? internalSelected;
|
||||
const setSelectedBase = setSelectedProp ?? setInternalSelected;
|
||||
@@ -97,21 +98,26 @@ const SongStyle = ({
|
||||
return (
|
||||
<>
|
||||
<View style={{ flex: 1, gap: 16, marginTop: 16 }}>
|
||||
<View style={{ gap: 10 }}>
|
||||
<View style={{ flex: 1, gap: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
title={`Quel est le style de ta chanson ?`}
|
||||
subTitle="Choisis l'ambiance émotions que tu veux faire passer."
|
||||
/>
|
||||
<ItemContainer>
|
||||
<ListSelection
|
||||
options={songStyleOptions}
|
||||
variant="titleDescription"
|
||||
selected={selected}
|
||||
setSelected={handleStyleSelect}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
itemContainerStyle={styles.itemContainer}
|
||||
/>
|
||||
</ItemContainer>
|
||||
<View
|
||||
style={{ flex: 1 }}
|
||||
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
|
||||
>
|
||||
<ItemContainer height={containerLayout?.height}>
|
||||
<ListSelection
|
||||
options={songStyleOptions}
|
||||
variant="titleDescription"
|
||||
selected={selected}
|
||||
setSelected={handleStyleSelect}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
itemContainerStyle={styles.itemContainer}
|
||||
/>
|
||||
</ItemContainer>
|
||||
</View>
|
||||
</View>
|
||||
{selected === OTHER_STYLE_OPTION ? (
|
||||
<View style={styles.otherStyleSummary}>
|
||||
|
||||
@@ -25,7 +25,8 @@ const WritingLyrics = () => {
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setVideoUrl(isWeb ? videos.celineWeb : videos?.celine);
|
||||
// setVideoUrl(isWeb ? videos.celineWeb : videos?.celine);
|
||||
setVideoUrl(videos?.test);
|
||||
}, []);
|
||||
|
||||
const startWriting = React.useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user