From c7cbd8508895557578c30f83e29688e5e53804ab Mon Sep 17 00:00:00 2001 From: leon-morival Date: Mon, 17 Nov 2025 14:51:04 +0100 Subject: [PATCH] feat: modal & payments --- src/components/Alert.js | 167 +++++++++++++++++++++----------- src/screens/Payments.js | 43 +++++--- src/screens/Studio/SongReady.js | 6 +- 3 files changed, 144 insertions(+), 72 deletions(-) diff --git a/src/components/Alert.js b/src/components/Alert.js index 34c23f8..ecb2790 100644 --- a/src/components/Alert.js +++ b/src/components/Alert.js @@ -1,17 +1,21 @@ import { PortalProvider } from "@gorhom/portal"; import React, { useState } from "react"; -import { Alert, Platform, Text } from "react-native"; +import { Alert, Platform, StyleSheet, Text, View } from "react-native"; import { BlurView } from "expo-blur"; -import { View } from "react-native"; -import { Fonts, gutters } from "../styles"; +import { Fonts, Palette, gutters } from "../styles"; import GradientButton from "./GradientButton"; +import { LinearGradient } from "./LinearGradient/LinearGradient"; import Overlay from "./Overlay"; const WebAlertModal = ({ title, description, options }) => { const [visible, setVisible] = useState(true); const confirmOption = options?.find(({ style }) => style !== "cancel"); const cancelOption = options?.find(({ style }) => style === "cancel"); + const hasSecondaryAction = Boolean(cancelOption); + const buttonContainerStyle = hasSecondaryAction + ? styles.actionButton + : styles.singleActionButton; const onConfirm = () => { setVisible(false); @@ -25,63 +29,46 @@ const WebAlertModal = ({ title, description, options }) => { return ( - - - + + - {title} - - - {description} - - - - - {cancelOption && ( - - )} - - + + {title} + {description} + + + {cancelOption && ( + + )} + + + + + ); @@ -126,3 +113,65 @@ export const showPremiumRequiredAlert = () => ); export default alert; + +const styles = StyleSheet.create({ + overlayContent: { + justifyContent: "center", + alignItems: "center", + }, + modalWrapper: { + width: "90%", + maxWidth: 460, + paddingHorizontal: 12, + }, + modalBorder: { + borderRadius: 24, + padding: 1, + }, + modalContainer: { + borderRadius: 23, + paddingHorizontal: gutters * 1.8, + paddingVertical: gutters, + backgroundColor: "rgba(15, 12, 20, 0.9)", + overflow: "hidden", + gap: gutters * 0.75, + }, + title: Fonts({ + type: "mainTitle", + fontSize: 3, + style: { textAlign: "center" }, + }), + description: Fonts({ + type: "default", + color: Palette.gray, + fontSize: 2, + style: { lineHeight: 22, textAlign: "center" }, + }), + divider: { + height: 1, + backgroundColor: "rgba(255,255,255,0.08)", + marginVertical: 0, + }, + actionsRow: { + flexDirection: "row", + gap: gutters, + width: "100%", + }, + actions: { + width: "100%", + gap: gutters, + }, + actionButton: { + flex: 1, + }, + singleActionButton: { + width: "100%", + }, + secondaryButtonGradient: { + borderWidth: 1, + borderColor: "rgba(255,255,255,0.2)", + }, + secondaryButtonText: { + color: Palette.gray, + }, +}); diff --git a/src/screens/Payments.js b/src/screens/Payments.js index a363d2c..1835b42 100644 --- a/src/screens/Payments.js +++ b/src/screens/Payments.js @@ -79,6 +79,18 @@ function SubscriptionCard({ plan, selected, onSelect }) { Number.isFinite(plan.coinsPerMonth) ? Math.round(plan.coinsPerMonth) : null; + const songsPerMonth = + coinsPerMonth !== null + ? Math.floor(coinsPerMonth / CREDITS_PER_MUSIC) + : null; + const songsPerMonthLabel = + songsPerMonth === null + ? null + : songsPerMonth >= 1 + ? `≈ ${songsPerMonth} ${ + songsPerMonth > 1 ? "musiques" : "musique" + } / mois` + : "Moins d'une musique / mois"; const planBadgeKey = getPlanKeyForBadge(plan); const planBadgeSource = planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null; @@ -145,6 +157,9 @@ function SubscriptionCard({ plan, selected, onSelect }) { / mois ) : null} + {songsPerMonthLabel ? ( + {songsPerMonthLabel} + ) : null} ) : null} @@ -225,7 +240,7 @@ const getPlanKeyForBadge = (plan) => { } const match = Object.entries(PLAN_SYNONYMS).find(([, variants]) => - variants.some((variant) => label.includes(variant)), + variants.some((variant) => label.includes(variant)) ); return match ? match[0] : null; @@ -270,7 +285,7 @@ const normalizePlans = (plans = [], periodKey) => { })) .sort( (planA, planB) => - getPlanPriority(planA, periodKey) - getPlanPriority(planB, periodKey), + getPlanPriority(planA, periodKey) - getPlanPriority(planB, periodKey) ); }; @@ -285,6 +300,7 @@ const PAGE_BACKGROUND_COLOR = "#303438"; const SUBSCRIPTION_DISCLAIMER = "Résiliable en un clic à tout moment. Les prix sont indiqués TTC. Contacte-nous pour des besoins spécifiques (facturation annuelle, volume, offres éducation)."; const CARD_MIN_HEIGHT = isWeb ? 320 : 240; +const CREDITS_PER_MUSIC = 8; export default function Payments() { const route = useRoute(); @@ -319,7 +335,7 @@ export default function Payments() { monthly: normalizePlans(subscriptions?.monthly, "monthly"), annual: normalizePlans(subscriptions?.annual, "annual"), }), - [subscriptions?.monthly, subscriptions?.annual], + [subscriptions?.monthly, subscriptions?.annual] ); const hasAnyPlan = (normalizedPlans?.monthly?.length || 0) > 0 || @@ -341,12 +357,12 @@ export default function Payments() { if (shouldApplyPack && normalizedPlans) { const desired = initialSubscriptionPack.trim().toLowerCase(); for (const [periodKey, mapping] of Object.entries( - PACK_PRICE_ID_BY_PERIOD, + PACK_PRICE_ID_BY_PERIOD )) { const candidateId = mapping[desired]; if (!candidateId) continue; const exists = (normalizedPlans[periodKey] || []).some( - (plan) => plan?.priceId === candidateId, + (plan) => plan?.priceId === candidateId ); if (exists) { matchedPeriod = periodKey; @@ -367,7 +383,7 @@ export default function Payments() { } const alreadySelected = periodPlans.some( - (plan) => plan.priceId === next[key], + (plan) => plan.priceId === next[key] ); if (shouldApplyPack && matchedPeriod === null) { @@ -413,7 +429,7 @@ export default function Payments() { })); setProcessingPriceId(null); }, - [billingPeriod], + [billingPeriod] ); const handleCheckout = React.useCallback(async () => { @@ -429,7 +445,7 @@ export default function Payments() { console.error("[Payments] checkout error", error); setErrorMessage( error?.message || - "Une erreur est survenue lors de la création de la session Stripe.", + "Une erreur est survenue lors de la création de la session Stripe." ); } finally { setProcessingPriceId(null); @@ -464,14 +480,14 @@ export default function Payments() { const currentPlanCount = currentPlans.length; const mobileActionSafePadding = React.useMemo( () => Math.max(insets.bottom, gutters), - [insets.bottom], + [insets.bottom] ); const mobileListContentInset = React.useMemo( () => ({ paddingBottom: mobileActionSafePadding + gutters * 3, }), - [mobileActionSafePadding], + [mobileActionSafePadding] ); const renderHeaderSection = React.useCallback(() => { @@ -535,7 +551,7 @@ export default function Payments() { /> ), - [handleSelect, selectedPriceId], + [handleSelect, selectedPriceId] ); const renderMobileEmptyComponent = React.useCallback(() => { @@ -909,6 +925,11 @@ const styles = StyleSheet.create({ fontSize: 13, color: Palette.primary, }, + songsPerMonth: { + fontFamily: FONT_FAMILY.InterRegular, + fontSize: 12, + color: "rgba(255, 255, 255, 0.75)", + }, priceValue: { fontFamily: FONT_FAMILY.InterBold, fontSize: isWeb ? 22 : 20, diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js index 1477fe5..f314091 100644 --- a/src/screens/Studio/SongReady.js +++ b/src/screens/Studio/SongReady.js @@ -31,7 +31,6 @@ import { gutters, size } from "../../styles/Style"; import { FONT_FAMILY } from "../../styles/Fonts"; import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; -import CreditAmount from "../../components/CreditAmount"; const MUSIC_GENERATION_COIN_COST = 8; const SONG_OPTIONS_PER_GENERATION = 2; @@ -528,7 +527,10 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => { const { width, height } = useWindowDimensions(); const isCompactWidth = width < 420; const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12); - const verticalPadding = Math.max(isCompactWidth ? gutters : gutters * 1.5, 12); + const verticalPadding = Math.max( + isCompactWidth ? gutters : gutters * 1.5, + 12 + ); const availableWidth = Math.max(width - horizontalPadding * 2, 0); const contentWidth = availableWidth > 0