barsa 7502068ea9 refactor: remove unused billing and payment components, enhance animation capabilities
- Deleted loading and page components for invoices and payment methods to streamline the billing section.
- Updated AnimatedContainer, InlineToast, and other components to utilize framer-motion for improved animations.
- Refactored AppShell and Sidebar components to enhance layout and integrate new animation features.
- Adjusted various sections across the portal to ensure consistent animation behavior and visual appeal.
2026-03-06 14:48:34 +09:00

178 lines
6.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { CreditCardIcon } from "@heroicons/react/24/outline";
import { PageLayout } from "@/components/templates/PageLayout";
import { ErrorBoundary } from "@/components/molecules";
import { useSession } from "@/features/auth/hooks";
import { useAuthStore } from "@/features/auth/stores/auth.store";
import { isApiError } from "@/core/api";
import { openSsoLink } from "@/features/billing/utils/sso";
import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
import {
PaymentMethodCard,
usePaymentMethods,
useCreatePaymentMethodsSsoLink,
} from "@/features/billing";
import type { PaymentMethodList } from "@customer-portal/domain/payments";
import { InlineToast } from "@/components/atoms/inline-toast";
import { Button } from "@/components/atoms/button";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
import { logger } from "@/core/logger";
function PaymentMethodsSkeleton() {
return (
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-6">
<div className="flex items-center justify-between mb-6">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-10 w-32" />
</div>
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="bg-muted rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-24" />
</div>
</div>
<Skeleton className="h-9 w-28" />
</div>
</div>
))}
</div>
</div>
);
}
function PaymentMethodsSection({
paymentMethodsData,
onManage,
isPending,
}: {
paymentMethodsData: PaymentMethodList;
onManage: () => void;
isPending: boolean;
}) {
const hasMethods = paymentMethodsData.paymentMethods.length > 0;
return (
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
<div className="px-6 py-5 border-b border-border">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-foreground">Payment Methods</h2>
<p className="text-sm text-muted-foreground mt-0.5">
{hasMethods
? `${paymentMethodsData.paymentMethods.length} payment method${paymentMethodsData.paymentMethods.length === 1 ? "" : "s"} on file`
: "No payment methods on file"}
</p>
</div>
<Button onClick={onManage} disabled={isPending} size="default">
{isPending ? "Opening..." : "Manage Cards"}
</Button>
</div>
</div>
{hasMethods && (
<div className="p-6">
<div className="space-y-4">
{paymentMethodsData.paymentMethods.map(paymentMethod => (
<PaymentMethodCard key={paymentMethod.id} paymentMethod={paymentMethod} />
))}
</div>
</div>
)}
</div>
);
}
export function BillingOverview() {
const [error, setError] = useState<string | null>(null);
const { isAuthenticated } = useSession();
const paymentMethodsQuery = usePaymentMethods();
const {
data: paymentMethodsData,
isLoading: isLoadingPaymentMethods,
isFetching: isFetchingPaymentMethods,
error: paymentMethodsError,
} = paymentMethodsQuery;
const createPaymentMethodsSsoLink = useCreatePaymentMethodsSsoLink();
const { hasCheckedAuth } = useAuthStore();
const paymentRefresh = usePaymentRefresh({
refetch: async () => {
const result = await paymentMethodsQuery.refetch();
return { data: result.data };
},
hasMethods: data => Boolean(data && (data.totalCount > 0 || data.paymentMethods.length > 0)),
attachFocusListeners: true,
});
const openPaymentMethods = async () => {
if (!isAuthenticated) {
setError("Please log in to access payment methods.");
return;
}
setError(null);
try {
const ssoLink = await createPaymentMethodsSsoLink.mutateAsync();
openSsoLink(ssoLink.url, { newTab: true });
} catch (err: unknown) {
logger.error("Failed to open payment methods", err);
if (
isApiError(err) &&
"response" in err &&
typeof err.response === "object" &&
err.response !== null &&
"status" in err.response &&
err.response.status === 401
) {
setError("Authentication failed. Please log in again.");
} else {
setError("Unable to access payment methods. Please try again later.");
}
}
};
const isPaymentLoading = !hasCheckedAuth || isLoadingPaymentMethods || isFetchingPaymentMethods;
const combinedError = (() => {
if (error) return new Error(error);
if (paymentMethodsError instanceof Error) return paymentMethodsError;
if (paymentMethodsError) return new Error(String(paymentMethodsError));
return null;
})();
return (
<PageLayout icon={<CreditCardIcon />} title="Billing" error={combinedError}>
<ErrorBoundary>
<InlineToast
visible={paymentRefresh.toast.visible}
text={paymentRefresh.toast.text}
tone={paymentRefresh.toast.tone}
/>
<div className="space-y-8">
{isPaymentLoading && <PaymentMethodsSkeleton />}
{!isPaymentLoading && paymentMethodsData && (
<PaymentMethodsSection
paymentMethodsData={paymentMethodsData}
onManage={() => void openPaymentMethods()}
isPending={createPaymentMethodsSsoLink.isPending}
/>
)}
<div>
<h2 className="text-lg font-semibold text-foreground mb-4">Invoices</h2>
<InvoicesList />
</div>
</div>
</ErrorBoundary>
</PageLayout>
);
}
export default BillingOverview;