diff --git a/frontend/src/api/billing-service/billing-service.api.ts b/frontend/src/api/billing-service/billing-service.api.ts index 2f78b910f6..7bc4fe63a7 100644 --- a/frontend/src/api/billing-service/billing-service.api.ts +++ b/frontend/src/api/billing-service/billing-service.api.ts @@ -1,8 +1,4 @@ import { openHands } from "../open-hands-axios"; -import { - CancelSubscriptionResponse, - SubscriptionAccess, -} from "./billing.types"; /** * Billing Service API - Handles all billing-related API endpoints @@ -44,41 +40,6 @@ class BillingService { ); 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; - } - - /** - * Create a subscription checkout session for subscribing to a plan - * @returns The redirect URL for the subscription checkout session - */ - static async createSubscriptionCheckoutSession(): Promise<{ - redirect_url?: string; - }> { - const { data } = await openHands.post( - "/api/billing/subscription-checkout-session", - ); - return data; - } - - /** - * Cancel the user's subscription - * @returns The response indicating the result of the cancellation request - */ - static async cancelSubscription(): Promise { - const { data } = await openHands.post( - "/api/billing/cancel-subscription", - ); - return data; - } } export default BillingService; diff --git a/frontend/src/api/billing-service/billing.types.ts b/frontend/src/api/billing-service/billing.types.ts deleted file mode 100644 index b6b05cfb0c..0000000000 --- a/frontend/src/api/billing-service/billing.types.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type SubscriptionAccess = { - start_at: string; - end_at: string; - created_at: string; - cancelled_at?: string | null; - stripe_subscription_id?: string | null; -}; - -export interface CancelSubscriptionResponse { - status: string; - message: string; -} diff --git a/frontend/src/components/features/payment/cancel-subscription-modal.tsx b/frontend/src/components/features/payment/cancel-subscription-modal.tsx deleted file mode 100644 index 5c4a67abfc..0000000000 --- a/frontend/src/components/features/payment/cancel-subscription-modal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from "react"; -import { useTranslation, Trans } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { BrandButton } from "#/components/features/settings/brand-button"; -import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; -import { useCancelSubscription } from "#/hooks/mutation/use-cancel-subscription"; -import { - displayErrorToast, - displaySuccessToast, -} from "#/utils/custom-toast-handlers"; - -interface CancelSubscriptionModalProps { - isOpen: boolean; - onClose: () => void; - endDate?: string; -} - -export function CancelSubscriptionModal({ - isOpen, - onClose, - endDate, -}: CancelSubscriptionModalProps) { - const { t } = useTranslation(); - const cancelSubscriptionMutation = useCancelSubscription(); - - const handleCancelSubscription = async () => { - try { - await cancelSubscriptionMutation.mutateAsync(); - displaySuccessToast(t(I18nKey.PAYMENT$SUBSCRIPTION_CANCELLED)); - onClose(); - } catch { - displayErrorToast(t(I18nKey.ERROR$GENERIC)); - } - }; - - if (!isOpen) return null; - - return ( - -
-

- {t(I18nKey.PAYMENT$CANCEL_SUBSCRIPTION_TITLE)} -

-

- {endDate ? ( - }} - /> - ) : ( - t(I18nKey.PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE) - )} -

-
- - {t(I18nKey.BUTTON$CONFIRM)} - - - {t(I18nKey.BUTTON$CANCEL)} - -
-
-
- ); -} diff --git a/frontend/src/hooks/mutation/stripe/use-create-subscription-checkout-session.ts b/frontend/src/hooks/mutation/stripe/use-create-subscription-checkout-session.ts deleted file mode 100644 index e70e0d4295..0000000000 --- a/frontend/src/hooks/mutation/stripe/use-create-subscription-checkout-session.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import BillingService from "#/api/billing-service/billing-service.api"; - -export const useCreateSubscriptionCheckoutSession = () => - useMutation({ - mutationFn: BillingService.createSubscriptionCheckoutSession, - onSuccess: (data) => { - if (data.redirect_url) { - window.location.href = data.redirect_url; - } - }, - }); diff --git a/frontend/src/hooks/mutation/use-cancel-subscription.ts b/frontend/src/hooks/mutation/use-cancel-subscription.ts deleted file mode 100644 index 291d4e38bf..0000000000 --- a/frontend/src/hooks/mutation/use-cancel-subscription.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import BillingService from "#/api/billing-service/billing-service.api"; - -export const useCancelSubscription = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: BillingService.cancelSubscription, - onSuccess: () => { - // Invalidate subscription access query to refresh the UI - queryClient.invalidateQueries({ - queryKey: ["user", "subscription_access"], - }); - }, - }); -}; diff --git a/frontend/src/hooks/query/use-subscription-access.ts b/frontend/src/hooks/query/use-subscription-access.ts deleted file mode 100644 index 2a2922c0ba..0000000000 --- a/frontend/src/hooks/query/use-subscription-access.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useConfig } from "./use-config"; -import BillingService from "#/api/billing-service/billing-service.api"; -import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page"; - -export const useSubscriptionAccess = () => { - const { data: config } = useConfig(); - const isOnTosPage = useIsOnTosPage(); - - return useQuery({ - queryKey: ["user", "subscription_access"], - queryFn: BillingService.getSubscriptionAccess, - enabled: - !isOnTosPage && - config?.app_mode === "saas" && - config?.feature_flags?.enable_billing, - }); -}; diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index f4f52e3eae..9129724374 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -574,10 +574,7 @@ export enum I18nKey { PAYMENT$MANAGE_CREDITS = "PAYMENT$MANAGE_CREDITS", PAYMENT$CANCEL_SUBSCRIPTION = "PAYMENT$CANCEL_SUBSCRIPTION", PAYMENT$CANCEL_SUBSCRIPTION_TITLE = "PAYMENT$CANCEL_SUBSCRIPTION_TITLE", - PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE = "PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE", - PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE_WITH_DATE = "PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE_WITH_DATE", PAYMENT$SUBSCRIPTION_CANCELLED = "PAYMENT$SUBSCRIPTION_CANCELLED", - PAYMENT$SUBSCRIPTION_CANCELLED_EXPIRES = "PAYMENT$SUBSCRIPTION_CANCELLED_EXPIRES", PAYMENT$NEXT_BILLING_DATE = "PAYMENT$NEXT_BILLING_DATE", WAITLIST$IF_NOT_JOINED = "WAITLIST$IF_NOT_JOINED", WAITLIST$PATIENCE_MESSAGE = "WAITLIST$PATIENCE_MESSAGE", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 2778c839ef..9f96c50e43 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -9183,38 +9183,7 @@ "de": "Abonnement kündigen", "uk": "Скасувати підписку" }, - "PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE": { - "en": "Are you sure you want to cancel your subscription? You will lose access to premium features at the end of your current billing period.", - "ja": "サブスクリプションをキャンセルしてもよろしいですか?現在の請求期間の終了時にプレミアム機能へのアクセスを失います。", - "zh-CN": "您确定要取消订阅吗?您将在当前计费周期结束时失去对高级功能的访问权限。", - "zh-TW": "您確定要取消訂閱嗎?您將在當前計費週期結束時失去對高級功能的訪問權限。", - "ko-KR": "구독을 취소하시겠습니까? 현재 결제 기간이 끝나면 프리미엄 기능에 대한 액세스를 잃게 됩니다.", - "no": "Er du sikker på at du vil avbryte abonnementet? Du vil miste tilgang til premium-funksjoner ved slutten av gjeldende faktureringsperiode.", - "it": "Sei sicuro di voler annullare l'abbonamento? Perderai l'accesso alle funzionalità premium alla fine del periodo di fatturazione corrente.", - "pt": "Tem certeza de que deseja cancelar sua assinatura? Você perderá o acesso aos recursos premium no final do período de cobrança atual.", - "es": "¿Estás seguro de que quieres cancelar tu suscripción? Perderás el acceso a las funciones premium al final de tu período de facturación actual.", - "ar": "هل أنت متأكد من أنك تريد إلغاء اشتراكك؟ ستفقد الوصول إلى الميزات المميزة في نهاية فترة الفوترة الحالية.", - "fr": "Êtes-vous sûr de vouloir annuler votre abonnement ? Vous perdrez l'accès aux fonctionnalités premium à la fin de votre période de facturation actuelle.", - "tr": "Aboneliğinizi iptal etmek istediğinizden emin misiniz? Mevcut faturalama döneminizin sonunda premium özelliklere erişiminizi kaybedeceksiniz.", - "de": "Sind Sie sicher, dass Sie Ihr Abonnement kündigen möchten? Sie verlieren den Zugang zu Premium-Funktionen am Ende Ihres aktuellen Abrechnungszeitraums.", - "uk": "Ві впевнені, що хочете скасувати підписку? Ви втратите доступ до преміум-функцій наприкінці поточного розрахункового періоду." - }, - "PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE_WITH_DATE": { - "en": "Are you sure you want to cancel your subscription? You will lose access to premium features on {{date}}.", - "ja": "サブスクリプションをキャンセルしてもよろしいですか?{{date}}にプレミアム機能へのアクセスを失います。", - "zh-CN": "您确定要取消订阅吗?您将在{{date}}失去对高级功能的访问权限。", - "zh-TW": "您確定要取消訂閱嗎?您將在{{date}}失去對高級功能的訪問權限。", - "ko-KR": "구독을 취소하시겠습니까? {{date}}에 프리미엄 기능에 대한 액세스를 잃게 됩니다.", - "no": "Er du sikker på at du vil avbryte abonnementet? Du vil miste tilgang til premium-funksjoner {{date}}.", - "it": "Sei sicuro di voler annullare l'abbonamento? Perderai l'accesso alle funzionalità premium il {{date}}.", - "pt": "Tem certeza de que deseja cancelar sua assinatura? Você perderá o acesso aos recursos premium em {{date}}.", - "es": "¿Estás seguro de que quieres cancelar tu suscripción? Perderás el acceso a las funciones premium el {{date}}.", - "ar": "هل أنت متأكد من أنك تريد إلغاء اشتراكك؟ ستفقد الوصول إلى الميزات المميزة في {{date}}.", - "fr": "Êtes-vous sûr de vouloir annuler votre abonnement ? Vous perdrez l'accès aux fonctionnalités premium le {{date}}.", - "tr": "Aboneliğinizi iptal etmek istediğinizden emin misiniz? {{date}} tarihinde premium özelliklere erişiminizi kaybedeceksiniz.", - "de": "Sind Sie sicher, dass Sie Ihr Abonnement kündigen möchten? Sie verlieren den Zugang zu Premium-Funktionen am {{date}}.", - "uk": "Ви впевнені, що хочете скасувати підписку? Ви втратите доступ до преміум-функцій {{date}}." - }, + "PAYMENT$SUBSCRIPTION_CANCELLED": { "en": "Subscription cancelled successfully", "ja": "サブスクリプションが正常にキャンセルされました", @@ -9231,22 +9200,7 @@ "de": "Abonnement erfolgreich gekündigt", "uk": "Підписку успішно скасовано" }, - "PAYMENT$SUBSCRIPTION_CANCELLED_EXPIRES": { - "en": "Your subscription has been cancelled and will expire on {{date}}", - "ja": "サブスクリプションはキャンセルされ、{{date}}に期限切れになります", - "zh-CN": "您的订阅已取消,将于{{date}}到期", - "zh-TW": "您的訂閱已取消,將於{{date}}到期", - "ko-KR": "구독이 취소되었으며 {{date}}에 만료됩니다", - "no": "Abonnementet ditt er avbrutt og utløper {{date}}", - "it": "Il tuo abbonamento è stato annullato e scadrà il {{date}}", - "pt": "Sua assinatura foi cancelada e expirará em {{date}}", - "es": "Tu suscripción ha sido cancelada y expirará el {{date}}", - "ar": "تم إلغاء اشتراكك وسينتهي في {{date}}", - "fr": "Votre abonnement a été annulé et expirera le {{date}}", - "tr": "Aboneliğiniz iptal edildi ve {{date}} tarihinde sona erecek", - "de": "Ihr Abonnement wurde gekündigt und läuft am {{date}} ab", - "uk": "Ваша підписка скасована і закінчиться {{date}}" - }, + "PAYMENT$NEXT_BILLING_DATE": { "en": "Next billing date: {{date}}", "ja": "次回請求日: {{date}}", diff --git a/frontend/src/mocks/billing-handlers.ts b/frontend/src/mocks/billing-handlers.ts index ee9418081b..0af1dbed3f 100644 --- a/frontend/src/mocks/billing-handlers.ts +++ b/frontend/src/mocks/billing-handlers.ts @@ -1,101 +1,22 @@ import { delay, http, HttpResponse } from "msw"; -import { SubscriptionAccess } from "#/api/billing-service/billing.types"; -// Mock data for different subscription scenarios -const MOCK_ACTIVE_SUBSCRIPTION: SubscriptionAccess = { - start_at: "2024-01-01T00:00:00Z", - end_at: "2024-12-31T23:59:59Z", - created_at: "2024-01-01T00:00:00Z", - cancelled_at: null, - stripe_subscription_id: "sub_mock123456789", -}; +export const BILLING_HANDLERS = [ + http.get("/api/billing/credits", async () => { + await delay(); + return HttpResponse.json({ credits: "100" }); + }), -const MOCK_CANCELLED_SUBSCRIPTION: SubscriptionAccess = { - start_at: "2024-01-01T00:00:00Z", - end_at: "2025-01-01T23:59:59Z", - created_at: "2024-01-01T00:00:00Z", - cancelled_at: "2024-06-15T10:30:00Z", - stripe_subscription_id: "sub_mock123456789", -}; + http.post("/api/billing/create-checkout-session", async () => { + await delay(); + return HttpResponse.json({ + redirect_url: "https://stripe.com/some-checkout", + }); + }), -// Expired subscription (end_at < now) - will be filtered out by backend logic -const MOCK_EXPIRED_SUBSCRIPTION: SubscriptionAccess = { - start_at: "2024-01-01T00:00:00Z", - end_at: "2024-06-01T00:00:00Z", // Expired - created_at: "2024-01-01T00:00:00Z", - cancelled_at: null, - stripe_subscription_id: "sub_mock123456789", -}; - -// Helper function to check if subscription is currently active (matches backend logic) -function isSubscriptionActive( - subscription: SubscriptionAccess | null, -): boolean { - if (!subscription) return false; - - const now = new Date(); - const startAt = new Date(subscription.start_at); - const endAt = new Date(subscription.end_at); - - // Backend filters: status == 'ACTIVE' AND start_at <= now AND end_at >= now - return startAt <= now && endAt >= now; -} - -// Factory function to create billing handlers with different subscription states -function createBillingHandlers(subscriptionData: SubscriptionAccess | null) { - return [ - http.get("/api/billing/credits", async () => { - await delay(); - return HttpResponse.json({ credits: "100" }); - }), - - http.get("/api/billing/subscription-access", async () => { - await delay(); - // Apply backend filtering logic - only return if subscription is currently active - const activeSubscription = isSubscriptionActive(subscriptionData) - ? subscriptionData - : null; - return HttpResponse.json(activeSubscription); - }), - - http.post("/api/billing/create-checkout-session", async () => { - await delay(); - return HttpResponse.json({ - redirect_url: "https://stripe.com/some-checkout", - }); - }), - - http.post("/api/billing/subscription-checkout-session", async () => { - await delay(); - return HttpResponse.json({ - redirect_url: "https://stripe.com/some-subscription-checkout", - }); - }), - - http.post("/api/billing/create-customer-setup-session", async () => { - await delay(); - return HttpResponse.json({ - redirect_url: "https://stripe.com/some-customer-setup", - }); - }), - - http.post("/api/billing/cancel-subscription", async () => { - await delay(); - return HttpResponse.json({ - status: "success", - message: "Subscription cancelled successfully", - }); - }), - ]; -} - -// Export different handler sets for different testing scenarios -export const STRIPE_BILLING_HANDLERS = createBillingHandlers( - MOCK_ACTIVE_SUBSCRIPTION, -); -export const STRIPE_BILLING_HANDLERS_NO_SUBSCRIPTION = - createBillingHandlers(null); -export const STRIPE_BILLING_HANDLERS_CANCELLED_SUBSCRIPTION = - createBillingHandlers(MOCK_CANCELLED_SUBSCRIPTION); -export const STRIPE_BILLING_HANDLERS_EXPIRED_SUBSCRIPTION = - createBillingHandlers(MOCK_EXPIRED_SUBSCRIPTION); // This will return null due to filtering + http.post("/api/billing/create-customer-setup-session", async () => { + await delay(); + return HttpResponse.json({ + redirect_url: "https://stripe.com/some-customer-setup", + }); + }), +]; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index afaf4c1587..6936b283e9 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,5 +1,5 @@ import { API_KEYS_HANDLERS } from "./api-keys-handlers"; -import { STRIPE_BILLING_HANDLERS } from "./billing-handlers"; +import { BILLING_HANDLERS } from "./billing-handlers"; import { FILE_SERVICE_HANDLERS } from "./file-service-handlers"; import { TASK_SUGGESTIONS_HANDLERS } from "./task-suggestions-handlers"; import { SECRETS_HANDLERS } from "./secrets-handlers"; @@ -16,7 +16,7 @@ import { ANALYTICS_HANDLERS } from "./analytics-handlers"; export const handlers = [ ...API_KEYS_HANDLERS, - ...STRIPE_BILLING_HANDLERS, + ...BILLING_HANDLERS, ...FILE_SERVICE_HANDLERS, ...TASK_SUGGESTIONS_HANDLERS, ...SECRETS_HANDLERS,