diff --git a/packages/server/src/models/PaymentIntegration.ts b/packages/server/src/models/PaymentIntegration.ts index 856e138c5..9f8467816 100644 --- a/packages/server/src/models/PaymentIntegration.ts +++ b/packages/server/src/models/PaymentIntegration.ts @@ -13,11 +13,11 @@ export class PaymentIntegration extends TenantModel { static get jsonSchema() { return { type: 'object', - required: ['name', 'service', 'enable'], + required: ['name', 'service', 'active'], properties: { id: { type: 'integer' }, service: { type: 'string' }, - enable: { type: 'boolean' }, + active: { type: 'boolean' }, accountId: { type: 'string' }, options: { type: 'object' }, createdAt: { type: 'string', format: 'date-time' }, diff --git a/packages/webapp/src/constants/dialogs.ts b/packages/webapp/src/constants/dialogs.ts index 6e53fa1b5..f9e51e735 100644 --- a/packages/webapp/src/constants/dialogs.ts +++ b/packages/webapp/src/constants/dialogs.ts @@ -79,4 +79,6 @@ export enum DialogsName { DisconnectBankAccountConfirmation = 'DisconnectBankAccountConfirmation', SharePaymentLink = 'SharePaymentLink', SelectPaymentMethod = 'SelectPaymentMethodsDialog', + + StripeSetup = 'StripeSetup' } diff --git a/packages/webapp/src/constants/drawers.ts b/packages/webapp/src/constants/drawers.ts index 340df6cbd..4c97ef783 100644 --- a/packages/webapp/src/constants/drawers.ts +++ b/packages/webapp/src/constants/drawers.ts @@ -32,5 +32,6 @@ export enum DRAWERS { CREDIT_NOTE_CUSTOMIZE = 'CREDIT_NOTE_CUSTOMIZE', PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE', BRANDING_TEMPLATES = 'BRANDING_TEMPLATES', - PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW' + PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW', + STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT' } diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx index 8f2bfedf1..378c602e1 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx @@ -1,9 +1,12 @@ -import React, { createContext, ReactNode, useContext } from 'react'; -import { useGetPaymentServicesState } from '@/hooks/query/payment-services'; +import { createContext, ReactNode, useContext } from 'react'; +import { + GetPaymentServicesStateResponse, + useGetPaymentServicesState, +} from '@/hooks/query/payment-services'; type PaymentMethodsContextType = { isPaymentMethodsStateLoading: boolean; - paymentMethodsState: any; + paymentMethodsState: GetPaymentServicesStateResponse | undefined; }; const PaymentMethodsContext = createContext( diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx index 654c21a62..d46b28b95 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx @@ -1,9 +1,18 @@ // @ts-nocheck import styled from 'styled-components'; import { Button, Classes, Intent, Text } from '@blueprintjs/core'; -import { Box, Card, Group, Stack } from '@/components'; +import { AppToaster, Box, Card, Group, Stack } from '@/components'; import { StripeLogo } from '@/icons/StripeLogo'; -import { PaymentMethodsBoot } from './PreferencesPaymentMethodsBoot'; +import { + PaymentMethodsBoot, + usePaymentMethodsBoot, +} from './PreferencesPaymentMethodsBoot'; +import { StripePreSetupDialog } from './dialogs/StripePreSetupDialog/StripePreSetupDialog'; +import { DialogsName } from '@/constants/dialogs'; +import { useDialogActions, useDrawerActions } from '@/hooks/state'; +import { useCreateStripeAccountLink } from '@/hooks/query/stripe-integration'; +import { StripeIntegrationEditDrawer } from './drawers/StripeIntegrationEditDrawer'; +import { DRAWERS } from '@/constants/drawers'; export default function PreferencesPaymentMethodsPage() { return ( @@ -18,19 +27,81 @@ export default function PreferencesPaymentMethodsPage() { + + + ); } function StripePaymentMethod() { + const { openDialog } = useDialogActions(); + const { openDrawer } = useDrawerActions(); + const { paymentMethodsState } = usePaymentMethodsBoot(); + const stripeState = paymentMethodsState?.stripe; + + const isAccountCreated = stripeState?.isStripeAccountCreated; + const isAccountActive = stripeState?.isStripePaymentActive; + const stripeAccountId = stripeState?.stripeAccountId; + + const { + mutateAsync: createStripeAccountLink, + isLoading: isCreateStripeLinkLoading, + } = useCreateStripeAccountLink(); + + // Handle Stripe setup button click. + const handleSetUpBtnClick = () => { + openDialog(DialogsName.StripeSetup); + }; + + // Handle complete Stripe setup button click. + const handleCompleteSetUpBtnClick = () => { + createStripeAccountLink({ stripeAccountId }) + .then((res) => { + const { clientSecret } = res; + + if (clientSecret.url) { + window.open(clientSecret.url, '_blank'); + } + }) + .catch(() => { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); + }); + }; + + const handleEditBtnClick = () => { + openDrawer(DRAWERS.STRIPE_PAYMENT_INTEGRATION_EDIT); + }; + return ( - - + + {!isAccountCreated && ( + + )} + {isAccountCreated && !isAccountActive && ( + + )} diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialog.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialog.tsx new file mode 100644 index 000000000..41eb0409c --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialog.tsx @@ -0,0 +1,33 @@ +// @ts-nocheck +import React from 'react'; +import { Dialog, DialogSuspense } from '@/components'; +import { compose } from '@/utils'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { StripePreSetupDialogContent } from './StripePreSetupDialogContent'; + +/** + * Select payment methods dialogs. + */ +function StripePreSetupDialogRoot({ dialogName, payload, isOpen }) { + return ( + + + + + + ); +} + +export const StripePreSetupDialog = compose(withDialogRedux())( + StripePreSetupDialogRoot, +); + +StripePreSetupDialogRoot.displayName = 'StripePreSetupDialog'; diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx new file mode 100644 index 000000000..bd64ff525 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx @@ -0,0 +1,55 @@ +import { + useCreateStripeAccount, + useCreateStripeAccountLink, +} from '@/hooks/query/stripe-integration'; +import { Button, DialogBody, DialogFooter, Intent } from '@blueprintjs/core'; + +export function StripePreSetupDialogContent() { + const { + mutateAsync: createStripeAccount, + isLoading: isCreateStripeAccountLoading, + } = useCreateStripeAccount(); + + const { + mutateAsync: createStripeAccountLink, + isLoading: isCreateStripeLinkLoading, + } = useCreateStripeAccountLink(); + + const handleSetUpBtnClick = () => { + createStripeAccount({}) + .then((response) => { + const { account_id: accountId } = response; + + return createStripeAccountLink({ stripeAccountId: accountId }); + }) + .then((res) => { + const { clientSecret } = res; + + if (clientSecret.url) { + window.location.href = clientSecret.url; + } + }); + }; + + const isLoading = isCreateStripeAccountLoading || isCreateStripeLinkLoading; + + return ( + <> + + + + + + Set It Up + + } + > + + ); +} diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx new file mode 100644 index 000000000..a9e3aaa1d --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useContext } from 'react'; +import { Spinner } from '@blueprintjs/core'; +import { useAccounts } from '@/hooks/query'; + +interface StripeIntegrationEditContextType { + accounts: any; + isAccountsLoading: boolean; +} + +const StripeIntegrationEditContext = + createContext( + {} as StripeIntegrationEditContextType, + ); + +export const useStripeIntegrationEditBoot = () => { + const context = useContext( + StripeIntegrationEditContext, + ); + + if (!context) { + throw new Error( + 'useStripeIntegrationEditContext must be used within a StripeIntegrationEditProvider', + ); + } + return context; +}; + +export const StripeIntegrationEditBoot: React.FC = ({ children }) => { + const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {}); + const value = { accounts, isAccountsLoading }; + const isLoading = isAccountsLoading; + + if (isLoading) { + return ; + } + return ( + + {children} + + ); +}; diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditContent.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditContent.tsx new file mode 100644 index 000000000..54eeb1841 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditContent.tsx @@ -0,0 +1,28 @@ +// @ts-nocheck +import { Classes } from '@blueprintjs/core'; +import { DrawerBody, DrawerHeaderContent } from '@/components'; +import { StripeIntegrationEditForm } from './StripeIntegrationEditForm'; +import { StripeIntegrationEditBoot } from './StripeIntegrationEditBoot'; +import { + StripeIntegrationEditFormContent, + StripeIntegrationEditFormFooter, +} from './StripeIntegrationEditFormContent'; + +export function StripeIntegrationEditContent() { + return ( + + + + + + + + +
+ +
+
+
+
+ ); +} diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditDrawer.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditDrawer.tsx new file mode 100644 index 000000000..6a31e4e65 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditDrawer.tsx @@ -0,0 +1,35 @@ +// @ts-nocheck +import React from 'react'; +import * as R from 'ramda'; +import { Drawer, DrawerSuspense } from '@/components'; +import withDrawers from '@/containers/Drawer/withDrawers'; + +const StripeIntegrationEditContent = React.lazy(() => + import('./StripeIntegrationEditContent').then((module) => ({ + default: module.StripeIntegrationEditContent, + })), +); + +/** + * Stripe integration edit drawer. + * @returns {React.ReactNode} + */ +function StripeIntegrationEditDrawerRoot({ + name, + + // #withDrawer + isOpen, + payload, +}) { + return ( + + + + + + ); +} + +export const StripeIntegrationEditDrawer = R.compose(withDrawers())( + StripeIntegrationEditDrawerRoot, +); diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditForm.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditForm.tsx new file mode 100644 index 000000000..7b4e1fc88 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditForm.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import * as Yup from 'yup'; +import { Formik, FormikHelpers } from 'formik'; + +interface StripeIntegrationFormValues { + paymentAccountId: string; + clearingAccountId: string; +} + +const initialValues = { + paymentAccountId: '', + clearingAccountId: '', +}; + +const validationSchema = Yup.object().shape({ + paymentAccountId: Yup.string().required('Payment Account is required'), + clearingAccountId: Yup.string().required('Clearing Account is required'), +}); + +interface StripeIntegrationEditFormProps { + children: React.ReactNode; +} + +export function StripeIntegrationEditForm({ + children, +}: StripeIntegrationEditFormProps) { + const onSubmit = ( + values: StripeIntegrationFormValues, + { setSubmitting }: FormikHelpers, + ) => { + }; + + return ( + + <>{children} + + ); +} diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx new file mode 100644 index 000000000..396a54250 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx @@ -0,0 +1,66 @@ +import { AccountsSelect, FFormGroup, Group, Stack } from '@/components'; +import { useStripeIntegrationEditBoot } from './StripeIntegrationEditBoot'; +import { Button, Intent } from '@blueprintjs/core'; +import { useFormikContext } from 'formik'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; +import { useDrawerActions } from '@/hooks/state'; + +export function StripeIntegrationEditFormContent() { + const { accounts } = useStripeIntegrationEditBoot(); + + return ( + + + + + + + + + + ); +} + +export function StripeIntegrationEditFormFooter() { + const { name } = useDrawerContext(); + const { closeDrawer } = useDrawerActions(); + const { submitForm } = useFormikContext(); + + const handleSubmitBtnClick = () => { + submitForm(); + }; + const handleCancelBtnClick = () => { + closeDrawer(name); + }; + + return ( + <> + + + + + + ); +} diff --git a/packages/webapp/src/hooks/query/payment-services.ts b/packages/webapp/src/hooks/query/payment-services.ts index 2cb3b0b4c..e9893b5de 100644 --- a/packages/webapp/src/hooks/query/payment-services.ts +++ b/packages/webapp/src/hooks/query/payment-services.ts @@ -33,7 +33,16 @@ export const useGetPaymentServices = ( ); }; -export interface GetPaymentServicesStateResponse {} +export interface GetPaymentServicesStateResponse { + stripe: { + isStripeAccountCreated: boolean; + isStripePaymentActive: boolean; + stripeAccountId: string; + stripeCurrencies: string[]; + stripePublishableKey: string; + stripeRedirectUrl: string; + }; +} /** * Retrieves the state of payment services. * @param {UseQueryOptions} options @@ -52,7 +61,7 @@ export const useGetPaymentServicesState = ( .then( (response) => transformToCamelCase( - response.data?.paymentServicesState, + response.data?.data, ) as GetPaymentServicesStateResponse, ), {