diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index 2ce8bbbc..4ccf247d 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -24,7 +24,8 @@ class SubscriptionController extends Controller return redirect()->route('dashboard'); } - $canUseFreePlan = ! $user->bankingConnections()->exists(); + $hasBankConnections = $user->bankingConnections()->exists(); + $canUseFreePlan = ! $hasBankConnections; // Mark the paywall as seen so the middleware stops redirecting here. if ($canUseFreePlan && ! $user->hasSeenPaywall()) { @@ -34,6 +35,9 @@ class SubscriptionController extends Controller return Inertia::render('subscription/paywall', [ 'stats' => $this->getUserStats($user), 'canUseFreePlan' => $canUseFreePlan, + 'canManageConnectionsForFreePlan' => $user->isOnboarded() + && $hasBankConnections + && $user->hasCanceledSubscription(), ]); } diff --git a/app/Models/User.php b/app/Models/User.php index 3948f4af..64a14057 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -179,6 +179,19 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma && ! $subscription->ended(); } + public function hasCanceledSubscription(): bool + { + if (! config('subscriptions.enabled')) { + return false; + } + + $subscription = $this->subscription('default'); + + return $subscription !== null + && $subscription->stripe_status === 'canceled' + && $subscription->ended(); + } + /** * The tax rates that should apply to the customer's subscriptions. * diff --git a/lang/es.json b/lang/es.json index 2281e414..f72f42d6 100644 --- a/lang/es.json +++ b/lang/es.json @@ -632,6 +632,7 @@ "Get started quickly with your existing financial data.": "Comienza rápidamente con tus datos financieros existentes.", "Github": "Github", "Go to Dashboard": "Ir al Panel", + "Go to Settings": "Ir a Configuración", "Go to your account's transaction history": "Ve al historial de transacciones de tu cuenta", "Go to your dashboard and click \"Import Transactions\". Select your CSV file and I'll map the columns automatically.": "Ve a tu panel y haz clic en \"Importar Transacciones\". Selecciona tu archivo CSV y mapearé las columnas automáticamente.", "Go to your dashboard and click \"Import Transactions\". Select your CSV file and we'll map the columns automatically.": "Ve a tu panel y haz clic en \"Importar Transacciones\". Selecciona tu archivo CSV y mapearemos las columnas automáticamente.", @@ -1508,6 +1509,7 @@ "Visualize your money flow over time. See income vs. expenses and spot trends before they become problems.": "Visualiza el flujo de tu dinero a lo largo del tiempo. Ve ingresos frente a gastos e identifica tendencias antes de que se conviertan en problemas.", "Víctor & Álvaro": "Víctor y Álvaro", "Want to connect your bank directly?": "¿Quieres conectar tu banco directamente?", + "Want to continue for free? Disconnect all bank connections in Settings.": "¿Quieres continuar gratis? Desconecta todas las conexiones bancarias en Configuración.", "Warning": "Advertencia", "We Can't Read It": "No Podemos Leerlo", "We also support Open Banking connections so your transactions sync automatically. Head to Settings > Connections to link your bank directly.": "También admitimos conexiones de Open Banking para que tus transacciones se sincronicen automáticamente. Ve a Configuración > Conexiones para vincular tu banco directamente.", diff --git a/resources/js/pages/subscription/paywall.tsx b/resources/js/pages/subscription/paywall.tsx index 3f75fbe9..f92b7e5d 100644 --- a/resources/js/pages/subscription/paywall.tsx +++ b/resources/js/pages/subscription/paywall.tsx @@ -4,6 +4,7 @@ import { useCountUp } from '@/hooks/use-count-up'; import { useLocale } from '@/hooks/use-locale'; import { cn } from '@/lib/utils'; import { dashboard } from '@/routes'; +import { index as connectionsIndex } from '@/routes/settings/connections'; import { checkout } from '@/routes/subscribe'; import { type SharedData } from '@/types'; import { Plan } from '@/types/pricing'; @@ -34,6 +35,7 @@ interface PaywallStats { interface PaywallPageProps extends SharedData { stats: PaywallStats; canUseFreePlan: boolean; + canManageConnectionsForFreePlan: boolean; } function getEquivalentBillingLabel( @@ -332,11 +334,13 @@ function PricingSection({ defaultPlan, currency, canUseFreePlan, + canManageConnectionsForFreePlan, }: { planEntries: [string, Plan][]; defaultPlan: string; currency: string; canUseFreePlan: boolean; + canManageConnectionsForFreePlan: boolean; }) { const [selectedPlan, setSelectedPlan] = useState(defaultPlan); const [freeButtonVisible, setFreeButtonVisible] = useState(false); @@ -380,6 +384,23 @@ function PricingSection({ + {canManageConnectionsForFreePlan && ( +
+

+ {__( + 'Want to continue for free? Disconnect all bank connections in Settings.', + )} +

+ +
+ )} + {canUseFreePlan && (
().props; const planEntries = Object.entries(pricing.plans); @@ -426,6 +447,9 @@ export default function Paywall() { defaultPlan={pricing.defaultPlan} currency={pricing.currency} canUseFreePlan={canUseFreePlan} + canManageConnectionsForFreePlan={ + canManageConnectionsForFreePlan + } />
diff --git a/tests/Feature/SubscriptionTest.php b/tests/Feature/SubscriptionTest.php index 6d18c0d0..6e920f13 100644 --- a/tests/Feature/SubscriptionTest.php +++ b/tests/Feature/SubscriptionTest.php @@ -150,6 +150,73 @@ test('canceled subscribed users cannot use paid bank connection features', funct $this->get(route('dashboard'))->assertRedirect(route('subscribe')); }); +test('canceled subscribed users with bank connections can access connections settings', function () { + $user = User::factory()->onboarded()->create(['paywall_seen_at' => now()]); + BankingConnection::factory()->for($user)->create(); + + $user->subscriptions()->create([ + 'type' => 'default', + 'stripe_id' => 'sub_canceled_settings_test123', + 'stripe_status' => 'canceled', + 'stripe_price' => 'price_test123', + 'ends_at' => now()->subMinute(), + ]); + + $this->actingAs($user); + + $this->get(route('settings.connections.index')) + ->assertOk() + ->assertInertia(fn ($page) => $page + ->component('settings/connections') + ->has('connections', 1) + ); +}); + +test('paywall lets canceled subscribed users with bank connections manage connections for free plan', function () { + $user = User::factory()->onboarded()->create(['paywall_seen_at' => now()]); + BankingConnection::factory()->for($user)->create(); + + $user->subscriptions()->create([ + 'type' => 'default', + 'stripe_id' => 'sub_canceled_manage_connections_test123', + 'stripe_status' => 'canceled', + 'stripe_price' => 'price_test123', + 'ends_at' => now()->subMinute(), + ]); + + $this->actingAs($user); + + $this->get(route('subscribe')) + ->assertOk() + ->assertInertia(fn ($page) => $page + ->component('subscription/paywall') + ->where('canUseFreePlan', false) + ->where('canManageConnectionsForFreePlan', true) + ); +}); + +test('paywall does not show manage connections option during onboarding', function () { + $user = User::factory()->notOnboarded()->create(['paywall_seen_at' => now()]); + BankingConnection::factory()->for($user)->create(); + + $user->subscriptions()->create([ + 'type' => 'default', + 'stripe_id' => 'sub_canceled_onboarding_test123', + 'stripe_status' => 'canceled', + 'stripe_price' => 'price_test123', + 'ends_at' => now()->subMinute(), + ]); + + $this->actingAs($user); + + $this->get(route('subscribe')) + ->assertOk() + ->assertInertia(fn ($page) => $page + ->component('subscription/paywall') + ->where('canManageConnectionsForFreePlan', false) + ); +}); + test('users can view the success page after subscribing', function () { $user = User::factory()->onboarded()->create(); @@ -336,6 +403,7 @@ test('paywall shows canUseFreePlan false when user has a bank connection', funct ->assertInertia(fn ($page) => $page ->component('subscription/paywall') ->where('canUseFreePlan', false) + ->where('canManageConnectionsForFreePlan', false) ); });