Compare commits

...

5 Commits

Author SHA1 Message Date
openhands
890de1b121 Fix: Remove remaining subscription-related mock handlers 2026-02-28 05:36:43 +00:00
openhands
c4e9aa6f3c Merge main into remove-unused-subscription-code 2026-02-28 04:51:08 +00:00
Shruti Jha
ab2a085da3 Fix billing mocks and formatting after removing subscription code 2026-01-23 00:33:15 +05:30
Shruti Jha
1a891b62e7 Fix billing mocks after removing subscription types 2026-01-22 22:41:59 +05:30
Shruti Jha
a7514ee96d Remove unused subscription-related frontend code 2026-01-22 22:09:56 +05:30
7 changed files with 7 additions and 242 deletions

View File

@@ -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
@@ -49,36 +45,16 @@ class BillingService {
* Get the user's subscription access information
* @returns The user's subscription access details or null if not available
*/
static async getSubscriptionAccess(): Promise<SubscriptionAccess | null> {
const { data } = await openHands.get<SubscriptionAccess | null>(
"/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<CancelSubscriptionResponse> {
const { data } = await openHands.post<CancelSubscriptionResponse>(
"/api/billing/cancel-subscription",
);
return data;
}
}
export default BillingService;

View File

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

View File

@@ -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 (
<ModalBackdrop>
<div
data-testid="cancel-subscription-modal"
className="bg-base-secondary p-6 rounded-xl flex flex-col gap-4 border border-tertiary w-[500px]"
>
<h3 className="text-xl font-bold">
{t(I18nKey.PAYMENT$CANCEL_SUBSCRIPTION_TITLE)}
</h3>
<p className="text-sm">
{endDate ? (
<Trans
i18nKey={I18nKey.PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE_WITH_DATE}
values={{ date: endDate }}
components={{ date: <span className="underline" /> }}
/>
) : (
t(I18nKey.PAYMENT$CANCEL_SUBSCRIPTION_MESSAGE)
)}
</p>
<div className="w-full flex gap-2 mt-2">
<BrandButton
testId="confirm-cancel-button"
type="button"
variant="primary"
className="grow"
onClick={handleCancelSubscription}
isDisabled={cancelSubscriptionMutation.isPending}
>
{t(I18nKey.BUTTON$CONFIRM)}
</BrandButton>
<BrandButton
testId="modal-cancel-button"
type="button"
variant="secondary"
className="grow"
onClick={onClose}
isDisabled={cancelSubscriptionMutation.isPending}
>
{t(I18nKey.BUTTON$CANCEL)}
</BrandButton>
</div>
</div>
</ModalBackdrop>
);
}

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,14 @@
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",
};
// Mock data for credit balance
const MOCK_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",
};
// 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) {
// Factory function to create billing handlers
function createBillingHandlers() {
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);
return HttpResponse.json({ credits: MOCK_CREDITS });
}),
http.post("/api/billing/create-checkout-session", async () => {
@@ -65,37 +18,14 @@ function createBillingHandlers(subscriptionData: SubscriptionAccess | null) {
});
}),
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
// Export handler set for testing
export const STRIPE_BILLING_HANDLERS = createBillingHandlers();