From 5aba498e7721394ded04614cbc3925badb99608d Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:37:07 +0700 Subject: [PATCH] refactor(frontend): move billing APIs to a dedicated service handler (#10958) --- .../features/payment/payment-form.test.tsx | 9 ++- .../billing-service/billing-service.api.ts | 57 +++++++++++++++++++ .../billing-service/billing.types.ts} | 0 frontend/src/api/open-hands.ts | 32 ----------- .../features/payment/setup-payment-modal.tsx | 4 +- .../use-create-stripe-checkout-session.ts | 4 +- frontend/src/hooks/query/use-balance.ts | 4 +- .../hooks/query/use-subscription-access.ts | 4 +- 8 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 frontend/src/api/billing-service/billing-service.api.ts rename frontend/src/{types/billing.tsx => api/billing-service/billing.types.ts} (100%) diff --git a/frontend/__tests__/components/features/payment/payment-form.test.tsx b/frontend/__tests__/components/features/payment/payment-form.test.tsx index b2c95372da..8c9f510a0e 100644 --- a/frontend/__tests__/components/features/payment/payment-form.test.tsx +++ b/frontend/__tests__/components/features/payment/payment-form.test.tsx @@ -2,13 +2,16 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest"; -import OpenHands from "#/api/open-hands"; +import BillingService from "#/api/billing-service/billing-service.api"; import OptionService from "#/api/option-service/option-service.api"; import { PaymentForm } from "#/components/features/payment/payment-form"; describe("PaymentForm", () => { - const getBalanceSpy = vi.spyOn(OpenHands, "getBalance"); - const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession"); + const getBalanceSpy = vi.spyOn(BillingService, "getBalance"); + const createCheckoutSessionSpy = vi.spyOn( + BillingService, + "createCheckoutSession", + ); const getConfigSpy = vi.spyOn(OptionService, "getConfig"); const renderPaymentForm = () => diff --git a/frontend/src/api/billing-service/billing-service.api.ts b/frontend/src/api/billing-service/billing-service.api.ts new file mode 100644 index 0000000000..0cfb692dcc --- /dev/null +++ b/frontend/src/api/billing-service/billing-service.api.ts @@ -0,0 +1,57 @@ +import { openHands } from "../open-hands-axios"; +import { SubscriptionAccess } from "./billing.types"; + +/** + * Billing Service API - Handles all billing-related API endpoints + */ +class BillingService { + /** + * Create a Stripe checkout session for credit purchase + * @param amount The amount to charge in dollars + * @returns The redirect URL for the checkout session + */ + static async createCheckoutSession(amount: number): Promise { + const { data } = await openHands.post( + "/api/billing/create-checkout-session", + { + amount, + }, + ); + return data.redirect_url; + } + + /** + * Create a customer setup session for payment method management + * @returns The redirect URL for the customer setup session + */ + static async createBillingSessionResponse(): Promise { + const { data } = await openHands.post( + "/api/billing/create-customer-setup-session", + ); + return data.redirect_url; + } + + /** + * Get the user's current credit balance + * @returns The user's credit balance as a string + */ + static async getBalance(): Promise { + const { data } = await openHands.get<{ credits: string }>( + "/api/billing/credits", + ); + return data.credits; + } + + /** + * Get the user's subscription access information + * @returns The user's subscription access details or null if not available + */ + static async getSubscriptionAccess(): Promise { + const { data } = await openHands.get( + "/api/billing/subscription-access", + ); + return data; + } +} + +export default BillingService; diff --git a/frontend/src/types/billing.tsx b/frontend/src/api/billing-service/billing.types.ts similarity index 100% rename from frontend/src/types/billing.tsx rename to frontend/src/api/billing-service/billing.types.ts diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index b8c963f58e..8c850ca819 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -19,7 +19,6 @@ import { openHands } from "./open-hands-axios"; import { Provider } from "#/types/settings"; import { SuggestedTask } from "#/utils/types"; import { BatchFeedbackData } from "#/hooks/query/use-batch-feedback"; -import { SubscriptionAccess } from "#/types/billing"; class OpenHands { private static currentConversation: Conversation | null = null; @@ -313,37 +312,6 @@ class OpenHands { return data; } - static async createCheckoutSession(amount: number): Promise { - const { data } = await openHands.post( - "/api/billing/create-checkout-session", - { - amount, - }, - ); - return data.redirect_url; - } - - static async createBillingSessionResponse(): Promise { - const { data } = await openHands.post( - "/api/billing/create-customer-setup-session", - ); - return data.redirect_url; - } - - static async getBalance(): Promise { - const { data } = await openHands.get<{ credits: string }>( - "/api/billing/credits", - ); - return data.credits; - } - - static async getSubscriptionAccess(): Promise { - const { data } = await openHands.get( - "/api/billing/subscription-access", - ); - return data; - } - static async getTrajectory( conversationId: string, ): Promise { diff --git a/frontend/src/components/features/payment/setup-payment-modal.tsx b/frontend/src/components/features/payment/setup-payment-modal.tsx index dfce7d9cd3..e62141dd37 100644 --- a/frontend/src/components/features/payment/setup-payment-modal.tsx +++ b/frontend/src/components/features/payment/setup-payment-modal.tsx @@ -4,14 +4,14 @@ import { I18nKey } from "#/i18n/declaration"; import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react"; import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; import { ModalBody } from "#/components/shared/modals/modal-body"; -import OpenHands from "#/api/open-hands"; +import BillingService from "#/api/billing-service/billing-service.api"; import { BrandButton } from "../settings/brand-button"; import { displayErrorToast } from "#/utils/custom-toast-handlers"; export function SetupPaymentModal() { const { t } = useTranslation(); const { mutate, isPending } = useMutation({ - mutationFn: OpenHands.createBillingSessionResponse, + mutationFn: BillingService.createBillingSessionResponse, onSuccess: (data) => { window.location.href = data; }, diff --git a/frontend/src/hooks/mutation/stripe/use-create-stripe-checkout-session.ts b/frontend/src/hooks/mutation/stripe/use-create-stripe-checkout-session.ts index e6a58c78e3..59ddcb0c4b 100644 --- a/frontend/src/hooks/mutation/stripe/use-create-stripe-checkout-session.ts +++ b/frontend/src/hooks/mutation/stripe/use-create-stripe-checkout-session.ts @@ -1,10 +1,10 @@ import { useMutation } from "@tanstack/react-query"; -import OpenHands from "#/api/open-hands"; +import BillingService from "#/api/billing-service/billing-service.api"; export const useCreateStripeCheckoutSession = () => useMutation({ mutationFn: async (variables: { amount: number }) => { - const redirectUrl = await OpenHands.createCheckoutSession( + const redirectUrl = await BillingService.createCheckoutSession( variables.amount, ); window.location.href = redirectUrl; diff --git a/frontend/src/hooks/query/use-balance.ts b/frontend/src/hooks/query/use-balance.ts index 21a0cef5eb..1d89454f74 100644 --- a/frontend/src/hooks/query/use-balance.ts +++ b/frontend/src/hooks/query/use-balance.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { useConfig } from "./use-config"; -import OpenHands from "#/api/open-hands"; +import BillingService from "#/api/billing-service/billing-service.api"; import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page"; export const useBalance = () => { @@ -9,7 +9,7 @@ export const useBalance = () => { return useQuery({ queryKey: ["user", "balance"], - queryFn: OpenHands.getBalance, + queryFn: BillingService.getBalance, enabled: !isOnTosPage && config?.APP_MODE === "saas" && diff --git a/frontend/src/hooks/query/use-subscription-access.ts b/frontend/src/hooks/query/use-subscription-access.ts index aab3ad2c6e..b770aed466 100644 --- a/frontend/src/hooks/query/use-subscription-access.ts +++ b/frontend/src/hooks/query/use-subscription-access.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { useConfig } from "./use-config"; -import OpenHands from "#/api/open-hands"; +import BillingService from "#/api/billing-service/billing-service.api"; import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page"; export const useSubscriptionAccess = () => { @@ -9,7 +9,7 @@ export const useSubscriptionAccess = () => { return useQuery({ queryKey: ["user", "subscription_access"], - queryFn: OpenHands.getSubscriptionAccess, + queryFn: BillingService.getSubscriptionAccess, enabled: !isOnTosPage && config?.APP_MODE === "saas" &&