343 lines
10 KiB
JavaScript
343 lines
10 KiB
JavaScript
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 (
|
|
<StripeContext.Provider value={providerValue}>
|
|
{children}
|
|
<Overlay
|
|
isVisible={clientSecret !== null}
|
|
setIsVisible={closeEmbeddedCheckout}
|
|
>
|
|
<View
|
|
style={[StyleSheet.absoluteFillObject, styles.overlayContent]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.embeddedWrapper,
|
|
{
|
|
width: isMobile ? "90%" : "80%",
|
|
},
|
|
]}
|
|
>
|
|
{clientSecret ? (
|
|
<EmbeddedCheckoutProvider
|
|
stripe={stripePromise}
|
|
options={{ clientSecret }}
|
|
>
|
|
<EmbeddedCheckout />
|
|
</EmbeddedCheckoutProvider>
|
|
) : null}
|
|
</View>
|
|
<Button
|
|
type="secondary"
|
|
isAbsoluteBottom
|
|
text="Fermer"
|
|
onPress={closeEmbeddedCheckout}
|
|
/>
|
|
</View>
|
|
</Overlay>
|
|
</StripeContext.Provider>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
overlayContent: {
|
|
position: "absolute",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
embeddedWrapper: {
|
|
alignSelf: "center",
|
|
height: "70vh",
|
|
borderRadius: mainBorderRadius,
|
|
overflow: "scroll",
|
|
},
|
|
});
|
|
|
|
export default StripeProvider;
|