feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+186 -243
View File
@@ -1,386 +1,340 @@
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 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";
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 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 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;
'https://dashboard.stripe.com/test/billing/starter-guide/checkout-success'
const STRIPE_CANCEL_URL = STRIPE_SUCCESS_URL
const isStripeTesting = true;
const isStripeTesting = true
const rawStripePublishableKey = isStripeTesting
? STRIPE_PUBLISHABLE_KEY_TEST
: STRIPE_PUBLISHABLE_KEY_LIVE;
: STRIPE_PUBLISHABLE_KEY_LIVE
const stripePublishableKey =
typeof rawStripePublishableKey === "string"
? rawStripePublishableKey.trim()
: "";
const stripePromise = stripePublishableKey
? loadStripe(stripePublishableKey)
: null;
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é.",
);
'[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;
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,
)
);
};
!/(chrome|crios|android|fxios|edgios|opr|ucbrowser)/i.test(navigator.userAgent)
)
}
const openSafariCheckoutWindow = () => {
if (!isWeb || typeof window === "undefined") {
return null;
if (!isWeb || typeof window === 'undefined') {
return null
}
if (!isSafariBrowser()) {
return null;
return null
}
try {
const placeholder = window.open("", "_blank", "noopener,noreferrer");
const placeholder = window.open('', '_blank', 'noopener,noreferrer')
if (!placeholder) {
return null;
return null
}
try {
placeholder.document.write(
"<p style='font-family: sans-serif; color: #111; padding: 16px;'>Chargement du paiement Stripe...</p>",
);
placeholder.document.title = "Stripe Checkout";
"<p style='font-family: sans-serif; color: #111; padding: 16px;'>Chargement du paiement Stripe...</p>"
)
placeholder.document.title = 'Stripe Checkout'
} catch {
// Ignore failures when the window gets reused or is cross-domain.
}
placeholder.focus?.();
return placeholder;
placeholder.focus?.()
return placeholder
} catch (error) {
console.warn("[StripeProvider] Safari window pre-open failed", error);
return null;
console.warn('[StripeProvider] Safari window pre-open failed', error)
return null
}
};
}
export const StripeContext = React.createContext(null);
export const StripeContext = React.createContext(null)
export const useStripe = () => {
const context = React.useContext(StripeContext);
const context = React.useContext(StripeContext)
if (!context) {
throw new Error("useStripe must be used within a StripeProvider");
throw new Error('useStripe must be used within a StripeProvider')
}
return context;
};
return context
}
const StripeProvider = ({ children }) => {
const { isMobile } = useLayoutType();
const { currentUserData } = useUserData() || {};
const canUseEmbeddedCheckout = isWeb && Boolean(stripePromise);
const { isMobile } = useLayoutType()
const { currentUserData } = useUserData() || {}
const canUseEmbeddedCheckout = isWeb && Boolean(stripePromise)
const [clientSecret, setClientSecret] = React.useState(null);
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 [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 stripeCustomerId = currentUserData?.stripeCustomerId || null
const localSubscriptionId =
currentUserData?.stripeSubscription?.id ||
currentUserData?.stripeSubscription?.subscriptionId ||
null;
null
const closeEmbeddedCheckout = React.useCallback(() => {
setClientSecret(null);
}, []);
setClientSecret(null)
}, [])
const redirectToCheckout = React.useCallback(async (checkoutUrl = {}) => {
if (!checkoutUrl) {
throw new Error("Session Stripe introuvable.");
throw new Error('Session Stripe introuvable.')
}
if (isWeb) {
throw new Error(
"Impossible d'ouvrir le paiement Stripe sans checkout intégré.",
);
throw new Error("Impossible d'ouvrir le paiement Stripe sans checkout intégré.")
}
const isMobileApp = Platform.OS === "ios" || Platform.OS === "android";
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;
dismissButtonStyle: 'close',
presentationStyle: WebBrowser?.WebBrowserPresentationStyle?.PAGE_SHEET,
})
return
} catch (webBrowserError) {
console.warn(
"[StripeProvider] WebBrowser checkout fallback",
webBrowserError,
);
console.warn('[StripeProvider] WebBrowser checkout fallback', webBrowserError)
}
}
const canOpen = await Linking.canOpenURL(checkoutUrl);
const canOpen = await Linking.canOpenURL(checkoutUrl)
if (!canOpen) {
throw new Error("Impossible d'ouvrir l'URL de paiement.");
throw new Error("Impossible d'ouvrir l'URL de paiement.")
}
await Linking.openURL(checkoutUrl);
}, []);
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.",
);
throw new Error('Le paiement intégré Stripe est indisponible sur cette plateforme.')
}
const wantsEmbeddedCheckout = useEmbeddedFlow && canUseEmbeddedCheckout;
const safariWindow = wantsEmbeddedCheckout
? null
: openSafariCheckoutWindow();
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;
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.");
throw new Error('Session Stripe introuvable.')
}
setClientSecret(clientSecret);
return;
setClientSecret(clientSecret)
return
}
if (!checkoutUrl) {
throw new Error("Session Stripe introuvable.");
throw new Error('Session Stripe introuvable.')
}
console.log("[StripeProvider] Stripe checkout URL", checkoutUrl);
await redirectToCheckout(checkoutUrl, { safariWindow });
console.log('[StripeProvider] Stripe checkout URL', checkoutUrl)
await redirectToCheckout(checkoutUrl, { safariWindow })
} catch (error) {
if (safariWindow && !safariWindow.closed) {
safariWindow.close();
safariWindow.close()
}
console.error(`[StripeProvider] ${logTag}`, error);
console.error(`[StripeProvider] ${logTag}`, error)
throw new Error(
error?.message ||
"Une erreur est survenue lors de la création de la session Stripe.",
);
error?.message || 'Une erreur est survenue lors de la création de la session Stripe.'
)
}
},
[canUseEmbeddedCheckout, redirectToCheckout, setClientSecret],
);
[canUseEmbeddedCheckout, redirectToCheckout, setClientSecret]
)
const createSubscriptionCheckout = React.useCallback(
async (priceId) => {
if (!priceId) {
throw new Error("Aucun abonnement sélectionné.");
throw new Error('Aucun abonnement sélectionné.')
}
if (isWeb && !canUseEmbeddedCheckout) {
console.error(
"[StripeProvider] checkout blocked (missing embedded support)",
{
hasStripeKey: Boolean(stripePublishableKey),
isClientSecretReady: false,
},
);
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).",
);
'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).'
)
}
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout;
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout
await runCheckoutSession({
callableName: "subscription-createSubscriptionCheckoutSession",
callableName: 'subscription-createSubscriptionCheckoutSession',
payload: {
priceId,
returnUrls: {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
uiMode: shouldUseEmbeddedCheckout ? "embedded" : "hosted",
uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted',
},
logTag: "subscription checkout error",
logTag: 'subscription checkout error',
useEmbeddedFlow: shouldUseEmbeddedCheckout,
});
})
},
[canUseEmbeddedCheckout, runCheckoutSession],
);
[canUseEmbeddedCheckout, runCheckoutSession]
)
const createCoinPackCheckout = React.useCallback(
async (productId) => {
if (!productId) {
throw new Error("Aucun pack sélectionné.");
throw new Error('Aucun pack sélectionné.')
}
if (isWeb && !canUseEmbeddedCheckout) {
console.error(
"[StripeProvider] checkout blocked (missing embedded support)",
{
hasStripeKey: Boolean(stripePublishableKey),
isClientSecretReady: false,
},
);
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).",
);
'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).'
)
}
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout;
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout
await runCheckoutSession({
callableName: "subscription-createCoinPackCheckoutSession",
callableName: 'subscription-createCoinPackCheckoutSession',
payload: {
productId,
returnUrls: {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
uiMode: shouldUseEmbeddedCheckout ? "embedded" : "hosted",
uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted',
},
logTag: "coin pack checkout error",
logTag: 'coin pack checkout error',
useEmbeddedFlow: shouldUseEmbeddedCheckout,
});
})
},
[canUseEmbeddedCheckout, runCheckoutSession],
);
[canUseEmbeddedCheckout, runCheckoutSession]
)
const fetchActiveSubscription = React.useCallback(async () => {
const hasLookupContext =
Boolean(stripeCustomerId) || Boolean(localSubscriptionId);
const hasLookupContext = Boolean(stripeCustomerId) || Boolean(localSubscriptionId)
if (!hasLookupContext) {
setActiveSubscription(null);
setActiveSubscriptionError(null);
setIsActiveSubscriptionLoading(false);
return;
setActiveSubscription(null)
setActiveSubscriptionError(null)
setIsActiveSubscriptionLoading(false)
return
}
setIsActiveSubscriptionLoading(true);
setActiveSubscriptionError(null);
setIsActiveSubscriptionLoading(true)
setActiveSubscriptionError(null)
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-getActiveSubscription",
);
const { data } = await callable();
setActiveSubscription(data?.subscription || null);
'subscription-getActiveSubscription'
)
const { data } = await callable()
setActiveSubscription(data?.subscription || null)
} catch (error) {
console.error("[StripeProvider] getActiveSubscription error", error);
console.error('[StripeProvider] getActiveSubscription error', error)
setActiveSubscriptionError(
error?.message ||
"Impossible de mettre à jour les informations d'abonnement.",
);
setActiveSubscription(null);
error?.message || "Impossible de mettre à jour les informations d'abonnement."
)
setActiveSubscription(null)
} finally {
setIsActiveSubscriptionLoading(false);
setIsActiveSubscriptionLoading(false)
}
}, [stripeCustomerId, localSubscriptionId]);
}, [stripeCustomerId, localSubscriptionId])
const fetchStripeCatalog = React.useCallback(async () => {
setIsCatalogLoading(true);
setCatalogError(null);
const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
let lastError = null;
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 || {};
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);
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 : []);
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);
lastError = error
console.error('[StripeProvider] listCoinPacks error', error)
}
if (lastError) {
setCatalogError(
lastError?.message ||
"Impossible de récupérer les informations Stripe pour le moment.",
);
lastError?.message || 'Impossible de récupérer les informations Stripe pour le moment.'
)
} else {
setCatalogError(null);
setCatalogError(null)
}
setIsCatalogLoading(false);
}, []);
setIsCatalogLoading(false)
}, [])
React.useEffect(() => {
fetchStripeCatalog();
}, [fetchStripeCatalog]);
fetchStripeCatalog()
}, [fetchStripeCatalog])
React.useEffect(() => {
fetchActiveSubscription();
}, [fetchActiveSubscription]);
fetchActiveSubscription()
}, [fetchActiveSubscription])
const activeSubscriptionInfo = React.useMemo(() => {
const subscription = activeSubscription || null;
const subscription = activeSubscription || null
const nextRenewalDate =
toDate(subscription?.currentPeriodEnd) ||
toDate(subscription?.current_period_end) ||
null;
toDate(subscription?.currentPeriodEnd) || toDate(subscription?.current_period_end) || null
const subscribedSinceDate =
toDate(subscription?.created) ||
toDate(subscription?.createdAt) ||
toDate(subscription?.created_at) ||
null;
null
return {
subscription,
nextRenewalDate,
nextRenewalLabel: nextRenewalDate ? formatDate(nextRenewalDate) : null,
subscribedSinceDate,
subscribedSinceLabel: subscribedSinceDate
? formatDate(subscribedSinceDate)
: null,
};
}, [activeSubscription]);
subscribedSinceLabel: subscribedSinceDate ? formatDate(subscribedSinceDate) : null,
}
}, [activeSubscription])
const providerValue = React.useMemo(
() => ({
@@ -413,64 +367,53 @@ const StripeProvider = ({ children }) => {
isActiveSubscriptionLoading,
activeSubscriptionError,
fetchActiveSubscription,
],
);
]
)
return (
<StripeContext.Provider value={providerValue}>
{children}
<Overlay
isVisible={clientSecret !== null}
setIsVisible={closeEmbeddedCheckout}
>
<Overlay isVisible={clientSecret !== null} setIsVisible={closeEmbeddedCheckout}>
<View style={[StyleSheet.absoluteFillObject, styles.overlayContent]}>
<View
style={[
styles.embeddedWrapper,
{
width: isMobile ? "99%" : "95%",
height: isMobile ? "92vh" : "96vh",
maxWidth: isMobile ? "100%" : "1400px",
width: isMobile ? '99%' : '95%',
height: isMobile ? '92vh' : '96vh',
maxWidth: isMobile ? '100%' : '1400px',
},
]}
>
{clientSecret && stripePromise ? (
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
<EmbeddedCheckout
onComplete={() => {
closeEmbeddedCheckout();
fetchActiveSubscription();
closeEmbeddedCheckout()
fetchActiveSubscription()
}}
/>
</EmbeddedCheckoutProvider>
) : null}
</View>
<Button
type="secondary"
isAbsoluteBottom
text="Fermer"
onPress={closeEmbeddedCheckout}
/>
<Button type="secondary" isAbsoluteBottom text="Fermer" onPress={closeEmbeddedCheckout} />
</View>
</Overlay>
</StripeContext.Provider>
);
};
)
}
const styles = StyleSheet.create({
overlayContent: {
position: "absolute",
justifyContent: "center",
alignItems: "center",
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
},
embeddedWrapper: {
alignSelf: "center",
alignSelf: 'center',
borderRadius: mainBorderRadius,
overflow: "hidden",
overflow: 'hidden',
},
});
})
export default StripeProvider;
export default StripeProvider