import React from "react"; import { ActivityIndicator, Image, Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native"; import { BlurView } from "expo-blur"; import { icons } from "../../assets"; import BorderGradientButton from "../BorderGradientButton"; import GradientButton from "../GradientButton"; import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader"; import { Palette, gutters } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { getFunctionsClient } from "../../config/firebase"; import { isWeb } from "../../hooks/useLayoutType"; const WEB_MODAL_MAX_WIDTH = 1000; const FUNCTIONS_REGION = "europe-west1"; const STRIPE_SUCCESS_URL = "https://dashboard.stripe.com/test/billing/starter-guide/checkout-success"; const STRIPE_CANCEL_URL = STRIPE_SUCCESS_URL; const formatCurrency = (amount, currency = "eur") => { if (typeof amount !== "number") { return null; } const normalized = amount / 100; const upperCurrency = typeof currency === "string" && currency.trim() ? currency.trim().toUpperCase() : "EUR"; if (typeof Intl !== "undefined" && Intl.NumberFormat) { try { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: upperCurrency, minimumFractionDigits: 2, }).format(normalized); } catch (_error) {} } return `${normalized.toFixed(2)} ${upperCurrency}`; }; function CoinPackCard({ pack, selected, onSelect }) { const handleSelect = React.useCallback(() => { if (typeof onSelect !== "function" || !pack?.productId) { return; } onSelect(pack.productId); }, [onSelect, pack?.productId]); const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency); return ( [ styles.cardWrapper, selected && styles.cardWrapperSelected, pressed && styles.cardWrapperPressed, ]} accessibilityRole="button" accessibilityState={{ selected }} > {pack?.coinAmount} pièces {pack?.name ? {pack.name} : null} {pack?.description ? ( {pack.description} ) : null} {formattedPrice ? ( {formattedPrice} ) : null} ); } const CoinPackModal = ({ visible, onClose }) => { const [coinPacks, setCoinPacks] = React.useState([]); const [selectedPackId, setSelectedPackId] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); const [isProcessing, setIsProcessing] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(null); const modalMaxWidth = isWeb ? WEB_MODAL_MAX_WIDTH : undefined; const fetchCoinPacks = React.useCallback(async () => { setIsLoading(true); setErrorMessage(null); try { const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( "subscription-listCoinPacks", ); const { data } = await callable(); const packs = Array.isArray(data?.packs) ? data.packs : []; setCoinPacks(packs); setSelectedPackId((current) => { if ( current && packs.some((pack) => pack?.productId && pack.productId === current) ) { return current; } return packs[0]?.productId || null; }); } catch (error) { console.error("[CoinPackModal] fetchCoinPacks error", error); setErrorMessage( error?.message || "Impossible de charger les packs de pièces. Réessaie plus tard.", ); } finally { setIsLoading(false); } }, []); React.useEffect(() => { if (!visible) { return; } fetchCoinPacks(); }, [visible, fetchCoinPacks]); const handleCheckout = React.useCallback(async () => { if (!selectedPackId) { return; } setIsProcessing(true); setErrorMessage(null); try { const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( "subscription-createCoinPackCheckoutSession", ); const { data } = await callable({ productId: selectedPackId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, }); const checkoutUrl = data?.url; if (!checkoutUrl) { throw new Error("Session Stripe introuvable."); } if (isWeb) { if (typeof window !== "undefined") { window.location.assign(checkoutUrl); } } else { const canOpen = await Linking.canOpenURL(checkoutUrl); if (!canOpen) { throw new Error("Impossible d'ouvrir l'URL de paiement."); } await Linking.openURL(checkoutUrl); } } catch (error) { console.error("[CoinPackModal] checkout error", error); setErrorMessage( error?.message || "Une erreur est survenue lors de la création de la session Stripe.", ); } finally { setIsProcessing(false); } }, [selectedPackId, isWeb]); const handleClose = React.useCallback(() => { if (isProcessing) { return; } onClose?.(); }, [isProcessing, onClose]); const renderContent = () => { if (isLoading) { return ( ); } if (!coinPacks.length) { return ( Aucun pack de pièces n'est disponible pour le moment. ); } return ( {coinPacks.map((pack) => ( ))} ); }; return ( Acheter des pièces Choisis un pack et finalise ton achat pour continuer. {errorMessage ? ( {errorMessage} ) : null} {renderContent()} Les pièces sont créditées dès que le paiement Stripe est validé. ); }; export default CoinPackModal; const styles = StyleSheet.create({ overlay: { flex: 1, justifyContent: "center", alignItems: "center", paddingHorizontal: gutters, paddingVertical: gutters * 1.5, }, container: { gap: 24, alignItems: "center", paddingBottom: gutters, width: "100%", maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : undefined, }, header: { gap: 8, alignItems: "center", paddingHorizontal: 16, }, title: { fontFamily: FONT_FAMILY.InterSemiBold, fontSize: 22, color: Palette.white, textAlign: "center", }, subtitle: { fontFamily: FONT_FAMILY.InterRegular, fontSize: 16, color: Palette.white, textAlign: "center", }, errorText: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 14, color: Palette.red, textAlign: "center", paddingHorizontal: 16, }, content: { width: "100%", maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : "100%", alignSelf: "center", maxHeight: isWeb ? 420 : 360, }, loaderContainer: { width: "100%", alignItems: "center", justifyContent: "center", paddingVertical: 32, }, emptyState: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 14, color: "rgba(255, 255, 255, 0.72)", textAlign: "center", paddingHorizontal: 24, }, packList: { width: "100%", maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : "100%", alignSelf: "center", gap: gutters, paddingHorizontal: isWeb ? 8 : 0, }, packListWeb: { flexDirection: "row", justifyContent: "center", flexWrap: "wrap", }, packListMobile: { paddingBottom: 12, }, cardWrapper: { flex: 1, borderRadius: 20, overflow: "hidden", borderWidth: 1, borderColor: "rgba(255, 255, 255, 0.12)", backgroundColor: "rgba(12, 14, 18, 0.45)", minWidth: isWeb ? 220 : undefined, }, cardWrapperSelected: { borderColor: Palette.primary, shadowColor: "#000", shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.3, shadowRadius: 18, elevation: 6, }, cardWrapperPressed: { transform: [{ scale: 0.97 }], }, cardBlur: { flex: 1, gap: 16, paddingVertical: 20, paddingHorizontal: 24, justifyContent: "space-between", backgroundColor: "rgba(48, 52, 56, 0.55)", }, cardHeader: { gap: 12, alignItems: "center", }, coinRow: { flexDirection: "row", alignItems: "center", gap: 10, }, coinIcon: { width: 26, height: 26, resizeMode: "contain", }, coinAmount: { fontFamily: FONT_FAMILY.InterBold, fontSize: 20, color: Palette.white, }, packName: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 16, color: Palette.white, textAlign: "center", }, packDescription: { fontFamily: FONT_FAMILY.InterRegular, fontSize: 14, color: "rgba(255, 255, 255, 0.72)", textAlign: "center", }, packPrice: { fontFamily: FONT_FAMILY.InterSemiBold, fontSize: 18, color: Palette.white, textAlign: "center", }, actions: { width: "85%", alignSelf: "center", gap: 16, }, disclaimer: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 13, color: "rgba(255, 255, 255, 0.7)", textAlign: "center", paddingHorizontal: 16, }, });