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