stripe embedded and other fixes

This commit is contained in:
Thomas Demirdjian
2025-11-18 11:40:39 +01:00
parent 016f7c27ac
commit 7ab8685180
17 changed files with 2346 additions and 2504 deletions
+101 -52
View File
@@ -25,10 +25,23 @@ 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,
);
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 (
@@ -86,6 +99,7 @@ export const useStripe = () => {
const StripeProvider = ({ children }) => {
const { isMobile } = useLayoutType();
const { currentUserData } = useUserData() || {};
const canUseEmbeddedCheckout = isWeb && Boolean(stripePromise);
const [clientSecret, setClientSecret] = React.useState(null);
const [subscriptions, setSubscriptions] = React.useState({
@@ -118,33 +132,12 @@ const StripeProvider = ({ children }) => {
}
if (isWeb) {
if (safariWindow && !safariWindow.closed) {
try {
safariWindow.location.replace(checkoutUrl);
} catch (navigationError) {
safariWindow.location.href = checkoutUrl;
}
safariWindow.focus?.();
return;
}
if (typeof window !== "undefined") {
const openedTab = window.open(
checkoutUrl,
"_blank",
"noopener,noreferrer",
);
if (openedTab) {
return;
}
}
await Linking.openURL(checkoutUrl);
return;
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 {
@@ -173,17 +166,37 @@ const StripeProvider = ({ children }) => {
);
const runCheckoutSession = React.useCallback(
async ({ callableName, payload, logTag }) => {
const safariWindow = openSafariCheckoutWindow();
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
callableName,
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) {
@@ -196,7 +209,7 @@ const StripeProvider = ({ children }) => {
);
}
},
[redirectToCheckout],
[canUseEmbeddedCheckout, redirectToCheckout, setClientSecret],
);
const createSubscriptionCheckout = React.useCallback(
@@ -204,6 +217,19 @@ const StripeProvider = ({ children }) => {
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: {
@@ -212,11 +238,13 @@ const StripeProvider = ({ children }) => {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
uiMode: shouldUseEmbeddedCheckout ? "embedded" : "hosted",
},
logTag: "subscription checkout error",
useEmbeddedFlow: shouldUseEmbeddedCheckout,
});
},
[runCheckoutSession],
[canUseEmbeddedCheckout, runCheckoutSession],
);
const createCoinPackCheckout = React.useCallback(
@@ -224,6 +252,19 @@ const StripeProvider = ({ children }) => {
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: {
@@ -232,11 +273,13 @@ const StripeProvider = ({ children }) => {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
uiMode: shouldUseEmbeddedCheckout ? "embedded" : "hosted",
},
logTag: "coin pack checkout error",
useEmbeddedFlow: shouldUseEmbeddedCheckout,
});
},
[runCheckoutSession],
[canUseEmbeddedCheckout, runCheckoutSession],
);
const fetchActiveSubscription = React.useCallback(async () => {
@@ -252,9 +295,9 @@ const StripeProvider = ({ children }) => {
setIsActiveSubscriptionLoading(true);
setActiveSubscriptionError(null);
try {
const callable = getFunctionsClient(
FUNCTIONS_REGION,
).httpsCallable("subscription-getActiveSubscription");
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-getActiveSubscription",
);
const { data } = await callable();
setActiveSubscription(data?.subscription || null);
} catch (error) {
@@ -276,8 +319,9 @@ const StripeProvider = ({ children }) => {
let lastError = null;
try {
const callable =
functionsClient.httpsCallable("subscription-listSubscriptionPlans");
const callable = functionsClient.httpsCallable(
"subscription-listSubscriptionPlans",
);
const { data } = await callable();
const nextPlans = data?.plans || {};
setSubscriptions({
@@ -290,8 +334,9 @@ const StripeProvider = ({ children }) => {
}
try {
const callable =
functionsClient.httpsCallable("subscription-listCoinPacks");
const callable = functionsClient.httpsCallable(
"subscription-listCoinPacks",
);
const { data } = await callable();
setCoinPacks(Array.isArray(data?.packs) ? data.packs : []);
} catch (error) {
@@ -382,23 +427,28 @@ const StripeProvider = ({ children }) => {
isVisible={clientSecret !== null}
setIsVisible={closeEmbeddedCheckout}
>
<View
style={[StyleSheet.absoluteFillObject, styles.overlayContent]}
>
<View style={[StyleSheet.absoluteFillObject, styles.overlayContent]}>
<View
style={[
styles.embeddedWrapper,
{
width: isMobile ? "90%" : "80%",
width: isMobile ? "99%" : "95%",
height: isMobile ? "92vh" : "96vh",
maxWidth: isMobile ? "100%" : "1400px",
},
]}
>
{clientSecret ? (
{clientSecret && stripePromise ? (
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckout />
<EmbeddedCheckout
onComplete={() => {
closeEmbeddedCheckout();
fetchActiveSubscription();
}}
/>
</EmbeddedCheckoutProvider>
) : null}
</View>
@@ -422,9 +472,8 @@ const styles = StyleSheet.create({
},
embeddedWrapper: {
alignSelf: "center",
height: "70vh",
borderRadius: mainBorderRadius,
overflow: "scroll",
overflow: "hidden",
},
});