'use client'; import { Stripe } from '@stripe/stripe-js'; import { FC, useEffect, useState } from 'react'; import { PaymentElement, BillingAddressElement, CheckoutProvider, useCheckout, } from '@stripe/react-stripe-js/checkout'; import { modeEmitter } from '@gitroom/frontend/components/layout/mode.component'; import useCookie from 'react-use-cookie'; import { Button } from '@gitroom/react/form/button'; import dayjs from 'dayjs'; import { useToaster } from '@gitroom/react/toaster/toaster'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; export const EmbeddedBilling: FC<{ stripe: Promise; secret: string; showCoupon?: boolean; autoApplyCoupon?: string; }> = ({ stripe, secret, showCoupon = false, autoApplyCoupon }) => { const [saveSecret, setSaveSecret] = useState(secret); const [loading, setLoading] = useState(false); const [mode, setMode] = useCookie('mode', 'dark'); useEffect(() => { modeEmitter.on('mode', (value) => { setMode(value); setLoading(true); }); return () => { modeEmitter.removeAllListeners(); }; }, []); useEffect(() => { if (loading) { setLoading(false); } }, [loading]); useEffect(() => { if (secret && saveSecret !== secret) { setSaveSecret(secret); } }, [secret, setSaveSecret]); if (saveSecret !== secret || loading) { return null; } return (
); }; const FormWrapper: FC<{ showCoupon?: boolean; autoApplyCoupon?: string }> = ({ showCoupon = false, autoApplyCoupon, }) => { const checkoutState = useCheckout(); const toaster = useToaster(); const [loading, setLoading] = useState(false); if (checkoutState.type !== 'success') { return null; } const handleSubmit = async (e: any) => { e.preventDefault(); setLoading(true); const { checkout } = checkoutState; const confirmResult = await checkout.confirm(); if (confirmResult.type === 'error') { toaster.show(confirmResult.error.message, 'warning'); } setLoading(false); }; return (
); }; const StripeInputs: FC<{ showCoupon: boolean; autoApplyCoupon?: string; loading: boolean; }> = ({ showCoupon, autoApplyCoupon, loading }) => { const checkout = useCheckout(); const t = useT(); const [ready, setReady] = useState(false); return ( <> {/*
*/} {/*

*/} {/* {checkout.type === 'loading'*/} {/* ? ''*/} {/* : t('billing_billing_address', 'Billing Address')}*/} {/*

*/} {/* */} {/*
*/}

{checkout.type === 'loading' ? '' : t('billing_payment', 'Payment')}

setReady(true)} /> {ready && } {showCoupon && ready && ( )} {ready && } {checkout.type === 'loading' ? null : (
{t('billing_powered_by_stripe', 'Secure payments processed by')}
)}
); }; const PriceBreakdown: FC = () => { const checkoutState = useCheckout(); const t = useT(); if (checkoutState.type !== 'success') { return null; } const { checkout } = checkoutState; const lineItem = checkout?.lineItems?.[0]; const recurring = checkout?.recurring; const discountAmounts = checkout?.discountAmounts; const hasDiscount = discountAmounts && discountAmounts.length > 0; // Get values const planName = lineItem?.name || t('billing_subscription', 'Subscription'); const unitAmount = lineItem?.unitAmount?.amount || '$0.00'; const discountDisplay = hasDiscount ? discountAmounts[0] : null; const dueToday = checkout?.total?.total?.amount || '$0.00'; const nextBillingTotal = recurring?.dueNext?.total?.amount; const nextBillingDate = recurring?.trial?.trialEnd ? dayjs(recurring.trial.trialEnd * 1000).format('MMMM D, YYYY') : null; const billingInterval = recurring?.interval === 'month' ? t('billing_monthly', 'Monthly') : t('billing_yearly', 'Yearly'); return (

{t('billing_order_summary', 'Order Summary')}

{/* Plan */}
{planName} {billingInterval}
{unitAmount}
{/* Discount */} {discountDisplay && (
{discountDisplay.displayName || discountDisplay.promotionCode} {discountDisplay.percentOff && ` (${discountDisplay.percentOff}% off)`}
{discountDisplay.amount !== '$0.00' ? `-${discountDisplay.amount}` : t('billing_applied', 'Applied')}
)} {/* Divider */}
{/* Due today */}
{t('billing_due_today', 'Due today')} {dueToday}
{/* Next billing info */} {nextBillingTotal && nextBillingDate && (
{t('billing_then', 'Then')} {nextBillingTotal}{' '} {t('billing_on', 'on')} {nextBillingDate}
)}
{t( 'billing_cancel_notice', 'Cancel anytime from settings without talking to a person and never be charged.' )}
); }; const AppliedCouponDisplay: FC<{ appliedCode: string; checkout: any; isApplying: boolean; onRemove: () => void; }> = ({ appliedCode, checkout, isApplying, onRemove }) => { const t = useT(); // Get discount display from checkout state const getDiscountDisplay = (): string | null => { // Try to get percentage from discountAmounts const percentOff = checkout?.discountAmounts?.[0]?.percentOff; if (percentOff && typeof percentOff === 'number' && percentOff > 0) { return `-${percentOff}%`; } // Try to get actual discount amount from recurring.dueNext.discount const recurringDiscount = checkout?.recurring?.dueNext?.discount?.minorUnitsAmount; if ( recurringDiscount && typeof recurringDiscount === 'number' && recurringDiscount > 0 ) { return `-$${(recurringDiscount / 100).toFixed(2)}`; } // Try lineItems discount const lineItemDiscount = checkout?.lineItems?.[0]?.discountAmounts?.[0]?.percentOff; if ( lineItemDiscount && typeof lineItemDiscount === 'number' && lineItemDiscount > 0 ) { return `-${lineItemDiscount}%`; } return null; }; // Get expiration date from checkout state (if available) const getExpirationDate = (): string | null => { const discount = checkout?.discountAmounts?.[0]; const lineItemDiscount = checkout?.lineItems?.[0]?.discountAmounts?.[0]; // Check for expiresAt in various locations (Unix timestamp) const expiresAt = discount?.expiresAt || discount?.expires_at || lineItemDiscount?.expiresAt || lineItemDiscount?.expires_at || checkout?.promotionCode?.expiresAt || checkout?.promotionCode?.expires_at; if (expiresAt && typeof expiresAt === 'number') { const date = new Date(expiresAt * 1000); return dayjs(date).format('MMMM D, YYYY'); } if (expiresAt && typeof expiresAt === 'string') { return dayjs(expiresAt).format('MMMM D, YYYY'); } return null; }; const discountDisplay = getDiscountDisplay(); const expirationDate = getExpirationDate(); return (
{appliedCode} {t('billing_discount_applied', 'applied')} {discountDisplay && ` (${discountDisplay})`}
{expirationDate && (

{t('billing_coupon_expires', 'Coupon expires on')} {expirationDate}

)}
); }; export const CouponInput: FC<{ autoApplyCoupon?: string }> = ({ autoApplyCoupon, }) => { const checkoutState = useCheckout(); const t = useT(); const toaster = useToaster(); const [couponCode, setCouponCode] = useState(''); const [isApplying, setIsApplying] = useState(false); const [appliedCode, setAppliedCode] = useState(null); const [showInput, setShowInput] = useState(false); const { checkout } = checkoutState.type === 'success' ? checkoutState : { checkout: null }; // Auto-apply coupon from backend when checkout is ready useEffect(() => { if (autoApplyCoupon) { handleApplyCoupon(undefined, autoApplyCoupon); } }, []); // Check if a coupon is already pre-applied (e.g., auto-apply coupon from backend) const preAppliedCode = checkout?.discountAmounts?.[0]?.promotionCode; const effectiveAppliedCode = appliedCode || preAppliedCode || null; const handleApplyCoupon = async (e?: any, coupon?: string) => { if (!coupon && !couponCode.trim()) return; setIsApplying(true); try { const result = await checkout.applyPromotionCode( coupon || couponCode.trim() ); if (result.type === 'error') { toaster.show( result.error.message || t('billing_invalid_coupon', 'Invalid coupon code'), 'warning' ); } else { setAppliedCode(coupon || couponCode.trim()); setCouponCode(''); setShowInput(false); toaster.show( t('billing_coupon_applied', 'Coupon applied successfully!'), 'success' ); } } catch (err: any) { toaster.show( err.message || t('billing_invalid_coupon', 'Invalid coupon code'), 'warning' ); } setIsApplying(false); }; const handleRemoveCoupon = async () => { setIsApplying(true); try { await checkout.removePromotionCode(); setAppliedCode(null); toaster.show(t('billing_coupon_removed', 'Coupon removed'), 'success'); } catch (err: any) { toaster.show( err.message || t('billing_error_removing_coupon', 'Error removing coupon'), 'warning' ); } setIsApplying(false); }; // Show applied coupon (either manually applied or pre-applied from backend) if (effectiveAppliedCode) { return (
); } // Show "Have a promo code?" link if (!showInput) { return (
); } // Show input field return (

{t('billing_discount_coupon', 'Discount Coupon')}

setCouponCode(e.target.value)} placeholder={t('billing_enter_coupon_code', 'Enter coupon code')} disabled={isApplying} autoFocus className="flex-1 h-[44px] px-[16px] rounded-[8px] border border-newColColor bg-newBgColor text-textColor placeholder:text-textColor/50 focus:outline-none focus:border-boxFocused disabled:opacity-50" onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleApplyCoupon(); } if (e.key === 'Escape') { setShowInput(false); setCouponCode(''); } }} />
); }; const SubmitBar: FC<{ loading: boolean }> = ({ loading }) => { const checkout = useCheckout(); const t = useT(); if (checkout.type === 'loading' || checkout.type === 'error') { return null; } return (
{checkout.checkout.recurring?.trial?.trialEnd ? (
{t('billing_your_7_day_trial_is', 'Your 7-day trial is')}{' '} {t('billing_100_percent_free', '100% free')} {' '} {t('billing_ending', 'ending')}{' '}
{dayjs( checkout.checkout.recurring?.trial?.trialEnd * 1000 ).format('MMMM D, YYYY')}{' '} —{' '} {t( 'billing_cancel_anytime_short', 'Cancel anytime from settings' )}
) : null}
); };