Compare commits

..

2 Commits

Author SHA1 Message Date
SD 5712cbcf56 feat: add errors 2024-10-28 15:28:14 +04:00
SD b31d2cf700 feat: add styles for payment 2024-10-26 00:38:30 +04:00
35 changed files with 568 additions and 442 deletions

63
package-lock.json generated
View File

@ -28,6 +28,7 @@
"react": "^18",
"react-dom": "^18",
"react-slick": "^0.29.0",
"react-stripe-js": "^1.1.5",
"slick-carousel": "^1.8.1",
"stripe": "^16.2.0",
"styled-components": "^6.1.1"
@ -5718,6 +5719,68 @@
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-stripe-js": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/react-stripe-js/-/react-stripe-js-1.1.5.tgz",
"integrity": "sha512-4lIucgf/FZj6Uxvf/TH+QQa/Qi3FXigwN/QY6H7naPyoEfw9LOuTzdgPAmm7aeSXj8nZJXVoigiGzzFZchXjew==",
"license": "MIT",
"dependencies": {
"@stripe/react-stripe-js": "1.7.2",
"@stripe/stripe-js": "1.29.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/react-stripe-js/node_modules/@stripe/react-stripe-js": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.7.2.tgz",
"integrity": "sha512-IAVg2nPUPoSwI//XDRCO7D8mGeK4+N3Xg63fYZHmlfEWAuFVcuaqJKTT67uzIdKYZhHZ/NMdZw/ttz+GOjP/rQ==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": "^1.26.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/react-stripe-js/node_modules/@stripe/stripe-js": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.29.0.tgz",
"integrity": "sha512-OsUxk0VLlum8E2d6onlEdKuQcvLMs7qTrOXCnl/BGV3fAm65qr6h3e1IZ5AX4lgUlPRrzRcddSOA5DvkKKYLvg==",
"license": "MIT"
},
"node_modules/react-stripe-js/node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"scheduler": "^0.20.2"
},
"peerDependencies": {
"react": "17.0.2"
}
},
"node_modules/react-stripe-js/node_modules/scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",

View File

@ -29,6 +29,7 @@
"react": "^18",
"react-dom": "^18",
"react-slick": "^0.29.0",
"react-stripe-js": "^1.1.5",
"slick-carousel": "^1.8.1",
"stripe": "^16.2.0",
"styled-components": "^6.1.1"

View File

@ -91,3 +91,11 @@ export const finishSession = (locale: string, token: string, sessionId: number):
locale,
token
});
export const sessionPaymentConfirm = (locale: string, token: string, sessionId: number): Promise<Session> => apiRequest({
url: '/home/session_pay_confirm',
method: 'post',
data: { id: sessionId },
locale,
token
});

View File

@ -1,6 +1,6 @@
"use server";
import {PaymentIntentCreateParams, Stripe} from "stripe";
import { Stripe } from "stripe";
import { headers } from "next/headers";
@ -52,20 +52,19 @@ export async function createCheckoutSession(
}
export async function createPaymentIntent(
data: any,
data: { amount: number, sessionId?: string },
): Promise<{ client_secret: string }> {
const params = {
amount: formatAmountForStripe(
Number(data['amount'] as string),
data.amount,
'eur',
),
automatic_payment_methods: { enabled: true },
currency: 'eur',
} as PaymentIntentCreateParams;
} as Stripe.PaymentIntentCreateParams;
// additional params
if (data.sessionId){
if (data?.sessionId){
params.metadata = {
sessionId : data.sessionId
}
@ -76,3 +75,5 @@ export async function createPaymentIntent(
return { client_secret: paymentIntent.client_secret as string };
}
export const getStripePaymentStatus = async (payment_intent: string): Promise<Stripe.PaymentIntent> => await stripe.paymentIntents.retrieve(payment_intent);

View File

@ -1,9 +1,9 @@
import React from 'react';
import { useTranslations } from 'next-intl';
// import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { getTranslations, unstable_setRequestLocale } from 'next-intl/server';
import { i18nText } from '../../../../i18nKeys';
import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts";
import Link from "next/link";
import { fetchBlogPosts } from '../../../../lib/contentful/blogPosts';
export default async function News({params: {locale}}: { params: { locale: string } }) {
unstable_setRequestLocale(locale);

View File

@ -57,7 +57,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
}
}, [jwt]);
return (
return data ? (
<Loader isLoading={loading}>
<ExpertProfile
isFull={isFull}
@ -66,5 +66,5 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
updateData={setData}
/>
</Loader>
);
) : null;
};

View File

@ -26,7 +26,7 @@ export default function SessionDetailItem({ params: { locale, slug } }: { params
<Suspense fallback={<p>Loading...</p>}>
<SessionDetails
locale={locale}
sessionId={sessionId}
sessionId={sessionId as number}
activeType={sessionType as SessionType}
/>
</Suspense>

View File

@ -3,7 +3,7 @@ import type { Metadata } from 'next';
import { unstable_setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
import { GeneralTopSection } from '../../../components/Page';
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel/index';
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel';
export const metadata: Metadata = {
title: 'Bbuddy - Become a BB expert',

View File

@ -9,7 +9,6 @@ import {CustomPagination} from "../../../components/view/CustomPagination";
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
interface BlogPostPageParams {
slug: string
}

View File

@ -1,20 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,19 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,21 +1,20 @@
'use client';
import React, { FC, useState } from 'react';
import React, { FC, useState, useEffect } from 'react';
import Image from 'next/image';
import { Tag, Image as AntdImage, Space, Button } from 'antd';
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
import { ExpertScheduler } from '../../types/experts';
import { SignupSessionData } 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 {ScheduleModal} from "../Modals/ScheduleModal";
import { getStorageValue } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
import { ScheduleModal } from '../Modals/ScheduleModal';
import { ScheduleModalResult } from '../Modals/ScheduleModalResult';
type ExpertDetailsProps = {
expert: ExpertDetails;
@ -36,9 +35,35 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
const isRus = locale === Locale.ru;
const checkSession = (data?: SignupSessionData) => {
if (data?.startAtUtc && data?.tagId) {
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
sessionStorage?.setItem(SESSION_DATA, JSON.stringify(data));
if (jwt) {
setMode('pay');
} else {
setShowSchedulerModal(false);
const showAuth = new Event('show_auth_enter');
document.dispatchEvent(showAuth);
}
}
}
const handleShowPayForm = () => {
setShowSchedulerModal(true);
setMode('pay');
}
useEffect(() => {
document.addEventListener('show_pay_form', handleShowPayForm);
return () => {
document.removeEventListener('show_pay_form', handleShowPayForm);
};
}, []);
const onSchedulerHandle = () => {
setMode('data');
setShowSchedulerModal(true)
setShowSchedulerModal(true);
};
return (
@ -112,7 +137,9 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
expertId={expertId as string}
locale={locale as string}
sessionCost={sessionCost}
checkSession={checkSession}
/>
<ScheduleModalResult locale={locale as string} />
</>
);
};

View File

@ -2,8 +2,8 @@
import React, {FC, useEffect, useState} from 'react';
import classNames from 'classnames';
import { Modal, Menu, Calendar, Radio, Button, Input, message } from 'antd';
import type { CalendarProps, RadioChangeEvent, MenuProps } from 'antd';
import { Modal, Menu, Calendar, Radio, Button, Input, message, Form } from 'antd';
import type { CalendarProps, MenuProps } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { CloseOutlined } from '@ant-design/icons';
import locale_ru from 'antd/lib/calendar/locale/ru_RU';
@ -12,7 +12,6 @@ import locale_de from 'antd/lib/calendar/locale/de_DE';
import locale_it from 'antd/lib/calendar/locale/it_IT';
import locale_es from 'antd/lib/calendar/locale/es_ES';
import locale_fr from 'antd/lib/calendar/locale/fr_FR';
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/ru';
import 'dayjs/locale/en';
@ -20,14 +19,15 @@ import 'dayjs/locale/de';
import 'dayjs/locale/it';
import 'dayjs/locale/fr';
import 'dayjs/locale/es';
import { ExpertScheduler, SignupSessionData } from "../../types/experts";
import { Tag } from "../../types/tags";
import { useLocalStorage } from "../../hooks/useLocalStorage";
import { AUTH_TOKEN_KEY } from "../../constants/common";
import { getSchedulerByExpertId, getSchedulerSession } from "../../actions/experts";
import { ElementsForm } from "../stripe/ElementsForm";
import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
import { ExpertScheduler, SignupSessionData } from '../../types/experts';
import { Tag } from '../../types/tags';
import { getSchedulerByExpertId, getSchedulerSession } from '../../actions/experts';
import { StripeElementsForm } from '../stripe/StripeElementsForm';
import { i18nText } from '../../i18nKeys';
import { CustomSelect } from '../../components/view/CustomSelect';
import { Loader } from '../view/Loader';
import { getStorageValue } from '../../hooks/useLocalStorage';
type ScheduleModalProps = {
open: boolean;
@ -37,6 +37,7 @@ type ScheduleModalProps = {
sessionCost: number;
expertId: string;
locale: string;
checkSession: (data?: SignupSessionData) => void;
};
type MenuItem = Required<MenuProps>['items'][number];
@ -56,7 +57,7 @@ const getLocale = (locale: string) => {
return locale_es;
default:
return locale_en;
};
}
}
return locale_en;
@ -79,20 +80,43 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
sessionCost,
locale,
expertId,
checkSession,
}) => {
const [selectDate, setSelectDate] = useState<Dayjs>(dayjs());
const [dates, setDates] = useState<any>();
const [dates, setDates] = useState<Record<string, { startTime: string, endTime: string }[]> | undefined>();
const [tags, setTags] = useState<Tag[] | undefined>();
const [sessionData, setSesssionData] = useState<SignupSessionData>({ coachId: +expertId });
const [sessionId, setSessionId] = useState<number>(-1);
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [isPayLoading, setIsPayLoading] = useState<boolean>(false);
const [sessionId, setSessionId] = useState<string>('');
const [form] = Form.useForm<{ clientComment?: string, startAtUtc?: string, tagId?: number }>();
dayjs.locale(locale);
const signupSession = () => {
const data = sessionStorage?.getItem(SESSION_DATA);
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
if (jwt && data) {
const parseData = JSON.parse(data);
setIsPayLoading(true);
getSchedulerSession(parseData as SignupSessionData, locale || 'en', jwt)
.then((session) => {
setSessionId(session?.sessionId);
console.log(session?.sessionId);
})
.catch((err) => {
console.log(err);
message.error('Не удалось провести оплату')
})
.finally(() => {
sessionStorage?.removeItem(SESSION_DATA);
setIsPayLoading(false);
})
}
};
useEffect(()=> {
if (open) {
if (open && mode !== 'pay') {
getSchedulerByExpertId(expertId as string, locale as string)
.then((data) => {
setRawScheduler(data);
@ -101,8 +125,18 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
console.log(err);
});
}
if (!open) {
form.resetFields();
}
}, [open]);
useEffect(() => {
if (open && mode === 'pay') {
signupSession();
}
}, [mode]);
useEffect(() => {
const map = {} as any
rawScheduler?.availableSlots.forEach((el) => {
@ -127,35 +161,6 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')];
const onChangeTimeSlot = (e: RadioChangeEvent) => setSesssionData({ ...sessionData, startAtUtc: e.target.value.startTime });
const onChangeTag = (tagId: number) => setSesssionData({ ...sessionData, tagId });
const singupSession = () => {
console.log(sessionData);
if (sessionData?.startAtUtc && sessionData?.tagId) {
if (jwt) {
setIsPayLoading(true);
getSchedulerSession(sessionData, locale, jwt)
.then((session) => {
console.log(session);
// тут должна быть проверка все ли с регистрацией сессии
setSessionId(+session?.sessionId);
updateMode('pay');
})
.catch((err) => {
console.log(err);
message.error('Не удалось провести оплату')
})
.finally(() => {
setIsPayLoading(false);
})
} else {
}
}
}
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
const isWeekend = date.day() === 6 || date.day() === 0;
return React.cloneElement(info.originNode, {
@ -175,6 +180,13 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
});
};
const onValidate = () => {
form.validateFields()
.then((values) => {
checkSession({ coachId: +expertId, ...values });
})
}
return (
<Modal
className="b-modal"
@ -231,43 +243,69 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
{selectDate.locale(locale).format('DD MMMM YYYY')}
</Button>
</div>
<Form form={form}>
<div className="b-schedule-select-tag">
{tags && (
<Form.Item
name="tagId"
rules={[{
required: true,
message: ''
}]}
>
<CustomSelect
label={i18nText('selectTopic', locale)}
value={sessionData?.tagId}
options={tags?.map(({id, name}) => ({value: id, label: name}))}
onChange={onChangeTag}
/>
</Form.Item>
)}
</div>
<div className="b-schedule-radio-list">
<Radio.Group name="radiogroupSlots" onChange={onChangeTimeSlot}>
{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>)
})}
<Form.Item
name="startAtUtc"
rules={[{
required: true,
message: ''
}]}
>
<Radio.Group>
{dates && dates[selectDate.format('YYYY-MM-DD')].map((el: any) => (
<Radio
key={el.startTime}
value={el.startTime}
>
{dayjs(el.startTime).format('HH:mm')} - {dayjs(el.endTime).format('HH:mm')}
</Radio>)
)}
</Radio.Group>
</Form.Item>
</div>
<div>
<Form.Item name="clientComment">
<Input.TextArea
className="b-textarea"
rows={1}
value={sessionData?.clientComment}
rows={2}
placeholder={i18nText('sessionWishes', locale)}
onChange={(e) => setSesssionData({ ...sessionData, clientComment: e.target.value })}
/>
</div>
</Form.Item>
</Form>
<Button
className="btn-apply"
onClick={singupSession}
loading={isPayLoading}
onClick={onValidate}
>
{i18nText('pay', locale)}
</Button>
</div>
)}
{mode === 'pay' && (
<ElementsForm amount={sessionCost}/>
<div className="b-schedule-payment">
<Loader isLoading={isPayLoading}>
<StripeElementsForm
amount={sessionCost}
locale={locale}
sessionId={sessionId}
/>
</Loader>
</div>
)}
</Modal>
);

View File

@ -0,0 +1,73 @@
'use client'
import React, { useEffect, useState } from 'react';
import { Modal, Result } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { useSearchParams, useRouter } from 'next/navigation';
import { Stripe } from 'stripe';
import { getStripePaymentStatus } from '../../actions/stripe';
import { sessionPaymentConfirm } from '../../actions/sessions';
import { getStorageValue } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../constants/common';
import { Session, SessionState } from '../../types/sessions';
import { i18nText } from '../../i18nKeys';
export const ScheduleModalResult = ({ locale }: { locale: string }) => {
const searchParams = useSearchParams();
const [paymentStatus, setPaymentStatus] = useState<Stripe.PaymentIntent.Status | undefined>();
const [session, setSession] = useState<Session | undefined>();
const [error, setError] = useState<any>();
const router = useRouter();
useEffect(() => {
setError(undefined);
const payment_intent = searchParams.get('payment_intent') || false;
if (payment_intent) {
getStripePaymentStatus(payment_intent)
.then((result) => {
setPaymentStatus(result?.status);
if (result?.status === 'succeeded' && result?.metadata?.sessionId) {
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
sessionPaymentConfirm(locale, jwt, +result.metadata.sessionId)
.then((session) => {
setSession(session);
})
.catch((err: any) => {
setError(err);
});
}
})
.catch((err: any) => {
setError(err);
})
}
}, [searchParams]);
const onClose = () => {
const { origin, pathname } = window?.location || {};
router.push(`${origin}${pathname}`);
setPaymentStatus(undefined);
setSession(undefined);
};
return (
<Modal
className="b-modal"
open={paymentStatus === 'succeeded' && session?.state === SessionState.PAID}
title={undefined}
onOk={undefined}
onCancel={onClose}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-schedule-payment-result">
<Result
status="success"
title={i18nText('successPayment', locale)}
/>
</div>
</Modal>
);
}

View File

@ -23,6 +23,31 @@ function HeaderAuthLinks ({
const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
const [token, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [isPayPath, setIsPayPath] = useState<boolean>(false);
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
setMode(mode);
setIsOpenModal(true);
};
const handleAuthRegister = () => {
setIsPayPath(true);
onOpen('register');
};
const handleAuthEnter = () => {
setIsPayPath(true);
onOpen('enter');
};
useEffect(() => {
document.addEventListener('show_auth_register', handleAuthRegister);
document.addEventListener('show_auth_enter', handleAuthEnter);
return () => {
document.removeEventListener('show_auth_register', handleAuthRegister);
document.removeEventListener('show_auth_enter', handleAuthEnter);
};
}, []);
useEffect(() => {
if (!isOpenModal) {
@ -30,9 +55,16 @@ function HeaderAuthLinks ({
}
}, [isOpenModal]);
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
setMode(mode);
setIsOpenModal(true);
useEffect(() => {
if (token && isPayPath) {
const showPayForm = new Event('show_pay_form');
document.dispatchEvent(showPayForm);
}
}, [token]);
const addNewEvent = (name: 'show_auth_register' | 'show_auth_enter') => {
const evt = new Event(name);
document.dispatchEvent(evt);
};
return token
@ -49,7 +81,7 @@ function HeaderAuthLinks ({
<Button
className="b-header__auth"
type="link"
onClick={() => onOpen('register')}
onClick={() => addNewEvent('show_auth_register')}
>
{i18nText('registration', locale)}
</Button>
@ -61,7 +93,7 @@ function HeaderAuthLinks ({
<Button
className="b-header__auth"
type="link"
onClick={() => onOpen('enter')}
onClick={() => addNewEvent('show_auth_enter')}
>
{i18nText('enter', locale)}
</Button>

View File

@ -1,195 +0,0 @@
"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

@ -1,10 +0,0 @@
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,165 @@
'use client';
import React, { FC, useEffect, useState } from 'react';
import type { StripeError } from '@stripe/stripe-js';
import {
useStripe,
useElements,
PaymentElement,
Elements,
} from '@stripe/react-stripe-js';
import { Form, Button, message } from 'antd';
import getStripe from '../../utils/get-stripe';
import { createPaymentIntent} from '../../actions/stripe';
import { Payment } from '../../types/payment';
import { i18nText } from '../../i18nKeys';
import { WithError } from '../view/WithError';
type PaymentFormProps = {
amount: number,
sessionId?: string,
locale: string
}
type PaymentInfo = 'initial' | 'error' | 'processing' | 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'succeeded';
const PaymentStatus = ({ status }: { status?: PaymentInfo }) => {
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>;
default:
return null;
}
};
export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => {
const [form] = Form.useForm<Payment>();
const formAmount = Form.useWatch('amount', form);
const [paymentType, setPaymentType] = useState<string>('');
const [payment, setPayment] = useState<{
status: PaymentInfo
}>({ status: 'initial' });
const [errorData, setErrorData] = useState<any>();
const stripe = useStripe();
const elements = useElements();
useEffect(() => {
elements?.update({ amount: formAmount * 100 });
}, [formAmount]);
const onSubmit = async () => {
try {
if (!elements || !stripe) return;
setErrorData(undefined);
setPayment({ status: "processing" });
const { error: submitError } = await elements.submit();
if (submitError) {
if (submitError.message) {
message.error(submitError.message);
}
return;
}
const { client_secret: clientSecret } = await createPaymentIntent(
{ amount, sessionId }
);
const { error: confirmError } = await stripe!.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: window.location.href,
payment_method_data: {
allow_redisplay: 'limited',
// billing_details: {
// name: input.cardholderName,
// },
},
},
});
if (confirmError) {
setErrorData({
title: i18nText('errorPayment', locale),
message: confirmError.message ?? 'An unknown error occurred'
});
}
} catch (err) {
const { message } = err as StripeError;
setErrorData({
title: i18nText('errorPayment', locale),
message: message ?? 'An unknown error occurred'
});
}
};
return (
<WithError errorData={errorData}>
<Form form={form} onFinish={onSubmit} style={{ display: 'flex', overflow: 'hidden', flexDirection: 'column', gap: 16, justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ width: '100%' }}>
<PaymentElement
onChange={(e) => {
setPaymentType(e.value.type);
}}
/>
</div>
<div>
<PaymentStatus status={payment.status}/>
</div>
<Button
className="btn-apply"
htmlType="submit"
disabled={
!["initial", "succeeded", "error"].includes(payment.status) ||
!stripe
}
>
{`${i18nText('pay', locale)} ${amount}`}
</Button>
</Form>
</WithError>
);
}
export const StripeElementsForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => {
return (
<Elements
stripe={getStripe()}
options={{
fonts: [{
cssSrc: 'https://fonts.googleapis.com/css2?family=Comfortaa&display=swap',
}],
appearance: {
variables: {
colorIcon: '#2c7873',
fontSizeBase: '16px',
colorPrimary: '#66A5AD',
colorBackground: '#F8F8F7',
colorText: '#000',
colorDanger: '#ff4d4f',
focusBoxShadow: 'none',
borderRadius: '8px'
},
},
currency: 'eur',
mode: "payment",
amount: amount*100,
}}
>
<CheckoutForm amount={amount} sessionId={sessionId} locale={locale} />
</Elements>
);
};

View File

@ -1,19 +0,0 @@
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>
);
}

View File

@ -16,8 +16,8 @@ export const WithError: FC<WithErrorProps> = ({
return (
<Result
status="error"
title="Submission Failed"
subTitle="Please check and modify the following information before resubmitting."
title={errorData?.title}
subTitle={errorData?.message}
extra={refresh ? (
<Button type="primary" onClick={refresh}>
Refresh page

View File

@ -1,6 +1,7 @@
export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api';
export const AUTH_TOKEN_KEY = 'bbuddy_token';
export const AUTH_USER = 'bbuddy_auth_user';
export const SESSION_DATA = 'bbuddy_session_data';
export const DEFAULT_PAGE_SIZE = 5;
export const DEFAULT_PAGE = 1;

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
function getStorageValue (key: string, defaultValue: any) {
export function getStorageValue (key: string, defaultValue: any) {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(key);
return saved || defaultValue;

View File

@ -146,8 +146,10 @@ export default {
saturday: 'Sa',
addNew: 'Neu hinzufügen',
mExperiences: 'Führungserfahrung',
pay: 'Pay',
sessionWishes: 'Write your wishes about the session',
pay: 'Zahlung',
sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung',
successPayment: 'Erfolgreiche Zahlung',
errorPayment: 'Zahlungsfehler',
errors: {
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',

View File

@ -148,6 +148,8 @@ export default {
mExperiences: 'Managerial Experience',
pay: 'Pay',
sessionWishes: 'Write your wishes about the session',
successPayment: 'Successful Payment',
errorPayment: 'Payment Error',
errors: {
invalidEmail: 'The email address is not valid',
emptyEmail: 'Please enter your E-mail',

View File

@ -146,8 +146,10 @@ export default {
saturday: 'S',
addNew: 'Añadir nuevo',
mExperiences: 'Experiencia de dirección',
pay: 'Pay',
sessionWishes: 'Write your wishes about the session',
pay: 'Pago',
sessionWishes: 'Escribe tus deseos sobre la sesión',
successPayment: 'Pago Exitoso',
errorPayment: 'Error de Pago',
errors: {
invalidEmail: 'La dirección de correo electrónico no es válida',
emptyEmail: 'Introduce tu correo electrónico',

View File

@ -146,8 +146,10 @@ export default {
saturday: 'Sa',
addNew: 'Ajouter un nouveau',
mExperiences: 'Expérience en gestion',
pay: 'Pay',
sessionWishes: 'Write your wishes about the session',
pay: 'Paiement',
sessionWishes: 'Écrivez vos souhaits concernant la session',
successPayment: 'Paiement Réussi',
errorPayment: 'Erreur de Paiement',
errors: {
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
emptyEmail: 'Veuillez saisir votre e-mail',

View File

@ -146,8 +146,10 @@ export default {
saturday: 'Sa',
addNew: 'Aggiungi nuovo',
mExperiences: 'Esperienza manageriale',
pay: 'Pay',
sessionWishes: 'Write your wishes about the session',
pay: 'Pagamento',
sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione',
successPayment: 'Pagamento Riuscito',
errorPayment: 'Errore di Pagamento',
errors: {
invalidEmail: 'L\'indirizzo e-mail non è valido',
emptyEmail: 'Inserisci l\'e-mail',

View File

@ -146,8 +146,10 @@ export default {
saturday: 'Сб',
addNew: 'Добавить',
mExperiences: 'Управленческий опыт',
pay: 'Pay',
sessionWishes: 'Write your wishes about the session',
pay: 'Оплата',
sessionWishes: 'Напишите свои пожелания по поводу сессии',
successPayment: 'Успешная оплата',
errorPayment: 'Ошибка оплаты',
errors: {
invalidEmail: 'Адрес электронной почты недействителен',
emptyEmail: 'Пожалуйста, введите ваш E-mail',

View File

@ -0,0 +1,3 @@
.ant-form-item-has-error .ant-radio-inner {
border-color: #ff4d4f !important;
}

View File

@ -23,4 +23,9 @@
gap: 12px;
}
}
&-payment {
padding: 44px 40px;
min-height: 300px;
}
}

View File

@ -3,7 +3,7 @@
height: 54px !important;
.ant-select-selector {
background-color: #F8F8F7 !important;
background-color: transparent !important;
border-color: #F8F8F7 !important;
border-radius: 8px !important;
padding: 22px 16px 8px !important;
@ -17,6 +17,12 @@
}
}
&.ant-select-status-error {
.ant-select-selector {
border-color: #ff4d4f !important;
}
}
.ant-select-selection-overflow-item {
margin-right: 4px;
}
@ -35,6 +41,9 @@
&-wrap {
position: relative;
width: 100%;
background-color: #F8F8F7;
border-radius: 8px;
&.b-multiselect__active .b-multiselect-label {
font-size: 12px;
font-weight: 300;
@ -49,7 +58,7 @@
font-weight: 400;
line-height: 24px;
color: #000;
opacity: .3;
opacity: .4;
position: absolute;
left: 16px;
top: 15px;
@ -70,11 +79,12 @@
height: 54px !important;
.ant-select-selector {
background-color: #F8F8F7 !important;
background-color: transparent !important;
border-color: #F8F8F7 !important;
border-radius: 8px !important;
padding: 22px 16px 8px !important;
box-shadow: none !important;
z-index: 1;
.ant-select-selection-item {
font-size: 15px !important;
@ -84,6 +94,12 @@
}
}
&.ant-select-status-error {
.ant-select-selector {
border-color: #ff4d4f !important;
}
}
.ant-select-arrow {
color: #2c7873 !important;
}
@ -98,6 +114,8 @@
&-wrap {
position: relative;
width: 100%;
background-color: #F8F8F7;
border-radius: 8px;
&.b-select__active .b-select-label {
font-size: 12px;
@ -113,7 +131,7 @@
font-weight: 400;
line-height: 24px;
color: #000;
opacity: .3;
opacity: .4;
position: absolute;
left: 16px;
top: 15px;

View File

@ -11,3 +11,4 @@
@import "_timepicker.scss";
@import "_calendar.scss";
@import "_schedule.scss";
@import "_radio.scss";

View File

@ -1,7 +1,4 @@
/**
* This is a singleton to ensure we only instantiate Stripe once.
*/
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { Stripe, loadStripe } from '@stripe/stripe-js';
let stripePromise: Promise<Stripe | null>;