Translate all hardcoded English strings to use i18n translation keys: Components: - TransactionDetailModal: payment details, refunds, technical info - ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup - StripeApiKeysForm: API key management - DomainPurchase: domain registration flow - Sidebar: navigation labels - Schedule/Sidebar, PendingSidebar: scheduler UI - MasqueradeBanner: masquerade status - Dashboard widgets: metrics, capacity, customers, tickets - Marketing: PricingTable, PluginShowcase, BenefitsSection - ConfirmationModal, ServiceList: common UI Pages: - Staff: invitation flow, role management - Customers: form placeholders - Payments: transactions, payouts, billing - BookingSettings: URL and redirect configuration - TrialExpired: upgrade prompts and features - PlatformSettings, PlatformBusinesses: admin UI - HelpApiDocs: API documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
227 lines
11 KiB
TypeScript
227 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate, useOutletContext } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Clock, ArrowRight, Check, X, CreditCard, TrendingDown, AlertTriangle } from 'lucide-react';
|
|
import { User, Business } from '../types';
|
|
|
|
/**
|
|
* TrialExpired Page
|
|
* Shown when a business trial has expired and they need to either upgrade or downgrade to free tier
|
|
*/
|
|
const TrialExpired: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { user, business } = useOutletContext<{ user: User; business: Business }>();
|
|
const isOwner = user.role === 'owner';
|
|
|
|
// Feature comparison based on tier
|
|
const getTierFeatures = (tier: string | undefined) => {
|
|
switch (tier) {
|
|
case 'Professional':
|
|
return [
|
|
{ name: t('trialExpired.features.professional.unlimitedAppointments'), included: true },
|
|
{ name: t('trialExpired.features.professional.onlineBooking'), included: true },
|
|
{ name: t('trialExpired.features.professional.emailNotifications'), included: true },
|
|
{ name: t('trialExpired.features.professional.smsReminders'), included: true },
|
|
{ name: t('trialExpired.features.professional.customBranding'), included: true },
|
|
{ name: t('trialExpired.features.professional.advancedAnalytics'), included: true },
|
|
{ name: t('trialExpired.features.professional.paymentProcessing'), included: true },
|
|
{ name: t('trialExpired.features.professional.prioritySupport'), included: true },
|
|
];
|
|
case 'Business':
|
|
return [
|
|
{ name: t('trialExpired.features.business.everythingInProfessional'), included: true },
|
|
{ name: t('trialExpired.features.business.multipleLocations'), included: true },
|
|
{ name: t('trialExpired.features.business.teamManagement'), included: true },
|
|
{ name: t('trialExpired.features.business.apiAccess'), included: true },
|
|
{ name: t('trialExpired.features.business.customDomain'), included: true },
|
|
{ name: t('trialExpired.features.business.whiteLabel'), included: true },
|
|
{ name: t('trialExpired.features.business.accountManager'), included: true },
|
|
];
|
|
case 'Enterprise':
|
|
return [
|
|
{ name: t('trialExpired.features.enterprise.everythingInBusiness'), included: true },
|
|
{ name: t('trialExpired.features.enterprise.unlimitedUsers'), included: true },
|
|
{ name: t('trialExpired.features.enterprise.customIntegrations'), included: true },
|
|
{ name: t('trialExpired.features.enterprise.slaGuarantee'), included: true },
|
|
{ name: t('trialExpired.features.enterprise.customContracts'), included: true },
|
|
{ name: t('trialExpired.features.enterprise.phoneSupport'), included: true },
|
|
{ name: t('trialExpired.features.enterprise.onPremise'), included: true },
|
|
];
|
|
default:
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const freeTierFeatures = [
|
|
{ name: t('trialExpired.features.free.upTo50Appointments'), included: true },
|
|
{ name: t('trialExpired.features.free.basicOnlineBooking'), included: true },
|
|
{ name: t('trialExpired.features.free.emailNotifications'), included: true },
|
|
{ name: t('trialExpired.features.free.smsReminders'), included: false },
|
|
{ name: t('trialExpired.features.free.customBranding'), included: false },
|
|
{ name: t('trialExpired.features.free.advancedAnalytics'), included: false },
|
|
{ name: t('trialExpired.features.free.paymentProcessing'), included: false },
|
|
{ name: t('trialExpired.features.free.prioritySupport'), included: false },
|
|
];
|
|
|
|
const paidTierFeatures = getTierFeatures(business.plan);
|
|
|
|
const handleUpgrade = () => {
|
|
navigate('/payments');
|
|
};
|
|
|
|
const handleDowngrade = () => {
|
|
if (window.confirm(t('trialExpired.confirmDowngrade'))) {
|
|
// TODO: Implement downgrade to free tier API call
|
|
console.log('Downgrading to free tier...');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center p-4">
|
|
<div className="max-w-4xl w-full">
|
|
{/* Main Card */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-red-500 to-orange-500 text-white p-8 text-center">
|
|
<div className="flex justify-center mb-4">
|
|
<div className="bg-white/20 backdrop-blur-sm rounded-full p-4">
|
|
<Clock size={48} />
|
|
</div>
|
|
</div>
|
|
<h1 className="text-3xl font-bold mb-2">{t('trialExpired.title')}</h1>
|
|
<p className="text-white/90 text-lg">
|
|
{t('trialExpired.subtitle', {
|
|
plan: business.plan,
|
|
date: business.trialEnd ? new Date(business.trialEnd).toLocaleDateString() : 'N/A'
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-8">
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t('trialExpired.whatHappensNow')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
{t('trialExpired.twoOptions')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Feature Comparison */}
|
|
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
|
{/* Free Tier Card */}
|
|
<div className="border-2 border-gray-200 dark:border-gray-700 rounded-xl p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">{t('trialExpired.freePlan')}</h3>
|
|
<span className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full text-sm font-medium">
|
|
{t('trialExpired.pricePerMonth')}
|
|
</span>
|
|
</div>
|
|
<ul className="space-y-3 mb-6">
|
|
{freeTierFeatures.map((feature, idx) => (
|
|
<li key={idx} className="flex items-start gap-2">
|
|
{feature.included ? (
|
|
<Check size={20} className="text-green-500 flex-shrink-0 mt-0.5" />
|
|
) : (
|
|
<X size={20} className="text-gray-400 flex-shrink-0 mt-0.5" />
|
|
)}
|
|
<span className={feature.included ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400 dark:text-gray-500 line-through'}>
|
|
{feature.name}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{isOwner && (
|
|
<button
|
|
onClick={handleDowngrade}
|
|
className="w-full px-4 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors font-medium flex items-center justify-center gap-2"
|
|
>
|
|
<TrendingDown size={20} />
|
|
{t('trialExpired.downgradeToFree')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Paid Tier Card */}
|
|
<div className="border-2 border-blue-500 dark:border-blue-400 rounded-xl p-6 bg-blue-50/50 dark:bg-blue-900/20 relative">
|
|
<div className="absolute top-4 right-4">
|
|
<span className="px-3 py-1 bg-blue-500 text-white rounded-full text-xs font-bold uppercase tracking-wide">
|
|
{t('trialExpired.recommended')}
|
|
</span>
|
|
</div>
|
|
<div className="mb-4">
|
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-1">
|
|
{business.plan} {t('common.plan', { defaultValue: 'Plan' })}
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">{t('trialExpired.continueWhereYouLeftOff')}</p>
|
|
</div>
|
|
<ul className="space-y-3 mb-6">
|
|
{paidTierFeatures.slice(0, 8).map((feature, idx) => (
|
|
<li key={idx} className="flex items-start gap-2">
|
|
<Check size={20} className="text-blue-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-gray-700 dark:text-gray-300">{feature.name}</span>
|
|
</li>
|
|
))}
|
|
{paidTierFeatures.length > 8 && (
|
|
<li className="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
{t('trialExpired.moreFeatures', { count: paidTierFeatures.length - 8 })}
|
|
</li>
|
|
)}
|
|
</ul>
|
|
{isOwner && (
|
|
<button
|
|
onClick={handleUpgrade}
|
|
className="w-full px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-lg hover:from-blue-700 hover:to-blue-600 transition-all font-medium flex items-center justify-center gap-2 shadow-lg shadow-blue-500/30"
|
|
>
|
|
<CreditCard size={20} />
|
|
{t('trialExpired.upgradeNow')}
|
|
<ArrowRight size={20} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Important Notice */}
|
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
|
<div className="flex gap-3">
|
|
<div className="flex-shrink-0">
|
|
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">
|
|
{isOwner
|
|
? t('trialExpired.ownerLimitedFunctionality')
|
|
: t('trialExpired.nonOwnerContactOwner')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{!isOwner && (
|
|
<div className="mt-6 text-center">
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
{t('trialExpired.businessOwner')} <span className="font-semibold text-gray-900 dark:text-white">{business.name}</span>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="text-center mt-6">
|
|
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
|
{t('trialExpired.supportQuestion')}{' '}
|
|
<a href={`mailto:${t('trialExpired.supportEmail')}`} className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
{t('trialExpired.supportEmail')}
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TrialExpired;
|