diff --git a/App.js b/App.js index 883b2fa..2f499d0 100644 --- a/App.js +++ b/App.js @@ -6,7 +6,7 @@ import * as SplashScreen from "expo-splash-screen"; import { StatusBar } from "expo-status-bar"; import moment from "moment"; import "moment/locale/fr"; -import { Platform } from "react-native"; +import { Platform, Text, TextInput } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import React, { setGlobal, @@ -45,6 +45,18 @@ console.reportErrorsAsExceptions = false; 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); SplashScreen.preventAutoHideAsync(); diff --git a/app.json b/app.json index 3c7d60b..fc69f09 100644 --- a/app.json +++ b/app.json @@ -18,7 +18,7 @@ "splash": { "image": "./assets/splash.png", "resizeMode": "cover", - "backgroundColor": "#0A0D12" + "backgroundColor": "#000" }, "ios": { "googleServicesFile": "./config/GoogleService-Info.plist", @@ -53,8 +53,8 @@ "softwareKeyboardLayoutMode": "resize", "googleServicesFile": "./config/google-services.json", "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#0A0D12" + "foregroundImage": "./assets/foreground.png", + "backgroundImage": "./assets/background.png" }, "package": "com.omedis.musicland", "permissions": [ diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png deleted file mode 100644 index 744f934..0000000 Binary files a/assets/adaptive-icon.png and /dev/null differ diff --git a/assets/background.png b/assets/background.png new file mode 100644 index 0000000..e637489 Binary files /dev/null and b/assets/background.png differ diff --git a/assets/desktopIcon.icns b/assets/desktopIcon.icns deleted file mode 100644 index e04d938..0000000 Binary files a/assets/desktopIcon.icns and /dev/null differ diff --git a/assets/foreground.png b/assets/foreground.png new file mode 100644 index 0000000..cb2a67e Binary files /dev/null and b/assets/foreground.png differ diff --git a/assets/icon.png b/assets/icon.png index 8e040cd..fa8553d 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/splash.png b/assets/splash.png index 182cc98..97ec94d 100644 Binary files a/assets/splash.png and b/assets/splash.png differ diff --git a/assets/splashold.png b/assets/splashold.png deleted file mode 100644 index c5d27a7..0000000 Binary files a/assets/splashold.png and /dev/null differ diff --git a/src/assets/icons/video.png b/src/assets/icons/video.png index 5dcfd08..db893c5 100644 Binary files a/src/assets/icons/video.png and b/src/assets/icons/video.png differ diff --git a/src/components/FullscreenIntroVideo.web.js b/src/components/FullscreenIntroVideo.web.js index a96bb57..6116e66 100644 --- a/src/components/FullscreenIntroVideo.web.js +++ b/src/components/FullscreenIntroVideo.web.js @@ -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} diff --git a/src/components/modal/CoinPackModal.js b/src/components/modal/CoinPackModal.js index b33f9e3..7dbb5b0 100644 --- a/src/components/modal/CoinPackModal.js +++ b/src/components/modal/CoinPackModal.js @@ -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 ( {pack?.name ? {pack.name} : null} + {pricingDetail?.isBestValue ? ( + + Meilleure offre + + ) : null} {pack?.description ? ( {pack.description} ) : null} - {formattedPrice ? ( - {formattedPrice} + + {formattedPrice ? ( + {formattedPrice} + ) : null} + + {perCoinPrice + ? `≈ ${perCoinPrice} / jeton` + : "Valeurs indicatives / jeton"} + + + {discountLabel ? ( + + + + {discountLabel} + + + {!pricingDetail?.isBasePack && typeof discountPercent === "number" ? ( + + Économie estimée vs pack de base + + ) : ( + + Référence prix/jeton + + )} + ) : null} @@ -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 }) => { {renderContent()} + + 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. + + { name={Routes.HomeStack} component={HomeStack} options={{ - tabBarLabel: "Créer", + tabBarLabel: "Menu", headerShown: false, tabBarShowLabel: true, tabBarIcon: ({ focused }) => renderIcon(tabs.addTab, focused), diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 4ddf631..682d2ff 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -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" /> - + ) : null; @@ -590,7 +585,6 @@ const Home = ({ navigation, route }) => { {stageCardsList} @@ -654,7 +648,6 @@ const Home = ({ navigation, route }) => { }} > @@ -662,7 +655,7 @@ const Home = ({ navigation, route }) => { diff --git a/src/screens/Home/components/StageCard.js b/src/screens/Home/components/StageCard.js index d5dd902..70b7dad 100644 --- a/src/screens/Home/components/StageCard.js +++ b/src/screens/Home/components/StageCard.js @@ -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" }, ]} > @@ -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 }, ]} > @@ -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, diff --git a/src/screens/LandingPage.js b/src/screens/LandingPage.js index 27da562..c32fc2b 100644 --- a/src/screens/LandingPage.js +++ b/src/screens/LandingPage.js @@ -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} /> diff --git a/src/screens/Payments.js b/src/screens/Payments.js index 1835b42..9f15ecb 100644 --- a/src/screens/Payments.js +++ b/src/screens/Payments.js @@ -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 ( + + + + Retour + + + ); + }, [handleBackPress, isMobile]); return ( @@ -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} > { 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 = diff --git a/src/screens/Register.js b/src/screens/Register.js index 26f1da8..b802ae1 100644 --- a/src/screens/Register.js +++ b/src/screens/Register.js @@ -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 = () => ( @@ -111,58 +75,13 @@ const Register = () => { ) : null} - - - - - Pays - - [ - styles.countryButton, - pressed ? styles.countryButtonPressed : null, - ]} - onPress={() => setIsCountryModalVisible(true)} - > - - {selectedCountry - ? selectedCountry.name - : "Sélectionne ton pays"} - - - - - - + { 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()} + + + Tu as déjà un compte ?{" "} + navigate(Routes.Login)} + > + Se connecter + + + )} - - - Tu as déjà un compte ?{" "} - navigate(Routes.Login)} - > - Se connecter - - - - - - - - Sélectionne ton pays - - Fermer - - - - item.code} - renderItem={({ item }) => ( - handleSelectCountry(item)} - > - {item.name} - - )} - keyboardShouldPersistTaps="handled" - showsVerticalScrollIndicator={false} - ListEmptyComponent={ - Aucun pays trouvé - } - /> - - - ); }; @@ -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, - }, }); diff --git a/src/screens/Studio/CustomizeVoice.js b/src/screens/Studio/CustomizeVoice.js index 5d6ae89..9d61059 100644 --- a/src/screens/Studio/CustomizeVoice.js +++ b/src/screens/Studio/CustomizeVoice.js @@ -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 ( { navigate(Routes.Home)} progress={95} - logo={icons.musicLandWriting} /> { + 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." /> - - - + setContainerLayout(event.nativeEvent.layout)} + > + + + + ); }; diff --git a/src/screens/Writing/SongStyle.js b/src/screens/Writing/SongStyle.js index f24787b..46b5636 100644 --- a/src/screens/Writing/SongStyle.js +++ b/src/screens/Writing/SongStyle.js @@ -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 ( <> - + - - - + setContainerLayout(event.nativeEvent.layout)} + > + + + + {selected === OTHER_STYLE_OPTION ? ( diff --git a/src/screens/Writing/WritingLyrics.js b/src/screens/Writing/WritingLyrics.js index a43a045..51a943f 100644 --- a/src/screens/Writing/WritingLyrics.js +++ b/src/screens/Writing/WritingLyrics.js @@ -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 () => {