finalizing subscription moduile, ready to test
This commit is contained in:
@@ -115,9 +115,19 @@ export default function OrderDetails() {
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold">Order #{order.order_number}</h1>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(order.status)}`}>
|
||||
{order.status.replace('-', ' ').toUpperCase()}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{['pending', 'failed', 'on-hold'].includes(order.status) && (
|
||||
<Link
|
||||
to={`/checkout/pay/${order.id}`}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground text-sm font-medium rounded-lg hover:bg-primary/90 transition-colors shadow-sm"
|
||||
>
|
||||
Pay Now
|
||||
</Link>
|
||||
)}
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(order.status)}`}>
|
||||
{order.status.replace('-', ' ').toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mb-6">
|
||||
|
||||
377
customer-spa/src/pages/Account/SubscriptionDetail.tsx
Normal file
377
customer-spa/src/pages/Account/SubscriptionDetail.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api/client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { ArrowLeft, Repeat, Pause, Play, XCircle, Calendar, Package, CreditCard, FileText } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import SEOHead from '@/components/SEOHead';
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
|
||||
interface SubscriptionOrder {
|
||||
id: number;
|
||||
order_id: number;
|
||||
order_type: 'parent' | 'renewal' | 'switch' | 'resubscribe';
|
||||
order_status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
id: number;
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
product_image: string;
|
||||
status: string;
|
||||
billing_period: string;
|
||||
billing_interval: number;
|
||||
billing_schedule: string;
|
||||
recurring_amount: string;
|
||||
start_date: string;
|
||||
trial_end_date: string | null;
|
||||
next_payment_date: string | null;
|
||||
end_date: string | null;
|
||||
last_payment_date: string | null;
|
||||
payment_method: string;
|
||||
pause_count: number;
|
||||
can_pause: boolean;
|
||||
can_resume: boolean;
|
||||
can_cancel: boolean;
|
||||
orders: SubscriptionOrder[];
|
||||
}
|
||||
|
||||
const statusStyles: Record<string, string> = {
|
||||
'pending': 'bg-yellow-100 text-yellow-800',
|
||||
'active': 'bg-green-100 text-green-800',
|
||||
'on-hold': 'bg-blue-100 text-blue-800',
|
||||
'cancelled': 'bg-gray-100 text-gray-800',
|
||||
'expired': 'bg-red-100 text-red-800',
|
||||
'pending-cancel': 'bg-orange-100 text-orange-800',
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
'pending': 'Pending',
|
||||
'active': 'Active',
|
||||
'on-hold': 'On Hold',
|
||||
'cancelled': 'Cancelled',
|
||||
'expired': 'Expired',
|
||||
'pending-cancel': 'Pending Cancel',
|
||||
};
|
||||
|
||||
const orderTypeLabels: Record<string, string> = {
|
||||
'parent': 'Initial Order',
|
||||
'renewal': 'Renewal',
|
||||
'switch': 'Plan Switch',
|
||||
'resubscribe': 'Resubscribe',
|
||||
};
|
||||
|
||||
export default function SubscriptionDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
|
||||
const { data: subscription, isLoading, error } = useQuery<Subscription>({
|
||||
queryKey: ['account-subscription', id],
|
||||
queryFn: () => api.get(`/account/subscriptions/${id}`),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const actionMutation = useMutation({
|
||||
mutationFn: (action: string) => api.post(`/account/subscriptions/${id}/${action}`),
|
||||
onSuccess: (_, action) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['account-subscription', id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['account-subscriptions'] });
|
||||
const actionLabels: Record<string, string> = {
|
||||
'pause': 'paused',
|
||||
'resume': 'resumed',
|
||||
'cancel': 'cancelled',
|
||||
};
|
||||
toast.success(`Subscription ${actionLabels[action] || action} successfully`);
|
||||
setActionLoading(false);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message || 'Action failed');
|
||||
setActionLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
setActionLoading(true);
|
||||
actionMutation.mutate(action);
|
||||
};
|
||||
|
||||
const handleRenew = async () => {
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const response = await api.post<{ order_id: number; status: string }>(`/account/subscriptions/${id}/renew`);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['account-subscription', id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['account-subscriptions'] });
|
||||
|
||||
toast.success('Renewal order created');
|
||||
|
||||
if (response.order_id) {
|
||||
// Determine destination based on functionality
|
||||
// If manual payment required or just improved UX, go to payment page
|
||||
navigate(`/order-pay/${response.order_id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to renew');
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Find pending renewal order
|
||||
const pendingRenewalOrder = subscription?.orders?.find(
|
||||
o => o.order_type === 'renewal' &&
|
||||
['pending', 'wc-pending', 'on-hold', 'wc-on-hold', 'failed', 'wc-failed'].includes(o.order_status)
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !subscription) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-500">Failed to load subscription</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => navigate('/my-account/subscriptions')}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Subscriptions
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SEOHead title={`Subscription #${subscription.id}`} description="Subscription details" />
|
||||
|
||||
{/* Back button */}
|
||||
<Link
|
||||
to="/my-account/subscriptions"
|
||||
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Back to Subscriptions
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Repeat className="h-6 w-6" />
|
||||
Subscription #{subscription.id}
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Started {new Date(subscription.start_date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${statusStyles[subscription.status] || 'bg-gray-100'}`}>
|
||||
{statusLabels[subscription.status] || subscription.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Product Info Card */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
{subscription.product_image ? (
|
||||
<img
|
||||
src={subscription.product_image}
|
||||
alt={subscription.product_name}
|
||||
className="w-20 h-20 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 bg-gray-100 rounded flex items-center justify-center">
|
||||
<Package className="h-10 w-10 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold">{subscription.product_name}</h2>
|
||||
<p className="text-gray-500">{subscription.billing_schedule}</p>
|
||||
<p className="text-2xl font-bold mt-2">
|
||||
{formatPrice(subscription.recurring_amount)}
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
/{subscription.billing_period}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Billing Details */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="font-semibold mb-4">Billing Details</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Start Date</p>
|
||||
<p className="font-medium">{new Date(subscription.start_date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
{subscription.next_payment_date && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Next Payment</p>
|
||||
<p className="font-medium flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4 text-gray-400" />
|
||||
{new Date(subscription.next_payment_date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{subscription.trial_end_date && new Date(subscription.trial_end_date) > new Date() && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Trial Ends</p>
|
||||
<p className="font-medium text-blue-600">
|
||||
{new Date(subscription.trial_end_date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{subscription.end_date && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">End Date</p>
|
||||
<p className="font-medium">{new Date(subscription.end_date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Payment Method</p>
|
||||
<p className="font-medium flex items-center gap-1">
|
||||
<CreditCard className="h-4 w-4 text-gray-400" />
|
||||
{subscription.payment_method || 'Not set'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Times Paused</p>
|
||||
<p className="font-medium">{subscription.pause_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{(subscription.can_pause || subscription.can_resume || subscription.can_cancel) && (
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="font-semibold mb-4">Manage Subscription</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
{subscription.can_pause && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleAction('pause')}
|
||||
disabled={actionLoading}
|
||||
>
|
||||
<Pause className="h-4 w-4 mr-2" />
|
||||
Pause Subscription
|
||||
</Button>
|
||||
)}
|
||||
{subscription.can_resume && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleAction('resume')}
|
||||
disabled={actionLoading}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Resume Subscription
|
||||
</Button>
|
||||
)}
|
||||
{/* Early Renewal Button */}
|
||||
{subscription.status === 'active' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRenew}
|
||||
disabled={actionLoading}
|
||||
>
|
||||
<Repeat className="h-4 w-4 mr-2" />
|
||||
Renew Early
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Pay Pending Order Button */}
|
||||
{pendingRenewalOrder && (
|
||||
<Button
|
||||
className='bg-green-600 hover:bg-green-700'
|
||||
onClick={() => navigate(`/order-pay/${pendingRenewalOrder.order_id}`)}
|
||||
>
|
||||
<CreditCard className="h-4 w-4 mr-2" />
|
||||
Pay Now (#{pendingRenewalOrder.order_id})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{subscription.can_cancel && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-500 hover:text-red-600 border-red-200 hover:border-red-300"
|
||||
disabled={actionLoading}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
Cancel Subscription
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Cancel Subscription</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to cancel this subscription?
|
||||
You will lose access at the end of your current billing period.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Keep Subscription</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleAction('cancel')}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
Yes, Cancel
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Related Orders */}
|
||||
{subscription.orders && subscription.orders.length > 0 && (
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="font-semibold mb-4 flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Payment History
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{subscription.orders.map((order) => (
|
||||
<Link
|
||||
key={order.id}
|
||||
to={`/my-account/orders/${order.order_id}`}
|
||||
className="flex items-center justify-between p-3 rounded border hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">Order #{order.order_id}</span>
|
||||
<span className="text-xs px-2 py-0.5 bg-gray-100 rounded">
|
||||
{orderTypeLabels[order.order_type] || order.order_type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{new Date(order.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
244
customer-spa/src/pages/Account/Subscriptions.tsx
Normal file
244
customer-spa/src/pages/Account/Subscriptions.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '@/lib/api/client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Repeat, ChevronRight, Pause, Play, XCircle, Calendar, Package } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import SEOHead from '@/components/SEOHead';
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
|
||||
interface Subscription {
|
||||
id: number;
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
product_image: string;
|
||||
status: 'pending' | 'active' | 'on-hold' | 'cancelled' | 'expired' | 'pending-cancel';
|
||||
billing_schedule: string;
|
||||
recurring_amount: string;
|
||||
next_payment_date: string | null;
|
||||
start_date: string;
|
||||
end_date: string | null;
|
||||
can_pause: boolean;
|
||||
can_resume: boolean;
|
||||
can_cancel: boolean;
|
||||
}
|
||||
|
||||
const statusStyles: Record<string, string> = {
|
||||
'pending': 'bg-yellow-100 text-yellow-800',
|
||||
'active': 'bg-green-100 text-green-800',
|
||||
'on-hold': 'bg-blue-100 text-blue-800',
|
||||
'cancelled': 'bg-gray-100 text-gray-800',
|
||||
'expired': 'bg-red-100 text-red-800',
|
||||
'pending-cancel': 'bg-orange-100 text-orange-800',
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
'pending': 'Pending',
|
||||
'active': 'Active',
|
||||
'on-hold': 'On Hold',
|
||||
'cancelled': 'Cancelled',
|
||||
'expired': 'Expired',
|
||||
'pending-cancel': 'Pending Cancel',
|
||||
};
|
||||
|
||||
export default function Subscriptions() {
|
||||
const queryClient = useQueryClient();
|
||||
const [actionLoading, setActionLoading] = useState<number | null>(null);
|
||||
|
||||
const { data: subscriptions = [], isLoading } = useQuery<Subscription[]>({
|
||||
queryKey: ['account-subscriptions'],
|
||||
queryFn: () => api.get('/account/subscriptions'),
|
||||
});
|
||||
|
||||
const actionMutation = useMutation({
|
||||
mutationFn: ({ id, action }: { id: number; action: string }) =>
|
||||
api.post(`/account/subscriptions/${id}/${action}`),
|
||||
onSuccess: (_, { action }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['account-subscriptions'] });
|
||||
const actionLabels: Record<string, string> = {
|
||||
'pause': 'paused',
|
||||
'resume': 'resumed',
|
||||
'cancel': 'cancelled',
|
||||
};
|
||||
toast.success(`Subscription ${actionLabels[action] || action} successfully`);
|
||||
setActionLoading(null);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message || 'Action failed');
|
||||
setActionLoading(null);
|
||||
},
|
||||
});
|
||||
|
||||
const handleAction = (id: number, action: string) => {
|
||||
setActionLoading(id);
|
||||
actionMutation.mutate({ id, action });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SEOHead title="My Subscriptions" description="Manage your subscriptions" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Repeat className="h-6 w-6" />
|
||||
My Subscriptions
|
||||
</h1>
|
||||
<p className="text-gray-500">
|
||||
Manage your recurring subscriptions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{subscriptions.length === 0 ? (
|
||||
<div className="bg-white rounded-lg border p-12 text-center">
|
||||
<Repeat className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-gray-500">You don't have any subscriptions yet.</p>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Purchase a subscription product to get started.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{subscriptions.map((sub) => (
|
||||
<div key={sub.id} className="bg-white rounded-lg border overflow-hidden">
|
||||
<div className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Product Image */}
|
||||
{sub.product_image ? (
|
||||
<img
|
||||
src={sub.product_image}
|
||||
alt={sub.product_name}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 bg-gray-100 rounded flex items-center justify-center">
|
||||
<Package className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subscription Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold">{sub.product_name}</h3>
|
||||
<p className="text-sm text-gray-500">{sub.billing_schedule}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${statusStyles[sub.status] || 'bg-gray-100'}`}>
|
||||
{statusLabels[sub.status] || sub.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-6 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Amount: </span>
|
||||
<span className="font-medium">
|
||||
{formatPrice(sub.recurring_amount)}
|
||||
</span>
|
||||
</div>
|
||||
{sub.next_payment_date && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-gray-500">Next: </span>
|
||||
<span className="font-medium">
|
||||
{new Date(sub.next_payment_date).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-4 pt-4 border-t flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{sub.can_pause && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAction(sub.id, 'pause')}
|
||||
disabled={actionLoading === sub.id}
|
||||
>
|
||||
<Pause className="h-4 w-4 mr-1" />
|
||||
Pause
|
||||
</Button>
|
||||
)}
|
||||
{sub.can_resume && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAction(sub.id, 'resume')}
|
||||
disabled={actionLoading === sub.id}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
)}
|
||||
{sub.can_cancel && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-500 hover:text-red-600"
|
||||
disabled={actionLoading === sub.id}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-1" />
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Cancel Subscription</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to cancel this subscription?
|
||||
You will lose access at the end of your current billing period.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Keep Subscription</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleAction(sub.id, 'cancel')}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
Yes, Cancel
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`/my-account/subscriptions/${sub.id}`}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
|
||||
>
|
||||
View Details
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ReactNode, useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut, Key } from 'lucide-react';
|
||||
import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut, Key, Repeat } from 'lucide-react';
|
||||
import { useModules } from '@/hooks/useModules';
|
||||
import { api } from '@/lib/api/client';
|
||||
import {
|
||||
@@ -50,17 +50,19 @@ export function AccountLayout({ children }: AccountLayoutProps) {
|
||||
const allMenuItems = [
|
||||
{ id: 'dashboard', label: 'Dashboard', path: '/my-account', icon: LayoutDashboard },
|
||||
{ id: 'orders', label: 'Orders', path: '/my-account/orders', icon: ShoppingBag },
|
||||
{ id: 'subscriptions', label: 'Subscriptions', path: '/my-account/subscriptions', icon: Repeat },
|
||||
{ id: 'licenses', label: 'Licenses', path: '/my-account/licenses', icon: Key },
|
||||
{ id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download },
|
||||
{ id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin },
|
||||
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
|
||||
{ id: 'licenses', label: 'Licenses', path: '/my-account/licenses', icon: Key },
|
||||
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
|
||||
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
|
||||
];
|
||||
|
||||
// Filter out wishlist if module disabled or settings disabled, licenses if licensing disabled
|
||||
const menuItems = allMenuItems.filter(item => {
|
||||
if (item.id === 'wishlist') return isEnabled('wishlist') && wishlistEnabled;
|
||||
if (item.id === 'licenses') return isEnabled('licensing');
|
||||
if (item.id === 'subscriptions') return isEnabled('subscription');
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import Addresses from './Addresses';
|
||||
import Wishlist from './Wishlist';
|
||||
import AccountDetails from './AccountDetails';
|
||||
import Licenses from './Licenses';
|
||||
import Subscriptions from './Subscriptions';
|
||||
import SubscriptionDetail from './SubscriptionDetail';
|
||||
|
||||
export default function Account() {
|
||||
const user = (window as any).woonoowCustomer?.user;
|
||||
@@ -32,6 +34,8 @@ export default function Account() {
|
||||
<Route path="addresses" element={<Addresses />} />
|
||||
<Route path="wishlist" element={<Wishlist />} />
|
||||
<Route path="licenses" element={<Licenses />} />
|
||||
<Route path="subscriptions" element={<Subscriptions />} />
|
||||
<Route path="subscriptions/:id" element={<SubscriptionDetail />} />
|
||||
<Route path="account-details" element={<AccountDetails />} />
|
||||
<Route path="*" element={<Navigate to="/my-account" replace />} />
|
||||
</Routes>
|
||||
|
||||
Reference in New Issue
Block a user