Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions app/Actions/User/CreateUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ public static function execute(array $data, array $utmParameters = []): User
{
$user = DB::transaction(function () use ($data, $utmParameters): User {
$isInviteRegistration = data_get($data, 'is_invite', false);

$account = Account::create([
$requiresCardForTrial = (bool) config('trypost.billing.require_card_for_trial', true);
$accountAttributes = [
'name' => data_get($data, 'name')."'s Account",
'billing_email' => data_get($data, 'email'),
'plan_id' => Plan::where('slug', Slug::Starter)->value('id'),
'trial_ends_at' => now()->addDays(config('cashier.trial_days')),
]);
];

if (! $requiresCardForTrial) {
$accountAttributes['plan_id'] = Plan::where('slug', Slug::Starter)->value('id');
$accountAttributes['trial_ends_at'] = now()->addDays(config('cashier.trial_days', 7));
}

$account = Account::create($accountAttributes);

$user = User::create(array_merge([
'name' => data_get($data, 'name'),
Expand Down
7 changes: 7 additions & 0 deletions app/Http/Controllers/App/BillingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ public function subscribe(Request $request): Response|RedirectResponse
return redirect()->route('app.billing.index');
}

$requiresCardForTrial = (bool) config('trypost.billing.require_card_for_trial', true);

return Inertia::render('billing/Subscribe', [
'plans' => Plan::active()->orderBy('sort')->get(),
'trialDays' => $requiresCardForTrial ? config('cashier.trial_days') : null,
]);
}

Expand Down Expand Up @@ -64,6 +67,10 @@ public function checkout(Request $request, Plan $plan): SymfonyResponse|Redirect
$subscription = $account->newSubscription(Account::SUBSCRIPTION_NAME, $priceId)
->allowPromotionCodes();

if ((bool) config('trypost.billing.require_card_for_trial', true)) {
$subscription->trialDays(config('cashier.trial_days'));
}

$checkoutSession = $subscription->checkout([
'success_url' => route('app.billing.processing').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('app.subscribe'),
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Middleware/App/EnsureAccountReady.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ public function handle(Request $request, Closure $next): Response
$account = $user->account;

if (! config('trypost.self_hosted')) {
$requiresCardForTrial = (bool) config('trypost.billing.require_card_for_trial', true);
$hasAccess = $account && (
$account->subscribed(Account::SUBSCRIPTION_NAME)
|| $account->isOnTrial()
|| (! $requiresCardForTrial && $account->isOnTrial())
);

if (! $hasAccess) {
Expand Down
16 changes: 10 additions & 6 deletions app/Models/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function hasActiveSubscription(): bool

public function isOnTrial(): bool
{
if ($this->onGenericTrial()) {
if (! (bool) config('trypost.billing.require_card_for_trial', true) && $this->onGenericTrial()) {
return true;
}

Expand All @@ -104,11 +104,15 @@ public function activeTrialEndsAt(): ?CarbonInterface
{
$subscription = $this->subscription(self::SUBSCRIPTION_NAME);

return match (true) {
(bool) $subscription?->onTrial() => $subscription->trial_ends_at,
$this->onGenericTrial() => $this->trial_ends_at,
default => null,
};
if (! $subscription?->onTrial()) {
if (! (bool) config('trypost.billing.require_card_for_trial', true) && $this->onGenericTrial()) {
return $this->trial_ends_at;
}

return null;
}

return $subscription->trial_ends_at;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion config/cashier.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,6 @@
|
*/

'trial_days' => env('CASHIER_TRIAL_DAYS', 7),
'trial_days' => env('CASHIER_TRIAL_DAYS', 8),

];
15 changes: 15 additions & 0 deletions config/trypost.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@

'self_hosted' => env('SELF_HOSTED', true),

/*
|--------------------------------------------------------------------------
| Billing
|--------------------------------------------------------------------------
|
| Control trial behavior for SaaS billing:
| - true: require card at checkout to start trial (Stripe trialing)
| - false: grant generic trial at signup without card
|
*/

'billing' => [
'require_card_for_trial' => true,
],

/*
|--------------------------------------------------------------------------
| Media Size Limits
Expand Down
2 changes: 2 additions & 0 deletions lang/en/billing.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'eyebrow' => 'Pricing',
'title' => 'Choose the right plan for you',
'description' => 'Pick the plan that fits you. Billed monthly or annually.',
'trial_info' => ':days-day free trial, then billed automatically',
'monthly' => 'Monthly',
'yearly' => 'Yearly',
'per_month' => 'monthly',
Expand All @@ -37,6 +38,7 @@
'everything_in' => 'Everything in :plan, plus:',
'save_months' => '2 months free',
'popular' => 'Most popular',
'start_trial' => 'Start :days-day free trial',
'subscribe_cta' => 'Subscribe',
'prices' => [
'starter' => ['monthly' => '$19', 'yearly_per_month' => '$16', 'yearly' => '$190'],
Expand Down
2 changes: 2 additions & 0 deletions lang/es/billing.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'eyebrow' => 'Precios',
'title' => 'Elige el plan ideal para ti',
'description' => 'Elige el plan que te queda. Facturación mensual o anual.',
'trial_info' => 'Prueba gratuita de :days días, luego se cobra automáticamente',
'monthly' => 'Mensual',
'yearly' => 'Anual',
'per_month' => 'mensual',
Expand All @@ -37,6 +38,7 @@
'everything_in' => 'Todo lo de :plan, más:',
'save_months' => '2 meses gratis',
'popular' => 'Más popular',
'start_trial' => 'Comenzar prueba de :days días',
'subscribe_cta' => 'Suscribirse',
'prices' => [
'starter' => ['monthly' => '$19', 'yearly_per_month' => '$16', 'yearly' => '$190'],
Expand Down
2 changes: 2 additions & 0 deletions lang/pt-BR/billing.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'eyebrow' => 'Preços',
'title' => 'Escolha o plano ideal pra você',
'description' => 'Escolha o plano que combina com você. Cobrança mensal ou anual.',
'trial_info' => ':days dias grátis, depois cobrança automática',
'monthly' => 'Mensal',
'yearly' => 'Anual',
'per_month' => 'mensal',
Expand All @@ -37,6 +38,7 @@
'everything_in' => 'Tudo do :plan, mais:',
'save_months' => '2 meses grátis',
'popular' => 'Mais popular',
'start_trial' => 'Iniciar teste de :days dias',
'subscribe_cta' => 'Assinar',
'prices' => [
'starter' => ['monthly' => 'R$ 95', 'yearly_per_month' => 'R$ 79', 'yearly' => 'R$ 950'],
Expand Down
13 changes: 11 additions & 2 deletions resources/js/pages/billing/Subscribe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ interface Highlight {
tooltip?: string;
}

defineProps<{
const { plans, trialDays } = defineProps<{
plans: Plan[];
trialDays: number | null;
}>();

const isYearly = ref(true);
Expand Down Expand Up @@ -121,6 +122,9 @@ const planTones: Record<PlanSlug, string> = {
<p class="mx-auto max-w-2xl text-balance text-base text-muted-foreground sm:text-lg">
{{ $t('billing.subscribe.description') }}
</p>
<p v-if="trialDays !== null" class="text-sm font-semibold text-foreground/70">
{{ trans('billing.subscribe.trial_info', { days: String(trialDays) }) }}
</p>
</div>
</div>

Expand Down Expand Up @@ -227,7 +231,12 @@ const planTones: Record<PlanSlug, string> = {
]"
@click="selectPlan(plan)"
>
{{ $t('billing.subscribe.subscribe_cta') }}
<template v-if="trialDays !== null">
{{ trans('billing.subscribe.start_trial', { days: String(trialDays) }) }}
</template>
<template v-else>
{{ $t('billing.subscribe.subscribe_cta') }}
</template>
<IconArrowRight class="size-4" />
</button>

Expand Down
41 changes: 41 additions & 0 deletions tests/Feature/Auth/SignupRequiresCheckoutTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

use App\Actions\User\CreateUser;
use Database\Seeders\PlanSeeder;

beforeEach(function () {
config(['trypost.self_hosted' => false]);
config(['trypost.billing.require_card_for_trial' => true]);
$this->seed(PlanSeeder::class);
});

test('new signup does not create a trial before checkout', function () {
$user = CreateUser::execute([
'name' => 'Alice',
'email' => 'alice@example.com',
'password' => 'password123',
'timezone' => 'UTC',
'registration_ip' => '127.0.0.1',
]);

expect($user->account->plan_id)->toBeNull();
expect($user->account->trial_ends_at)->toBeNull();
expect($user->account->stripe_id)->toBeNull();
});

test('new signup creates generic trial when card is not required', function () {
config(['trypost.billing.require_card_for_trial' => false]);

$user = CreateUser::execute([
'name' => 'Alice',
'email' => 'alice+nocard@example.com',
'password' => 'password123',
'timezone' => 'UTC',
'registration_ip' => '127.0.0.1',
]);

expect($user->account->plan_id)->not->toBeNull();
expect($user->account->trial_ends_at)->not->toBeNull();
});
63 changes: 0 additions & 63 deletions tests/Feature/Auth/TrialOnSignupTest.php

This file was deleted.

36 changes: 18 additions & 18 deletions tests/Feature/BillingControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use App\Models\Workspace;

beforeEach(function () {
config(['trypost.billing.require_card_for_trial' => true]);

$this->account = Account::factory()->create();
$this->user = User::factory()->create([
'account_id' => $this->account->id,
Expand Down Expand Up @@ -92,22 +94,6 @@
);
});

test('billing index exposes onTrial=true and trialEndsAt for generic-trial-only account', function () {
config(['trypost.self_hosted' => false]);

$endsAt = now()->addDays(7)->startOfSecond();
$this->account->update(['trial_ends_at' => $endsAt]);

$response = $this->actingAs($this->user->fresh())->get(route('app.billing.index'));

$response->assertInertia(fn ($page) => $page
->component('settings/account/Billing', false)
->where('hasSubscription', false)
->where('onTrial', true)
->where('trialEndsAt', $endsAt->toIso8601ZuluString('microsecond'))
);
});

test('billing index exposes onTrial=true and trialEndsAt for subscription-trial account', function () {
config(['trypost.self_hosted' => false]);

Expand Down Expand Up @@ -148,14 +134,28 @@
);
});

test('subscribe page does not expose trialDays prop anymore', function () {
test('subscribe page exposes trialDays prop', function () {
config(['trypost.self_hosted' => false]);

$response = $this->actingAs($this->user)->get(route('app.subscribe'));

$response->assertInertia(fn ($page) => $page
->component('billing/Subscribe', false)
->missing('trialDays')
->where('trialDays', config('cashier.trial_days'))
);
});

test('subscribe page exposes null trialDays when card is not required', function () {
config([
'trypost.self_hosted' => false,
'trypost.billing.require_card_for_trial' => false,
]);

$response = $this->actingAs($this->user)->get(route('app.subscribe'));

$response->assertInertia(fn ($page) => $page
->component('billing/Subscribe', false)
->where('trialDays', null)
);
});

Expand Down
Loading
Loading