import React from 'react' import { Linking, Platform, StyleSheet, View } from 'react-native' import { EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js' import { loadStripe } from '@stripe/stripe-js' import * as WebBrowser from 'expo-web-browser' import Overlay from '../components/Overlay' import Button from '../components/Button' import { Palette } from '../styles' 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 = true const rawStripePublishableKey = isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE const stripePublishableKey = typeof rawStripePublishableKey === 'string' ? rawStripePublishableKey.trim() : '' const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : null if (!stripePublishableKey) { console.warn( '[StripeProvider] Aucune clé publique Stripe fournie, le checkout intégré sera désactivé.' ) } const isSafariBrowser = () => { if (typeof navigator !== 'object' || typeof navigator.userAgent !== 'string') { return false } return ( /safari/i.test(navigator.userAgent) && !/(chrome|crios|android|fxios|edgios|opr|ucbrowser)/i.test(navigator.userAgent) ) } const openSafariCheckoutWindow = () => { if (!isWeb || typeof window === 'undefined') { return null } if (!isSafariBrowser()) { return null } try { const placeholder = window.open('', '_blank', 'noopener,noreferrer') if (!placeholder) { return null } try { placeholder.document.write( "

Chargement du paiement Stripe...

" ) placeholder.document.title = 'Stripe Checkout' } catch { // Ignore failures when the window gets reused or is cross-domain. } placeholder.focus?.() return placeholder } catch (error) { console.warn('[StripeProvider] Safari window pre-open failed', error) return null } } 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 { currentUserData } = useUserData() || {} const canUseEmbeddedCheckout = isWeb && Boolean(stripePromise) 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) { throw new Error("Impossible d'ouvrir le paiement Stripe sans checkout intégré.") } const isMobileApp = Platform.OS === 'ios' || Platform.OS === 'android' if (isMobileApp) { try { await WebBrowser.openBrowserAsync(checkoutUrl, { enableDefaultShareMenu: false, dismissButtonStyle: 'close', presentationStyle: WebBrowser?.WebBrowserPresentationStyle?.PAGE_SHEET, }) return } catch (webBrowserError) { console.warn('[StripeProvider] WebBrowser checkout fallback', webBrowserError) } } 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, useEmbeddedFlow = false }) => { if (useEmbeddedFlow && !canUseEmbeddedCheckout) { throw new Error('Le paiement intégré Stripe est indisponible sur cette plateforme.') } const wantsEmbeddedCheckout = useEmbeddedFlow && canUseEmbeddedCheckout const safariWindow = wantsEmbeddedCheckout ? null : openSafariCheckoutWindow() try { const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(callableName) const { data } = await callable(payload) const checkoutUrl = data?.url const clientSecret = data?.client_secret || data?.clientSecret if (wantsEmbeddedCheckout) { if (!clientSecret) { throw new Error('Session Stripe introuvable.') } setClientSecret(clientSecret) return } if (!checkoutUrl) { throw new Error('Session Stripe introuvable.') } console.log('[StripeProvider] Stripe checkout URL', checkoutUrl) await redirectToCheckout(checkoutUrl, { safariWindow }) } catch (error) { if (safariWindow && !safariWindow.closed) { safariWindow.close() } console.error(`[StripeProvider] ${logTag}`, error) throw new Error( error?.message || 'Une erreur est survenue lors de la création de la session Stripe.' ) } }, [canUseEmbeddedCheckout, redirectToCheckout, setClientSecret] ) const createSubscriptionCheckout = React.useCallback( async (priceId) => { if (!priceId) { throw new Error('Aucun abonnement sélectionné.') } if (isWeb && !canUseEmbeddedCheckout) { console.error('[StripeProvider] checkout blocked (missing embedded support)', { hasStripeKey: Boolean(stripePublishableKey), isClientSecretReady: false, }) throw new Error( 'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).' ) } const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout await runCheckoutSession({ callableName: 'subscription-createSubscriptionCheckoutSession', payload: { priceId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted', }, logTag: 'subscription checkout error', useEmbeddedFlow: shouldUseEmbeddedCheckout, }) }, [canUseEmbeddedCheckout, runCheckoutSession] ) const createSongDownloadCheckout = React.useCallback( async (projectId) => { if (!projectId) { throw new Error('Aucun projet sélectionné.') } if (isWeb && !canUseEmbeddedCheckout) { console.error('[StripeProvider] checkout blocked (missing embedded support)', { hasStripeKey: Boolean(stripePublishableKey), isClientSecretReady: false, }) throw new Error( 'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).' ) } const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout await runCheckoutSession({ callableName: 'subscription-createSongDownloadCheckoutSession', payload: { projectId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted', }, logTag: 'song download checkout error', useEmbeddedFlow: shouldUseEmbeddedCheckout, }) }, [canUseEmbeddedCheckout, runCheckoutSession] ) const createPlaybackDownloadCheckout = React.useCallback( async (projectId) => { if (!projectId) { throw new Error('Aucun projet sélectionné.') } if (isWeb && !canUseEmbeddedCheckout) { console.error('[StripeProvider] checkout blocked (missing embedded support)', { hasStripeKey: Boolean(stripePublishableKey), isClientSecretReady: false, }) throw new Error( 'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).' ) } const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout await runCheckoutSession({ callableName: 'subscription-createPlaybackDownloadCheckoutSession', payload: { projectId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted', }, logTag: 'playback download checkout error', useEmbeddedFlow: shouldUseEmbeddedCheckout, }) }, [canUseEmbeddedCheckout, runCheckoutSession] ) const createCoinPackCheckout = React.useCallback( async (productId) => { if (!productId) { throw new Error('Aucun pack sélectionné.') } if (isWeb && !canUseEmbeddedCheckout) { console.error('[StripeProvider] checkout blocked (missing embedded support)', { hasStripeKey: Boolean(stripePublishableKey), isClientSecretReady: false, }) throw new Error( 'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).' ) } const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout await runCheckoutSession({ callableName: 'subscription-createCoinPackCheckoutSession', payload: { productId, returnUrls: { successUrl: STRIPE_SUCCESS_URL, cancelUrl: STRIPE_CANCEL_URL, }, uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted', }, logTag: 'coin pack checkout error', useEmbeddedFlow: shouldUseEmbeddedCheckout, }) }, [canUseEmbeddedCheckout, 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, createSongDownloadCheckout, createPlaybackDownloadCheckout, createCoinPackCheckout, openEmbeddedCheckout: setClientSecret, closeEmbeddedCheckout, activeSubscription: activeSubscriptionInfo.subscription, activeSubscriptionInfo, isActiveSubscriptionLoading, activeSubscriptionError, refreshActiveSubscription: fetchActiveSubscription, }), [ subscriptions, coinPacks, isCatalogLoading, catalogError, fetchStripeCatalog, createSubscriptionCheckout, createSongDownloadCheckout, createPlaybackDownloadCheckout, createCoinPackCheckout, setClientSecret, closeEmbeddedCheckout, activeSubscriptionInfo, isActiveSubscriptionLoading, activeSubscriptionError, fetchActiveSubscription, ] ) return ( {children} {clientSecret && stripePromise ? ( { closeEmbeddedCheckout() fetchActiveSubscription() }} /> ) : null}