211 lines
7.7 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { PageLayout } from "@/components/layout/PageLayout";
import { ErrorBoundary } from "@/components/common";
import { SubCard } from "@/components/ui/sub-card";
import { useSession } from "@/features/auth/hooks";
import { useAuthStore } from "@/features/auth/services/auth.store";
// ApiClientError import removed - using generic error handling
import { apiClient } from "@/core/api";
import { openSsoLink } from "@/features/billing/utils/sso";
import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
import { PaymentMethodCard, usePaymentMethods } from "@/features/billing";
import { CreditCardIcon, PlusIcon } from "@heroicons/react/24/outline";
import { InlineToast } from "@/components/ui/inline-toast";
import { SectionHeader } from "@/components/common";
import { Button } from "@/components/ui/button";
import { AsyncBlock } from "@/components/common/AsyncBlock";
import { LoadingCard, Skeleton } from "@/components/ui/loading-skeleton";
import { logger } from "@customer-portal/logging";
import { EmptyState } from "@/components/ui/empty-state";
export function PaymentMethodsContainer() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { isAuthenticated } = useSession();
const paymentMethodsQuery = usePaymentMethods();
const {
data: paymentMethodsData,
isLoading: isLoadingPaymentMethods,
isFetching: isFetchingPaymentMethods,
error: paymentMethodsError,
} = paymentMethodsQuery;
// Auth hydration flag to avoid showing empty state before auth is checked
const { hasCheckedAuth } = useAuthStore();
const paymentRefresh = usePaymentRefresh({
refetch: async () => {
const result = await paymentMethodsQuery.refetch();
return { data: result.data };
},
hasMethods: (data?: { totalCount?: number }) => !!data && (data.totalCount || 0) > 0,
attachFocusListeners: true,
});
const openPaymentMethods = async () => {
try {
setIsLoading(true);
setError(null);
if (!isAuthenticated) {
setError("Please log in to access payment methods.");
setIsLoading(false);
return;
}
const response = await apiClient.POST('/auth/sso-link', {
body: { path: "index.php?rp=/account/paymentmethods" }
});
const sso = response.data!;
openSsoLink(sso.url, { newTab: true });
setIsLoading(false);
} catch (error) {
logger.error(error, "Failed to open payment methods");
if (error && typeof error === 'object' && 'status' in error && (error as any).status === 401)
setError("Authentication failed. Please log in again.");
else setError("Unable to access payment methods. Please try again later.");
setIsLoading(false);
}
};
useEffect(() => {
// Placeholder hook for future logic when returning from WHMCS
}, [isAuthenticated]);
if (error || paymentMethodsError) {
const errorObj =
error || (paymentMethodsError instanceof Error ? paymentMethodsError : new Error("Unexpected error"));
return (
<PageLayout
icon={<CreditCardIcon />}
title="Payment Methods"
description="Manage your saved payment methods and billing information"
>
<AsyncBlock error={errorObj} variant="page">
<></>
</AsyncBlock>
</PageLayout>
);
}
return (
<PageLayout
icon={<CreditCardIcon />}
title="Payment Methods"
description="Manage your payment methods in the billing portal."
>
<ErrorBoundary>
<InlineToast
visible={paymentRefresh.toast.visible}
text={paymentRefresh.toast.text}
tone={paymentRefresh.toast.tone}
/>
{/* Simplified: remove verbose banner; controls exist via buttons */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
{!hasCheckedAuth || isLoadingPaymentMethods || isFetchingPaymentMethods ? (
<>
<LoadingCard />
<SubCard>
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-12" />
<div className="space-y-2">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-24" />
</div>
</div>
<Skeleton className="h-9 w-28" />
</div>
))}
</div>
</SubCard>
</>
) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? (
<SubCard
header={
<SectionHeader title="Your Payment Methods">
<Button
onClick={() => {
void openPaymentMethods();
}}
disabled={isLoading}
size="sm"
>
<PlusIcon className="w-4 h-4" />
Manage Cards
</Button>
</SectionHeader>
}
>
<div className="space-y-4">
{paymentMethodsData.paymentMethods.map(paymentMethod => (
<PaymentMethodCard
key={paymentMethod.id}
paymentMethod={paymentMethod}
className="focus:outline-none focus:ring-2 focus:ring-blue-500"
showActions={false}
/>
))}
</div>
</SubCard>
) : (
<SubCard>
{(!hasCheckedAuth && !paymentMethodsData) ? (
<AsyncBlock isLoading loadingText="Loading payment methods...">
<></>
</AsyncBlock>
) : (
<>
<EmptyState
icon={<CreditCardIcon className="h-12 w-12" />}
title="No Payment Methods"
description="Open the billing portal to add a card."
action={{
label: isLoading ? "Opening..." : "Manage Cards",
onClick: () => void openPaymentMethods(),
}}
/>
<p className="text-sm text-gray-500 text-center">Opens in a new tab for security</p>
</>
)}
</SubCard>
)}
</div>
<div className="space-y-6">
<div className="bg-blue-50 rounded-lg p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<CreditCardIcon className="h-5 w-5 text-blue-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-blue-800">Secure & Encrypted</h3>
<p className="text-sm text-blue-700 mt-1">
All payment information is securely encrypted and protected with industry-standard
security.
</p>
</div>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-800 mb-2">Supported Payment Methods</h3>
<ul className="text-sm text-gray-600 space-y-1">
<li> Credit Cards (Visa, MasterCard, American Express)</li>
<li> Debit Cards</li>
</ul>
</div>
</div>
</div>
</ErrorBoundary>
</PageLayout>
);
}
export default PaymentMethodsContainer;