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 () => {