fix: allow managing canceled connections (#417)
## Summary - add paywall settings CTA for onboarded users with ended canceled subscriptions and bank connections - keep onboarding and normal unpaid connected users out of this escape path - cover paywall props and connection settings access ## Tests - php artisan test --compact tests/Feature/SubscriptionTest.php - npx eslint resources/js/pages/subscription/paywall.tsx
This commit is contained in:
parent
88faa5beb6
commit
eaba315196
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</Button>
|
||||
</a>
|
||||
|
||||
{canManageConnectionsForFreePlan && (
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-center">
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
{__(
|
||||
'Want to continue for free? Disconnect all bank connections in Settings.',
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => router.visit(connectionsIndex().url)}
|
||||
>
|
||||
{__('Go to Settings')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canUseFreePlan && (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -403,7 +424,7 @@ function PricingSection({
|
|||
}
|
||||
|
||||
export default function Paywall() {
|
||||
const { pricing, stats, canUseFreePlan } =
|
||||
const { pricing, stats, canUseFreePlan, canManageConnectionsForFreePlan } =
|
||||
usePage<PaywallPageProps>().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
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue