diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php new file mode 100644 index 0000000..bdfff2b --- /dev/null +++ b/app/Http/Controllers/CheckoutController.php @@ -0,0 +1,38 @@ + ['name' => 'Personal', 'billing' => 'Monthly', 'display_price' => '£1.99 / month', 'price_id' => $paddle['personal_monthly_price_id'], 'activation_limit' => 1], + 'personal-annual' => ['name' => 'Personal', 'billing' => 'Annual', 'display_price' => '£14.99 / year', 'price_id' => $paddle['personal_annual_price_id'], 'activation_limit' => 1], + 'professional-monthly' => ['name' => 'Professional', 'billing' => 'Monthly', 'display_price' => '£3.99 / month', 'price_id' => $paddle['professional_monthly_price_id'], 'activation_limit' => 3], + 'professional-annual' => ['name' => 'Professional', 'billing' => 'Annual', 'display_price' => '£29.99 / year', 'price_id' => $paddle['professional_annual_price_id'], 'activation_limit' => 3], + ]; + + foreach ($plans as $plan) { + if (trim((string) $plan['price_id']) === '') { + throw new RuntimeException('One or more Paddle price IDs are not configured.'); + } + } + + return view('checkout.buy', [ + 'plans' => $plans, + 'paddleToken' => $token, + 'paddleEnvironment' => (string) ($paddle['environment'] ?? 'sandbox'), + ]); + } +} diff --git a/app/Services/Licensing/LicenseDeliveryService.php b/app/Services/Licensing/LicenseDeliveryService.php new file mode 100644 index 0000000..73637a7 --- /dev/null +++ b/app/Services/Licensing/LicenseDeliveryService.php @@ -0,0 +1,38 @@ +whereNull('delivered_at') + ->whereNotNull('recipient_email') + ->where('recipient_email', '!=', '') + ->oldest() + ->limit(max(1, min($limit, 100))) + ->get(); + + foreach ($pending as $outbox) { + $plain = Crypt::decryptString($outbox->encrypted_license_key); + $recipient = strtolower(trim((string) $outbox->recipient_email)); + if ($recipient === '') continue; + + Mail::raw( + "Thank you for choosing Rsync Studio.\n\nYour licence key is:\n\n{$plain}\n\nOpen Rsync Studio → Settings → Licence and enter this key to activate your Mac.\n\nKeep this key private. You can manage activations from the Rsync Studio licence portal.", + fn ($message) => $message->to($recipient)->subject('Your Rsync Studio licence key') + ); + + $outbox->update(['delivered_at' => now()]); + $delivered++; + } + + return $delivered; + } +} diff --git a/resources/views/checkout/buy.blade.php b/resources/views/checkout/buy.blade.php new file mode 100644 index 0000000..6b4ea18 --- /dev/null +++ b/resources/views/checkout/buy.blade.php @@ -0,0 +1,91 @@ +@extends('layouts.app') +@section('content') +
+ Rsync Studio +

Choose your licence

+

Every subscription starts with a 7-day free trial. Cancel before the trial ends and you won't be charged.

+ @if($paddleEnvironment === 'sandbox')
Sandbox checkout — no real payment is taken
@endif +
+ +
+@foreach($plans as $code => $plan) +
+
+
+
+

{{ $plan['name'] }}

+
{{ $plan['billing'] }} billing
+
+ @if($plan['billing'] === 'Annual')Best value@endif +
+
{{ $plan['display_price'] }}
+
    +
  • 7-day free trial
  • +
  • {{ $plan['activation_limit'] }} active Mac{{ $plan['activation_limit'] === 1 ? '' : 's' }}
  • +
  • Secure online activation
  • +
  • Licence management portal
  • +
+
+ +
+@endforeach +
+ + + + + +@endsection diff --git a/resources/views/support.blade.php b/resources/views/support.blade.php new file mode 100644 index 0000000..0632136 --- /dev/null +++ b/resources/views/support.blade.php @@ -0,0 +1,3 @@ +@extends('layouts.app') @section('content') +

Rsync Studio Support

For licence recovery, create or sign in to your portal account using the same email address used at checkout.

Open licence portal

For activation problems, include the app version and the exact error message. Never send SSH passwords, private keys, or your full licence token.

+@endsection diff --git a/tests/Feature/CheckoutPageTest.php b/tests/Feature/CheckoutPageTest.php new file mode 100644 index 0000000..eeb07d4 --- /dev/null +++ b/tests/Feature/CheckoutPageTest.php @@ -0,0 +1,27 @@ +set('rsync-studio.paddle.client_side_token', 'test_example'); + config()->set('rsync-studio.paddle.environment', 'sandbox'); + config()->set('rsync-studio.paddle.personal_monthly_price_id', 'pri_personal_monthly'); + config()->set('rsync-studio.paddle.personal_annual_price_id', 'pri_personal_annual'); + config()->set('rsync-studio.paddle.professional_monthly_price_id', 'pri_professional_monthly'); + config()->set('rsync-studio.paddle.professional_annual_price_id', 'pri_professional_annual'); + + $this->get('/buy') + ->assertOk() + ->assertSee('7-day free trial') + ->assertSee('pri_personal_monthly') + ->assertSee('pri_personal_annual') + ->assertSee('pri_professional_monthly') + ->assertSee('pri_professional_annual') + ->assertSee('test_example'); + } +}