import React, { useEffect, useState } from 'react'; import { FaCoins } from 'react-icons/fa'; import { useAppStateContext } from '../context/user.data.context'; import { formatDateLabel } from '../utils/dateUtils'; import { FREE_PLAN_SUMMARY, Plan, PlanType, basePlans, freePlan, isOnFreePlan, } from '../utils/plan'; import { cancelSubscription, paymentCheckout, } from '../service/user/user.service'; import { Check } from '../components/ui/Check'; import { recomaze_ai_personalization_env } from '../env'; import { getBaseUrl } from '../utils/domain'; import Button from '../components/widgets/Button'; import VoucherRedeem from '../components/pricing/VoucherRedeem'; import { AppSumoPricing } from '../components/pricing/AppSumoPricing'; import { NormalPricing } from '../components/pricing/NormalPricing'; import { LAUNCH_DISCOUNT_PERCENT } from '../components/launch-discount/plans'; import { CreditHistoryTable } from '../components/billing/CreditHistoryTable'; import { CreditCostList } from '../components/billing/CreditCostList'; import InfoTooltip from '../components/agent-analytics/InfoTooltip'; import type { ICreditBalance } from '../service/credits/credits.interface'; import { getCreditBalance } from '../service/credits/credits.service'; const formatUSD = (amount: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: amount % 1 === 0 ? 0 : 2, }).format(amount); const Pricing = (): JSX.Element => { const { user, fetchUser, token, clientId } = useAppStateContext(); const [liveCredits, setLiveCredits] = useState({}); useEffect(() => { if (!token) return; let cancelled = false; void (async () => { try { const balance = await getCreditBalance(token, clientId); if (!cancelled) setLiveCredits(balance); } catch { // Benign: credit balance unavailable for this store. } })(); return () => { cancelled = true; }; }, [token, clientId]); // The authoritative /v2/credits/balance endpoint wins over the user snapshot. const credits: ICreditBalance = { ...(user?.credits ?? {}), ...liveCredits, }; const creditsGranted = credits.granted ?? 0; const creditsUsed = credits.used ?? 0; const creditsBalance = typeof credits.balance === 'number' ? credits.balance : Math.max(0, creditsGranted - creditsUsed); const [cancelOpen, setCancelOpen] = useState(false); const [cancelStep, setCancelStep] = useState<1 | 2>(1); const [cancelReason, setCancelReason] = useState(''); const [cancelFeedback, setCancelFeedback] = useState(''); const [cancelling, setCancelling] = useState(false); const [cancelError, setCancelError] = useState(null); const [checkingOutKey, setCheckingOutKey] = useState(null); const plans = basePlans; const btnBase = 'inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition ' + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/30 focus-visible:ring-offset-2 ' + 'disabled:opacity-50 disabled:cursor-not-allowed'; const primaryBtn = `${btnBase} w-full bg-[#B7007C] text-white hover:bg-[#900063]`; const secondaryBtn = `${btnBase} w-full bg-white text-gray-900 ring-1 ring-gray-900/10 hover:bg-gray-900/5`; const isAppSumo = !!user?.is_app_sumo; const hasActiveSub = Boolean(user?.has_active_subscription); const isUserLoaded = !!user; const trialDaysLeft = Number.isFinite(user?.trial_days) ? (user?.trial_days as number) : 0; // App-side counter derived from the signup date. It grants no plan and no // extra credits, so it never means the merchant is on a paid tier. Only the // Stripe-native launch-discount trial below is a real trial. const hasTrialDaysLeft = trialDaysLeft > 0 && !hasActiveSub; const currentPlan = hasActiveSub ? plans.find(p => p.key === user?.plan_snapshot) || null : null; const billingLabel = user && user.plan_period_snapshot ? String(user.plan_period_snapshot).toUpperCase() === 'MONTHLY' ? 'Monthly' : 'Annual' : 'Monthly'; const stripeStatusRaw = (user?.stripe_subscription_status || '') .toString() .toLowerCase(); const isCanceled = stripeStatusRaw === 'canceled'; // Live access decides it; plan_snapshot and the Stripe status persist after a // subscription dies, so a lapsed merchant is on Free and must read that way. const onFreePlan: boolean = isUserLoaded && isOnFreePlan(user); const bannerPlan: Plan | null = currentPlan ?? (onFreePlan ? freePlan : null); const launchDiscountEligible = user?.launch_discount_status === 'ELIGIBLE'; const bannerMonthly = bannerPlan?.monthly || 0; const bannerDiscountedMonthly = Math.round(bannerMonthly * (1 - LAUNCH_DISCOUNT_PERCENT / 100) * 100) / 100; const autoRenew = user ? !user.stripe_cancel_at_period_end : null; const periodEndISO = user?.stripe_current_period_end || null; const accessUntilISO = user?.subscription_access_until || null; const remainingDays = typeof user?.subscription_remaining_days === 'number' ? user!.subscription_remaining_days : null; const previousPlan = plans.find(p => p.key === (user?.plan_snapshot as PlanType | undefined)) || null; const tookLaunchDiscount = user?.launch_discount_status === 'USED'; const launchTrial = tookLaunchDiscount && (stripeStatusRaw === 'trialing' || hasTrialDaysLeft); const launchPlan = currentPlan || previousPlan || bannerPlan; const launchMonthly = launchPlan?.monthly || 0; const launchFirstMonth = Math.round(launchMonthly * (1 - LAUNCH_DISCOUNT_PERCENT / 100) * 100) / 100; const statusTone = (s: string) => { switch (s) { case 'active': return 'bg-emerald-50 text-emerald-700 ring-emerald-600/20'; case 'trialing': return 'bg-blue-50 text-blue-700 ring-blue-600/20'; case 'past_due': return 'bg-amber-50 text-amber-700 ring-amber-600/20'; case 'unpaid': case 'incomplete': return 'bg-red-50 text-red-700 ring-red-600/20'; case 'canceled': case 'incomplete_expired': return 'bg-gray-100 text-gray-700 ring-gray-500/20'; default: return 'bg-gray-50 text-gray-600 ring-gray-500/10'; } }; type CheckoutResponse = { success: boolean; message: string; session?: { id: string; url?: string }; }; const onInitializePayment = async (priceId: string, planKey: PlanType) => { try { setCheckingOutKey(planKey); const url = getBaseUrl(recomaze_ai_personalization_env?.wp_api_url); const res: CheckoutResponse = await paymentCheckout({ price_id: priceId, cancel_url: `${url}/wp-admin/admin.php?page=recomaze#/agent-analytics`, success_url: `${url}/wp-admin/admin.php?page=recomaze#/agent-analytics`, }); if (!res?.success || !res.session) { throw new Error(res?.message || 'Failed to create checkout session'); } if (res.session.url) { window.location.replace(res.session.url); return; } throw new Error('No checkout URL returned'); } catch (error) { console.error('payment error:', error); } finally { setCheckingOutKey(null); } }; const handleCancelConfirm = async () => { if (!user?.subscription_id) return; try { setCancelling(true); setCancelError(null); const response = await cancelSubscription(user.subscription_id); if (response?.success) { await fetchUser(); } else if (response?.message) { setCancelError(response.message); } } catch (e) { setCancelError(String(e)); } finally { setCancelling(false); setCancelOpen(false); setCancelStep(1); setCancelReason(''); setCancelFeedback(''); } }; const scrollToPlans = () => { const el = document.getElementById('plans-grid'); if (el) el.scrollIntoView({ behavior: 'smooth' }); setCancelOpen(false); setCancelStep(1); }; return (

Plans & Billing

Monthly plans with tiered pricing based on your feature requirements.

{launchTrial && hasTrialDaysLeft && (
{trialDaysLeft}

{`Days left in your ${launchPlan?.name ?? 'plan'} trial`}

{`Full access now. When your trial ends you're charged ${formatUSD( launchFirstMonth )} for your first month (${LAUNCH_DISCOUNT_PERCENT}% off), then ${formatUSD( launchMonthly )}/mo. Cancel anytime.`}

)} {(bannerPlan || isCanceled || launchTrial) && (
{!isCanceled ? (

{launchTrial ? launchPlan?.name : bannerPlan?.name}

{launchTrial ? `Free Trial · ${LAUNCH_DISCOUNT_PERCENT}% off first month` : 'Current Plan'} {stripeStatusRaw && !hasTrialDaysLeft && ( {stripeStatusRaw.replace(/_/g, ' ')} )}

{launchTrial ? `${formatUSD( launchFirstMonth )} first month (${LAUNCH_DISCOUNT_PERCENT}% off), then ${formatUSD( launchMonthly )}/mo after your trial.` : onFreePlan ? FREE_PLAN_SUMMARY : `${formatUSD(bannerMonthly)}/mo — You can change plans at any time.`}

{!launchTrial && !hasTrialDaysLeft && !onFreePlan && launchDiscountEligible && bannerPlan && (

First month {formatUSD(bannerDiscountedMonthly)} ( {LAUNCH_DISCOUNT_PERCENT}% off)

)}
) : (

{previousPlan?.name || 'No active plan'}

Canceled

{formatDateLabel(accessUntilISO) ? `You have access until ${formatDateLabel(accessUntilISO)}.` : 'Your billing has been stopped.'}

{previousPlan && !previousPlan.enterprise && ( )}
)} {!hasTrialDaysLeft && !onFreePlan && (
Billing Period
{billingLabel}
Auto-renew
{autoRenew === null ? '-' : autoRenew ? 'Active' : 'Disabled'}
Current Period Ends
{formatDateLabel(periodEndISO) || '-'}
Remaining Days
{remainingDays ?? '-'}
Access Until
{formatDateLabel(accessUntilISO) || '-'}
{user?.stripe_cancel_at_period_end && !isCanceled && (
Your subscription is scheduled to cancel at the end of the current billing period.
)} {cancelError && (

Cancellation failed

{cancelError}

)}
)}
)} {/* AppSumo lifetime deal OR regular plan grid */} {isAppSumo ? ( ) : ( )} {/* Credit balance + history */} {token && (

Credit balance

)}

How billing works when you switch plans

Upgrading Plans

Moving to a higher plan? You only pay the{' '} prorated difference {' '} for the days remaining in your current billing cycle.

Example: Fast Growing ($39/mo) → Scaling ($99/mo) at mid-cycle = $39 + ($99 − $39) × (15/30) ={' '} $69 charge .
Downgrading Plans

Moving to a lower plan? We apply an{' '} account credit {' '} for the unused portion of your current cycle toward future charges.

Example:{' '} Scaling ($99/mo) → Fast Growing ($39/mo) at mid-cycle = ($99 − $39) × (15/30) ={' '} $30 credit .
{cancelOpen && (
!cancelling && setCancelOpen(false)} />
{cancelStep === 1 ? (

Cancel Subscription

Before you cancel, here is what will happen to your account:

  • Keep access to all features until{' '} {formatDateLabel(periodEndISO) || 'the end of your billing cycle'} .
  • You can switch to a lower-cost plan anytime instead of canceling completely.
{[ 'Too expensive', 'Not seeing value', 'Temporary pause', 'Switching tools', ].map(r => ( ))}