From 7756b5b3042a588d6c554dfd333accc602fe841f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 21 Sep 2024 16:50:22 +0200 Subject: [PATCH] feat: Stripe payment integration --- .../PaymentServicesController.ts | 76 ++++++++++- .../StripeIntegrationController.ts | 26 ++-- .../StripeWebhooksController.ts | 4 +- .../20240915155403_payment_integration.js | 2 +- .../server/src/interfaces/StripePayment.ts | 3 +- .../DeletePaymentMethodService.ts | 54 ++++++++ .../EditPaymentMethodService.ts | 60 +++++++++ .../PaymentServices/GetPaymentMethodsState.ts | 46 +++++++ .../PaymentServicesApplication.ts | 61 ++++++++- .../src/services/PaymentServices/types.ts | 25 ++++ .../StripePayment/CreateStripeAccountLink.ts | 16 +++ .../CreateStripeAccountService.ts | 2 +- .../StripePayment/StripePaymentApplication.ts | 17 +++ .../StripePayment/StripePaymentService.ts | 27 +++- packages/server/src/subscribers/events.ts | 8 ++ .../PaymentPortal/PaymentPortal.tsx | 4 +- .../Integrations/IntegrationsPage.tsx | 4 +- .../PreferencesPaymentMethodsBoot.tsx | 40 ++++++ .../PreferencesPaymentMethodsPage.tsx | 19 +-- .../StripePayment/StripeIntegration.tsx | 122 ++++++++++-------- .../webapp/src/hooks/query/payment-methods.ts | 69 ++++++++++ .../src/hooks/query/payment-services.ts | 29 ++++- .../src/hooks/query/stripe-integration.ts | 47 +++++++ packages/webapp/src/icons/ArrowBottomLeft.tsx | 32 ++--- 24 files changed, 691 insertions(+), 102 deletions(-) create mode 100644 packages/server/src/services/PaymentServices/DeletePaymentMethodService.ts create mode 100644 packages/server/src/services/PaymentServices/EditPaymentMethodService.ts create mode 100644 packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts create mode 100644 packages/server/src/services/PaymentServices/types.ts create mode 100644 packages/server/src/services/StripePayment/CreateStripeAccountLink.ts create mode 100644 packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx create mode 100644 packages/webapp/src/hooks/query/payment-methods.ts diff --git a/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts b/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts index e51ce634d..52250db51 100644 --- a/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts +++ b/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts @@ -1,5 +1,6 @@ import { Service, Inject } from 'typedi'; import { Request, Response, Router, NextFunction } from 'express'; +import { body, param } from 'express-validator'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import BaseController from '@/api/controllers/BaseController'; import { PaymentServicesApplication } from '@/services/PaymentServices/PaymentServicesApplication'; @@ -19,6 +20,25 @@ export class PaymentServicesController extends BaseController { '/', asyncMiddleware(this.getPaymentServicesSpecificInvoice.bind(this)) ); + router.get('/state', this.getPaymentMethodsState.bind(this)); + router.post( + '/:paymentMethodId', + [ + param('paymentMethodId').exists(), + body('name').optional().isString(), + body('options.bankAccountId').optional().isNumeric(), + body('options.clearningAccountId').optional().isNumeric(), + body('options.showVisa').optional().isBoolean(), + body('options.showMasterCard').optional().isBoolean(), + body('options.showDiscover').optional().isBoolean(), + body('options.showAmer').optional().isBoolean(), + body('options.showJcb').optional().isBoolean(), + body('options.showDiners').optional().isBoolean(), + ], + this.validationResult, + asyncMiddleware(this.updatePaymentMethod.bind(this)) + ); + return router; } @@ -26,7 +46,7 @@ export class PaymentServicesController extends BaseController { * Retrieve accounts types list. * @param {Request} req - Request. * @param {Response} res - Response. - * @return {Response} + * @return {Promise} */ private async getPaymentServicesSpecificInvoice( req: Request<{ invoiceId: number }>, @@ -44,4 +64,58 @@ export class PaymentServicesController extends BaseController { next(error); } } + + /** + * Edits the given payment method settings. + * @param {Request} req - Request. + * @param {Response} res - Response. + * @return {Promise} + */ + private async updatePaymentMethod( + req: Request<{ paymentMethodId: number }>, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { paymentMethodId } = req.params; + const updatePaymentMethodDTO = this.matchedBodyData(req); + + try { + await this.paymentServicesApp.editPaymentMethod( + tenantId, + paymentMethodId, + updatePaymentMethodDTO + ); + return res.status(200).send({ + id: paymentMethodId, + message: 'The given payment method has been updated.', + }); + } catch (error) { + next(error); + } + } + + /** + * Retrieves the payment state providing state. + * @param {Request} req - Request. + * @param {Response} res - Response. + * @param {NextFunction} next - Next function. + * @return {Promise} + */ + private async getPaymentMethodsState( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + + try { + const paymentMethodsState = + await this.paymentServicesApp.getPaymentMethodsState(tenantId); + + return res.status(200).send({ data: paymentMethodsState }); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts index c6f4778cc..4364e537b 100644 --- a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts +++ b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts @@ -1,26 +1,29 @@ import { NextFunction, Request, Response, Router } from 'express'; +import { body } from 'express-validator'; import { Service, Inject } from 'typedi'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication'; +import BaseController from '../BaseController'; @Service() -export class StripeIntegrationController { +export class StripeIntegrationController extends BaseController { @Inject() private stripePaymentApp: StripePaymentApplication; - router() { + public router() { const router = Router(); router.post('/account', asyncMiddleware(this.createAccount.bind(this))); router.post( - '/account_session', - asyncMiddleware(this.createAccountSession.bind(this)) + '/account_link', + [body('stripe_account_id').exists()], + this.validationResult, + asyncMiddleware(this.createAccountLink.bind(this)) ); router.post( '/:linkId/create_checkout_session', this.createCheckoutSession.bind(this) ); - return router; } @@ -65,8 +68,7 @@ export class StripeIntegrationController { const accountId = await this.stripePaymentApp.createStripeAccount( tenantId ); - - res.status(201).json({ + return res.status(201).json({ accountId, message: 'The Stripe account has been created successfully.', }); @@ -82,20 +84,20 @@ export class StripeIntegrationController { * @param {NextFunction} next - The Express next middleware function. * @returns {Promise} */ - public async createAccountSession( + public async createAccountLink( req: Request, res: Response, next: NextFunction ) { const { tenantId } = req; - const { account } = req.body; + const { stripeAccountId } = this.matchedBodyData(req); try { - const clientSecret = await this.stripePaymentApp.createStripeAccount( + const clientSecret = await this.stripePaymentApp.createAccountLink( tenantId, - account + stripeAccountId ); - res.status(200).json({ clientSecret }); + return res.status(200).json({ clientSecret }); } catch (error) { next(error); } diff --git a/packages/server/src/api/controllers/StripeIntegration/StripeWebhooksController.ts b/packages/server/src/api/controllers/StripeIntegration/StripeWebhooksController.ts index 0d9017e27..a9ad582fc 100644 --- a/packages/server/src/api/controllers/StripeIntegration/StripeWebhooksController.ts +++ b/packages/server/src/api/controllers/StripeIntegration/StripeWebhooksController.ts @@ -15,7 +15,7 @@ export class StripeWebhooksController { @Inject() private eventPublisher: EventPublisher; - router() { + public router() { const router = Router(); router.post( @@ -35,7 +35,7 @@ export class StripeWebhooksController { * @param {Response} res - The Express response object. * @param {NextFunction} next - The Express next middleware function. */ - public async handleWebhook(req: Request, res: Response, next: NextFunction) { + private async handleWebhook(req: Request, res: Response, next: NextFunction) { try { let event = req.body; const sig = req.headers['stripe-signature']; diff --git a/packages/server/src/database/migrations/20240915155403_payment_integration.js b/packages/server/src/database/migrations/20240915155403_payment_integration.js index 98cae0081..12db20dd0 100644 --- a/packages/server/src/database/migrations/20240915155403_payment_integration.js +++ b/packages/server/src/database/migrations/20240915155403_payment_integration.js @@ -8,7 +8,7 @@ exports.up = function (knex) { table.string('service'); table.string('name'); table.string('slug'); - table.boolean('enable').defaultTo(true); + table.boolean('active').defaultTo(false); table.string('account_id'); table.json('options'); table.timestamps(); diff --git a/packages/server/src/interfaces/StripePayment.ts b/packages/server/src/interfaces/StripePayment.ts index a2d4ac6b4..9f07c1e5a 100644 --- a/packages/server/src/interfaces/StripePayment.ts +++ b/packages/server/src/interfaces/StripePayment.ts @@ -9,9 +9,8 @@ export interface StripeCheckoutSessionCompletedEventPayload { event: any; } - export interface StripeInvoiceCheckoutSessionPOJO { sessionId: string; publishableKey: string; redirectTo: string; -} \ No newline at end of file +} diff --git a/packages/server/src/services/PaymentServices/DeletePaymentMethodService.ts b/packages/server/src/services/PaymentServices/DeletePaymentMethodService.ts new file mode 100644 index 000000000..9e0802220 --- /dev/null +++ b/packages/server/src/services/PaymentServices/DeletePaymentMethodService.ts @@ -0,0 +1,54 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import HasTenancyService from '../Tenancy/TenancyService'; +import UnitOfWork from '../UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; + +@Service() +export class DeletePaymentMethodService { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + /** + * Deletes the given payment integration. + * @param {number} tenantId + * @param {number} paymentIntegrationId + * @returns {Promise} + */ + public async deletePaymentMethod( + tenantId: number, + paymentIntegrationId: number + ): Promise { + const { PaymentIntegration, TransactionPaymentServiceEntry } = + this.tenancy.models(tenantId); + + const paymentIntegration = await PaymentIntegration.query() + .findById(paymentIntegrationId) + .throwIfNotFound(); + + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Delete payment methods links. + await TransactionPaymentServiceEntry.query(trx) + .where('paymentIntegrationId', paymentIntegrationId) + .delete(); + + // Delete the payment integration. + await PaymentIntegration.query(trx) + .findById(paymentIntegrationId) + .delete(); + + // Triggers `onPaymentMethodDeleted` event. + await this.eventPublisher.emitAsync(events.paymentMethod.onDeleted, { + tenantId, + paymentIntegrationId, + }); + }); + } +} diff --git a/packages/server/src/services/PaymentServices/EditPaymentMethodService.ts b/packages/server/src/services/PaymentServices/EditPaymentMethodService.ts new file mode 100644 index 000000000..75a140c78 --- /dev/null +++ b/packages/server/src/services/PaymentServices/EditPaymentMethodService.ts @@ -0,0 +1,60 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import HasTenancyService from '../Tenancy/TenancyService'; +import UnitOfWork from '../UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { EditPaymentMethodDTO } from './types'; +import events from '@/subscribers/events'; + +@Service() +export class EditPaymentMethodService { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + /** + * Edits the given payment method. + * @param {number} tenantId + * @param {number} paymentIntegrationId + * @param {EditPaymentMethodDTO} editPaymentMethodDTO + * @returns {Promise} + */ + async editPaymentMethod( + tenantId: number, + paymentIntegrationId: number, + editPaymentMethodDTO: EditPaymentMethodDTO + ): Promise { + const { PaymentIntegration } = this.tenancy.models(tenantId); + + const paymentMethod = await PaymentIntegration.query() + .findById(paymentIntegrationId) + .throwIfNotFound(); + + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onPaymentMethodEditing` event. + await this.eventPublisher.emitAsync(events.paymentMethod.onEditing, { + tenantId, + paymentIntegrationId, + editPaymentMethodDTO, + trx, + }); + await PaymentIntegration.query(trx) + .findById(paymentIntegrationId) + .patch({ + ...editPaymentMethodDTO, + }); + // Triggers `onPaymentMethodEdited` event. + await this.eventPublisher.emitAsync(events.paymentMethod.onEdited, { + tenantId, + paymentIntegrationId, + editPaymentMethodDTO, + trx, + }); + }); + } +} diff --git a/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts b/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts new file mode 100644 index 000000000..bb95fadfc --- /dev/null +++ b/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts @@ -0,0 +1,46 @@ +import { Inject, Service } from 'typedi'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { GetPaymentMethodsPOJO } from './types'; +import config from '@/config'; + +@Service() +export class GetPaymentMethodsStateService { + @Inject() + private tenancy: HasTenancyService; + + /** + * Retrieves the payment state provising state. + * @param {number} tenantId + * @returns {Promise} + */ + public async getPaymentMethodsState( + tenantId: number + ): Promise { + const { PaymentIntegration } = this.tenancy.models(tenantId); + + const stripePayment = await PaymentIntegration.query() + .orderBy('createdAt', 'ASC') + .findOne({ + service: 'Stripe', + }); + const isStripeAccountCreated = !!stripePayment; + const isStripePaymentActive = !!(stripePayment?.active || null); + + const stripeAccountId = stripePayment?.accountId || null; + const stripePublishableKey = config.stripePayment.publishableKey; + const stripeCurrencies = ['USD', 'EUR']; + const stripeRedirectUrl = 'https://your-stripe-redirect-url.com'; + + const paymentMethodPOJO: GetPaymentMethodsPOJO = { + stripe: { + isStripeAccountCreated, + isStripePaymentActive, + stripeAccountId, + stripePublishableKey, + stripeCurrencies, + stripeRedirectUrl, + }, + }; + return paymentMethodPOJO; + } +} diff --git a/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts b/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts index 77ee56252..4e453547b 100644 --- a/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts +++ b/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts @@ -1,20 +1,79 @@ import { Service, Inject } from 'typedi'; import { GetPaymentServicesSpecificInvoice } from './GetPaymentServicesSpecificInvoice'; +import { DeletePaymentMethodService } from './DeletePaymentMethodService'; +import { EditPaymentMethodService } from './EditPaymentMethodService'; +import { EditPaymentMethodDTO, GetPaymentMethodsPOJO } from './types'; +import { GetPaymentMethodsStateService } from './GetPaymentMethodsState'; @Service() export class PaymentServicesApplication { @Inject() private getPaymentServicesSpecificInvoice: GetPaymentServicesSpecificInvoice; + @Inject() + private deletePaymentMethodService: DeletePaymentMethodService; + + @Inject() + private editPaymentMethodService: EditPaymentMethodService; + + @Inject() + private getPaymentMethodsStateService: GetPaymentMethodsStateService; + /** * Retrieves the payment services for a specific invoice. * @param {number} tenantId - The ID of the tenant. * @param {number} invoiceId - The ID of the invoice. * @returns {Promise} The payment services for the specified invoice. */ - async getPaymentServicesForInvoice(tenantId: number): Promise { + public async getPaymentServicesForInvoice(tenantId: number): Promise { return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice( tenantId ); } + + /** + * Deletes the given payment method. + * @param {number} tenantId + * @param {number} paymentIntegrationId + * @returns {Promise} + */ + public async deletePaymentMethod( + tenantId: number, + paymentIntegrationId: number + ): Promise { + return this.deletePaymentMethodService.deletePaymentMethod( + tenantId, + paymentIntegrationId + ); + } + + /** + * Edits the given payment method. + * @param {number} tenantId + * @param {number} paymentIntegrationId + * @param {EditPaymentMethodDTO} editPaymentMethodDTO + * @returns {Promise} + */ + public async editPaymentMethod( + tenantId: number, + paymentIntegrationId: number, + editPaymentMethodDTO: EditPaymentMethodDTO + ): Promise { + return this.editPaymentMethodService.editPaymentMethod( + tenantId, + paymentIntegrationId, + editPaymentMethodDTO + ); + } + + /** + * Retrieves the payment state providing state. + * @param {number} tenantId + * @returns {Promise} + */ + public async getPaymentMethodsState( + tenantId: number + ): Promise { + return this.getPaymentMethodsStateService.getPaymentMethodsState(tenantId); + } } diff --git a/packages/server/src/services/PaymentServices/types.ts b/packages/server/src/services/PaymentServices/types.ts new file mode 100644 index 000000000..42d444897 --- /dev/null +++ b/packages/server/src/services/PaymentServices/types.ts @@ -0,0 +1,25 @@ +export interface EditPaymentMethodDTO { + name?: string; + options?: { + bankAccountId?: number; // bank account. + clearningAccountId?: number; // current liability. + + showVisa?: boolean; + showMasterCard?: boolean; + showDiscover?: boolean; + showAmer?: boolean; + showJcb?: boolean; + showDiners?: boolean; + }; +} + +export interface GetPaymentMethodsPOJO { + stripe: { + isStripeAccountCreated: boolean; + isStripePaymentActive: boolean; + stripeAccountId: string | null; + stripePublishableKey: string | null; + stripeCurrencies: Array; + stripeRedirectUrl: string | null; + }; +} diff --git a/packages/server/src/services/StripePayment/CreateStripeAccountLink.ts b/packages/server/src/services/StripePayment/CreateStripeAccountLink.ts new file mode 100644 index 000000000..c28522d27 --- /dev/null +++ b/packages/server/src/services/StripePayment/CreateStripeAccountLink.ts @@ -0,0 +1,16 @@ +import { Service, Inject } from 'typedi'; +import { StripePaymentService } from './StripePaymentService'; + +@Service() +export class CreateStripeAccountLinkService { + @Inject() + private stripePaymentService: StripePaymentService; + + /** + * Creates a new Stripe account id. + * @param {number} tenantId + */ + public createAccountLink(tenantId: number, stripeAccountId: string) { + return this.stripePaymentService.createAccountLink(stripeAccountId); + } +} diff --git a/packages/server/src/services/StripePayment/CreateStripeAccountService.ts b/packages/server/src/services/StripePayment/CreateStripeAccountService.ts index 83b0405f6..19a7b1492 100644 --- a/packages/server/src/services/StripePayment/CreateStripeAccountService.ts +++ b/packages/server/src/services/StripePayment/CreateStripeAccountService.ts @@ -38,7 +38,7 @@ export class CreateStripeAccountService { await PaymentIntegration.query().insert({ name: parsedStripeAccountDTO.name, accountId: stripeAccountId, - enable: false, + active: false, // Active will turn true after onboarding. service: 'Stripe', }); // Triggers `onStripeIntegrationAccountCreated` event. diff --git a/packages/server/src/services/StripePayment/StripePaymentApplication.ts b/packages/server/src/services/StripePayment/StripePaymentApplication.ts index 5955835b2..cf7823879 100644 --- a/packages/server/src/services/StripePayment/StripePaymentApplication.ts +++ b/packages/server/src/services/StripePayment/StripePaymentApplication.ts @@ -2,12 +2,16 @@ import { Inject } from 'typedi'; import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession'; import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; import { CreateStripeAccountService } from './CreateStripeAccountService'; +import { CreateStripeAccountLinkService } from './CreateStripeAccountLink'; import { CreateStripeAccountDTO } from './types'; export class StripePaymentApplication { @Inject() private createStripeAccountService: CreateStripeAccountService; + @Inject() + private createStripeAccountLinkService: CreateStripeAccountLinkService; + @Inject() private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; @@ -26,6 +30,19 @@ export class StripePaymentApplication { ); } + /** + * Creates a new Stripe account link of the given Stripe accoun.. + * @param {number} tenantId + * @param {string} stripeAccountId + * @returns {} + */ + public createAccountLink(tenantId: number, stripeAccountId: string) { + return this.createStripeAccountLinkService.createAccountLink( + tenantId, + stripeAccountId + ); + } + /** * Creates the Stripe checkout session from the given sale invoice. * @param {number} tenantId diff --git a/packages/server/src/services/StripePayment/StripePaymentService.ts b/packages/server/src/services/StripePayment/StripePaymentService.ts index 2a19e1e20..c10fe16c0 100644 --- a/packages/server/src/services/StripePayment/StripePaymentService.ts +++ b/packages/server/src/services/StripePayment/StripePaymentService.ts @@ -2,6 +2,8 @@ import { Service } from 'typedi'; import stripe from 'stripe'; import config from '@/config'; +const origin = 'http://localhost:4000'; + @Service() export class StripePaymentService { public stripe; @@ -13,8 +15,8 @@ export class StripePaymentService { } /** - * - * @param {number} accountId + * + * @param {number} accountId * @returns {Promise} */ public async createAccountSession(accountId: string): Promise { @@ -35,6 +37,27 @@ export class StripePaymentService { /** * + * @param {number} accountId + * @returns + */ + public async createAccountLink(accountId: string) { + try { + const accountLink = await this.stripe.accountLinks.create({ + account: accountId, + return_url: `${origin}/return/${accountId}`, + refresh_url: `${origin}/refresh/${accountId}`, + type: 'account_onboarding', + }); + return accountLink; + } catch (error) { + throw new Error( + 'An error occurred when calling the Stripe API to create an account link:' + ); + } + } + + /** + * * @returns {Promise} */ public async createAccount(): Promise { diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index c29090d87..d0444c425 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -703,6 +703,14 @@ export default { onAssigningDefault: 'onPdfTemplateAssigningDefault', }, + // Payment method. + paymentMethod: { + onEditing: 'onPaymentMethodEditing', + onEdited: 'onPaymentMethodEdited', + + onDeleted: 'onPaymentMethodDeleted', + }, + // Payment methods integrations paymentIntegrationLink: { onPaymentIntegrationLink: 'onPaymentIntegrationLink', diff --git a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx index 87f2fa03e..cd8a7bfb9 100644 --- a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx +++ b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx @@ -78,7 +78,7 @@ export function PaymentPortal() { Total - + {sharableLinkMeta?.totalFormatted} @@ -96,7 +96,7 @@ export function PaymentPortal() { className={clsx(styles.totalItem, styles.borderBottomDark)} > Due Amount - + {sharableLinkMeta?.dueAmountFormatted} diff --git a/packages/webapp/src/containers/Preferences/Integrations/IntegrationsPage.tsx b/packages/webapp/src/containers/Preferences/Integrations/IntegrationsPage.tsx index cc7434825..6b909000a 100644 --- a/packages/webapp/src/containers/Preferences/Integrations/IntegrationsPage.tsx +++ b/packages/webapp/src/containers/Preferences/Integrations/IntegrationsPage.tsx @@ -1,5 +1,5 @@ -import { StripeIntegration } from '@/containers/StripePayment/StripeIntegration'; +import { StripeIntegration2 } from '@/containers/StripePayment/StripeIntegration'; export default function IntegrationsPage() { - return + return } diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx new file mode 100644 index 000000000..8f2bfedf1 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsBoot.tsx @@ -0,0 +1,40 @@ +import React, { createContext, ReactNode, useContext } from 'react'; +import { useGetPaymentServicesState } from '@/hooks/query/payment-services'; + +type PaymentMethodsContextType = { + isPaymentMethodsStateLoading: boolean; + paymentMethodsState: any; +}; + +const PaymentMethodsContext = createContext( + {} as PaymentMethodsContextType, +); + +type PaymentMethodsProviderProps = { + children: ReactNode; +}; + +const PaymentMethodsBoot = ({ children }: PaymentMethodsProviderProps) => { + const { data: paymentMethodsState, isLoading: isPaymentMethodsStateLoading } = + useGetPaymentServicesState(); + + const value = { isPaymentMethodsStateLoading, paymentMethodsState }; + + return ( + + {children} + + ); +}; + +const usePaymentMethodsBoot = () => { + const context = useContext(PaymentMethodsContext); + if (context === undefined) { + throw new Error( + 'usePaymentMethods must be used within a PaymentMethodsProvider', + ); + } + return context; +}; + +export { PaymentMethodsBoot, usePaymentMethodsBoot }; diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx index 180bedda2..654c21a62 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage.tsx @@ -1,20 +1,23 @@ // @ts-nocheck import styled from 'styled-components'; +import { Button, Classes, Intent, Text } from '@blueprintjs/core'; import { Box, Card, Group, Stack } from '@/components'; import { StripeLogo } from '@/icons/StripeLogo'; -import { Button, Classes, Intent, Text } from '@blueprintjs/core'; +import { PaymentMethodsBoot } from './PreferencesPaymentMethodsBoot'; export default function PreferencesPaymentMethodsPage() { return ( - - Accept payments from all the major debit and credit card networks - through the supported payment gateways. - + + + Accept payments from all the major debit and credit card networks + through the supported payment gateways. + - - - + + + + ); } diff --git a/packages/webapp/src/containers/StripePayment/StripeIntegration.tsx b/packages/webapp/src/containers/StripePayment/StripeIntegration.tsx index 520f75442..22d1a3888 100644 --- a/packages/webapp/src/containers/StripePayment/StripeIntegration.tsx +++ b/packages/webapp/src/containers/StripePayment/StripeIntegration.tsx @@ -1,37 +1,17 @@ import React, { useState } from 'react'; import { - ConnectAccountOnboarding, - ConnectComponentsProvider, -} from '@stripe/react-connect-js'; -import { useStripeConnect } from './use-stripe-connect'; -import { useCreateStripeAccount } from '@/hooks/query/stripe-integration'; + useCreateStripeAccount, + useCreateStripeAccountLink, +} from '@/hooks/query/stripe-integration'; -export function StripeIntegration() { - const [accountCreatePending, setAccountCreatePending] = - useState(false); - const [onboardingExited, setOnboardingExited] = useState(false); - const [error, setError] = useState(false); - const [connectedAccountId, setConnectedAccountId] = useState( - null, - ); - const stripeConnectInstance = useStripeConnect(connectedAccountId || ''); - const { mutateAsync: createAccount } = useCreateStripeAccount(); - - const handleSignupBtnClick = () => { - setAccountCreatePending(true); - setError(false); - - createAccount({}) - .then((account) => { - setConnectedAccountId(account.account_id); - }) - .catch(() => { - setError(true); - }) - .finally(() => { - setAccountCreatePending(false); - }); - }; +export const StripeIntegration2 = () => { + const [accountCreatePending, setAccountCreatePending] = useState(false); + const [accountLinkCreatePending, setAccountLinkCreatePending] = + useState(false); + const [error, setError] = useState(false); + const [connectedAccountId, setConnectedAccountId] = useState(); + const { mutateAsync: createStripeAccount } = useCreateStripeAccount(); + const { mutateAsync: createStripeAccountLink } = useCreateStripeAccountLink(); return (
@@ -40,29 +20,70 @@ export function StripeIntegration() {
{!connectedAccountId &&

Get ready for take off

} - {connectedAccountId && !stripeConnectInstance && ( -

Add information to start accepting money

- )} {!connectedAccountId && (

Bigcapital Technology, Inc. is the world's leading air travel platform: join our team of pilots to help people travel faster.

)} - {!accountCreatePending && !connectedAccountId && ( -
- -
+ {connectedAccountId && ( +

Add information to start accepting money

)} - {stripeConnectInstance && ( - - setOnboardingExited(true)} - /> - + {connectedAccountId && ( +

+ Matt's Mats partners with Stripe to help you receive payments and + keep your personal bank and details secure. +

+ )} + {!accountCreatePending && !connectedAccountId && ( + + )} + {connectedAccountId && !accountLinkCreatePending && ( + )} {error &&

Something went wrong!

} - {(connectedAccountId || accountCreatePending || onboardingExited) && ( + {(connectedAccountId || + accountCreatePending || + accountLinkCreatePending) && (
{connectedAccountId && (

@@ -71,17 +92,14 @@ export function StripeIntegration() {

)} {accountCreatePending &&

Creating a connected account...

} - {onboardingExited && ( -

The Account Onboarding component has exited

- )} + {accountLinkCreatePending &&

Creating a new Account Link...

}
)}

- This is a sample app for Connect onboarding using the Account - Onboarding embedded component.{' '} + This is a sample app for Stripe-hosted Connect onboarding.{' '} @@ -92,4 +110,4 @@ export function StripeIntegration() {

); -} +}; diff --git a/packages/webapp/src/hooks/query/payment-methods.ts b/packages/webapp/src/hooks/query/payment-methods.ts new file mode 100644 index 000000000..cb3d7da24 --- /dev/null +++ b/packages/webapp/src/hooks/query/payment-methods.ts @@ -0,0 +1,69 @@ +// @ts-nocheck +import { + useMutation, + UseMutationOptions, + UseMutationResult, +} from 'react-query'; +import useApiRequest from '../useRequest'; + + +// # Delete payment method +// ----------------------------------------- +interface DeletePaymentMethodValues { + paymentMethodId: number; +} +export const useDeletePaymentMethod = ( + options?: UseMutationOptions, +): UseMutationResult => { + const apiRequest = useApiRequest(); + + return useMutation( + ({ paymentMethodId }) => { + return apiRequest + .delete(`/payment-methods/${paymentMethodId}`) + .then((res) => res.data); + }, + { ...options }, + ); +}; + +// # Edit payment method +// ----------------------------------------- +interface EditPaymentMethodValues { + paymentMethodId: number; + name?: string; + bankAccountId?: number; + clearningAccountId?: number; + showVisa?: boolean; + showMasterCard?: boolean; + showDiscover?: boolean; + showAmer?: boolean; + showJcb?: boolean; + showDiners?: boolean; +} +interface EditPaymentMethodResponse { + id: number; + message: string; +} +export const useEditPaymentMethod = ( + options?: UseMutationOptions< + EditPaymentMethodResponse, + Error, + EditPaymentMethodValues + >, +): UseMutationResult< + EditPaymentMethodResponse, + Error, + EditPaymentMethodValues +> => { + const apiRequest = useApiRequest(); + + return useMutation( + ({ paymentMethodId, ...editData }) => { + return apiRequest + .put(`/payment-methods/${paymentMethodId}`, editData) + .then((res) => res.data); + }, + { ...options }, + ); +}; diff --git a/packages/webapp/src/hooks/query/payment-services.ts b/packages/webapp/src/hooks/query/payment-services.ts index 170d0a82f..2cb3b0b4c 100644 --- a/packages/webapp/src/hooks/query/payment-services.ts +++ b/packages/webapp/src/hooks/query/payment-services.ts @@ -6,7 +6,6 @@ import { transformToCamelCase } from '@/utils'; const PaymentServicesQueryKey = 'PaymentServices'; export interface GetPaymentServicesResponse {} - /** * Retrieves the integrated payment services. * @param {UseQueryOptions} options @@ -33,3 +32,31 @@ export const useGetPaymentServices = ( }, ); }; + +export interface GetPaymentServicesStateResponse {} +/** + * Retrieves the state of payment services. + * @param {UseQueryOptions} options + * @returns {UseQueryResult} + */ +export const useGetPaymentServicesState = ( + options?: UseQueryOptions, +): UseQueryResult => { + const apiRequest = useApiRequest(); + + return useQuery( + ['PaymentServicesState'], + () => + apiRequest + .get('/payment-services/state') + .then( + (response) => + transformToCamelCase( + response.data?.paymentServicesState, + ) as GetPaymentServicesStateResponse, + ), + { + ...options, + }, + ); +}; diff --git a/packages/webapp/src/hooks/query/stripe-integration.ts b/packages/webapp/src/hooks/query/stripe-integration.ts index f2619aac0..d141def5b 100644 --- a/packages/webapp/src/hooks/query/stripe-integration.ts +++ b/packages/webapp/src/hooks/query/stripe-integration.ts @@ -7,6 +7,49 @@ import { import useApiRequest from '../useRequest'; import { transformToCamelCase } from '@/utils'; + +// Create Stripe Account Link. +// ------------------------------------ +interface StripeAccountLinkResponse { + clientSecret: { + created: number; + expiresAt: number; + object: string; + url: string; + }; +} +interface StripeAccountLinkValues { + stripeAccountId: string; +} + +export const useCreateStripeAccountLink = ( + options?: UseMutationOptions< + StripeAccountLinkResponse, + Error, + StripeAccountLinkValues + >, +): UseMutationResult< + StripeAccountLinkResponse, + Error, + StripeAccountLinkValues +> => { + const apiRequest = useApiRequest(); + + return useMutation( + (values: StripeAccountLinkValues) => { + return apiRequest + .post('/stripe_integration/account_link', { + stripe_account_id: values?.stripeAccountId, + }) + .then((res) => transformToCamelCase(res.data)); + }, + { ...options }, + ); +}; + + +// Create Stripe Account Session. +// ------------------------------------ interface AccountSessionValues { connectedAccountId?: string; } @@ -40,6 +83,8 @@ export const useCreateStripeAccountSession = ( ); }; +// Create Stripe Account. +// ------------------------------------ interface CreateStripeAccountValues {} interface CreateStripeAccountResponse { account_id: string; @@ -64,6 +109,8 @@ export const useCreateStripeAccount = ( ); }; +// Create Stripe Checkout Session. +// ------------------------------------ interface CreateCheckoutSessionValues { linkId: string; } diff --git a/packages/webapp/src/icons/ArrowBottomLeft.tsx b/packages/webapp/src/icons/ArrowBottomLeft.tsx index 789e5cbb2..494bbb788 100644 --- a/packages/webapp/src/icons/ArrowBottomLeft.tsx +++ b/packages/webapp/src/icons/ArrowBottomLeft.tsx @@ -9,20 +9,22 @@ export const ArrowBottomLeft: React.FC = ({ ...props }) => { return ( - - - + + + + + ); };