161 lines
5.2 KiB
TypeScript
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,
|
|
};
|
|
}
|