more tickets
This commit is contained in:
@@ -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, {}],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user