ui-nyla/src/hooks/useSubscription.ts
2026-03-22 17:23:47 +01:00

161 lines
5.2 KiB
TypeScript

/**
* useSubscription Hook — state-machine-aligned subscription management.
*
* Exposes the operative subscription, any scheduled successor, available plans,
* and ID-based mutation functions (activate, cancel, reactivate).
*/
import { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi';
import {
fetchSelectablePlans,
fetchSubscriptionStatus,
activatePlan as activatePlanApi,
cancelSubscription as cancelSubscriptionApi,
reactivateSubscription as reactivateSubscriptionApi,
verifyCheckout as verifyCheckoutApi,
type SubscriptionPlan,
type MandateSubscription,
type SubscriptionStatusResponse,
} from '../api/subscriptionApi';
export interface UseSubscriptionReturn {
plans: SubscriptionPlan[];
subscription: MandateSubscription | null;
plan: SubscriptionPlan | null;
scheduled: MandateSubscription | null;
active: boolean;
loading: boolean;
error: string | null;
loadPlans: () => Promise<void>;
loadStatus: () => Promise<void>;
activatePlan: (planKey: string) => Promise<void>;
cancelSubscription: (subscriptionId: string) => Promise<void>;
reactivateSubscription: (subscriptionId: string) => Promise<void>;
verifyCheckout: (sessionId: string) => Promise<{ status: string; message: string }>;
refetch: () => Promise<void>;
}
export function useSubscription(mandateId?: string): UseSubscriptionReturn {
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const [subscription, setSubscription] = useState<MandateSubscription | null>(null);
const [plan, setPlan] = useState<SubscriptionPlan | null>(null);
const [scheduled, setScheduled] = useState<MandateSubscription | null>(null);
const [active, setActive] = useState(false);
const { request, isLoading: loading, error: apiError } = useApiRequest();
const [error, setError] = useState<string | null>(null);
const loadPlans = useCallback(async () => {
try {
const data = await fetchSelectablePlans(request, mandateId);
setPlans(Array.isArray(data) ? data : []);
} catch (err) {
console.error('Error loading plans:', err);
setPlans([]);
}
}, [request, mandateId]);
const loadStatus = useCallback(async () => {
try {
const data: SubscriptionStatusResponse = await fetchSubscriptionStatus(request, mandateId);
setActive(data.active);
setSubscription(data.subscription ?? null);
setPlan(data.plan ?? null);
setScheduled(data.scheduled ?? null);
} catch (err) {
console.error('Error loading subscription status:', err);
setActive(false);
setSubscription(null);
setPlan(null);
setScheduled(null);
}
}, [request, mandateId]);
const activatePlan = useCallback(async (planKey: string) => {
try {
setError(null);
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('success');
currentUrl.searchParams.delete('canceled');
currentUrl.searchParams.delete('session_id');
currentUrl.searchParams.set('tab', 'subscription');
if (mandateId) currentUrl.searchParams.set('mandate', mandateId);
const returnUrl = `${currentUrl.origin}${currentUrl.pathname}${currentUrl.search}`;
const result = await activatePlanApi(request, planKey, mandateId, returnUrl);
if (result?.redirectUrl) {
window.location.href = result.redirectUrl;
return;
}
await loadStatus();
} catch (err: any) {
const msg = err?.response?.data?.detail || err.message || 'Fehler beim Aktivieren';
setError(msg);
throw err;
}
}, [request, mandateId, loadStatus]);
const cancelSub = useCallback(async (subscriptionId: string) => {
try {
setError(null);
await cancelSubscriptionApi(request, subscriptionId, mandateId);
await loadStatus();
} catch (err: any) {
const msg = err?.response?.data?.detail || err.message || 'Fehler beim Kündigen';
setError(msg);
throw err;
}
}, [request, mandateId, loadStatus]);
const reactivateSub = useCallback(async (subscriptionId: string) => {
try {
setError(null);
await reactivateSubscriptionApi(request, subscriptionId, mandateId);
await loadStatus();
} catch (err: any) {
const msg = err?.response?.data?.detail || err.message || 'Fehler beim Reaktivieren';
setError(msg);
throw err;
}
}, [request, mandateId, loadStatus]);
const verifyCheckout = useCallback(async (sessionId: string) => {
const result = await verifyCheckoutApi(request, sessionId, mandateId);
await loadStatus();
return result;
}, [request, mandateId, loadStatus]);
const refetch = useCallback(async () => {
await Promise.all([loadPlans(), loadStatus()]);
}, [loadPlans, loadStatus]);
useEffect(() => {
if (mandateId) {
loadPlans();
loadStatus();
} else {
setPlans([]);
setSubscription(null);
setPlan(null);
setScheduled(null);
setActive(false);
}
}, [mandateId]);
return {
plans,
subscription,
plan,
scheduled,
active,
loading,
error: error || (apiError ? String(apiError) : null),
loadPlans,
loadStatus,
activatePlan,
cancelSubscription: cancelSub,
reactivateSubscription: reactivateSub,
verifyCheckout,
refetch,
};
}