stripe payment

This commit is contained in:
dzfelix 2024-07-13 13:13:58 +03:00 committed by SD
parent b141a6ad44
commit 59de68d611
21 changed files with 904 additions and 9 deletions

View File

@ -1,5 +1,6 @@
import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts';
import { apiRequest } from './helpers';
import { apiClient } from '../lib/apiClient';
import {GeneralFilter, ExpertsData, ExpertDetails, ExpertScheduler, ExpertSchedulerSession} from '../types/experts';
export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise<ExpertsData> => apiRequest({
url: '/home/coachsearch1',
@ -14,3 +15,33 @@ export const getExpertById = (id: string, locale: string): Promise<ExpertDetails
data: { id },
locale
});
export const getSchedulerByExpertId = async (expertId: string, locale: string, jwt: string) => {
const response = await apiClient.post(
'/home/sessionsignupdata',
{ id: expertId },
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
);
return response.data as ExpertScheduler || null;
};
export const getSchedulerSession = async (data: { coachId: number, tagId: number, startAtUtc: string, clientComment: string }, locale: string, jwt: string) => {
const response = await apiClient.post(
'/home/sessionsignupsubmit',
data,
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
);
return response.data as ExpertSchedulerSession || null;
};

78
src/actions/stripe.ts Normal file
View File

@ -0,0 +1,78 @@
"use server";
import {PaymentIntentCreateParams, Stripe} from "stripe";
import { headers } from "next/headers";
import { formatAmountForStripe } from "../utils/stripe-helpers";
import { stripe } from "../lib/stripe";
export async function createCheckoutSession(
data: FormData,
): Promise<{ client_secret: string | null; url: string | null }> {
const ui_mode = data.get(
"uiMode",
) as Stripe.Checkout.SessionCreateParams.UiMode;
console.log('DATA', data)
const origin: string = headers().get("origin") as string;
const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create({
mode: "payment",
submit_type: "donate",
line_items: [
{
quantity: 1,
price_data: {
currency: 'eur',
product_data: {
name: "Custom amount donation",
},
unit_amount: formatAmountForStripe(
Number(data.get("customDonation") as string),
'eur',
),
},
},
],
...(ui_mode === "hosted" && {
success_url: `${origin}/payment/with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/with-checkout`,
}),
...(ui_mode === "embedded" && {
return_url: `${origin}/payment/with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
}),
ui_mode,
});
return {
client_secret: checkoutSession.client_secret,
url: checkoutSession.url,
};
}
export async function createPaymentIntent(
data: any,
): Promise<{ client_secret: string }> {
const params = {
amount: formatAmountForStripe(
Number(data['amount'] as string),
'eur',
),
automatic_payment_methods: { enabled: true },
currency: 'eur',
} as PaymentIntentCreateParams;
// additional params
if (data.sessionId){
params.metadata = {
sessionId : data.sessionId
}
}
const paymentIntent: Stripe.PaymentIntent =
await stripe.paymentIntents.create(params);
return { client_secret: paymentIntent.client_secret as string };
}

View File

@ -15,7 +15,8 @@ import React, { ReactNode } from 'react';
export default function MainLayout({ children, news, experts }: {
children: ReactNode,
news: ReactNode,
experts: ReactNode
experts: ReactNode,
payment: ReactNode
}) {
return (
<>
@ -24,4 +25,4 @@ export default function MainLayout({ children, news, experts }: {
{experts}
</>
);
};
}

View File

@ -12,6 +12,7 @@ import {
import { Details } from '../../../../types/education';
import { BackButton } from '../../../../components/view/BackButton';
import { i18nText } from '../../../../i18nKeys';
import {SchedulerModal} from "../../../../components/Modals/SchedulerModal";
export const metadata: Metadata = {
title: 'Bbuddy - Experts item',
@ -82,7 +83,7 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
</BackButton>
</Suspense>
</div>
<ExpertCard expert={expert} locale={locale} />
<ExpertCard expert={expert} locale={locale} expertId={expertId}/>
<ExpertInformation expert={expert} locale={locale} />
<h2 className="title-h2">{i18nText('expertBackground', locale)}</h2>

View File

@ -42,4 +42,4 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
</ConfigProvider>
</AntdRegistry>
);
};
}

View File

@ -0,0 +1,20 @@
import type { Metadata } from "next";
import {ElementsForm} from "../../../../components/stripe/ElementsForm";
export const metadata: Metadata = {
title: "Payment",
};
export default function PaymentElementPage({
searchParams,
}: {
searchParams?: { payment_intent_client_secret?: string };
}) {
return (
<div className="page-container">
<h1>Pay</h1>
<ElementsForm />
</div>
);
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
import { GeneralTopSection } from '../../../components/Page';
import PaymentElementPage from "./@payment/page";
export const metadata: Metadata = {
title: 'Bbuddy - Take the lead with BB',
description: 'Bbuddy desc Take the lead with BB'
};
export default function BbClientPage({ params: { locale } }: { params: { locale: string } }) {
unstable_setRequestLocale(locale);
const t = useTranslations('BbClient');
return (
<>
<GeneralTopSection
title={t('header')}
mainImage="banner-phone.png"
/>
<div className="main-articles bb-client">
<PaymentElementPage />
</div>
</>
);
};

View File

@ -0,0 +1,19 @@
import type { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "Payment Intent Result",
};
export default function ResultLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="page-container">
<h1>Payment Intent Result</h1>
{children}
</div>
);
}

View File

@ -0,0 +1,27 @@
import type { Stripe } from "stripe";
import { stripe} from "../../../../lib/stripe";
import PrintObject from "../../../../components/stripe/PrintObject";
export default async function ResultPage({
searchParams,
}: {
searchParams: { payment_intent: string };
}) {
if (!searchParams.payment_intent)
throw new Error("Please provide a valid payment_intent (`pi_...`)");
const paymentIntent: Stripe.PaymentIntent =
await stripe.paymentIntents.retrieve(searchParams.payment_intent);
// Тут под идее тыкнуться в бек на тему того - прошла ли оплата. в зависимости от этого показать что все ок или нет
// также стоит расшить ссылкой КУДА переходить после того как показали что все ок.
return (
<>
<h2>Status: {paymentIntent.status}</h2>
<h3>Payment Intent response:</h3>
<PrintObject content={paymentIntent} />
</>
);
}

View File

@ -0,0 +1,66 @@
import type { Stripe } from "stripe";
import { NextResponse } from "next/server";
import { stripe } from "../../../lib/stripe";
export async function POST(req: Request) {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
await (await req.blob()).text(),
req.headers.get("stripe-signature") as string,
process.env.STRIPE_WEBHOOK_SECRET as string,
);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
// On error, log and return the error message.
if (err! instanceof Error) console.log(err);
console.log(`❌ Error message: ${errorMessage}`);
return NextResponse.json(
{ message: `Webhook Error: ${errorMessage}` },
{ status: 400 },
);
}
// Successfully constructed event.
console.log("✅ Success:", event.id);
const permittedEvents: string[] = [
"checkout.session.completed",
"payment_intent.succeeded",
"payment_intent.payment_failed",
];
if (permittedEvents.includes(event.type)) {
let data;
try {
switch (event.type) {
case "checkout.session.completed":
data = event.data.object as Stripe.Checkout.Session;
console.log(`💰 CheckoutSession status: ${data.payment_status}`);
break;
case "payment_intent.payment_failed":
data = event.data.object as Stripe.PaymentIntent;
console.log(`❌ Payment failed: ${data.last_payment_error?.message}`);
break;
case "payment_intent.succeeded":
data = event.data.object as Stripe.PaymentIntent;
console.log(`💰 PaymentIntent status: ${data.status}`);
break;
default:
throw new Error(`Unhandled event: ${event.type}`);
}
} catch (error) {
console.log(error);
return NextResponse.json(
{ message: "Webhook handler failed" },
{ status: 500 },
);
}
}
// Return a response to acknowledge receipt of the event.
return NextResponse.json({ message: "Received" }, { status: 200 });
}

View File

@ -1,19 +1,26 @@
'use client';
import React, { FC } from 'react';
import React, {FC, useEffect, useState} from 'react';
import Image from 'next/image';
import { Tag, Image as AntdImage, Space } from 'antd';
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
import { ExpertScheduler } from '../../types/experts';
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
import { ExpertDocument } from '../../types/file';
import { Locale } from '../../types/locale';
import { CustomRate } from '../view/CustomRate';
import { i18nText } from '../../i18nKeys';
import { FilledYellowButton } from '../view/FilledButton';
import {getSchedulerByExpertId} from "../../actions/experts";
import {useLocalStorage} from "../../hooks/useLocalStorage";
import {AUTH_TOKEN_KEY} from "../../constants/common";
import dayjs from "dayjs";
import {SchedulerModal} from "../Modals/SchedulerModal";
type ExpertDetailsProps = {
expert: ExpertDetails;
locale?: string;
expertId?: string;
};
type ExpertPracticeProps = {
@ -22,8 +29,18 @@ type ExpertPracticeProps = {
locale?: string;
};
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) => {
const { publicCoachDetails } = expert || {};
const [showSchedulerModal, setShowSchedulerModal] = useState<boolean>(false);
const [mode, setMode] = useState<'data' | 'time' | 'pay' | 'finish'>('data');
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0 } } = expert || {};
const onSchedulerHandle = async () => {
console.log('sessionCost', sessionCost);
setMode('data');
setShowSchedulerModal(true)
// отмаппим.
}
return (
<div className="expert-card">
@ -45,7 +62,7 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
</div>
</div>
<div className="expert-card__wrap-btn">
<a href="#" className="btn-apply">
<a href="#" className="btn-apply" onClick={onSchedulerHandle}>
<img src="/images/calendar-outline.svg" className="" alt="" />
{i18nText('schedule', locale)}
</a>
@ -56,6 +73,15 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
</a>
*/}
</div>
<SchedulerModal
open={showSchedulerModal}
handleCancel={() => setShowSchedulerModal(false)}
updateMode={setMode}
mode={mode}
expertId={expertId as string}
locale={locale as string}
sessionCost={sessionCost}
/>
</div>
);
};

View File

@ -114,7 +114,6 @@ export const ExpertsFilter = ({
...getObjectByAdditionalFilter(searchParams)
};
const search = getSearchParamsString(newFilter);
console.log('basePath', basePath);
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);

View File

@ -0,0 +1,290 @@
'use client';
import React, {Dispatch, FC, SetStateAction, useEffect, useState} from 'react';
import { usePathname } from 'next/navigation';
import classNames from 'classnames';
import Link from 'next/link';
import {Modal, Form, Calendar, Radio } from 'antd';
import type { CalendarProps, RadioChangeEvent } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
import dayjs, { Dayjs } from 'dayjs';
import {ExpertDetails, ExpertScheduler, Tags} from "../../types/experts";
import { createStyles } from 'antd-style';
import {useLocalStorage} from "../../hooks/useLocalStorage";
import {AUTH_TOKEN_KEY} from "../../constants/common";
import {getSchedulerByExpertId, getSchedulerSession} from "../../actions/experts";
import {ElementsForm} from "../stripe/ElementsForm";
type SchedulerModalProps = {
open: boolean;
handleCancel: () => void;
mode: 'data' | 'time' | 'pay' | 'finish';
updateMode: (mode: 'data' | 'time' | 'pay' | 'finish') => void;
sessionCost: number;
expertId: string;
locale: string;
};
const useStyle = createStyles(({ token, css, cx }) => {
const lunar = css`
color: ${token.colorTextTertiary};
font-size: ${token.fontSizeSM}px;
`;
return {
wrapper: css`
width: 450px;
border: 1px solid ${token.colorBorderSecondary};
border-radius: ${token.borderRadiusOuter};
padding: 5px;
`,
dateCell: css`
position: relative;
&:before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
max-width: 40px;
max-height: 40px;
background: transparent;
transition: background 300ms;
border-radius: ${token.borderRadiusOuter}px;
border: 1px solid transparent;
box-sizing: border-box;
}
&:hover:before {
background: rgba(0, 0, 0, 0.04);
}
`,
today: css`
&:before {
border: 1px solid ${token.colorPrimary};
}
`,
text: css`
position: relative;
z-index: 1;
`,
lunar,
current: css`
color: ${token.colorTextLightSolid};
&:before {
background: ${token.colorPrimary};
}
&:hover:before {
background: ${token.colorPrimary};
opacity: 0.8;
}
.${cx(lunar)} {
color: ${token.colorTextLightSolid};
opacity: 0.9;
}
`,
monthCell: css`
width: 120px;
color: ${token.colorTextBase};
border-radius: ${token.borderRadiusOuter}px;
padding: 5px 0;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
`,
monthCellCurrent: css`
color: ${token.colorTextLightSolid};
background: ${token.colorPrimary};
&:hover {
background: ${token.colorPrimary};
opacity: 0.8;
}
`,
weekend: css`
color: ${token.colorError};
&.gray {
opacity: 0.4;
}
`,
};
});
export const SchedulerModal: FC<SchedulerModalProps> = ({
open,
handleCancel,
mode,
updateMode,
sessionCost,
locale,
expertId,
}) => {
const { styles } = useStyle({ test: true });
const [selectDate, setSelectDate] = React.useState<Dayjs>(dayjs());
const [dates, setDates] = React.useState<any>();
const [tags, setTags] = React.useState<Tags[]>([]);
const [tag, setTag] = React.useState<number>(-1);
const [slot, setSlot] = React.useState<string>('');
const [sessionId, setSessionId] = React.useState<number>(-1);
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
useEffect( ()=> {
async function loadScheduler(){
const rawScheduler = await getSchedulerByExpertId(expertId as string, locale as string, jwt)
setRawScheduler(rawScheduler)
}
if (open) {
loadScheduler()
}
}, [open])
useEffect(() => {
const map = {} as any
rawScheduler?.availableSlots.forEach((el)=>{
const key = dayjs(el.startTime).format('YYYY-MM-DD');
if (!map[key]){
map[key] = []
}
map[key].push(el)
})
console.log(rawScheduler, map)
setDates(map)
setTags(rawScheduler?.tags)
}, [rawScheduler]);
const onPanelChange = (value: Dayjs, mode: CalendarProps<Dayjs>['mode']) => {
console.log(value.format('YYYY-MM-DD'), mode);
};
const onDateChange: CalendarProps<Dayjs>['onSelect'] = (value, selectInfo) => {
if (selectInfo.source === 'date') {
setSelectDate(value);
updateMode('time')
}
};
const disabledDate = (currentDate: Dayjs) => {
return !dates || !dates[currentDate.format('YYYY-MM-DD')]
}
const handleTimeslot = (e: RadioChangeEvent) => {
setSlot(e.target.value.startTime)
console.log('radio checked', e.target.value);
};
const handleTag = (e: RadioChangeEvent) => {
setTag(e.target.value)
console.log('tag radio checked', e.target.value);
};
const handleSingupSession = async () => {
const data = {coachId: expertId, tagId: tag, startAtUtc: slot, clientComment:''}
console.log(data)
const session = await getSchedulerSession({coachId: expertId, tagId: tag, startAtUtc: slot, clientComment:''}, locale, jwt)
console.log(session);
// тут должна быть проверка все ли с регистрацией сессии
setSessionId(session?.sessionId)
updateMode('pay')
}
const currentDay = dayjs()
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
const isWeekend = date.day() === 6 || date.day() === 0;
return React.cloneElement(info.originNode, {
...info.originNode.props,
className: classNames(styles.dateCell, {
[styles.current]: selectDate.isSame(date, 'date'),
[styles.today]: date.isSame(dayjs(), 'date'),
}),
children: (
<div className={styles.text}>
<span
className={classNames({
[styles.weekend]: isWeekend,
})}
>
{date.get('date')}
</span>
</div>
),
});
}
return (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div>
{tags && (
<Radio.Group name="radiogroupTags" onChange={handleTag}>
{tags?.map((tag)=>(
<Radio key={tag.id} value={tag.id}>{tag.name}</Radio>
)
)}
</Radio.Group>
)
}
</div>
{mode === 'data' && (
<Calendar
fullscreen={false}
onPanelChange={onPanelChange}
fullCellRender={cellRender}
onSelect={onDateChange}
disabledDate={disabledDate}
headerRender={({ value, type, onChange, onTypeChange }) => {
const start = 0;
const end = 12;
const monthOptions = [];
let current = currentDay.clone();
const localeData = value.locale();
const months = [];
for(let i=0; i<6; i++){
const m = current.clone()
months.push(m);
current = current.add(1,'month')
}
return (<>
{months.map((m, i)=>(
<button key={'SchedulerMonth'+i} onClick={()=> onChange(m)}>
{m.month()}
</button>
))}
</>)
}}
/>
)}
{mode === 'time' && (
<>
<div>
<button onClick={()=>{updateMode('data')}}>back</button>
</div>
<Radio.Group name="radiogroupSlots" onChange={handleTimeslot}>
{dates[selectDate.format('YYYY-MM-DD')].map( (el) => {
return (<Radio key={dayjs(el.startTime).format()} value={el}>{dayjs(el.startTime).format('hh-mm')} - {dayjs(el.endTime).format('hh-mm')}</Radio>)
})}
</Radio.Group>
<button onClick={handleSingupSession}>Записаться</button>
</>
)}
{mode === 'pay' && (
<ElementsForm amount={sessionCost}/>
)}
</Modal>
);
};

View File

@ -0,0 +1,195 @@
"use client";
import type { StripeError } from "@stripe/stripe-js";
import * as React from "react";
import {
useStripe,
useElements,
PaymentElement,
Elements,
} from "@stripe/react-stripe-js";
import StripeTestCards from "./StripeTestCards";
import getStripe from "../../utils/get-stripe";
import { createPaymentIntent} from "../../actions/stripe";
import {Form} from "antd";
import {Payment} from "../../types/payment";
import {CustomInput} from "../view/CustomInput";
import {i18nText} from "../../i18nKeys";
import {FC, useEffect} from "react";
import {getPersonalData} from "../../actions/profile";
type PaymentFormProps = {
amount: number,
sessionId?: string
}
export const CheckoutForm: FC<PaymentFormProps> = ({amount, sessionId}) => {
const [input, setInput] = React.useState<{
paySumm: number;
cardholderName: string;
}>({
paySumm: 1,
cardholderName: "",
});
const [form, ] = Form.useForm<Payment>();
const formAmount = Form.useWatch('amount', form);
const [paymentType, setPaymentType] = React.useState<string>("");
const [payment, setPayment] = React.useState<{
status: "initial" | "processing" | "error";
}>({ status: "initial" });
const [errorMessage, setErrorMessage] = React.useState<string>("");
const stripe = useStripe();
const elements = useElements();
const PaymentStatus = ({ status }: { status: string }) => {
switch (status) {
case "processing":
case "requires_payment_method":
case "requires_confirmation":
return <h2>Processing...</h2>;
case "requires_action":
return <h2>Authenticating...</h2>;
case "succeeded":
return <h2>Payment Succeeded 🥳</h2>;
case "error":
return (
<>
<h2>Error 😭</h2>
<p className="error-message">{errorMessage}</p>
</>
);
default:
return null;
}
};
useEffect(() => {
elements?.update({ amount: formAmount * 100 });
}, [formAmount]);
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setInput({
...input,
[e.currentTarget.name]: e.currentTarget.value,
});
};
const onSubmit = async (data) => {
try {
if (!elements || !stripe) return;
setPayment({ status: "processing" });
const { error: submitError } = await elements.submit();
if (submitError) {
setPayment({ status: "error" });
setErrorMessage(submitError.message ?? "An unknown error occurred");
return;
}
// Create a PaymentIntent with the specified amount.
console.log('DATA', data);
const { client_secret: clientSecret } = await createPaymentIntent(
{amount: amount},
);
// Use your card Element with other Stripe.js APIs
const { error: confirmError } = await stripe!.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/ru/payment/result`,
payment_method_data: {
allow_redisplay: 'limited',
billing_details: {
name: input.cardholderName,
},
},
},
});
if (confirmError) {
setPayment({ status: "error" });
setErrorMessage(confirmError.message ?? "An unknown error occurred");
}
} catch (err) {
const { message } = err as StripeError;
setPayment({ status: "error" });
setErrorMessage(message ?? "An unknown error occurred");
}
};
return (
<>
<Form form={form} onFinish={onSubmit}>
<fieldset className="elements-style">
<StripeTestCards/>
<legend>Your payment details:</legend>
{paymentType === "card" ? (
<input
placeholder="Cardholder name"
className="elements-style"
type="Text"
name="cardholderName"
onChange={handleInputChange}
required
/>
) : null}
<div className="FormRow elements-style">
<PaymentElement
onChange={(e) => {
setPaymentType(e.value.type);
}}
/>
</div>
</fieldset>
<button
className="elements-style-background"
type="submit"
disabled={
!["initial", "succeeded", "error"].includes(payment.status) ||
!stripe
}
>
Pay
</button>
</Form>
<PaymentStatus status={payment.status}/>
</>
);
}
export const ElementsForm: FC<PaymentFormProps> = ({amount, sessionId}) => {
return (
<Elements
stripe={getStripe()}
options={{
appearance: {
variables: {
colorIcon: "#6772e5",
fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif",
},
},
currency: 'eur',
mode: "payment",
amount: amount*100,
}}
>
<CheckoutForm amount={amount} sessionId={sessionId}/>
</Elements>
)
}

View File

@ -0,0 +1,10 @@
import type { Stripe } from "stripe";
export default function PrintObject({
content,
}: {
content: Stripe.PaymentIntent | Stripe.Checkout.Session;
}): JSX.Element {
const formattedContent: string = JSON.stringify(content, null, 2);
return <pre>{formattedContent}</pre>;
}

View File

@ -0,0 +1,19 @@
export default function StripeTestCards(): JSX.Element {
return (
<div className="test-card-notice">
Use any of the{" "}
<a
href="https://stripe.com/docs/testing#cards"
target="_blank"
rel="noopener noreferrer"
>
Stripe test cards
</a>{" "}
for demo, e.g.{" "}
<div className="card-number">
4242<span></span>4242<span></span>4242<span></span>4242
</div>
.
</div>
);
}

11
src/lib/stripe.ts Normal file
View File

@ -0,0 +1,11 @@
import "server-only";
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
apiVersion: "2024-06-20",
appInfo: {
name: "bbuddy-ui",
url: "",
},
});

View File

@ -70,3 +70,29 @@ export type ExpertDetails = {
associations?: Association[];
associationLevels?: AssociationLevel[];
};
export type Tags = {
id: number,
groupId: number,
name: string,
couchCount: number,
group: {
id: number,
name: string,
tags: string[];
}
}
export type Slot = {
startTime: string;
endTime: string;
}
export type ExpertScheduler = {
tags: Tags[],
availableSlots: Slot[];
}
export type ExpertSchedulerSession = {
sessionId: string
}

3
src/types/payment.ts Normal file
View File

@ -0,0 +1,3 @@
export type Payment = {
amount: number;
}

15
src/utils/get-stripe.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* This is a singleton to ensure we only instantiate Stripe once.
*/
import { Stripe, loadStripe } from "@stripe/stripe-js";
let stripePromise: Promise<Stripe | null>;
export default function getStripe(): Promise<Stripe | null> {
if (!stripePromise)
stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string,
);
return stripePromise;
}

View File

@ -0,0 +1,30 @@
export function formatAmountForDisplay(
amount: number,
currency: string,
): string {
let numberFormat = new Intl.NumberFormat(["en-US"], {
style: "currency",
currency: currency,
currencyDisplay: "symbol",
});
return numberFormat.format(amount);
}
export function formatAmountForStripe(
amount: number,
currency: string,
): number {
let numberFormat = new Intl.NumberFormat(["en-US"], {
style: "currency",
currency: currency,
currencyDisplay: "symbol",
});
const parts = numberFormat.formatToParts(amount);
let zeroDecimalCurrency: boolean = true;
for (let part of parts) {
if (part.type === "decimal") {
zeroDecimalCurrency = false;
}
}
return zeroDecimalCurrency ? amount : Math.round(amount * 100);
}