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 { 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<Response | void>}
*/
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<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 { 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<void>}
*/
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);
}

View File

@@ -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'];

View File

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

View File

@@ -9,9 +9,8 @@ export interface StripeCheckoutSessionCompletedEventPayload {
event: any;
}
export interface StripeInvoiceCheckoutSessionPOJO {
sessionId: string;
publishableKey: string;
redirectTo: 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 { 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<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(
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({
name: parsedStripeAccountDTO.name,
accountId: stripeAccountId,
enable: false,
active: false, // Active will turn true after onboarding.
service: 'Stripe',
});
// Triggers `onStripeIntegrationAccountCreated` event.

View File

@@ -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

View File

@@ -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<string>}
*/
public async createAccountSession(accountId: string): Promise<string> {
@@ -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<string>}
*/
public async createAccount(): Promise<string> {

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { StripeIntegration } from '@/containers/StripePayment/StripeIntegration';
import { StripeIntegration2 } from '@/containers/StripePayment/StripeIntegration';
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,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 (
<PaymentMethodsRoot>
<Text className={Classes.TEXT_MUTED} style={{ marginBottom: 20 }}>
Accept payments from all the major debit and credit card networks
through the supported payment gateways.
</Text>
<PaymentMethodsBoot>
<Text className={Classes.TEXT_MUTED} style={{ marginBottom: 20 }}>
Accept payments from all the major debit and credit card networks
through the supported payment gateways.
</Text>
<Stack>
<StripePaymentMethod />
</Stack>
<Stack>
<StripePaymentMethod />
</Stack>
</PaymentMethodsBoot>
</PaymentMethodsRoot>
);
}

View File

@@ -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<boolean>(false);
const [onboardingExited, setOnboardingExited] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const [connectedAccountId, setConnectedAccountId] = useState<string | null>(
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<string>();
const { mutateAsync: createStripeAccount } = useCreateStripeAccount();
const { mutateAsync: createStripeAccountLink } = useCreateStripeAccountLink();
return (
<div className="container">
@@ -40,29 +20,70 @@ export function StripeIntegration() {
</div>
<div className="content">
{!connectedAccountId && <h2>Get ready for take off</h2>}
{connectedAccountId && !stripeConnectInstance && (
<h2>Add information to start accepting money</h2>
)}
{!connectedAccountId && (
<p>
Bigcapital Technology, Inc. is the world's leading air travel
platform: join our team of pilots to help people travel faster.
</p>
)}
{!accountCreatePending && !connectedAccountId && (
<div>
<button onClick={handleSignupBtnClick}>Sign up</button>
</div>
{connectedAccountId && (
<h2>Add information to start accepting money</h2>
)}
{stripeConnectInstance && (
<ConnectComponentsProvider connectInstance={stripeConnectInstance}>
<ConnectAccountOnboarding
onExit={() => setOnboardingExited(true)}
/>
</ConnectComponentsProvider>
{connectedAccountId && (
<p>
Matt's Mats partners with Stripe to help you receive payments and
keep your personal bank and details secure.
</p>
)}
{!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>}
{(connectedAccountId || accountCreatePending || onboardingExited) && (
{(connectedAccountId ||
accountCreatePending ||
accountLinkCreatePending) && (
<div className="dev-callout">
{connectedAccountId && (
<p>
@@ -71,17 +92,14 @@ export function StripeIntegration() {
</p>
)}
{accountCreatePending && <p>Creating a connected account...</p>}
{onboardingExited && (
<p>The Account Onboarding component has exited</p>
)}
{accountLinkCreatePending && <p>Creating a new Account Link...</p>}
</div>
)}
<div className="info-callout">
<p>
This is a sample app for Connect onboarding using the Account
Onboarding embedded component.{' '}
This is a sample app for Stripe-hosted Connect onboarding.{' '}
<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"
rel="noopener noreferrer"
>
@@ -92,4 +110,4 @@ export function StripeIntegration() {
</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';
export interface GetPaymentServicesResponse {}
/**
* Retrieves the integrated payment services.
* @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 { 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;
}

View File

@@ -9,20 +9,22 @@ export const ArrowBottomLeft: React.FC<ArrowBottomLeftProps> = ({
...props
}) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 3C14 2.45 13.55 2 13 2C12.72 2 12.47 2.11 12.29 2.29L4 10.59V6C4 5.45 3.55 5 3 5S2 5.45 2 6V13C2 13.55 2.45 14 3 14H10C10.55 14 11 13.55 11 13C11 12.45 10.55 12 10 12H5.41L13.7 3.71C13.89 3.53 14 3.28 14 3Z"
fill="currentColor"
/>
</svg>
<span className={'bp4-icon bp4-icon-arrow-bottom-left'}>
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 3C14 2.45 13.55 2 13 2C12.72 2 12.47 2.11 12.29 2.29L4 10.59V6C4 5.45 3.55 5 3 5S2 5.45 2 6V13C2 13.55 2.45 14 3 14H10C10.55 14 11 13.55 11 13C11 12.45 10.55 12 10 12H5.41L13.7 3.71C13.89 3.53 14 3.28 14 3Z"
fill="currentColor"
/>
</svg>
</span>
);
};