-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #380 from ungdev/dev
Shop 2024
- Loading branch information
Showing
42 changed files
with
633 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
'use client'; | ||
import { useEffect } from 'react'; | ||
import { useRouter, useSearchParams } from 'next/navigation'; | ||
import { toast } from 'react-toastify'; | ||
import { useStripe } from '@stripe/react-stripe-js'; | ||
import { PaymentIntentResult } from '@stripe/stripe-js'; | ||
import { Loader } from '@/components/UI'; | ||
import styles from '../style.module.scss'; | ||
|
||
const Payment = () => { | ||
const router = useRouter(); | ||
const search = useSearchParams(); | ||
const clientSecret = search.get('payment_intent_client_secret'); | ||
const stripe = useStripe(); | ||
|
||
useEffect(() => { | ||
if (!stripe || !clientSecret) { | ||
return; | ||
} | ||
|
||
stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }: PaymentIntentResult) => { | ||
if (!paymentIntent) { | ||
return; | ||
} | ||
|
||
if (paymentIntent.status === 'succeeded') { | ||
toast.success('Paiement effectué avec succès'); | ||
} else if (paymentIntent.status === 'requires_payment_method') { | ||
toast.error('Le paiement a échoué. Veuillez réessayer.'); | ||
} else if (paymentIntent.status === 'processing') { | ||
toast.error( | ||
'Le paiement est en cours de traitement. Vous recevrez un email de confirmation une fois le paiement effectué.', | ||
); | ||
} else { | ||
toast.error("Une erreur inattendue s'est produite lors du paiement."); | ||
} | ||
|
||
router.push('/dashboard'); | ||
}); | ||
}, [stripe, clientSecret]); | ||
|
||
return ( | ||
<div className={styles.loader}> | ||
<Loader /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Payment; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
'use client'; | ||
|
||
import { Elements } from '@stripe/react-stripe-js'; | ||
import { Appearance, StripeElementsOptions } from '@stripe/stripe-js'; | ||
|
||
import variables from '@/variables.module.scss'; | ||
import { useSearchParams } from 'next/navigation'; | ||
import { stripe } from '@/utils/stripe'; | ||
|
||
const stripePromise = stripe; | ||
const PaymentLayout = ({ children }: { children: React.ReactNode }) => { | ||
const search = useSearchParams(); | ||
const stripeToken = search.get('stripeToken') ?? search.get('payment_intent_client_secret'); | ||
|
||
const appearance = { | ||
theme: 'stripe', | ||
variables: { | ||
colorPrimary: variables.primaryColor, | ||
colorBackground: variables.primaryBackground, | ||
colorText: variables.lightColor, | ||
}, | ||
} satisfies Appearance; | ||
const options = { | ||
clientSecret: stripeToken, | ||
appearance, | ||
} as StripeElementsOptions; | ||
|
||
return ( | ||
<Elements stripe={stripePromise} options={options}> | ||
{children} | ||
</Elements> | ||
); | ||
}; | ||
|
||
export default PaymentLayout; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,184 @@ | ||
'use client'; | ||
import { useEffect } from 'react'; | ||
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation'; | ||
import { toast } from 'react-toastify'; | ||
|
||
interface SearchParams extends ReadonlyURLSearchParams { | ||
type?: string; | ||
error?: string; | ||
} | ||
import { Button, Loader, Title } from '@/components/UI'; | ||
import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'; | ||
import { StripePaymentElementOptions } from '@stripe/stripe-js'; | ||
import { useEffect, useState, useMemo } from 'react'; | ||
import styles from './style.module.scss'; | ||
import Cart from '@/components/dashboard/Cart'; | ||
import { useAppDispatch, useAppSelector } from '@/lib/hooks'; | ||
import { CartItem, Item, User, UserType } from '@/types'; | ||
import { fetchItems } from '@/modules/items'; | ||
import { getTicketPrice } from '@/modules/users'; | ||
import { fetchCurrentTeam } from '@/modules/team'; | ||
import { useSearchParams } from 'next/navigation'; | ||
|
||
const Payment = () => { | ||
const router = useRouter(); | ||
const query: SearchParams = useSearchParams(); | ||
const stripe = useStripe(); | ||
const elements = useElements(); | ||
|
||
useEffect(() => { | ||
if (query.type === 'success') { | ||
toast.success('Paiement effectué avec succès'); | ||
} else if (query.type === 'error') { | ||
switch (query.error) { | ||
case 'CART_NOT_FOUND': | ||
toast.error('Panier introuvable'); | ||
break; | ||
case 'TRANSACTION_ERROR': | ||
toast.error('La transaction a échoué'); | ||
break; | ||
case 'NO_PAYLOAD': | ||
toast.error('Requête erronée'); | ||
break; | ||
default: | ||
break; | ||
const dispatch = useAppDispatch(); | ||
|
||
const user = useAppSelector((state) => state.login.user) as User; | ||
|
||
const [message, setMessage] = useState<string | null>(null); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const cartStr = useSearchParams().get('cart'); | ||
const cart = useMemo(() => (cartStr ? JSON.parse(cartStr) : null), [cartStr]); | ||
|
||
const [items, setItems] = useState<Item[] | null>(null); | ||
// The team the player is in | ||
const team = useAppSelector((state) => state.team.team); | ||
// The members of the team are the players and the coaches | ||
const [teamMembers, setTeamMembers] = useState<User[] | null>(null); | ||
|
||
// Contains the ticket items for each ticket in the cart. This is an object, keys are userIds and values are the items | ||
const [tickets, setTickets] = useState< | ||
| { | ||
[userId: string]: CartItem; | ||
} | ||
| undefined | ||
>(undefined); | ||
|
||
// Fetch items, team and checks if user already have an attendant | ||
useEffect(() => { | ||
if (user.teamId) { | ||
dispatch(fetchCurrentTeam()); | ||
} else if (user.type === UserType.spectator) { | ||
// Organizers should not be able to buy tickets if they are not in a team | ||
setTeamMembers([ | ||
{ | ||
id: user.id, | ||
hasPaid: user.hasPaid, | ||
username: user.username, | ||
age: user.age, | ||
attendant: user.attendant, | ||
type: user.type, | ||
} as User, | ||
]); | ||
} else { | ||
setTeamMembers([]); | ||
} | ||
router.push('/dashboard'); | ||
|
||
(async () => { | ||
setItems(await fetchItems()); | ||
})(); | ||
}, []); | ||
|
||
return false; | ||
// Initializing teamMembers | ||
useEffect(() => { | ||
if (!team) { | ||
return; | ||
} | ||
setTeamMembers(team.players.concat(team.coaches)); | ||
}, [team]); | ||
|
||
// Checks if the place of the user is already in the cart | ||
// Checks if the user already have an attendant | ||
// Initializes teamMembersWithoutTicket | ||
// Fills tickets | ||
useEffect(() => { | ||
if (!cart || !teamMembers || !items) return; | ||
|
||
(async () => { | ||
// First, we make all the requests | ||
const ticketsArray = ( | ||
await Promise.allSettled( | ||
cart.tickets.userIds.map((userId: string) => | ||
userId === user.id ? items.find((item) => item.id === `ticket-${user.type}`) : getTicketPrice(userId), | ||
), | ||
) | ||
) | ||
.filter((val): val is PromiseFulfilledResult<Item> => val.status === 'fulfilled') | ||
// Then, we only keep the return value of the Promises | ||
.map((result) => result.value) | ||
// And finally, we remove failed Promises | ||
.filter((ticket, i) => { | ||
if (!ticket) { | ||
// toast.error( | ||
// `Une erreur est survenue en cherchant le prix du ticket de l'utilisateur avec l'identifiant ${cart.tickets.userIds[i]}. Si ce problème persiste, contacte le support`, | ||
// ); | ||
const newCart = { ...cart }; | ||
newCart.tickets.userIds.splice(i, 1); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
setTickets(ticketsArray.reduce((prev, curr, i) => ({ ...prev, [cart.tickets.userIds[i]]: curr }), {})); | ||
})(); | ||
}, [items, cart, teamMembers]); | ||
|
||
if (!items || !teamMembers || !tickets || !cart) { | ||
return ( | ||
<div className={styles.loader}> | ||
<Loader /> | ||
</div> | ||
); | ||
} | ||
|
||
const handleSubmit = async (e: React.FormEvent) => { | ||
e.preventDefault(); | ||
|
||
if (!stripe || !elements) { | ||
return; | ||
} | ||
|
||
setIsLoading(true); | ||
|
||
const { error } = await stripe.confirmPayment({ | ||
elements, | ||
confirmParams: { | ||
return_url: process.env.NEXT_PUBLIC_URL + '/dashboard/payment/callback', | ||
}, | ||
}); | ||
|
||
// This point will only be reached if there is an immediate error when | ||
// confirming the payment. Otherwise, the customer will be redirected to | ||
// `/payment/callback`. | ||
if (error.type === 'card_error' || error.type === 'validation_error') { | ||
setMessage(error.message!.toString()); | ||
} else { | ||
setMessage("Une erreur inconnue s'est produite"); | ||
} | ||
|
||
setIsLoading(false); | ||
}; | ||
|
||
const paymentElementOptions = { | ||
layout: 'tabs', | ||
} as StripePaymentElementOptions; | ||
|
||
return ( | ||
<div className={styles.paymentContainer}> | ||
<div className={styles.stripePaymentContainer}> | ||
<form id="payment-form" onSubmit={handleSubmit} className={styles.stripeForm}> | ||
<Title level={2} align="center"> | ||
Paiement | ||
</Title> | ||
<PaymentElement id="payment-element" options={paymentElementOptions} /> | ||
<Button primary type="submit" disabled={isLoading || !stripe || !elements}> | ||
<span id="button-text">{isLoading ? <div className="spinner" id="spinner"></div> : 'Payer'}</span> | ||
</Button> | ||
{/* Show any error or success messages */} | ||
{message && <div id="payment-message">{message}</div>} | ||
</form> | ||
<div className={styles.bill}> | ||
<Cart | ||
cart={cart} | ||
tickets={tickets!} | ||
items={items!} | ||
teamMembers={teamMembers!} | ||
onItemRemoved={null} | ||
onTicketRemoved={null} | ||
onCartReset={null} | ||
/> | ||
</div> | ||
</div> | ||
<p> | ||
Paiement géré par <a href="https://stripe.com/fr">Stripe Payments Europe, Ltd.</a> | ||
</p> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Payment; |
Oops, something went wrong.