mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: sharable payment link dialog
This commit is contained in:
@@ -59,7 +59,7 @@ function AppInsider({ history }) {
|
|||||||
children={<EmailConfirmation />}
|
children={<EmailConfirmation />}
|
||||||
/>
|
/>
|
||||||
<Route path={'/auth'} children={<AuthenticationPage />} />
|
<Route path={'/auth'} children={<AuthenticationPage />} />
|
||||||
<Route path={'/payment'} children={<PaymentPortalPage />} />
|
<Route path={'/payment/:linkId'} children={<PaymentPortalPage />} />
|
||||||
<Route path={'/'} children={<DashboardPrivatePages />} />
|
<Route path={'/'} children={<DashboardPrivatePages />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
import '@/style/components/Dialog/Dialog.scss';
|
import '@/style/components/Dialog/Dialog.scss';
|
||||||
|
import { DialogProvider } from './DialogProvider';
|
||||||
|
|
||||||
function DialogComponent(props) {
|
function DialogComponent(props) {
|
||||||
const { name, children, closeDialog, onClose } = props;
|
const { name, children, closeDialog, onClose } = props;
|
||||||
@@ -15,7 +16,7 @@ function DialogComponent(props) {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog {...props} onClose={handleClose}>
|
<Dialog {...props} onClose={handleClose}>
|
||||||
{children}
|
<DialogProvider value={props}>{children}</DialogProvider>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/webapp/src/components/Dialog/DialogProvider.tsx
Normal file
20
packages/webapp/src/components/Dialog/DialogProvider.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { createContext, useContext, ReactNode } from 'react';
|
||||||
|
|
||||||
|
const DialogContext = createContext<any>(null);
|
||||||
|
|
||||||
|
export const useDialogContext = () => {
|
||||||
|
return useContext(DialogContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DialogProviderProps {
|
||||||
|
value: any;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DialogProvider: React.FC<DialogProviderProps> = ({ value, children }) => {
|
||||||
|
return (
|
||||||
|
<DialogContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</DialogContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -52,6 +52,7 @@ import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDi
|
|||||||
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
|
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
|
||||||
import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog';
|
import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog';
|
||||||
import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog';
|
import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog';
|
||||||
|
import { SharePaymentLinkDialog } from '@/containers/PaymentLink/dialogs/SharePaymentLinkDialog/SharePaymentLinkDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialogs container.
|
* Dialogs container.
|
||||||
@@ -151,6 +152,7 @@ export default function DialogsContainer() {
|
|||||||
<DisconnectBankAccountDialog
|
<DisconnectBankAccountDialog
|
||||||
dialogName={DialogsName.DisconnectBankAccountConfirmation}
|
dialogName={DialogsName.DisconnectBankAccountConfirmation}
|
||||||
/>
|
/>
|
||||||
|
<SharePaymentLinkDialog dialogName={DialogsName.SharePaymentLink} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,4 +77,5 @@ export enum DialogsName {
|
|||||||
Export = 'Export',
|
Export = 'Export',
|
||||||
BankRuleForm = 'BankRuleForm',
|
BankRuleForm = 'BankRuleForm',
|
||||||
DisconnectBankAccountConfirmation = 'DisconnectBankAccountConfirmation',
|
DisconnectBankAccountConfirmation = 'DisconnectBankAccountConfirmation',
|
||||||
|
SharePaymentLink = 'SharePaymentLink'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ function InvoiceDetailActionsBar({
|
|||||||
openDialog(DialogsName.InvoiceMail, { invoiceId });
|
openDialog(DialogsName.InvoiceMail, { invoiceId });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShareButtonClick = () => {
|
||||||
|
openDialog(DialogsName.SharePaymentLink, {
|
||||||
|
transactionId: invoiceId,
|
||||||
|
transactionType: 'SaleInvoice',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerActionsBar>
|
<DrawerActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -150,6 +157,11 @@ function InvoiceDetailActionsBar({
|
|||||||
onClick={handleDeleteInvoice}
|
onClick={handleDeleteInvoice}
|
||||||
/>
|
/>
|
||||||
</Can>
|
</Can>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={'Share'}
|
||||||
|
onClick={handleShareButtonClick}
|
||||||
|
/>
|
||||||
<Can I={SaleInvoiceAction.Writeoff} a={AbilitySubject.Invoice}>
|
<Can I={SaleInvoiceAction.Writeoff} a={AbilitySubject.Invoice}>
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
<BadDebtMenuItem
|
<BadDebtMenuItem
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { DialogBody } from '@blueprintjs/core';
|
||||||
|
import { SharePaymentLinkForm } from './SharePaymentLinkForm';
|
||||||
|
import { SharePaymentLinkFormContent } from './SharePaymentLinkFormContent';
|
||||||
|
import { SharePaymentLinkProvider } from './SharePaymentLinkProvider';
|
||||||
|
|
||||||
|
export function SharePaymentLinkContent() {
|
||||||
|
return (
|
||||||
|
<DialogBody>
|
||||||
|
<SharePaymentLinkProvider>
|
||||||
|
<SharePaymentLinkForm>
|
||||||
|
<SharePaymentLinkFormContent />
|
||||||
|
</SharePaymentLinkForm>
|
||||||
|
</SharePaymentLinkProvider>
|
||||||
|
</DialogBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogSuspense } from '@/components';
|
||||||
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const SharePaymentLinkContent = React.lazy(() =>
|
||||||
|
import('./SharePaymentLinkContent').then((module) => ({
|
||||||
|
default: module.SharePaymentLinkContent,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function SharePaymentLinkDialogRoot({ dialogName, payload, isOpen }) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
name={dialogName}
|
||||||
|
isOpen={isOpen}
|
||||||
|
payload={payload}
|
||||||
|
title={'Share Link'}
|
||||||
|
canEscapeJeyClose={true}
|
||||||
|
autoFocus={true}
|
||||||
|
style={{ width: 400 }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<SharePaymentLinkContent />
|
||||||
|
</DialogSuspense>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SharePaymentLinkDialog = compose(withDialogRedux())(
|
||||||
|
SharePaymentLinkDialogRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
SharePaymentLinkDialog.displayName = 'SharePaymentLinkDialog';
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
export const SharePaymentLinkFormSchema = Yup.object().shape({
|
||||||
|
publicity: Yup.string()
|
||||||
|
.oneOf(['private', 'public'], 'Invalid publicity type')
|
||||||
|
.required('Publicity is required'),
|
||||||
|
expiryDate: Yup.date()
|
||||||
|
.nullable()
|
||||||
|
.required('Expiration date is required')
|
||||||
|
.min(new Date(), 'Expiration date must be in the future'),
|
||||||
|
transactionId: Yup.string()
|
||||||
|
.required('Transaction ID is required'),
|
||||||
|
transactionType: Yup.string()
|
||||||
|
.required('Transaction type is required'),
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { Formik, Form, FormikHelpers } from 'formik';
|
||||||
|
import { useCreatePaymentLink } from '@/hooks/query/payment-link';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { SharePaymentLinkFormSchema } from './SharePaymentLinkForm.schema';
|
||||||
|
import { useDialogContext } from '@/components/Dialog/DialogProvider';
|
||||||
|
import { useDialogActions } from '@/hooks/state';
|
||||||
|
import { useSharePaymentLink } from './SharePaymentLinkProvider';
|
||||||
|
|
||||||
|
interface SharePaymentLinkFormProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SharePaymentLinkFormValues {
|
||||||
|
publicity: string;
|
||||||
|
expiryDate: string;
|
||||||
|
transactionId: string;
|
||||||
|
transactionType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
publicity: '',
|
||||||
|
expiryDate: '',
|
||||||
|
transactionId: '',
|
||||||
|
transactionType: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SharePaymentLinkForm = ({
|
||||||
|
children,
|
||||||
|
}: SharePaymentLinkFormProps) => {
|
||||||
|
const { mutateAsync: generateShareLink } = useCreatePaymentLink();
|
||||||
|
const { payload } = useDialogContext();
|
||||||
|
const { setUrl } = useSharePaymentLink();
|
||||||
|
|
||||||
|
const transactionId = payload?.transactionId;
|
||||||
|
const transactionType = payload?.transactionType;
|
||||||
|
|
||||||
|
const formInitialValues = {
|
||||||
|
...initialValues,
|
||||||
|
transactionType,
|
||||||
|
transactionId,
|
||||||
|
};
|
||||||
|
const handleFormSubmit = (
|
||||||
|
values: SharePaymentLinkFormValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<SharePaymentLinkFormValues>,
|
||||||
|
) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
generateShareLink(values)
|
||||||
|
.then((res) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
setUrl(res.link?.link);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Formik<SharePaymentLinkFormValues>
|
||||||
|
initialValues={formInitialValues}
|
||||||
|
validationSchema={SharePaymentLinkFormSchema}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
>
|
||||||
|
<Form>{children}</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
SharePaymentLinkForm.displayName = 'SharePaymentLinkForm';
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
Intent,
|
||||||
|
Position,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import {
|
||||||
|
DialogFooter,
|
||||||
|
FDateInput,
|
||||||
|
FFormGroup,
|
||||||
|
FSelect,
|
||||||
|
Icon,
|
||||||
|
Stack,
|
||||||
|
} from '@/components';
|
||||||
|
import { useSharePaymentLink } from './SharePaymentLinkProvider';
|
||||||
|
import { useClipboard } from '@/hooks/utils/useClipboard';
|
||||||
|
|
||||||
|
export function SharePaymentLinkFormContent() {
|
||||||
|
const { url } = useSharePaymentLink();
|
||||||
|
|
||||||
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
|
const handleCopyBtnClick = () => {
|
||||||
|
clipboard.copy(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack>
|
||||||
|
<FSelect
|
||||||
|
name={'publicity'}
|
||||||
|
items={[
|
||||||
|
{ value: 'private', text: 'Private' },
|
||||||
|
{ value: 'public', text: 'Public' },
|
||||||
|
]}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
<FFormGroup name={'expiryDate'} label={'Expiration Date'} fastField>
|
||||||
|
<FDateInput
|
||||||
|
name={'expiryDate'}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
|
parseDate={(str) => new Date(str)}
|
||||||
|
inputProps={{
|
||||||
|
fill: true,
|
||||||
|
leftElement: <Icon icon={'date-range'} />,
|
||||||
|
}}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{url && (
|
||||||
|
<FormGroup name={'link'} label={'Payment Link'}>
|
||||||
|
<InputGroup
|
||||||
|
name={'link'}
|
||||||
|
value={url}
|
||||||
|
disabled
|
||||||
|
leftElement={
|
||||||
|
<Button onClick={handleCopyBtnClick} minimal>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{url ? (
|
||||||
|
<Button intent={Intent.PRIMARY} onClick={handleCopyBtnClick}>
|
||||||
|
Copy Link
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button type={'submit'} intent={Intent.PRIMARY}>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
<Button>Cancel</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface SharePaymentLinkContextType {
|
||||||
|
url: string;
|
||||||
|
setUrl: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharePaymentLinkContext =
|
||||||
|
createContext<SharePaymentLinkContextType | null>(null);
|
||||||
|
|
||||||
|
interface SharePaymentLinkProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SharePaymentLinkProvider: React.FC<
|
||||||
|
SharePaymentLinkProviderProps
|
||||||
|
> = ({ children }) => {
|
||||||
|
const [url, setUrl] = useState<string>('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SharePaymentLinkContext.Provider value={{ url, setUrl }}>
|
||||||
|
{children}
|
||||||
|
</SharePaymentLinkContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSharePaymentLink = () => {
|
||||||
|
const context = useContext(SharePaymentLinkContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useSharePaymentLink must be used within a SharePaymentLinkProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -2,27 +2,31 @@ import { Text, Classes, Button, Intent } from '@blueprintjs/core';
|
|||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { Box, Group, Stack } from '@/components';
|
import { Box, Group, Stack } from '@/components';
|
||||||
import styles from './PaymentPortal.module.scss';
|
import styles from './PaymentPortal.module.scss';
|
||||||
|
import { usePaymentPortalBoot } from './PaymentPortalBoot';
|
||||||
|
|
||||||
export function PaymentPortal() {
|
export function PaymentPortal() {
|
||||||
|
const { sharableLinkMeta } = usePaymentPortalBoot();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.root}>
|
<Box className={styles.root}>
|
||||||
<Stack className={styles.body}>
|
<Stack className={styles.body}>
|
||||||
<Group spacing={8}>
|
<Group spacing={8}>
|
||||||
<Box className={styles.companyLogoWrap}></Box>
|
<Box className={styles.companyLogoWrap}></Box>
|
||||||
<Text>Bigcapital Technology, Inc.</Text>
|
<Text>{sharableLinkMeta?.companyName}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
<h1 className={styles.bigTitle}>
|
<h1 className={styles.bigTitle}>
|
||||||
Bigcapital Technology, Inc. Sent an Invoice for $1000.00
|
{sharableLinkMeta?.companyName} Sent an Invoice for{' '}
|
||||||
|
{sharableLinkMeta?.totalFormatted}
|
||||||
</h1>
|
</h1>
|
||||||
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
|
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
|
||||||
Invoice due September 13, 2024
|
Invoice due {sharableLinkMeta?.dueDateFormatted}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box className={styles.customerName}>Ahmed Bouhuolia</Box>
|
<Box className={styles.customerName}>{sharableLinkMeta?.customerName}</Box>
|
||||||
<Box>Bigcapital Technology, Inc.</Box>
|
<Box>Bigcapital Technology, Inc.</Box>
|
||||||
<Box>131 Continental Dr Suite 305 Newark,</Box>
|
<Box>131 Continental Dr Suite 305 Newark,</Box>
|
||||||
<Box>Delaware 19713</Box>
|
<Box>Delaware 19713</Box>
|
||||||
@@ -30,7 +34,9 @@ export function PaymentPortal() {
|
|||||||
<Box>ahmed@bigcapital.app</Box>
|
<Box>ahmed@bigcapital.app</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<h2 className={styles.invoiceNumber}>Invoice INV-000001</h2>
|
<h2 className={styles.invoiceNumber}>
|
||||||
|
Invoice {sharableLinkMeta?.invoiceNo}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<Stack spacing={0} className={styles.totals}>
|
<Stack spacing={0} className={styles.totals}>
|
||||||
<Group
|
<Group
|
||||||
@@ -38,12 +44,14 @@ export function PaymentPortal() {
|
|||||||
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
||||||
>
|
>
|
||||||
<Text>Sub Total</Text>
|
<Text>Sub Total</Text>
|
||||||
<Text>11.00</Text>
|
<Text>{sharableLinkMeta?.subtotalFormatted}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group position={'apart'} className={styles.totalItem}>
|
<Group position={'apart'} className={styles.totalItem}>
|
||||||
<Text>Total</Text>
|
<Text>Total</Text>
|
||||||
<Text style={{ fontWeight: 600 }}>11.00</Text>
|
<Text style={{ fontWeight: 600 }}>
|
||||||
|
{sharableLinkMeta?.totalFormatted}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group
|
<Group
|
||||||
@@ -51,7 +59,7 @@ export function PaymentPortal() {
|
|||||||
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
||||||
>
|
>
|
||||||
<Text>Paid Amount (-)</Text>
|
<Text>Paid Amount (-)</Text>
|
||||||
<Text>11.00</Text>
|
<Text>{sharableLinkMeta?.paymentAmountFormatted}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group
|
<Group
|
||||||
@@ -59,7 +67,9 @@ export function PaymentPortal() {
|
|||||||
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
||||||
>
|
>
|
||||||
<Text>Due Amount</Text>
|
<Text>Due Amount</Text>
|
||||||
<Text style={{ fontWeight: 600 }}>11.00</Text>
|
<Text style={{ fontWeight: 600 }}>
|
||||||
|
{sharableLinkMeta?.dueAmountFormatted}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -79,7 +89,7 @@ export function PaymentPortal() {
|
|||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
className={clsx(styles.footerButton, styles.buyButton)}
|
className={clsx(styles.footerButton, styles.buyButton)}
|
||||||
>
|
>
|
||||||
Pay $10,000.00
|
Pay {sharableLinkMeta?.totalFormatted}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,37 @@
|
|||||||
import React, { createContext, useContext, ReactNode } from 'react';
|
import React, { createContext, useContext, ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
GetSharableLinkMetaResponse,
|
||||||
|
useGetSharableLinkMeta,
|
||||||
|
} from '@/hooks/query/payment-link';
|
||||||
|
|
||||||
interface PaymentPortalContextType {
|
interface PaymentPortalContextType {
|
||||||
// Define the context type here
|
sharableLinkMeta: GetSharableLinkMetaResponse | undefined;
|
||||||
paymentAmount: number;
|
isSharableLinkMetaLoading: boolean;
|
||||||
setPaymentAmount: (amount: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaymentPortalContext = createContext<PaymentPortalContextType>(
|
const PaymentPortalContext = createContext<PaymentPortalContextType>(
|
||||||
{} as PaymentPortalContextType,
|
{} as PaymentPortalContextType,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PaymentPortalBoot: React.FC<{ children: ReactNode }> = ({
|
interface PaymentPortalBootProps {
|
||||||
|
linkId: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentPortalBoot: React.FC<PaymentPortalBootProps> = ({
|
||||||
|
linkId,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [paymentAmount, setPaymentAmount] = React.useState<number>(0);
|
const { data: sharableLinkMeta, isLoading: isSharableLinkMetaLoading } =
|
||||||
|
useGetSharableLinkMeta(linkId);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
sharableLinkMeta,
|
||||||
|
isSharableLinkMetaLoading,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaymentPortalContext.Provider value={{ paymentAmount, setPaymentAmount }}>
|
<PaymentPortalContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</PaymentPortalContext.Provider>
|
</PaymentPortalContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { PaymentPortal } from './PaymentPortal';
|
import { PaymentPortal } from './PaymentPortal';
|
||||||
|
import { PaymentPortalBoot } from './PaymentPortalBoot';
|
||||||
|
|
||||||
export default function PaymentPortalPage() {
|
export default function PaymentPortalPage() {
|
||||||
return <PaymentPortal />;
|
const { linkId } = useParams<{ linkId: string}>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentPortalBoot linkId={linkId}>
|
||||||
|
<PaymentPortal />
|
||||||
|
</PaymentPortalBoot>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
94
packages/webapp/src/hooks/query/payment-link.ts
Normal file
94
packages/webapp/src/hooks/query/payment-link.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
UseMutationOptions,
|
||||||
|
UseMutationResult,
|
||||||
|
UseQueryOptions,
|
||||||
|
UseQueryResult,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from 'react-query';
|
||||||
|
import useApiRequest from '../useRequest';
|
||||||
|
import { transformToCamelCase, transfromToSnakeCase } from '@/utils';
|
||||||
|
|
||||||
|
interface CreatePaymentLinkValues {
|
||||||
|
publicity: string;
|
||||||
|
transactionType: string;
|
||||||
|
transactionId: number | string;
|
||||||
|
expiryDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreatePaymentLinkResponse {
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new payment link.
|
||||||
|
* @param {UseMutationOptions<CreatePaymentLinkResponse, Error, CreatePaymentLinkValues>} options
|
||||||
|
* @returns {UseMutationResult<CreatePaymentLinkResponse, Error, CreatePaymentLinkValues>}
|
||||||
|
*/
|
||||||
|
export function useCreatePaymentLink(
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
CreatePaymentLinkResponse,
|
||||||
|
Error,
|
||||||
|
CreatePaymentLinkValues
|
||||||
|
>,
|
||||||
|
): UseMutationResult<
|
||||||
|
CreatePaymentLinkResponse,
|
||||||
|
Error,
|
||||||
|
CreatePaymentLinkValues
|
||||||
|
> {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation<CreatePaymentLinkResponse, Error, CreatePaymentLinkValues>(
|
||||||
|
(values) =>
|
||||||
|
apiRequest
|
||||||
|
.post('/payment-links/generate', transfromToSnakeCase(values))
|
||||||
|
.then((res) => res.data),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSharableLinkMetaResponse {
|
||||||
|
dueAmount: number;
|
||||||
|
dueAmountFormatted: string;
|
||||||
|
dueDate: string;
|
||||||
|
dueDateFormatted: string;
|
||||||
|
invoiceDateFormatted: string;
|
||||||
|
invoiceNo: string;
|
||||||
|
paymentAmount: number;
|
||||||
|
paymentAmountFormatted: string;
|
||||||
|
subtotal: number;
|
||||||
|
subtotalFormatted: string;
|
||||||
|
subtotalLocalFormatted: string;
|
||||||
|
total: number;
|
||||||
|
totalFormatted: string;
|
||||||
|
totalLocalFormatted: string;
|
||||||
|
customerName: string;
|
||||||
|
companyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the sharable link metadata for a given link ID.
|
||||||
|
* @param {string} linkId - The ID of the link to fetch metadata for.
|
||||||
|
* @param {UseQueryOptions<GetSharableLinkMetaResponse, Error>} options - Optional query options.
|
||||||
|
* @returns {UseQueryResult<GetSharableLinkMetaResponse, Error>} The query result.
|
||||||
|
*/
|
||||||
|
export function useGetSharableLinkMeta(
|
||||||
|
linkId: string,
|
||||||
|
options?: UseQueryOptions<GetSharableLinkMetaResponse, Error>,
|
||||||
|
): UseQueryResult<GetSharableLinkMetaResponse, Error> {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetSharableLinkMetaResponse, Error>(
|
||||||
|
['sharable-link-meta', linkId],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/sharable-links/meta/invoice/${linkId}`)
|
||||||
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
32
packages/webapp/src/hooks/utils/useClipboard.ts
Normal file
32
packages/webapp/src/hooks/utils/useClipboard.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function useClipboard({ timeout = 2000 } = {}) {
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copyTimeout, setCopyTimeout] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleCopyResult = (value: boolean) => {
|
||||||
|
window.clearTimeout(copyTimeout!);
|
||||||
|
setCopyTimeout(window.setTimeout(() => setCopied(false), timeout));
|
||||||
|
setCopied(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = (valueToCopy: any) => {
|
||||||
|
if ('clipboard' in navigator) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(valueToCopy)
|
||||||
|
.then(() => handleCopyResult(true))
|
||||||
|
.catch((err) => setError(err));
|
||||||
|
} else {
|
||||||
|
setError(new Error('useClipboard: navigator.clipboard is not supported'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setCopied(false);
|
||||||
|
setError(null);
|
||||||
|
window.clearTimeout(copyTimeout!);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { copy, reset, error, copied };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user