import React from "react"; import { Linking, StyleSheet, View } from "react-native"; import { EmbeddedCheckout, EmbeddedCheckoutProvider, } from "@stripe/react-stripe-js"; import { loadStripe } from "@stripe/stripe-js"; import Overlay from "../components/Overlay"; import Button from "../components/Button"; import useLayoutType from "../hooks/useLayoutType"; import { mainBorderRadius } from "../styles/Style"; import { STRIPE_PUBLISHABLE_KEY_LIVE, STRIPE_PUBLISHABLE_KEY_TEST, } from "../data/keys"; import { getFunctionsClient } from "../config/firebase"; import { isWeb } from "../hooks/useLayoutType"; import { useUserData } from "./UserDataProvider"; import { formatDate, toDate } from "../utils/dateFormatting"; 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 isStripeTesting = false; const stripePromise = loadStripe( isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE, ); export const StripeContext = React.createContext(null); export const useStripe = () => { const context = React.useContext(StripeContext); if (!context) { throw new Error("useStripe must be used within a StripeProvider"); } return context; }; const StripeProvider = ({ children }) => { const { isMobile } = useLayoutType(); const { currentUserData } = useUserData() || {}; const [clientSecret, setClientSecret] = React.useState(null); const [subscriptions, setSubscriptions] = React.useState({ monthly: [], annual: [], }); const [coinPacks, setCoinPacks] = React.useState([]); const [isCatalogLoading, setIsCatalogLoading] = React.useState(true); const [catalogError, setCatalogError] = React.useState(null); const [activeSubscription, setActiveSubscription] = React.useState(null); const [isActiveSubscriptionLoading, setIsActiveSubscriptionLoading] = React.useState(false); const [activeSubscriptionError, setActiveSubscriptionError] = React.useState(null); const stripeCustomerId = currentUserData?.stripeCustomerId || null; const localSubscriptionId = currentUserData?.stripeSubscription?.id || currentUserData?.stripeSubscription?.subscriptionId || null; const closeEmbeddedCheckout = React.useCallback(() => { setClientSecret(null); }, []); const redirectToCheckout = React.useCallback(async (checkoutUrl) => { if (!checkoutUrl) { throw new Error("Session Stripe introuvable."); } if (isWeb) { if (typeof window !== "undefined") { window.location.assign(checkoutUrl); return; } throw new Error("Navigation Stripe impossible dans cet environnement."); } const canOpen = await Linking.canOpenURL(checkoutUrl); if (!canOpen) { throw new Error("Impossible d'ouvrir l'URL de paiement."); } await Linking.openURL(checkoutUrl); }, []); const runCheckoutSession = React.useCallback( async ({ callableName, payload, logTag }) => { try { const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( callableName, ); const { data } = await callable(payload); const checkoutUrl = data?.url; if (!checkoutUrl) { throw new Error("Session Stripe introuvable."); } await redirectToCheckout(checkoutUrl); } catch (error) { console.error(`[StripeProvider] ${logTag}`, error); throw new Error( error?.message || "Une erreur est survenue lors de la création de la session Stripe.", ); } }, [redirectToCheckout], ); const createSubscriptionCheckout = React.useCallback( async (priceId) => { if (!priceId) { throw new Error("Aucun abonnement sélectionné."); } await runCheckoutSession({ callableName: "subscription-createSubscriptionCheckoutSession", payload: { priceId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, }, logTag: "subscription checkout error", }); }, [runCheckoutSession], ); const createCoinPackCheckout = React.useCallback( async (productId) => { if (!productId) { throw new Error("Aucun pack sélectionné."); } await runCheckoutSession({ callableName: "subscription-createCoinPackCheckoutSession", payload: { productId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, }, logTag: "coin pack checkout error", }); }, [runCheckoutSession], ); const fetchActiveSubscription = React.useCallback(async () => { const hasLookupContext = Boolean(stripeCustomerId) || Boolean(localSubscriptionId); if (!hasLookupContext) { setActiveSubscription(null); setActiveSubscriptionError(null); setIsActiveSubscriptionLoading(false); return; } setIsActiveSubscriptionLoading(true); setActiveSubscriptionError(null); try { const callable = getFunctionsClient( FUNCTIONS_REGION, ).httpsCallable("subscription-getActiveSubscription"); const { data } = await callable(); setActiveSubscription(data?.subscription || null); } catch (error) { console.error("[StripeProvider] getActiveSubscription error", error); setActiveSubscriptionError( error?.message || "Impossible de mettre à jour les informations d'abonnement.", ); setActiveSubscription(null); } finally { setIsActiveSubscriptionLoading(false); } }, [stripeCustomerId, localSubscriptionId]); const fetchStripeCatalog = React.useCallback(async () => { setIsCatalogLoading(true); setCatalogError(null); const functionsClient = getFunctionsClient(FUNCTIONS_REGION); let lastError = null; try { const callable = functionsClient.httpsCallable("subscription-listSubscriptionPlans"); const { data } = await callable(); const nextPlans = data?.plans || {}; setSubscriptions({ monthly: Array.isArray(nextPlans?.monthly) ? nextPlans.monthly : [], annual: Array.isArray(nextPlans?.annual) ? nextPlans.annual : [], }); } catch (error) { lastError = error; console.error("[StripeProvider] listSubscriptionPlans error", error); } try { const callable = functionsClient.httpsCallable("subscription-listCoinPacks"); const { data } = await callable(); setCoinPacks(Array.isArray(data?.packs) ? data.packs : []); } catch (error) { lastError = error; console.error("[StripeProvider] listCoinPacks error", error); } if (lastError) { setCatalogError( lastError?.message || "Impossible de récupérer les informations Stripe pour le moment.", ); } else { setCatalogError(null); } setIsCatalogLoading(false); }, []); React.useEffect(() => { fetchStripeCatalog(); }, [fetchStripeCatalog]); React.useEffect(() => { fetchActiveSubscription(); }, [fetchActiveSubscription]); const activeSubscriptionInfo = React.useMemo(() => { const subscription = activeSubscription || null; const nextRenewalDate = toDate(subscription?.currentPeriodEnd) || toDate(subscription?.current_period_end) || null; const subscribedSinceDate = toDate(subscription?.created) || toDate(subscription?.createdAt) || toDate(subscription?.created_at) || null; return { subscription, nextRenewalDate, nextRenewalLabel: nextRenewalDate ? formatDate(nextRenewalDate) : null, subscribedSinceDate, subscribedSinceLabel: subscribedSinceDate ? formatDate(subscribedSinceDate) : null, }; }, [activeSubscription]); const providerValue = React.useMemo( () => ({ subscriptions, coinPacks, isCatalogLoading, catalogError, refreshCatalog: fetchStripeCatalog, createSubscriptionCheckout, createCoinPackCheckout, openEmbeddedCheckout: setClientSecret, closeEmbeddedCheckout, activeSubscription: activeSubscriptionInfo.subscription, activeSubscriptionInfo, isActiveSubscriptionLoading, activeSubscriptionError, refreshActiveSubscription: fetchActiveSubscription, }), [ subscriptions, coinPacks, isCatalogLoading, catalogError, fetchStripeCatalog, createSubscriptionCheckout, createCoinPackCheckout, setClientSecret, closeEmbeddedCheckout, activeSubscriptionInfo, isActiveSubscriptionLoading, activeSubscriptionError, fetchActiveSubscription, ], ); return ( {children} {clientSecret ? ( ) : null}