+stripe integration

This commit is contained in:
AntoninoP 2024-09-26 16:18:48 +02:00
parent f13473c2f8
commit a7044a9dc7
12 changed files with 274 additions and 152 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="src/assets/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Servii - Hebergement facile</title>
<!-- charger les polices -->

View File

@ -5,7 +5,9 @@ import 'react-toastify/dist/ReactToastify.css';
import { auth } from './service/firebase';
import styles from './App.module.scss';
import Loading from './pages/Loading/loading';
import Pricing from './pages/Payement/Pricing';
import Pricing from './pages/Payement/Pricing/Pricing';
import PaymentForm from './pages/Payement/PaymentForm/PaymentForm';
import Checkout from './pages/Payement/Checkout';
const LoginPage = lazy(() => import('./pages/LoginPage/LoginPage'));
const ServerDetails = lazy(() => import('./pages/ServerDetails/ServerDetails'));
@ -57,7 +59,8 @@ const App = () => {
<Route path="/createServer/modpack" element={user ? <Modpack user={user} /> : <Navigate to="/login" />} />
<Route path="/server/:serverName/*" element={user ? <ServerDetails user={user} /> : <Navigate to="/login" />} />
<Route path="/pricing" element={user ? <Pricing user={user} /> : <Navigate to="/login" />} />
<Route path="/payement/*" element={user ? <PaymentForm user={user} /> : <Navigate to="/login" />} />
<Route path="/return/*" element={user ? <Checkout user={user} /> : <Navigate to="/login" />} />
<Route path="/" element={<Navigate to={user ? "/dashboard" : "/login"} />} />
<Route path="*" element={user ? <DashboardPage user={user} /> : <Navigate to="/login" />} />
</Routes>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -52,7 +52,7 @@ const Navbar = ({
</div>
)}
{Subsribebtn && ( // Updated: Conditional rendering for the subscribe button
{Subsribebtn && (
<div className={styles.subscribeSection}>
<div className={styles.subscribeBtn} onClick={() => navigate('/Pricing')}>
S&apos;abonner
@ -101,7 +101,7 @@ Navbar.propTypes = {
showBackButton: PropTypes.bool,
onBackClick: PropTypes.func,
backButtonText: PropTypes.string,
Subsribebtn: PropTypes.bool // Updated: Prop type corrected
Subsribebtn: PropTypes.bool
};
export default Navbar;

View File

@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import ServerCard from '../../components/serverCards/DefaultServerCard';
import Navbar from '../../components/navbar/Navbar';
import Loading from '../Loading/loading';
import { getUserSubdomain } from "../../service/firebase";
import { getUserSubdomain, getUserSubscription } from "../../service/firebase";
import serviiApi from "../../service/api.tsx";
import PropTypes from "prop-types";
import styles from './DashboardPage.module.scss';
@ -21,6 +21,7 @@ const DashboardPage = ({ user }) => {
});
const [subdomain, setSubdomain] = useState(' ');
const [subscription, setSubscription] = useState(0);
const [loading, setLoading] = useState(servers.length === 0);
const [searchTerm, setSearchTerm] = useState('');
const [newSubdomain, setNewSubdomain] = useState(' ');
@ -33,6 +34,8 @@ const DashboardPage = ({ user }) => {
if (user?.uid) {
const userSubdomain = await getUserSubdomain(user.uid);
setSubdomain(userSubdomain || null);
const userSubscription = await getUserSubscription(user.uid);
setSubscription(userSubscription || 0);
}
const ApiResponse = await serviiApi.fetchServers();
@ -55,14 +58,44 @@ const DashboardPage = ({ user }) => {
const handleCreateServer = () => navigate('/CreateServer');
const handleRunServer = async (serverName) => {
const handleRunServer = async (serverName, framework) => {
try {
await serviiApi.serverRun(serverName);
updateServersFromApi();
if (subscription === 0) {
if (framework === "paper") {
navigate('/payement?package=Gratuit');
} if (framework === "Bedrock") {
navigate('/payement?package=Standard');
}
else {
navigate('/payement?package=Standard');
}
}
else if (subscription === 1) {
if (framework === "paper") {
await serviiApi.serverRun(serverName);
updateServersFromApi();
} else {
navigate('/payement?package=Standard');
}
}
else if (subscription === 2) {
if (framework === "paper" || framework === "Bedrock") {
await serviiApi.serverRun(serverName);
updateServersFromApi();
} else {
navigate('/payement?package=Premium');
}
}
else if (subscription === 3) {
await serviiApi.serverRun(serverName);
updateServersFromApi();
}
} catch (error) {
console.error('Error starting server:', error);
}
};
const handleStopServer = async (serverName) => {
try {
@ -193,7 +226,7 @@ const DashboardPage = ({ user }) => {
framework={favoriteServer.framework}
maxPlayers={favoriteServer.maxPlayers}
countPlayers={favoriteServer.onlinePlayers}
onRunClick={() => handleRunServer(favoriteServer.name)}
onRunClick={() => handleRunServer(favoriteServer.name , favoriteServer.framework)}
onStopClick={() => handleStopServer(favoriteServer.name)}
onDeleteClick={() => {
setServerToDelete(favoriteServer.name);
@ -219,7 +252,7 @@ const DashboardPage = ({ user }) => {
framework={server.framework}
maxPlayers={server.maxPlayers}
countPlayers={server.onlinePlayers}
onRunClick={() => handleRunServer(server.name)}
onRunClick={() => handleRunServer(server.name , server.framework)}
onStopClick={() => handleStopServer(server.name)}
onDeleteClick={() => {
setServerToDelete(server.name);

View File

@ -0,0 +1,51 @@
import { useState, useEffect } from 'react';
const Checkout = () => {
const [sessionStatus, setSessionStatus] = useState(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const session_id = params.get('session_id');
console.log('session_id:', session_id);
if (session_id) {
const fetchSessionStatus = async () => {
try {
const response = await fetch(`http://192.168.68.114:3000/get-session-status?session_id=${session_id}`);
const data = await response.json();
setSessionStatus(data);
} catch (error) {
console.error('Error fetching session status:', error);
}
};
fetchSessionStatus();
} else {
console.error('No session_id found in URL');
}
}, []);
if (!sessionStatus) {
return <div>Loading...</div>;
}
return (
<div>
{sessionStatus.status === 'open' && (
<div>
<h1>Checkout is still open</h1>
</div>
)}
{sessionStatus.status === 'complete' && (
<div>
<h1>Success!</h1>
<p>Payment Status: {sessionStatus.payment_status}</p>
<p>Customer Email: {sessionStatus.customer_email}</p>
</div>
)}
</div>
);
};
export default Checkout;

View File

@ -1,49 +1,97 @@
import React, { useState } from 'react';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { useCallback, useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import Navbar from '../../../components/navbar/Navbar';
import PropTypes from 'prop-types';
import { loadStripe } from '@stripe/stripe-js';
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
import styles from './PaymentForm.module.scss';
const PaymentForm = ({ groups, closeModal }) => {
const stripe = useStripe();
const elements = useElements();
const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const stripePromise = loadStripe("pk_test_51PyIYTP3VLLeb9GlXCKgD4ylbemZPx72I3HkEAu0bRtcsfK31nqb3WtUbXKXUcKmyfrxKLfuJzZCPyp7Ymtlq9zy00c7VmkL6G");
const handleSubmit = async (event) => {
event.preventDefault();
const PackageNumber = (selectedPackage) => {
switch (selectedPackage) {
case 'Gratuit':
return 1;
case 'Standard':
return 2;
case 'Premium':
return 3;
default:
return 1;
}
};
if (!stripe || !elements) return;
const CheckoutForm = ({ email }) => {
const location = useLocation();
const [clientSecret, setClientSecret] = useState('');
const queryParams = new URLSearchParams(location.search);
const selectedPackage = queryParams.get('package');
const fetchClientSecret = useCallback(() => {
return fetch('http://192.168.68.114:3000/get-checkout-session', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'permission': PackageNumber(selectedPackage),
'email': email,
},
})
.then((res) => res.json())
.then((data) => {
if (data && data.clientSecret) {
setClientSecret(data.clientSecret);
}
})
.catch((error) => {
console.error('Error fetching client secret:', error);
});
}, [selectedPackage, email]);
useEffect(() => {
fetchClientSecret();
}, [fetchClientSecret]);
return { clientSecret };
};
const PaymentForm = ({ user }) => {
const navigate = useNavigate();
const { clientSecret } = CheckoutForm({ email: user.email });
const options = { clientSecret };
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
setErrorMessage(error.message);
setSuccessMessage(null);
} else {
setSuccessMessage(`Paiement réussi! Méthode de paiement ID: ${paymentMethod.id}`);
setErrorMessage(null);
// Ici, vous devriez également envoyer paymentMethod.id à votre serveur
// Fermer la modal après un court délai ou après l'envoi à votre serveur
setTimeout(() => closeModal(), 2000);
}
};
return (
<form onSubmit={handleSubmit} className={styles.paymentForm}>
<CardElement />
{errorMessage && <p className={styles.error}>{errorMessage}</p>}
{successMessage && <p className={styles.success}>{successMessage}</p>}
<button type="submit" disabled={!stripe} className={styles.submitButton}>
Payer {groups.price}
</button>
<button type="button" onClick={closeModal} className={styles.closeButton}>
Fermer
</button>
</form>
<div className={styles.container}>
<Navbar
user={user}
hasShadow={true}
showBackButton={true}
onBackClick={() => navigate('/Pricing')}
backButtonText="Retour"
/>
<div id="checkout">
<EmbeddedCheckoutProvider stripe={stripePromise} options={options} >
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
</div>
);
};
export default PaymentForm;
PaymentForm.propTypes = {
user: PropTypes.shape({
uid: PropTypes.string.isRequired,
email: PropTypes.string,
photoURL: PropTypes.string,
}).isRequired,
};
export default PaymentForm;

View File

@ -1,19 +1,7 @@
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modalContent {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 300px;
}
.container{
margin-top: 5.5rem;
}
.Footer-PoweredBy-Text{
display: none;
}

View File

@ -1,68 +0,0 @@
import React, { useState } from 'react';
import styles from './Pricing.module.scss';
import Navbar from '../../components/navbar/Navbar';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import PaymentForm from './PaymentForm/PaymentForm';
const stripePromise = loadStripe('pk_test_51PyIYTP3VLLeb9GlXCKgD4ylbemZPx72I3HkEAu0bRtcsfK31nqb3WtUbXKXUcKmyfrxKLfuJzZCPyp7Ymtlq9zy00c7VmkL6G');
const Pricing = ({ user }) => {
const navigate = useNavigate();
const [selectedPackage, setSelectedPackage] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const groups = [
{ title: 'Gratuit', price: '0 €', features: ['Accès limité', 'Support par e-mail'] },
{ title: 'Standard', price: '2 €', features: ['Accès complet', 'Support prioritaire'] },
{ title: 'Premium', price: '9 €', features: ['Accès illimité', 'Support 24/7'] },
];
const handleSubscribe = (pkg) => {
setSelectedPackage(pkg);
setIsModalOpen(true);
};
return (
<div className={styles.pricingContainer}>
<Navbar
user={user}
hasShadow={true}
showBackButton={true}
onBackClick={() => navigate('/Dashboard')}
/>
{groups.map((pkg, index) => (
<div key={index} className={styles.packageCard}>
<h2 className={styles.title}>{pkg.title}</h2>
<p className={styles.price}>{pkg.price}</p>
<ul className={styles.features}>
{pkg.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
<button className={styles.button} onClick={() => handleSubscribe(pkg)}>Sinscrire</button>
</div>
))}
{isModalOpen && (
<div className={styles.modal}>
<div className={styles.modalContent}>
<h2>Paiement pour {selectedPackage.title}</h2>
<Elements stripe={stripePromise}>
<PaymentForm groups={selectedPackage} closeModal={() => setIsModalOpen(false)} />
</Elements>
</div>
</div>
)}
</div>
);
};
Pricing.propTypes = {
user: PropTypes.object.isRequired,
};
export default Pricing;

View File

@ -0,0 +1,52 @@
import styles from './Pricing.module.scss';
import Navbar from '../../../components/navbar/Navbar';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
const Pricing = ({ user }) => {
const navigate = useNavigate();
const groups = [
{ title: 'Gratuit', price: '0 €', features: ['Accès limité', 'Support par e-mail'] },
{ title: 'Standard', price: '2 €', features: ['Accès complet', 'Support prioritaire'] },
{ title: 'Premium', price: '9 €', features: ['Accès illimité', 'Support 24/7'] },
];
const handleSubscribe = (pkg) => {
navigate(`/payement?package=${pkg.title}`);
};
return (
<div className={styles.pricingContainer}>
<Navbar
user={user}
hasShadow={true}
showBackButton={true}
onBackClick={() => navigate('/Dashboard')}
/>
<div className={styles.packageList}>
{groups.map((pkg, index) => (
<div key={index} className={styles.packageCard}>
<h2 className={styles.title}>{pkg.title}</h2>
<p className={styles.price}>{pkg.price}</p>
<ul className={styles.features}>
{pkg.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
<button className={styles.button} onClick={() => handleSubscribe(pkg)}>
S&apos;inscrire
</button>
</div>
))}
</div>
</div>
);
};
Pricing.propTypes = {
user: PropTypes.object.isRequired,
};
export default Pricing;

View File

@ -2,35 +2,43 @@ $primary-color: #000;
$secondary-color: #fff;
.pricingContainer {
margin-top: 5rem;
display: flex;
justify-content: center;
gap: 20px;
padding: 40px;
flex-direction: row;
gap: 2rem;
padding: 4rem;
background-color: $secondary-color;
}
.packageList{
display: flex;
flex-direction: row;
gap: 2rem;
}
.packageCard {
background-color: $primary-color;
color: $secondary-color;
border-radius: 8px;
padding: 20px;
border-radius: .8rem;
padding: 5rem;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
&:hover {
transform: scale(1.05);
transform: scale(1.01);
}
}
.title {
font-size: 24px;
margin-bottom: 10px;
font-size: 2rem;
margin-bottom: .6rem;
}
.price {
font-size: 32px;
margin: 10px 0;
font-size: 2.5rem;
margin: 1rem 0;
}
.features {
@ -52,9 +60,4 @@ $secondary-color: #fff;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
&:hover {
background-color: $primary-color;
color: $secondary-color;
}
}
}

View File

@ -37,4 +37,16 @@ const getUserSubdomain = async (userId) => {
}
};
export { auth, googleProvider, signInWithPopup, getUserSubdomain, app };
const getUserSubscription = async (userId) => {
const userDocRef = doc(db, 'users', userId);
const userDocSnap = await getDoc(userDocRef);
if (userDocSnap.exists()) {
const userData = userDocSnap.data();
return userData.subscription;
} else {
throw new Error("No such document!");
}
};
export { auth, googleProvider, signInWithPopup, getUserSubdomain, getUserSubscription, app };