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

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