Skip to content

Commit e1308be

Browse files
committed
add: fawry express checkout backend impl.
1 parent ad30580 commit e1308be

File tree

21 files changed

+589
-160
lines changed

21 files changed

+589
-160
lines changed

apps/web/src/components/Checkout/Forms/Card.tsx

Lines changed: 188 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import React, { useCallback, useEffect, useMemo } from "react";
2-
import { Button } from "@litespace/ui/Button";
3-
import AddCard from "@litespace/assets/AddCard";
42
import { useFormatMessage } from "@litespace/ui/hooks/intl";
53
import { useMakeValidators } from "@litespace/ui/hooks/validation";
6-
import { Typography } from "@litespace/ui/Typography";
74
import { useForm } from "@litespace/headless/form";
85
import { validateCvv, validatePhone } from "@litespace/ui/lib/validate";
9-
import { Select, SelectList } from "@litespace/ui/Select";
10-
import { PatternInput } from "@litespace/ui/PatternInput";
6+
import { SelectList } from "@litespace/ui/Select";
117
import {
128
useGetAddCardUrl,
139
useFindCardTokens,
@@ -16,7 +12,7 @@ import {
1612
useCancelUnpaidOrder,
1713
} from "@litespace/headless/fawry";
1814
import { IframeDialog } from "@litespace/ui/IframeDilaog";
19-
import { first, isEmpty } from "lodash";
15+
import { first } from "lodash";
2016
import { useHotkeys } from "react-hotkeys-hook";
2117
import { env } from "@/lib/env";
2218
import { useOnError } from "@/hooks/error";
@@ -26,12 +22,16 @@ import { IframeMessage } from "@/constants/iframe";
2622
import { useLogger } from "@litespace/headless/logger";
2723
import { ConfirmationDialog } from "@litespace/ui/ConfirmationDialog";
2824
import RemoveCard from "@litespace/assets/RemoveCard";
29-
import Lock from "@litespace/assets/Lock";
3025
import { useBlock } from "@litespace/ui/hooks/common";
3126
import { useRender } from "@litespace/headless/common";
3227
import { TxTypeData } from "@/components/Checkout/types";
33-
import { useCreateLessonWithCard } from "@litespace/headless/lessons";
28+
import {
29+
useCreateLessonWithCard,
30+
useLessonCheckoutUrl,
31+
} from "@litespace/headless/lessons";
3432
import { track } from "@/lib/analytics";
33+
import { PLAN_PERIOD_LITERAL_TO_PLAN_PERIOD } from "@litespace/utils";
34+
import { usePlanCheckoutUrl } from "@litespace/headless/plans";
3535

3636
type Form = {
3737
card: string;
@@ -95,6 +95,40 @@ const Payment: React.FC<{
9595
const createLesson = useCreateLessonWithCard({ onError: onPayError });
9696

9797
// ==================== form ====================
98+
const planCheckoutQuery = usePlanCheckoutUrl(
99+
txTypeData.type === "paid-plan"
100+
? {
101+
planId: txTypeData.data.plan?.id || -1,
102+
period: PLAN_PERIOD_LITERAL_TO_PLAN_PERIOD[txTypeData.data.period],
103+
returnUrl: document.location.toString(),
104+
paymentMethod: "CARD",
105+
saveCardInfo: true,
106+
}
107+
: undefined
108+
);
109+
110+
const lessonCheckoutQuery = useLessonCheckoutUrl(
111+
txTypeData.type === "paid-lesson"
112+
? {
113+
duration: txTypeData.data.duration,
114+
tutorId: txTypeData.data.tutor?.id || -1,
115+
slotId: txTypeData.data.slotId,
116+
start: txTypeData.data.start,
117+
returnUrl: document.location.toString(),
118+
paymentMethod: "CARD",
119+
saveCardInfo: true,
120+
}
121+
: undefined
122+
);
123+
124+
const fawryExpressUrl = useMemo(
125+
() =>
126+
txTypeData.type === "paid-plan"
127+
? planCheckoutQuery?.data
128+
: lessonCheckoutQuery?.data,
129+
[planCheckoutQuery?.data, lessonCheckoutQuery?.data, txTypeData.type]
130+
);
131+
98132
const validators = useMakeValidators<Form>({
99133
phone: {
100134
required: true,
@@ -281,146 +315,152 @@ const Payment: React.FC<{
281315

282316
return (
283317
<div>
284-
<form
285-
name="pay-with-card"
286-
onSubmit={(e) => {
287-
e.preventDefault();
288-
form.submit();
289-
}}
290-
className="flex flex-col gap-6 md:gap-4 lg:gap-6"
291-
>
292-
<div className="flex flex-col gap-4">
293-
<Typography tag="p" className="text-caption md:text-body font-medium">
294-
{intl("checkout.payment.description")}
295-
</Typography>
296-
297-
<Select
298-
id="card"
299-
label={intl("checkout.payment.card.card-number")}
300-
className="flex-1"
301-
value={form.state.card}
302-
options={cardOptions}
303-
placeholder={intl("checkout.payment.card.card-number-placeholder")}
304-
valueDir="ltr"
305-
onChange={(value) => {
306-
form.set("card", value);
307-
track("select_card", "checkout");
308-
}}
309-
state={form.errors.card ? "error" : undefined}
310-
helper={form.errors.card}
311-
asButton={isEmpty(cardOptions)}
312-
onTriggerClick={() => {
313-
if (!isEmpty(cardOptions)) return;
314-
addCardDialog.show();
315-
track("add_card", "checkout");
316-
}}
317-
onOpenChange={(open) => {
318-
if (!open || !isEmpty(cardOptions)) return;
319-
addCardDialog.show();
320-
track("add_card", "checkout");
321-
}}
322-
post={
323-
<Button
324-
type="natural"
325-
variant="primary"
326-
size="large"
327-
htmlType="button"
328-
startIcon={<AddCard className="icon" />}
329-
disabled={false}
330-
loading={false}
331-
onClick={() => {
332-
addCardDialog.show();
333-
track("add_card", "checkout");
334-
}}
335-
className="ms-2 lg:ms-4 flex-shrink-0"
336-
>
337-
<Typography
338-
tag="span"
339-
className="text-body font-medium text-natural-700"
340-
>
341-
{intl("checkout.payment.card.add-card")}
342-
</Typography>
343-
</Button>
344-
}
345-
/>
346-
347-
<div className="flex flex-col lg:flex-row gap-4">
348-
<PatternInput
349-
id="cvv"
350-
size={3}
351-
mask=" "
352-
format="###"
353-
idleDir="rtl"
354-
inputSize="large"
355-
label={intl("checkout.payment.card.cvv")}
356-
placeholder={intl("checkout.payment.card.cvv-placeholder")}
357-
state={form.errors.cvv ? "error" : undefined}
358-
helper={form.errors.cvv}
359-
onValueChange={({ value }) => form.set("cvv", value)}
360-
autoComplete="off"
361-
onBlur={() => {
362-
track("enter_cvv", "checkout", form.state.cvv);
363-
}}
364-
/>
365-
<PatternInput
366-
id="phone"
367-
mask=" "
368-
idleDir="ltr"
369-
inputSize="large"
370-
name="phone"
371-
format="### #### ####"
372-
label={intl("checkout.payment.card.phone")}
373-
placeholder={intl("checkout.payment.card.phone-placeholder")}
374-
state={form.errors.phone ? "error" : undefined}
375-
helper={form.errors.phone}
376-
value={form.state.phone}
377-
autoComplete="off"
378-
disabled={!!phone}
379-
onValueChange={({ value }) => form.set("phone", value)}
380-
onBlur={() => {
381-
track("enter_phone", "checkout", form.state.phone);
382-
}}
383-
/>
384-
</div>
385-
</div>
386-
387-
<div>
388-
<Typography
389-
tag="p"
390-
className="hidden md:block text-caption text-brand-700 mb-1"
391-
>
392-
{intl("checkout.payment.conditions-acceptance")}
393-
</Typography>
394-
<Button
395-
type="main"
396-
size="large"
397-
htmlType="submit"
398-
className="w-full"
399-
disabled={pay.isPending || createLesson.isPending}
400-
loading={pay.isPending || createLesson.isPending}
401-
>
402-
<Typography tag="span" className="text text-body font-medium">
403-
{intl("checkout.payment.confirm")}
404-
</Typography>
405-
</Button>
406-
</div>
407-
408-
<Typography tag="p" className="text-tiny font-normal text-natural-800">
409-
{intl("checkout.payment.card.confirmation-code-note")}
410-
</Typography>
411-
412-
<div className="hidden md:flex flex-col gap-2">
413-
<div className="flex gap-2">
414-
<Lock className="w-6 h-6" />
415-
<Typography tag="p" className="text-body font-semibold">
416-
{intl("checkout.payment.safe-and-crypted")}
417-
</Typography>
418-
</div>
419-
<Typography tag="p" className="text-caption text-natural-600">
420-
{intl("checkout.payment.ensure-your-financial-privacy")}
421-
</Typography>
422-
</div>
423-
</form>
318+
{/* <iframe */}
319+
{/* name="iframe" */}
320+
{/* className="h-[400px] w-full sm:rounded-md" */}
321+
{/* src={fawryExpressUrl || ""} */}
322+
{/* /> */}
323+
<a href={fawryExpressUrl || ""}>Fawry Express</a>
324+
{/* <form */}
325+
{/* name="pay-with-card" */}
326+
{/* onSubmit={(e) => { */}
327+
{/* e.preventDefault(); */}
328+
{/* form.submit(); */}
329+
{/* }} */}
330+
{/* className="flex flex-col gap-6 md:gap-4 lg:gap-6" */}
331+
{/* > */}
332+
{/* <div className="flex flex-col gap-4"> */}
333+
{/* <Typography tag="p" className="text-caption md:text-body font-medium"> */}
334+
{/* {intl("checkout.payment.description")} */}
335+
{/* </Typography> */}
336+
{/**/}
337+
{/* <Select */}
338+
{/* id="card" */}
339+
{/* label={intl("checkout.payment.card.card-number")} */}
340+
{/* className="flex-1" */}
341+
{/* value={form.state.card} */}
342+
{/* options={cardOptions} */}
343+
{/* placeholder={intl("checkout.payment.card.card-number-placeholder")} */}
344+
{/* valueDir="ltr" */}
345+
{/* onChange={(value) => { */}
346+
{/* form.set("card", value); */}
347+
{/* track("select_card", "checkout"); */}
348+
{/* }} */}
349+
{/* state={form.errors.card ? "error" : undefined} */}
350+
{/* helper={form.errors.card} */}
351+
{/* asButton={isEmpty(cardOptions)} */}
352+
{/* onTriggerClick={() => { */}
353+
{/* if (!isEmpty(cardOptions)) return; */}
354+
{/* addCardDialog.show(); */}
355+
{/* track("add_card", "checkout"); */}
356+
{/* }} */}
357+
{/* onOpenChange={(open) => { */}
358+
{/* if (!open || !isEmpty(cardOptions)) return; */}
359+
{/* addCardDialog.show(); */}
360+
{/* track("add_card", "checkout"); */}
361+
{/* }} */}
362+
{/* post={ */}
363+
{/* <Button */}
364+
{/* type="natural" */}
365+
{/* variant="primary" */}
366+
{/* size="large" */}
367+
{/* htmlType="button" */}
368+
{/* startIcon={<AddCard className="icon" />} */}
369+
{/* disabled={false} */}
370+
{/* loading={false} */}
371+
{/* onClick={() => { */}
372+
{/* addCardDialog.show(); */}
373+
{/* track("add_card", "checkout"); */}
374+
{/* }} */}
375+
{/* className="ms-2 lg:ms-4 flex-shrink-0" */}
376+
{/* > */}
377+
{/* <Typography */}
378+
{/* tag="span" */}
379+
{/* className="text-body font-medium text-natural-700" */}
380+
{/* > */}
381+
{/* {intl("checkout.payment.card.add-card")} */}
382+
{/* </Typography> */}
383+
{/* </Button> */}
384+
{/* } */}
385+
{/* /> */}
386+
{/**/}
387+
{/* <div className="flex flex-col lg:flex-row gap-4"> */}
388+
{/* <PatternInput */}
389+
{/* id="cvv" */}
390+
{/* size={3} */}
391+
{/* mask=" " */}
392+
{/* format="###" */}
393+
{/* idleDir="rtl" */}
394+
{/* inputSize="large" */}
395+
{/* label={intl("checkout.payment.card.cvv")} */}
396+
{/* placeholder={intl("checkout.payment.card.cvv-placeholder")} */}
397+
{/* state={form.errors.cvv ? "error" : undefined} */}
398+
{/* helper={form.errors.cvv} */}
399+
{/* onValueChange={({ value }) => form.set("cvv", value)} */}
400+
{/* autoComplete="off" */}
401+
{/* onBlur={() => { */}
402+
{/* track("enter_cvv", "checkout", form.state.cvv); */}
403+
{/* }} */}
404+
{/* /> */}
405+
{/* <PatternInput */}
406+
{/* id="phone" */}
407+
{/* mask=" " */}
408+
{/* idleDir="ltr" */}
409+
{/* inputSize="large" */}
410+
{/* name="phone" */}
411+
{/* format="### #### ####" */}
412+
{/* label={intl("checkout.payment.card.phone")} */}
413+
{/* placeholder={intl("checkout.payment.card.phone-placeholder")} */}
414+
{/* state={form.errors.phone ? "error" : undefined} */}
415+
{/* helper={form.errors.phone} */}
416+
{/* value={form.state.phone} */}
417+
{/* autoComplete="off" */}
418+
{/* disabled={!!phone} */}
419+
{/* onValueChange={({ value }) => form.set("phone", value)} */}
420+
{/* onBlur={() => { */}
421+
{/* track("enter_phone", "checkout", form.state.phone); */}
422+
{/* }} */}
423+
{/* /> */}
424+
{/* </div> */}
425+
{/* </div> */}
426+
{/**/}
427+
{/* <div> */}
428+
{/* <Typography */}
429+
{/* tag="p" */}
430+
{/* className="hidden md:block text-caption text-brand-700 mb-1" */}
431+
{/* > */}
432+
{/* {intl("checkout.payment.conditions-acceptance")} */}
433+
{/* </Typography> */}
434+
{/* <Button */}
435+
{/* type="main" */}
436+
{/* size="large" */}
437+
{/* htmlType="submit" */}
438+
{/* className="w-full" */}
439+
{/* disabled={pay.isPending || createLesson.isPending} */}
440+
{/* loading={pay.isPending || createLesson.isPending} */}
441+
{/* > */}
442+
{/* <Typography tag="span" className="text text-body font-medium"> */}
443+
{/* {intl("checkout.payment.confirm")} */}
444+
{/* </Typography> */}
445+
{/* </Button> */}
446+
{/* </div> */}
447+
{/**/}
448+
{/* <Typography tag="p" className="text-tiny font-normal text-natural-800"> */}
449+
{/* {intl("checkout.payment.card.confirmation-code-note")} */}
450+
{/* </Typography> */}
451+
{/**/}
452+
{/* <div className="hidden md:flex flex-col gap-2"> */}
453+
{/* <div className="flex gap-2"> */}
454+
{/* <Lock className="w-6 h-6" /> */}
455+
{/* <Typography tag="p" className="text-body font-semibold"> */}
456+
{/* {intl("checkout.payment.safe-and-crypted")} */}
457+
{/* </Typography> */}
458+
{/* </div> */}
459+
{/* <Typography tag="p" className="text-caption text-natural-600"> */}
460+
{/* {intl("checkout.payment.ensure-your-financial-privacy")} */}
461+
{/* </Typography> */}
462+
{/* </div> */}
463+
{/* </form> */}
424464

425465
<IframeDialog
426466
open={addCardDialog.open}

apps/web/src/components/Checkout/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import zod from "zod";
22
import { Tab, TxTypePayload } from "@/components/Checkout/types";
33
import { UrlQueryOf, Web } from "@litespace/utils/routes";
44
import { nstr } from "@litespace/utils";
5+
import { ITransaction } from "@litespace/types";
56

67
const schema: Zod.Schema<TxTypePayload> = zod.union([
78
zod.object({
@@ -51,3 +52,11 @@ export function asSearchUrlParams(
5152
export function isValidTab(tab: string): tab is Tab {
5253
return tab === "card" || tab === "ewallet" || tab === "fawry";
5354
}
55+
56+
export function parseTxType(
57+
type: "paid-lesson" | "paid-plan"
58+
): ITransaction.Type | null {
59+
if (type === "paid-plan") return ITransaction.Type.PaidPlan;
60+
if (type === "paid-lesson") return ITransaction.Type.PaidLesson;
61+
return null;
62+
}

0 commit comments

Comments
 (0)