feat: Stripe payment integration

This commit is contained in:
Ahmed Bouhuolia
2024-09-21 16:50:22 +02:00
parent 8de8695b25
commit 7756b5b304
24 changed files with 691 additions and 102 deletions

View File

@@ -1,5 +1,6 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Request, Response, Router, NextFunction } from 'express'; import { Request, Response, Router, NextFunction } from 'express';
import { body, param } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { PaymentServicesApplication } from '@/services/PaymentServices/PaymentServicesApplication'; import { PaymentServicesApplication } from '@/services/PaymentServices/PaymentServicesApplication';
@@ -19,6 +20,25 @@ export class PaymentServicesController extends BaseController {
'/', '/',
asyncMiddleware(this.getPaymentServicesSpecificInvoice.bind(this)) 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; return router;
} }
@@ -26,7 +46,7 @@ export class PaymentServicesController extends BaseController {
* Retrieve accounts types list. * Retrieve accounts types list.
* @param {Request} req - Request. * @param {Request} req - Request.
* @param {Response} res - Response. * @param {Response} res - Response.
* @return {Response} * @return {Promise<Response | void>}
*/ */
private async getPaymentServicesSpecificInvoice( private async getPaymentServicesSpecificInvoice(
req: Request<{ invoiceId: number }>, req: Request<{ invoiceId: number }>,
@@ -44,4 +64,58 @@ export class PaymentServicesController extends BaseController {
next(error); next(error);
} }
} }
/**
* Edits the given payment method settings.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Promise<Response | void>}
*/
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<Response | void>}
*/
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);
}
}
} }

View File

@@ -1,26 +1,29 @@
import { NextFunction, Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import { body } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication'; import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication';
import BaseController from '../BaseController';
@Service() @Service()
export class StripeIntegrationController { export class StripeIntegrationController extends BaseController {
@Inject() @Inject()
private stripePaymentApp: StripePaymentApplication; private stripePaymentApp: StripePaymentApplication;
router() { public router() {
const router = Router(); const router = Router();
router.post('/account', asyncMiddleware(this.createAccount.bind(this))); router.post('/account', asyncMiddleware(this.createAccount.bind(this)));
router.post( router.post(
'/account_session', '/account_link',
asyncMiddleware(this.createAccountSession.bind(this)) [body('stripe_account_id').exists()],
this.validationResult,
asyncMiddleware(this.createAccountLink.bind(this))
); );
router.post( router.post(
'/:linkId/create_checkout_session', '/:linkId/create_checkout_session',
this.createCheckoutSession.bind(this) this.createCheckoutSession.bind(this)
); );
return router; return router;
} }
@@ -65,8 +68,7 @@ export class StripeIntegrationController {
const accountId = await this.stripePaymentApp.createStripeAccount( const accountId = await this.stripePaymentApp.createStripeAccount(
tenantId tenantId
); );
return res.status(201).json({
res.status(201).json({
accountId, accountId,
message: 'The Stripe account has been created successfully.', message: 'The Stripe account has been created successfully.',
}); });
@@ -82,20 +84,20 @@ export class StripeIntegrationController {
* @param {NextFunction} next - The Express next middleware function. * @param {NextFunction} next - The Express next middleware function.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async createAccountSession( public async createAccountLink(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId } = req;
const { account } = req.body; const { stripeAccountId } = this.matchedBodyData(req);
try { try {
const clientSecret = await this.stripePaymentApp.createStripeAccount( const clientSecret = await this.stripePaymentApp.createAccountLink(
tenantId, tenantId,
account stripeAccountId
); );
res.status(200).json({ clientSecret }); return res.status(200).json({ clientSecret });
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -15,7 +15,7 @@ export class StripeWebhooksController {
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -35,7 +35,7 @@ export class StripeWebhooksController {
* @param {Response} res - The Express response object. * @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function. * @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 { try {
let event = req.body; let event = req.body;
const sig = req.headers['stripe-signature']; const sig = req.headers['stripe-signature'];

View File

@@ -8,7 +8,7 @@ exports.up = function (knex) {
table.string('service'); table.string('service');
table.string('name'); table.string('name');
table.string('slug'); table.string('slug');
table.boolean('enable').defaultTo(true); table.boolean('active').defaultTo(false);
table.string('account_id'); table.string('account_id');
table.json('options'); table.json('options');
table.timestamps(); table.timestamps();

View File

@@ -9,7 +9,6 @@ export interface StripeCheckoutSessionCompletedEventPayload {
event: any; event: any;
} }
export interface StripeInvoiceCheckoutSessionPOJO { export interface StripeInvoiceCheckoutSessionPOJO {
sessionId: string; sessionId: string;
publishableKey: string; publishableKey: string;

View File

@@ -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<void>}
*/
public async deletePaymentMethod(
tenantId: number,
paymentIntegrationId: number
): Promise<void> {
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,
});
});
}
}

View File

@@ -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<void>}
*/
async editPaymentMethod(
tenantId: number,
paymentIntegrationId: number,
editPaymentMethodDTO: EditPaymentMethodDTO
): Promise<void> {
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,
});
});
}
}

View File

@@ -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<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethodsState(
tenantId: number
): Promise<GetPaymentMethodsPOJO> {
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;
}
}

View File

@@ -1,20 +1,79 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { GetPaymentServicesSpecificInvoice } from './GetPaymentServicesSpecificInvoice'; import { GetPaymentServicesSpecificInvoice } from './GetPaymentServicesSpecificInvoice';
import { DeletePaymentMethodService } from './DeletePaymentMethodService';
import { EditPaymentMethodService } from './EditPaymentMethodService';
import { EditPaymentMethodDTO, GetPaymentMethodsPOJO } from './types';
import { GetPaymentMethodsStateService } from './GetPaymentMethodsState';
@Service() @Service()
export class PaymentServicesApplication { export class PaymentServicesApplication {
@Inject() @Inject()
private getPaymentServicesSpecificInvoice: GetPaymentServicesSpecificInvoice; private getPaymentServicesSpecificInvoice: GetPaymentServicesSpecificInvoice;
@Inject()
private deletePaymentMethodService: DeletePaymentMethodService;
@Inject()
private editPaymentMethodService: EditPaymentMethodService;
@Inject()
private getPaymentMethodsStateService: GetPaymentMethodsStateService;
/** /**
* Retrieves the payment services for a specific invoice. * Retrieves the payment services for a specific invoice.
* @param {number} tenantId - The ID of the tenant. * @param {number} tenantId - The ID of the tenant.
* @param {number} invoiceId - The ID of the invoice. * @param {number} invoiceId - The ID of the invoice.
* @returns {Promise<any>} The payment services for the specified invoice. * @returns {Promise<any>} The payment services for the specified invoice.
*/ */
async getPaymentServicesForInvoice(tenantId: number): Promise<any> { public async getPaymentServicesForInvoice(tenantId: number): Promise<any> {
return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice( return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice(
tenantId tenantId
); );
} }
/**
* Deletes the given payment method.
* @param {number} tenantId
* @param {number} paymentIntegrationId
* @returns {Promise<void>}
*/
public async deletePaymentMethod(
tenantId: number,
paymentIntegrationId: number
): Promise<void> {
return this.deletePaymentMethodService.deletePaymentMethod(
tenantId,
paymentIntegrationId
);
}
/**
* Edits the given payment method.
* @param {number} tenantId
* @param {number} paymentIntegrationId
* @param {EditPaymentMethodDTO} editPaymentMethodDTO
* @returns {Promise<void>}
*/
public async editPaymentMethod(
tenantId: number,
paymentIntegrationId: number,
editPaymentMethodDTO: EditPaymentMethodDTO
): Promise<void> {
return this.editPaymentMethodService.editPaymentMethod(
tenantId,
paymentIntegrationId,
editPaymentMethodDTO
);
}
/**
* Retrieves the payment state providing state.
* @param {number} tenantId
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethodsState(
tenantId: number
): Promise<GetPaymentMethodsPOJO> {
return this.getPaymentMethodsStateService.getPaymentMethodsState(tenantId);
}
} }

View File

@@ -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<string>;
stripeRedirectUrl: string | null;
};
}

View File

@@ -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);
}
}

View File

@@ -38,7 +38,7 @@ export class CreateStripeAccountService {
await PaymentIntegration.query().insert({ await PaymentIntegration.query().insert({
name: parsedStripeAccountDTO.name, name: parsedStripeAccountDTO.name,
accountId: stripeAccountId, accountId: stripeAccountId,
enable: false, active: false, // Active will turn true after onboarding.
service: 'Stripe', service: 'Stripe',
}); });
// Triggers `onStripeIntegrationAccountCreated` event. // Triggers `onStripeIntegrationAccountCreated` event.

View File

@@ -2,12 +2,16 @@ import { Inject } from 'typedi';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession'; import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
import { CreateStripeAccountService } from './CreateStripeAccountService'; import { CreateStripeAccountService } from './CreateStripeAccountService';
import { CreateStripeAccountLinkService } from './CreateStripeAccountLink';
import { CreateStripeAccountDTO } from './types'; import { CreateStripeAccountDTO } from './types';
export class StripePaymentApplication { export class StripePaymentApplication {
@Inject() @Inject()
private createStripeAccountService: CreateStripeAccountService; private createStripeAccountService: CreateStripeAccountService;
@Inject()
private createStripeAccountLinkService: CreateStripeAccountLinkService;
@Inject() @Inject()
private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; 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. * Creates the Stripe checkout session from the given sale invoice.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -2,6 +2,8 @@ import { Service } from 'typedi';
import stripe from 'stripe'; import stripe from 'stripe';
import config from '@/config'; import config from '@/config';
const origin = 'http://localhost:4000';
@Service() @Service()
export class StripePaymentService { export class StripePaymentService {
public stripe; public stripe;
@@ -33,6 +35,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<string>} * @returns {Promise<string>}

View File

@@ -703,6 +703,14 @@ export default {
onAssigningDefault: 'onPdfTemplateAssigningDefault', onAssigningDefault: 'onPdfTemplateAssigningDefault',
}, },
// Payment method.
paymentMethod: {
onEditing: 'onPaymentMethodEditing',
onEdited: 'onPaymentMethodEdited',
onDeleted: 'onPaymentMethodDeleted',
},
// Payment methods integrations // Payment methods integrations
paymentIntegrationLink: { paymentIntegrationLink: {
onPaymentIntegrationLink: 'onPaymentIntegrationLink', onPaymentIntegrationLink: 'onPaymentIntegrationLink',

View File

@@ -78,7 +78,7 @@ export function PaymentPortal() {
<Group position={'apart'} className={styles.totalItem}> <Group position={'apart'} className={styles.totalItem}>
<Text>Total</Text> <Text>Total</Text>
<Text style={{ fontWeight: 600 }}> <Text style={{ fontWeight: 500 }}>
{sharableLinkMeta?.totalFormatted} {sharableLinkMeta?.totalFormatted}
</Text> </Text>
</Group> </Group>
@@ -96,7 +96,7 @@ 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 }}> <Text style={{ fontWeight: 500 }}>
{sharableLinkMeta?.dueAmountFormatted} {sharableLinkMeta?.dueAmountFormatted}
</Text> </Text>
</Group> </Group>

View File

@@ -1,5 +1,5 @@
import { StripeIntegration } from '@/containers/StripePayment/StripeIntegration'; import { StripeIntegration2 } from '@/containers/StripePayment/StripeIntegration';
export default function IntegrationsPage() { export default function IntegrationsPage() {
return <StripeIntegration /> return <StripeIntegration2 />
} }

View File

@@ -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<PaymentMethodsContextType>(
{} as PaymentMethodsContextType,
);
type PaymentMethodsProviderProps = {
children: ReactNode;
};
const PaymentMethodsBoot = ({ children }: PaymentMethodsProviderProps) => {
const { data: paymentMethodsState, isLoading: isPaymentMethodsStateLoading } =
useGetPaymentServicesState();
const value = { isPaymentMethodsStateLoading, paymentMethodsState };
return (
<PaymentMethodsContext.Provider value={value}>
{children}
</PaymentMethodsContext.Provider>
);
};
const usePaymentMethodsBoot = () => {
const context = useContext<PaymentMethodsContextType>(PaymentMethodsContext);
if (context === undefined) {
throw new Error(
'usePaymentMethods must be used within a PaymentMethodsProvider',
);
}
return context;
};
export { PaymentMethodsBoot, usePaymentMethodsBoot };

View File

@@ -1,12 +1,14 @@
// @ts-nocheck // @ts-nocheck
import styled from 'styled-components'; import styled from 'styled-components';
import { Button, Classes, Intent, Text } from '@blueprintjs/core';
import { Box, Card, Group, Stack } from '@/components'; import { Box, Card, Group, Stack } from '@/components';
import { StripeLogo } from '@/icons/StripeLogo'; import { StripeLogo } from '@/icons/StripeLogo';
import { Button, Classes, Intent, Text } from '@blueprintjs/core'; import { PaymentMethodsBoot } from './PreferencesPaymentMethodsBoot';
export default function PreferencesPaymentMethodsPage() { export default function PreferencesPaymentMethodsPage() {
return ( return (
<PaymentMethodsRoot> <PaymentMethodsRoot>
<PaymentMethodsBoot>
<Text className={Classes.TEXT_MUTED} style={{ marginBottom: 20 }}> <Text className={Classes.TEXT_MUTED} style={{ marginBottom: 20 }}>
Accept payments from all the major debit and credit card networks Accept payments from all the major debit and credit card networks
through the supported payment gateways. through the supported payment gateways.
@@ -15,6 +17,7 @@ export default function PreferencesPaymentMethodsPage() {
<Stack> <Stack>
<StripePaymentMethod /> <StripePaymentMethod />
</Stack> </Stack>
</PaymentMethodsBoot>
</PaymentMethodsRoot> </PaymentMethodsRoot>
); );
} }

View File

@@ -1,37 +1,17 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
ConnectAccountOnboarding, useCreateStripeAccount,
ConnectComponentsProvider, useCreateStripeAccountLink,
} from '@stripe/react-connect-js'; } from '@/hooks/query/stripe-integration';
import { useStripeConnect } from './use-stripe-connect';
import { useCreateStripeAccount } from '@/hooks/query/stripe-integration';
export function StripeIntegration() { export const StripeIntegration2 = () => {
const [accountCreatePending, setAccountCreatePending] = const [accountCreatePending, setAccountCreatePending] = useState(false);
useState<boolean>(false); const [accountLinkCreatePending, setAccountLinkCreatePending] =
const [onboardingExited, setOnboardingExited] = useState<boolean>(false); useState(false);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState(false);
const [connectedAccountId, setConnectedAccountId] = useState<string | null>( const [connectedAccountId, setConnectedAccountId] = useState<string>();
null, const { mutateAsync: createStripeAccount } = useCreateStripeAccount();
); const { mutateAsync: createStripeAccountLink } = useCreateStripeAccountLink();
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);
});
};
return ( return (
<div className="container"> <div className="container">
@@ -40,29 +20,70 @@ export function StripeIntegration() {
</div> </div>
<div className="content"> <div className="content">
{!connectedAccountId && <h2>Get ready for take off</h2>} {!connectedAccountId && <h2>Get ready for take off</h2>}
{connectedAccountId && !stripeConnectInstance && (
<h2>Add information to start accepting money</h2>
)}
{!connectedAccountId && ( {!connectedAccountId && (
<p> <p>
Bigcapital Technology, Inc. is the world's leading air travel Bigcapital Technology, Inc. is the world's leading air travel
platform: join our team of pilots to help people travel faster. platform: join our team of pilots to help people travel faster.
</p> </p>
)} )}
{!accountCreatePending && !connectedAccountId && ( {connectedAccountId && (
<div> <h2>Add information to start accepting money</h2>
<button onClick={handleSignupBtnClick}>Sign up</button>
</div>
)} )}
{stripeConnectInstance && ( {connectedAccountId && (
<ConnectComponentsProvider connectInstance={stripeConnectInstance}> <p>
<ConnectAccountOnboarding Matt's Mats partners with Stripe to help you receive payments and
onExit={() => setOnboardingExited(true)} keep your personal bank and details secure.
/> </p>
</ConnectComponentsProvider> )}
{!accountCreatePending && !connectedAccountId && (
<button
onClick={async () => {
setAccountCreatePending(true);
setError(false);
createStripeAccount({}).then((response) => {
const { account_id: accountId } = response;
setAccountCreatePending(false);
if (accountId) {
setConnectedAccountId(accountId);
}
if (error) {
setError(true);
}
});
}}
>
Create an account!
</button>
)}
{connectedAccountId && !accountLinkCreatePending && (
<button
onClick={() => {
setAccountLinkCreatePending(true);
setError(false);
createStripeAccountLink({
stripeAccountId: connectedAccountId,
})
.then((res) => {
const { clientSecret } = res;
setAccountLinkCreatePending(false);
if (clientSecret.url) {
window.location.href = clientSecret.url;
}
})
.catch(() => {
setError(true);
});
}}
>
Add information
</button>
)} )}
{error && <p className="error">Something went wrong!</p>} {error && <p className="error">Something went wrong!</p>}
{(connectedAccountId || accountCreatePending || onboardingExited) && ( {(connectedAccountId ||
accountCreatePending ||
accountLinkCreatePending) && (
<div className="dev-callout"> <div className="dev-callout">
{connectedAccountId && ( {connectedAccountId && (
<p> <p>
@@ -71,17 +92,14 @@ export function StripeIntegration() {
</p> </p>
)} )}
{accountCreatePending && <p>Creating a connected account...</p>} {accountCreatePending && <p>Creating a connected account...</p>}
{onboardingExited && ( {accountLinkCreatePending && <p>Creating a new Account Link...</p>}
<p>The Account Onboarding component has exited</p>
)}
</div> </div>
)} )}
<div className="info-callout"> <div className="info-callout">
<p> <p>
This is a sample app for Connect onboarding using the Account This is a sample app for Stripe-hosted Connect onboarding.{' '}
Onboarding embedded component.{' '}
<a <a
href="https://docs.stripe.com/connect/onboarding/quickstart?connect-onboarding-surface=embedded" href="https://docs.stripe.com/connect/onboarding/quickstart?connect-onboarding-surface=hosted"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@@ -92,4 +110,4 @@ export function StripeIntegration() {
</div> </div>
</div> </div>
); );
} };

View File

@@ -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<void, Error, DeletePaymentMethodValues>,
): UseMutationResult<void, Error, DeletePaymentMethodValues> => {
const apiRequest = useApiRequest();
return useMutation<void, Error, DeletePaymentMethodValues>(
({ 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<EditPaymentMethodResponse, Error, EditPaymentMethodValues>(
({ paymentMethodId, ...editData }) => {
return apiRequest
.put(`/payment-methods/${paymentMethodId}`, editData)
.then((res) => res.data);
},
{ ...options },
);
};

View File

@@ -6,7 +6,6 @@ import { transformToCamelCase } from '@/utils';
const PaymentServicesQueryKey = 'PaymentServices'; const PaymentServicesQueryKey = 'PaymentServices';
export interface GetPaymentServicesResponse {} export interface GetPaymentServicesResponse {}
/** /**
* Retrieves the integrated payment services. * Retrieves the integrated payment services.
* @param {UseQueryOptions<GetPaymentServicesResponse, Error>} options * @param {UseQueryOptions<GetPaymentServicesResponse, Error>} options
@@ -33,3 +32,31 @@ export const useGetPaymentServices = (
}, },
); );
}; };
export interface GetPaymentServicesStateResponse {}
/**
* Retrieves the state of payment services.
* @param {UseQueryOptions<GetPaymentServicesStateResponse, Error>} options
* @returns {UseQueryResult<GetPaymentServicesStateResponse, Error>}
*/
export const useGetPaymentServicesState = (
options?: UseQueryOptions<GetPaymentServicesStateResponse, Error>,
): UseQueryResult<GetPaymentServicesStateResponse, Error> => {
const apiRequest = useApiRequest();
return useQuery<GetPaymentServicesStateResponse, Error>(
['PaymentServicesState'],
() =>
apiRequest
.get('/payment-services/state')
.then(
(response) =>
transformToCamelCase(
response.data?.paymentServicesState,
) as GetPaymentServicesStateResponse,
),
{
...options,
},
);
};

View File

@@ -7,6 +7,49 @@ import {
import useApiRequest from '../useRequest'; import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils'; 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 { interface AccountSessionValues {
connectedAccountId?: string; connectedAccountId?: string;
} }
@@ -40,6 +83,8 @@ export const useCreateStripeAccountSession = (
); );
}; };
// Create Stripe Account.
// ------------------------------------
interface CreateStripeAccountValues {} interface CreateStripeAccountValues {}
interface CreateStripeAccountResponse { interface CreateStripeAccountResponse {
account_id: string; account_id: string;
@@ -64,6 +109,8 @@ export const useCreateStripeAccount = (
); );
}; };
// Create Stripe Checkout Session.
// ------------------------------------
interface CreateCheckoutSessionValues { interface CreateCheckoutSessionValues {
linkId: string; linkId: string;
} }

View File

@@ -9,6 +9,7 @@ export const ArrowBottomLeft: React.FC<ArrowBottomLeftProps> = ({
...props ...props
}) => { }) => {
return ( return (
<span className={'bp4-icon bp4-icon-arrow-bottom-left'}>
<svg <svg
width={size} width={size}
height={size} height={size}
@@ -24,5 +25,6 @@ export const ArrowBottomLeft: React.FC<ArrowBottomLeftProps> = ({
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
</span>
); );
}; };