more tickets

This commit is contained in:
Thomas Demirdjian
2025-11-13 18:31:38 +01:00
parent dd4cf9b4d4
commit 5457030d4a
32 changed files with 913 additions and 558 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ import { SheetProvider } from "react-native-actions-sheet";
import NotificationProvider from "./NotificationProvider";
import PlayerProvider from "./PlayerProvider";
import SplashAnimationProvider from "./SplashAnimationProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
import StripeProvider from "./StripeProvider";
import UniversalLinkProvider from "./UniversalLinkProvider";
import UserDataProvider from "./UserDataProvider";
import WebViewProvider from "./WebViewProvider";
@@ -19,7 +19,7 @@ const SharedProviders = ({ children }) => {
[SplashAnimationProvider, {}],
[WebViewProvider, {}],
[UserDataProvider, {}],
[StripeEmbeddedProvider, {}],
[StripeProvider, {}],
[UniversalLinkProvider, {}],
[BottomSheetModalProvider, {}],
[SheetProvider, {}],
-73
View File
@@ -1,73 +0,0 @@
import {
EmbeddedCheckoutProvider,
EmbeddedCheckout,
} from "@stripe/react-stripe-js";
import { useState, createContext } from "react";
import { View, StyleSheet } from "react-native";
import { loadStripe } from "@stripe/stripe-js";
import {
STRIPE_PUBLISHABLE_KEY_LIVE,
STRIPE_PUBLISHABLE_KEY_TEST,
} from "../data/keys";
import { mainBorderRadius } from "../styles/Style";
import Overlay from "../components/Overlay";
import Button from "../components/Button";
import useLayoutType from "../hooks/useLayoutType";
const isStripeTesting = false;
const stripePromise = loadStripe(
isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE
);
export const StripeEmbeddedContext = createContext();
export default ({ children }) => {
const { isMobile } = useLayoutType();
const [clientSecret, setClientSecret] = useState(null);
return (
<StripeEmbeddedContext.Provider value={{ setClientSecret }}>
{children}
<Overlay
isVisible={clientSecret !== null}
setIsVisible={() => setClientSecret(null)}
>
<View
style={{
position: "absolute",
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
alignSelf: "center",
width: isMobile ? "90%" : "80%",
height: "70vh",
borderRadius: mainBorderRadius,
overflow: "scroll",
}}
>
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</View>
<Button
type="secondary"
isAbsoluteBottom
text="Fermer"
onPress={() => setClientSecret(null)}
/>
</View>
</Overlay>
</StripeEmbeddedContext.Provider>
);
};
+342
View File
@@ -0,0 +1,342 @@
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;
+1 -4
View File
@@ -2,7 +2,6 @@ import React from "react";
import LoadingProvider from "./LoadingProvider";
import TooltipProvider from "./TooltipProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
import SharedProviders from "./SharedProviders";
@@ -10,9 +9,7 @@ export default ({ children }) => {
return (
<LoadingProvider>
<TooltipProvider>
<StripeEmbeddedProvider>
<SharedProviders>{children}</SharedProviders>
</StripeEmbeddedProvider>
<SharedProviders>{children}</SharedProviders>
</TooltipProvider>
</LoadingProvider>
);