diff --git a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts index 4364e537b..58940e5ec 100644 --- a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts +++ b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts @@ -13,6 +13,13 @@ export class StripeIntegrationController extends BaseController { public router() { const router = Router(); + router.get('/link', this.getStripeConnectLink.bind(this)); + router.post( + '/callback', + [body('code').exists()], + this.validationResult, + this.exchangeOAuth.bind(this) + ); router.post('/account', asyncMiddleware(this.createAccount.bind(this))); router.post( '/account_link', @@ -27,6 +34,47 @@ export class StripeIntegrationController extends BaseController { return router; } + /** + * Retrieves Stripe OAuth2 connect link. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + public async getStripeConnectLink( + req: Request, + res: Response, + next: NextFunction + ) { + try { + const authorizationUri = this.stripePaymentApp.getStripeConnectLink(); + + return res.status(200).send({ url: authorizationUri }); + } catch (error) { + next(error); + } + } + + /** + * Exchanges the given Stripe authorization code to Stripe user id and access token. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + public async exchangeOAuth(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { code } = this.matchedBodyData(req); + + try { + await this.stripePaymentApp.exchangeStripeOAuthToken(tenantId, code); + + return res.status(200).send({}); + } catch (error) { + next(error); + } + } + /** * Creates a Stripe checkout session for the given payment link id. * @param {Request} req diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index 37c5e9225..c7c8061ee 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -268,6 +268,8 @@ module.exports = { stripePayment: { secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY || '', publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY || '', + clientId: process.env.STRIPE_PAYMENT_CLIENT_ID || '', + redirectTo: process.env.STRIPE_PAYMENT_REDIRECT_URL || '', webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET || '', }, }; diff --git a/packages/server/src/database/migrations/20240915155403_payment_integration.js b/packages/server/src/database/migrations/20240915155403_payment_integration.js index 12db20dd0..01fac495c 100644 --- a/packages/server/src/database/migrations/20240915155403_payment_integration.js +++ b/packages/server/src/database/migrations/20240915155403_payment_integration.js @@ -8,7 +8,8 @@ exports.up = function (knex) { table.string('service'); table.string('name'); table.string('slug'); - table.boolean('active').defaultTo(false); + table.boolean('payment_enabled').defaultTo(false); + table.boolean('payout_enabled').defaultTo(false); table.string('account_id'); table.json('options'); table.timestamps(); diff --git a/packages/server/src/database/seeds/data/accounts.js b/packages/server/src/database/seeds/data/accounts.js index 8e55665bd..6e171ae0b 100644 --- a/packages/server/src/database/seeds/data/accounts.js +++ b/packages/server/src/database/seeds/data/accounts.js @@ -31,6 +31,17 @@ export const PrepardExpenses = { predefined: true, }; +export const StripeClearingAccount = { + name: 'Stripe Clearing', + slug: 'stripe-clearing', + account_type: 'other-current-liability', + parent_account_id: null, + code: '50006', + active: true, + index: 1, + predefined: true, +} + export default [ { name: 'Bank Account', diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index d2ad4c909..97814413f 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -120,6 +120,7 @@ import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/ev import { EventsTrackerListeners } from '@/services/EventsTracker/events/events'; import { InvoicePaymentIntegrationSubscriber } from '@/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber'; import { StripeWebhooksSubscriber } from '@/services/StripePayment/events/StripeWebhooksSubscriber'; +import { SeedStripeAccountsOnOAuthGrantedSubscriber } from '@/services/StripePayment/events/SeedStripeAccounts'; export default () => { return new EventPublisher(); @@ -294,6 +295,7 @@ export const susbcribers = () => { // Stripe Payment InvoicePaymentIntegrationSubscriber, StripeWebhooksSubscriber, + SeedStripeAccountsOnOAuthGrantedSubscriber, ...EventsTrackerListeners ]; diff --git a/packages/server/src/models/PaymentIntegration.ts b/packages/server/src/models/PaymentIntegration.ts index 9f8467816..04ed8fb31 100644 --- a/packages/server/src/models/PaymentIntegration.ts +++ b/packages/server/src/models/PaymentIntegration.ts @@ -1,7 +1,10 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; -export class PaymentIntegration extends TenantModel { +export class PaymentIntegration extends Model { + paymentEnabled!: boolean; + payoutEnabled!: boolean; + static get tableName() { return 'payment_integrations'; } @@ -10,16 +13,35 @@ export class PaymentIntegration extends TenantModel { return 'id'; } + static get virtualAttributes() { + return ['fullEnabled']; + } + + static get jsonAttributes() { + return ['options']; + } + + get fullEnabled() { + return this.paymentEnabled && this.payoutEnabled; + } + static get jsonSchema() { return { type: 'object', - required: ['name', 'service', 'active'], + required: ['name', 'service'], properties: { id: { type: 'integer' }, service: { type: 'string' }, - active: { type: 'boolean' }, + paymentEnabled: { type: 'boolean' }, + payoutEnabled: { type: 'boolean' }, accountId: { type: 'string' }, - options: { type: 'object' }, + options: { + type: 'object', + properties: { + bankAccountId: { type: 'number' }, + clearingAccountId: { type: 'number' }, + }, + }, createdAt: { type: 'string', format: 'date-time' }, updatedAt: { type: 'string', format: 'date-time' }, }, diff --git a/packages/server/src/repositories/AccountRepository.ts b/packages/server/src/repositories/AccountRepository.ts index 19aaaa70f..53b26e284 100644 --- a/packages/server/src/repositories/AccountRepository.ts +++ b/packages/server/src/repositories/AccountRepository.ts @@ -4,6 +4,7 @@ import { IAccount } from '@/interfaces'; import { Knex } from 'knex'; import { PrepardExpenses, + StripeClearingAccount, TaxPayableAccount, UnearnedRevenueAccount, } from '@/database/seeds/data/accounts'; @@ -247,4 +248,37 @@ export default class AccountRepository extends TenantRepository { } return result; } + + + /** + * Finds or creates the stripe clearing account. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreateStripeClearing( + extraAttrs: Record = {}, + trx?: Knex.Transaction + ) { + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ + tenantId: this.tenantId, + }); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + + let result = await this.model + .query(trx) + .findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...StripeClearingAccount, + ..._extraAttrs, + }); + } + return result; + } } diff --git a/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts b/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts index edcb6a987..122fdcea4 100644 --- a/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts +++ b/packages/server/src/services/PaymentServices/GetPaymentMethodsState.ts @@ -3,12 +3,16 @@ import HasTenancyService from '../Tenancy/TenancyService'; import { GetPaymentMethodsPOJO } from './types'; import config from '@/config'; import { isStripePaymentConfigured } from './utils'; +import { GetStripeAuthorizationLinkService } from '../StripePayment/GetStripeAuthorizationLink'; @Service() export class GetPaymentMethodsStateService { @Inject() private tenancy: HasTenancyService; + @Inject() + private getStripeAuthorizationLinkService: GetStripeAuthorizationLinkService; + /** * Retrieves the payment state provising state. * @param {number} tenantId @@ -25,7 +29,9 @@ export class GetPaymentMethodsStateService { service: 'Stripe', }); const isStripeAccountCreated = !!stripePayment; - const isStripePaymentActive = !!(stripePayment?.active || null); + const isStripePaymentEnabled = stripePayment?.paymentEnabled; + const isStripePayoutEnabled = stripePayment?.payoutEnabled; + const isStripeEnabled = stripePayment?.fullEnabled; const stripePaymentMethodId = stripePayment?.id || null; const stripeAccountId = stripePayment?.accountId || null; @@ -33,16 +39,21 @@ export class GetPaymentMethodsStateService { const stripeCurrencies = ['USD', 'EUR']; const stripeRedirectUrl = 'https://your-stripe-redirect-url.com'; const isStripeServerConfigured = isStripePaymentConfigured(); + const stripeAuthLink = + this.getStripeAuthorizationLinkService.getStripeAuthLink(); const paymentMethodPOJO: GetPaymentMethodsPOJO = { stripe: { isStripeAccountCreated, - isStripePaymentActive, + isStripePaymentEnabled, + isStripePayoutEnabled, + isStripeEnabled, isStripeServerConfigured, stripeAccountId, stripePaymentMethodId, stripePublishableKey, stripeCurrencies, + stripeAuthLink, stripeRedirectUrl, }, }; diff --git a/packages/server/src/services/PaymentServices/types.ts b/packages/server/src/services/PaymentServices/types.ts index 663d3c638..f3ec7b41f 100644 --- a/packages/server/src/services/PaymentServices/types.ts +++ b/packages/server/src/services/PaymentServices/types.ts @@ -16,11 +16,17 @@ export interface EditPaymentMethodDTO { export interface GetPaymentMethodsPOJO { stripe: { isStripeAccountCreated: boolean; - isStripePaymentActive: boolean; + + isStripePaymentEnabled: boolean; + isStripePayoutEnabled: boolean; + isStripeEnabled: boolean; + isStripeServerConfigured: boolean; + stripeAccountId: string | null; stripePaymentMethodId: number | null; stripePublishableKey: string | null; + stripeAuthLink: string; stripeCurrencies: Array; stripeRedirectUrl: string | null; }; diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts index f788354e0..35e203049 100644 --- a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts @@ -35,7 +35,8 @@ export class GetSaleInvoice { .withGraphFetched('customer') .withGraphFetched('branch') .withGraphFetched('taxes.taxRate') - .withGraphFetched('attachments'); + .withGraphFetched('attachments') + .withGraphFetched('paymentMethods'); // Validates the given sale invoice existance. this.validators.validateInvoiceExistance(saleInvoice); diff --git a/packages/server/src/services/StripePayment/ExchangeStripeOauthToken.ts b/packages/server/src/services/StripePayment/ExchangeStripeOauthToken.ts new file mode 100644 index 000000000..97091ec65 --- /dev/null +++ b/packages/server/src/services/StripePayment/ExchangeStripeOauthToken.ts @@ -0,0 +1,73 @@ +import { Inject, Service } from 'typedi'; +import { StripePaymentService } from './StripePaymentService'; +import events from '@/subscribers/events'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import UnitOfWork from '../UnitOfWork'; +import { Knex } from 'knex'; +import { StripeOAuthCodeGrantedEventPayload } from './types'; + +@Service() +export class ExchangeStripeOAuthTokenService { + @Inject() + private stripePaymentService: StripePaymentService; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + /** + * Exchange stripe oauth authorization code to access token and user id. + * @param {number} tenantId + * @param {string} authorizationCode + */ + public async excahngeStripeOAuthToken( + tenantId: number, + authorizationCode: string + ) { + const { PaymentIntegration } = this.tenancy.models(tenantId); + const stripe = this.stripePaymentService.stripe; + + const response = await stripe.oauth.token({ + grant_type: 'authorization_code', + code: authorizationCode, + }); + // const accessToken = response.access_token; + // const refreshToken = response.refresh_token; + const stripeUserId = response.stripe_user_id; + + // Retrieves details of the Stripe account. + const account = await stripe.accounts.retrieve(stripeUserId, { + expand: ['business_profile'], + }); + const companyName = account.business_profile?.name || 'Unknow name'; + const paymentEnabled = account.charges_enabled; + const payoutEnabled = account.payouts_enabled; + + // + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Stores the details of the Stripe account. + const paymentIntegration = await PaymentIntegration.query(trx).insert({ + name: companyName, + service: 'Stripe', + accountId: stripeUserId, + paymentEnabled, + payoutEnabled, + }); + // Triggers `onStripeOAuthCodeGranted` event. + await this.eventPublisher.emitAsync( + events.stripeIntegration.onOAuthCodeGranted, + { + tenantId, + paymentIntegrationId: paymentIntegration.id, + trx, + } as StripeOAuthCodeGrantedEventPayload + ); + }); + } +} diff --git a/packages/server/src/services/StripePayment/GetStripeAuthorizationLink.ts b/packages/server/src/services/StripePayment/GetStripeAuthorizationLink.ts new file mode 100644 index 000000000..026fde40b --- /dev/null +++ b/packages/server/src/services/StripePayment/GetStripeAuthorizationLink.ts @@ -0,0 +1,14 @@ +import { Service } from 'typedi'; +import config from '@/config'; + +@Service() +export class GetStripeAuthorizationLinkService { + public getStripeAuthLink() { + const clientId = config.stripePayment.clientId; + const redirectUrl = config.stripePayment.redirectTo; + + const authorizationUri = `https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write&redirect_uri=${redirectUrl}`; + + return authorizationUri; + } +} diff --git a/packages/server/src/services/StripePayment/StripePaymentApplication.ts b/packages/server/src/services/StripePayment/StripePaymentApplication.ts index cf7823879..876616ef7 100644 --- a/packages/server/src/services/StripePayment/StripePaymentApplication.ts +++ b/packages/server/src/services/StripePayment/StripePaymentApplication.ts @@ -4,6 +4,8 @@ import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; import { CreateStripeAccountService } from './CreateStripeAccountService'; import { CreateStripeAccountLinkService } from './CreateStripeAccountLink'; import { CreateStripeAccountDTO } from './types'; +import { ExchangeStripeOAuthTokenService } from './ExchangeStripeOauthToken'; +import { GetStripeAuthorizationLinkService } from './GetStripeAuthorizationLink'; export class StripePaymentApplication { @Inject() @@ -15,6 +17,12 @@ export class StripePaymentApplication { @Inject() private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; + @Inject() + private exchangeStripeOAuthTokenService: ExchangeStripeOAuthTokenService; + + @Inject() + private getStripeConnectLinkService: GetStripeAuthorizationLinkService; + /** * Creates a new Stripe account for Bigcapital. * @param {number} tenantId @@ -58,4 +66,24 @@ export class StripePaymentApplication { paymentLinkId ); } + + /** + * Retrieves Stripe OAuth2 connect link. + * @returns {string} + */ + public getStripeConnectLink() { + return this.getStripeConnectLinkService.getStripeAuthLink(); + } + + /** + * Exchanges the given Stripe authorization code to Stripe user id and access token. + * @param {string} authorizationCode + * @returns + */ + public exchangeStripeOAuthToken(tenantId: number, authorizationCode: string) { + return this.exchangeStripeOAuthTokenService.excahngeStripeOAuthToken( + tenantId, + authorizationCode + ); + } } diff --git a/packages/server/src/services/StripePayment/StripePaymentService.ts b/packages/server/src/services/StripePayment/StripePaymentService.ts index c10fe16c0..caf44dca4 100644 --- a/packages/server/src/services/StripePayment/StripePaymentService.ts +++ b/packages/server/src/services/StripePayment/StripePaymentService.ts @@ -2,11 +2,11 @@ import { Service } from 'typedi'; import stripe from 'stripe'; import config from '@/config'; -const origin = 'http://localhost:4000'; +const origin = 'https://cfdf-102-164-97-88.ngrok-free.app'; @Service() export class StripePaymentService { - public stripe; + public stripe: stripe; constructor() { this.stripe = new stripe(config.stripePayment.secretKey, { @@ -36,9 +36,9 @@ export class StripePaymentService { } /** - * - * @param {number} accountId - * @returns + * + * @param {number} accountId + * @returns */ public async createAccountLink(accountId: string) { try { @@ -62,8 +62,9 @@ export class StripePaymentService { */ public async createAccount(): Promise { try { - const account = await this.stripe.accounts.create({}); - + const account = await this.stripe.accounts.create({ + type: 'standard', + }); return account; } catch (error) { throw new Error( diff --git a/packages/server/src/services/StripePayment/constants.ts b/packages/server/src/services/StripePayment/constants.ts deleted file mode 100644 index c08967f19..000000000 --- a/packages/server/src/services/StripePayment/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const STRIPE_PAYMENT_LINK_REDIRECT = 'https://your_redirect_url.com'; diff --git a/packages/server/src/services/StripePayment/events/SeedStripeAccounts.ts b/packages/server/src/services/StripePayment/events/SeedStripeAccounts.ts new file mode 100644 index 000000000..3708b95e1 --- /dev/null +++ b/packages/server/src/services/StripePayment/events/SeedStripeAccounts.ts @@ -0,0 +1,49 @@ +import { Inject, Service } from 'typedi'; +import events from '@/subscribers/events'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { StripeOAuthCodeGrantedEventPayload } from '../types'; + +@Service() +export class SeedStripeAccountsOnOAuthGrantedSubscriber { + @Inject() + private tenancy: HasTenancyService; + + /** + * Attaches the subscriber to the event dispatcher. + */ + public attach(bus) { + bus.subscribe( + events.stripeIntegration.onOAuthCodeGranted, + this.handleSeedStripeAccount.bind(this) + ); + } + + /** + * Seeds the default integration settings once oauth authorization code granted. + * @param {StripeCheckoutSessionCompletedEventPayload} payload - + */ + async handleSeedStripeAccount({ + tenantId, + paymentIntegrationId, + trx, + }: StripeOAuthCodeGrantedEventPayload) { + const { PaymentIntegration } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); + + const clearingAccount = await accountRepository.findOrCreateStripeClearing( + {}, + trx + ); + const bankAccount = await accountRepository.findBySlug('bank-account'); + + // Patch the Stripe integration default settings. + await PaymentIntegration.query(trx) + .findById(paymentIntegrationId) + .patch({ + options: { + bankAccountId: bankAccount.id, + clearingAccountId: clearingAccount.id, + }, + }); + } +} diff --git a/packages/server/src/services/StripePayment/types.ts b/packages/server/src/services/StripePayment/types.ts index 3629ac048..3260b68fa 100644 --- a/packages/server/src/services/StripePayment/types.ts +++ b/packages/server/src/services/StripePayment/types.ts @@ -1,6 +1,11 @@ - +import { Knex } from 'knex'; export interface CreateStripeAccountDTO { name?: string; } +export interface StripeOAuthCodeGrantedEventPayload { + tenantId: number; + paymentIntegrationId: number; + trx?: Knex.Transaction +} \ No newline at end of file diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 89b01f4e2..d3498f5d4 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -723,7 +723,9 @@ export default { onAccountDeleted: 'onStripeIntegrationAccountDeleted', onPaymentLinkCreated: 'onStripePaymentLinkCreated', - onPaymentLinkInactivated: 'onStripePaymentLinkInactivated' + onPaymentLinkInactivated: 'onStripePaymentLinkInactivated', + + onOAuthCodeGranted: 'onStripeOAuthCodeGranted', }, // Stripe Payment Webhooks diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesStripeCallback.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesStripeCallback.tsx new file mode 100644 index 000000000..3ead35ee6 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/PreferencesStripeCallback.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useSetStripeAccountCallback } from '@/hooks/query/stripe-integration'; + +function useQuery() { + const { search } = useLocation(); + + return React.useMemo(() => new URLSearchParams(search), [search]); +} + +export default function PreferencesStripeCallback() { + const query = useQuery(); + const code = query.get('code') as string; + const { mutateAsync: stripeAccountCallback } = useSetStripeAccountCallback(); + + const history = useHistory(); + + useEffect(() => { + stripeAccountCallback({ code }).then(() => { + history.push('/preferences/payment-methods') + }); + }, [history, stripeAccountCallback, code]); + + return null; +} diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/StripePaymentMethod.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/StripePaymentMethod.tsx index d35529299..a8b934fdf 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/StripePaymentMethod.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/StripePaymentMethod.tsx @@ -10,8 +10,9 @@ import { Popover, Tag, Text, + Tooltip, } from '@blueprintjs/core'; -import { AppToaster, Box, Card, Group, Stack } from '@/components'; +import { Box, Card, Group, Stack } from '@/components'; import { StripeLogo } from '@/icons/StripeLogo'; import { usePaymentMethodsBoot } from './PreferencesPaymentMethodsBoot'; import { DialogsName } from '@/constants/dialogs'; @@ -20,7 +21,6 @@ import { useDialogActions, useDrawerActions, } from '@/hooks/state'; -import { useCreateStripeAccountLink } from '@/hooks/query/stripe-integration'; import { DRAWERS } from '@/constants/drawers'; import { MoreIcon } from '@/icons/More'; import { STRIPE_PRICING_LINK } from './constants'; @@ -34,39 +34,17 @@ export function StripePaymentMethod() { const stripeState = paymentMethodsState?.stripe; const isAccountCreated = stripeState?.isStripeAccountCreated; - const isAccountActive = stripeState?.isStripePaymentActive; - const stripeAccountId = stripeState?.stripeAccountId; + const isPaymentEnabled = stripeState?.isStripePaymentEnabled; + const isPayoutEnabled = stripeState?.isStripePayoutEnabled; + const isStripeEnabled = stripeState?.isStripeEnabled; const stripePaymentMethodId = stripeState?.stripePaymentMethodId; const isStripeServerConfigured = stripeState?.isStripeServerConfigured; - const { - mutateAsync: createStripeAccountLink, - isLoading: isCreateStripeLinkLoading, - } = useCreateStripeAccountLink(); - // Handle Stripe setup button click. const handleSetUpBtnClick = () => { openDialog(DialogsName.StripeSetup); }; - // Handle complete Stripe setup button click. - const handleCompleteSetUpBtnClick = () => { - createStripeAccountLink({ stripeAccountId }) - .then((res) => { - const { clientSecret } = res; - - if (clientSecret.url) { - window.open(clientSecret.url, '_blank'); - } - }) - .catch(() => { - AppToaster.show({ - message: 'Something went wrong.', - intent: Intent.DANGER, - }); - }); - }; - // Handle edit button click. const handleEditBtnClick = () => { openDrawer(DRAWERS.STRIPE_PAYMENT_INTEGRATION_EDIT, { @@ -87,14 +65,30 @@ export function StripePaymentMethod() { - {isAccountActive && ( - - Active - - )} + + {isStripeEnabled && ( + + Active + + )} + {!isPaymentEnabled && isAccountCreated && ( + + + Payment Not Enabled + + + )} + {!isPayoutEnabled && isAccountCreated && ( + + + Payout Not Enabled + + + )} + - {isAccountActive && ( + {isAccountCreated && ( @@ -104,16 +98,6 @@ export function StripePaymentMethod() { Set it Up )} - {isAccountCreated && !isAccountActive && ( - - )} {isAccountCreated && ( Stripe payment is not configured from the server.{' '} - + )} diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx index 139f29842..0fa2d7511 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/dialogs/StripePreSetupDialog/StripePreSetupDialogContent.tsx @@ -1,53 +1,32 @@ +import { useState } from 'react'; import { Button, DialogBody, DialogFooter, Intent } from '@blueprintjs/core'; import styled from 'styled-components'; import { Stack } from '@/components'; import { useDialogContext } from '@/components/Dialog/DialogProvider'; -import { - useCreateStripeAccount, - useCreateStripeAccountLink, -} from '@/hooks/query/stripe-integration'; import { useDialogActions } from '@/hooks/state'; import { CreditCard2Icon } from '@/icons/CreditCard2'; import { DollarIcon } from '@/icons/Dollar'; import { LayoutAutoIcon } from '@/icons/LayoutAuto'; import { SwitchIcon } from '@/icons/SwitchIcon'; +import { usePaymentMethodsBoot } from '../../PreferencesPaymentMethodsBoot'; export function StripePreSetupDialogContent() { const { name } = useDialogContext(); const { closeDialog } = useDialogActions(); - - const { - mutateAsync: createStripeAccount, - isLoading: isCreateStripeAccountLoading, - } = useCreateStripeAccount(); - - const { - mutateAsync: createStripeAccountLink, - isLoading: isCreateStripeLinkLoading, - } = useCreateStripeAccountLink(); + const { paymentMethodsState } = usePaymentMethodsBoot(); + const [isRedirecting, setIsRedirecting] = useState(false); const handleSetUpBtnClick = () => { - createStripeAccount({}) - .then((response) => { - const { account_id: accountId } = response; - - return createStripeAccountLink({ stripeAccountId: accountId }); - }) - .then((res) => { - const { clientSecret } = res; - - if (clientSecret.url) { - window.location.href = clientSecret.url; - } - }); + if (paymentMethodsState?.stripe.stripeAuthLink) { + setIsRedirecting(true); + window.location.href = paymentMethodsState?.stripe.stripeAuthLink; + } }; - + // Handle cancel button click. const handleCancelBtnClick = () => { closeDialog(name); }; - const isLoading = isCreateStripeAccountLoading || isCreateStripeLinkLoading; - return ( <> @@ -92,7 +71,7 @@ export function StripePreSetupDialogContent() { diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx index 16f7e141b..1700114a5 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditFormContent.tsx @@ -4,6 +4,7 @@ import { Button, Intent } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerActions } from '@/hooks/state'; +import { ACCOUNT_TYPE } from '@/constants'; export function StripeIntegrationEditFormContent() { const { accounts } = useStripeIntegrationEditBoot(); @@ -19,6 +20,7 @@ export function StripeIntegrationEditFormContent() { } paymentMethods + * @returns {Array<{ payment_integration_id: string; enable: boolean }>} + */ const transformPaymentMethodsToRequest = ( paymentMethods: Record, ): Array<{ payment_integration_id: string; enable: boolean }> => { @@ -237,6 +243,20 @@ const transformPaymentMethodsToRequest = ( })); }; +/** + * Transformes payment methods from request to form. + * @param {Array<{ payment_integration_id: number; enable: boolean }>} paymentMethods + * @returns {Record} + */ +const transformPaymentMethodsToForm = ( + paymentMethods: Array<{ payment_integration_id: number; enable: boolean }>, +): Record => { + return paymentMethods?.reduce((acc, method) => { + acc[method.payment_integration_id] = { enable: method.enable }; + return acc; + }, {}); +}; + export const useSetPrimaryWarehouseToForm = () => { const { setFieldValue } = useFormikContext(); const { warehouses, isWarehousesSuccess } = useInvoiceFormContext(); diff --git a/packages/webapp/src/hooks/query/payment-services.ts b/packages/webapp/src/hooks/query/payment-services.ts index ac4859575..886f4b98d 100644 --- a/packages/webapp/src/hooks/query/payment-services.ts +++ b/packages/webapp/src/hooks/query/payment-services.ts @@ -12,7 +12,6 @@ import { transformToCamelCase, transfromToSnakeCase } from '@/utils'; const PaymentServicesQueryKey = 'PaymentServices'; const PaymentServicesStateQueryKey = 'PaymentServicesState'; - // # Get payment services. // ----------------------------------------- export interface GetPaymentServicesResponse {} @@ -48,12 +47,15 @@ export const useGetPaymentServices = ( export interface GetPaymentServicesStateResponse { stripe: { isStripeAccountCreated: boolean; - isStripePaymentActive: boolean; + isStripePaymentEnabled: boolean; + isStripePayoutEnabled: boolean; + isStripeEnabled: boolean; isStripeServerConfigured: boolean; stripeAccountId: string | null; stripePaymentMethodId: number | null; stripeCurrencies: string[]; stripePublishableKey: string; + stripeAuthLink: string; stripeRedirectUrl: string; }; } diff --git a/packages/webapp/src/hooks/query/stripe-integration.ts b/packages/webapp/src/hooks/query/stripe-integration.ts index d141def5b..216f57192 100644 --- a/packages/webapp/src/hooks/query/stripe-integration.ts +++ b/packages/webapp/src/hooks/query/stripe-integration.ts @@ -7,7 +7,6 @@ import { import useApiRequest from '../useRequest'; import { transformToCamelCase } from '@/utils'; - // Create Stripe Account Link. // ------------------------------------ interface StripeAccountLinkResponse { @@ -47,7 +46,6 @@ export const useCreateStripeAccountLink = ( ); }; - // Create Stripe Account Session. // ------------------------------------ interface AccountSessionValues { @@ -149,3 +147,70 @@ export const useCreateStripeCheckoutSession = ( { ...options }, ); }; + +// Create Stripe Account OAuth Link. +// ------------------------------------ +interface StripeAccountLinkResponse { + clientSecret: { + created: number; + expiresAt: number; + object: string; + url: string; + }; +} + +interface StripeAccountLinkValues { + stripeAccountId: string; +} + +export const useGetStripeAccountLink = ( + options?: UseQueryOptions, +): UseQueryResult => { + const apiRequest = useApiRequest(); + return useQuery( + 'getStripeAccountLink', + () => { + return apiRequest + .get('/stripe_integration/link') + .then((res) => transformToCamelCase(res.data)); + }, + { ...options }, + ); +}; + +// Get Stripe Account OAuth Callback Mutation. +// ------------------------------------ +interface StripeAccountCallbackMutationValues { + code: string; +} + +interface StripeAccountCallbackMutationResponse { + success: boolean; +} + +export const useSetStripeAccountCallback = ( + options?: UseMutationOptions< + StripeAccountCallbackMutationResponse, + Error, + StripeAccountCallbackMutationValues + >, +): UseMutationResult< + StripeAccountCallbackMutationResponse, + Error, + StripeAccountCallbackMutationValues +> => { + const apiRequest = useApiRequest(); + return useMutation( + (values: StripeAccountCallbackMutationValues) => { + return apiRequest + .post(`/stripe_integration/callback`, values) + .then( + (res) => + transformToCamelCase( + res.data, + ) as StripeAccountCallbackMutationResponse, + ); + }, + { ...options }, + ); +}; diff --git a/packages/webapp/src/routes/preferences.tsx b/packages/webapp/src/routes/preferences.tsx index 926913982..5f3cc1088 100644 --- a/packages/webapp/src/routes/preferences.tsx +++ b/packages/webapp/src/routes/preferences.tsx @@ -28,6 +28,13 @@ export const getPreferenceRoutes = () => [ ), exact: true, }, + { + path: `${BASE_URL}/payment-methods/stripe/callback`, + component: lazy( + () => import('../containers/Preferences/PaymentMethods/PreferencesStripeCallback'), + ), + exact: true, + }, { path: `${BASE_URL}/credit-notes`, component: lazy(() =>