diff --git a/lang/es.json b/lang/es.json index edbbe9d7..44a5136d 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1,6 +1,16 @@ { "This subscription is no longer eligible for a self-service refund.": "Esta suscripción ya no es elegible para una devolución automática.", "Your payment was refunded, your subscription was canceled, and your bank connections were disconnected.": "Te hemos devuelto el pago, cancelado la suscripción y desconectado tus cuentas bancarias.", + "You'll be charged today": "Se te cobrará hoy", + "Changed your mind? Get a full refund yourself from Settings within the first :days days — no questions asked.": "¿Cambias de idea? Solicita tú mismo la devolución completa desde Ajustes durante los primeros :days días, sin preguntas.", + ":days-day free trial": "Prueba gratis de :days días", + "You won't be charged until your :days-day trial ends. Cancel anytime before then.": "No se te cobrará hasta que termine tu prueba de :days días. Cancela cuando quieras antes de eso.", + "Money-back guarantee": "Garantía de devolución", + "Changed your mind? Request a full refund until :date. This cancels your subscription and disconnects your bank accounts — the data you already imported is kept.": "¿Cambias de idea? Solicita la devolución completa hasta el :date. Esto cancela tu suscripción y desconecta tus cuentas bancarias; los datos que ya importaste se conservan.", + "Changed your mind? Request a full refund. This cancels your subscription and disconnects your bank accounts — the data you already imported is kept.": "¿Cambias de idea? Solicita la devolución completa. Esto cancela tu suscripción y desconecta tus cuentas bancarias; los datos que ya importaste se conservan.", + "Confirm refund": "Confirmar devolución", + "Keep my plan": "Mantener mi plan", + "Request a refund": "Solicitar devolución", "AI Categorization": "Categorización con IA", "Let AI suggest categories for your transactions automatically.": "Deja que la IA sugiera categorías para tus transacciones automáticamente.", "Allow AI categorization": "Permitir categorización con IA", diff --git a/resources/js/pages/settings/billing.tsx b/resources/js/pages/settings/billing.tsx index 7f51c236..0a1ff167 100644 --- a/resources/js/pages/settings/billing.tsx +++ b/resources/js/pages/settings/billing.tsx @@ -11,13 +11,13 @@ import { store as storeConsent, } from '@/routes/ai/consent'; import { billing } from '@/routes/settings'; -import { portal } from '@/routes/settings/billing'; +import { portal, refund as refundRoute } from '@/routes/settings/billing'; import { checkout } from '@/routes/subscribe'; import { type BreadcrumbItem, type SharedData } from '@/types'; import { Plan } from '@/types/pricing'; import { formatCurrency } from '@/utils/currency'; import { __ } from '@/utils/i18n'; -import { Head, usePage } from '@inertiajs/react'; +import { Head, router, usePage } from '@inertiajs/react'; import axios from 'axios'; import { CheckIcon, @@ -245,16 +245,95 @@ function UpgradeSection({ ); } +interface RefundInfo { + canSelfRefund: boolean; + deadline: string | null; +} + +function RefundCard({ + deadline, + locale, +}: { + deadline: string | null; + locale: string; +}) { + const [confirming, setConfirming] = useState(false); + const [processing, setProcessing] = useState(false); + + const deadlineLabel = deadline + ? new Date(deadline).toLocaleDateString(locale, { + day: 'numeric', + month: 'long', + }) + : null; + + const submit = () => { + setProcessing(true); + router.post( + refundRoute.url(), + {}, + { onFinish: () => setProcessing(false) }, + ); + }; + + return ( +
+

+ {__('Money-back guarantee')} +

+

+ {deadlineLabel + ? __( + 'Changed your mind? Request a full refund until :date. This cancels your subscription and disconnects your bank accounts — the data you already imported is kept.', + { date: deadlineLabel }, + ) + : __( + 'Changed your mind? Request a full refund. This cancels your subscription and disconnects your bank accounts — the data you already imported is kept.', + )} +

+ + {confirming ? ( +
+ + +
+ ) : ( + + )} +
+ ); +} + function SubscribedSection({ isDemoAccount, defaultPlan, currency, locale, + refund, }: { isDemoAccount: boolean; defaultPlan: Plan | undefined; currency: string; locale: string; + refund?: RefundInfo; }) { return (
@@ -302,6 +381,10 @@ function SubscribedSection({ )}
+ + {refund?.canSelfRefund && ( + + )} ); } @@ -393,8 +476,8 @@ function AiConsentSection({ } export default function Billing() { - const { auth, pricing, locale, features, hasAiConsent } = usePage< - SharedData & { hasAiConsent: boolean } + const { auth, pricing, locale, features, hasAiConsent, refund } = usePage< + SharedData & { hasAiConsent: boolean; refund?: RefundInfo } >().props; const isDemoAccount = auth?.isDemoAccount ?? false; const hasProPlan = auth?.hasProPlan ?? false; @@ -413,6 +496,7 @@ export default function Billing() { defaultPlan={defaultPlan} currency={pricing.currency} locale={locale} + refund={refund} /> ) : ( ; } +interface ExperimentOffer { + variant: string; + payNow: boolean; + refundWindowDays: number; + trialDays: Record; +} + interface PaywallPageProps extends SharedData { stats: PaywallStats; canUseFreePlan: boolean; canManageConnectionsForFreePlan: boolean; + offer: ExperimentOffer; +} + +function TrialTerms({ + offer, + planKey, +}: { + offer: ExperimentOffer; + planKey: string; +}) { + if (offer.payNow) { + return ( +
+

+ {__("You'll be charged today")} +

+

+ {__( + 'Changed your mind? Get a full refund yourself from Settings within the first :days days — no questions asked.', + { days: offer.refundWindowDays }, + )} +

+
+ ); + } + + const days = offer.trialDays[planKey] ?? 0; + + if (days <= 0) { + return null; + } + + return ( +
+

+ {__(':days-day free trial', { days })} +

+

+ {__( + "You won't be charged until your :days-day trial ends. Cancel anytime before then.", + { days }, + )} +

+
+ ); } function getEquivalentBillingLabel( @@ -335,12 +387,14 @@ function PricingSection({ currency, canUseFreePlan, canManageConnectionsForFreePlan, + offer, }: { planEntries: [string, Plan][]; defaultPlan: string; currency: string; canUseFreePlan: boolean; canManageConnectionsForFreePlan: boolean; + offer: ExperimentOffer; }) { const [selectedPlan, setSelectedPlan] = useState(defaultPlan); const [freeButtonVisible, setFreeButtonVisible] = useState(false); @@ -379,6 +433,8 @@ function PricingSection({ ))} + +