add stripe subscription
This commit is contained in:
parent
2d8f166e4a
commit
39890b319a
@ -30,7 +30,7 @@ class Controller extends BaseController
|
|||||||
if (!is_cloud()) {
|
if (!is_cloud()) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
return view('subscription', [
|
return view('subscription.show', [
|
||||||
'settings' => InstanceSettings::get(),
|
'settings' => InstanceSettings::get(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -69,4 +69,8 @@ class Actions extends Component
|
|||||||
return general_error_handler($e, $this);
|
return general_error_handler($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public function stripeCustomerPortal() {
|
||||||
|
$session = getStripeCustomerPortalSession(currentTeam());
|
||||||
|
redirect($session->url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
66
app/Http/Livewire/Subscription/PricingPlans.php
Normal file
66
app/Http/Livewire/Subscription/PricingPlans.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Livewire\Subscription;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Stripe\Stripe;
|
||||||
|
use Stripe\Checkout\Session;
|
||||||
|
|
||||||
|
class PricingPlans extends Component
|
||||||
|
{
|
||||||
|
public function subscribeStripe($type)
|
||||||
|
{
|
||||||
|
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||||
|
switch ($type) {
|
||||||
|
case 'basic-monthly':
|
||||||
|
$priceId = config('subscription.stripe_price_id_basic_monthly');
|
||||||
|
break;
|
||||||
|
case 'basic-yearly':
|
||||||
|
$priceId = config('subscription.stripe_price_id_basic_yearly');
|
||||||
|
break;
|
||||||
|
case 'ultimate-monthly':
|
||||||
|
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
|
||||||
|
break;
|
||||||
|
case 'pro-monthly':
|
||||||
|
$priceId = config('subscription.stripe_price_id_pro_monthly');
|
||||||
|
break;
|
||||||
|
case 'pro-yearly':
|
||||||
|
$priceId = config('subscription.stripe_price_id_pro_yearly');
|
||||||
|
break;
|
||||||
|
case 'ultimate-yearly':
|
||||||
|
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$priceId = config('subscription.stripe_price_id_basic_monthly');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!$priceId) {
|
||||||
|
$this->emit('error', 'Price ID not found! Please contact the administrator.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$payload = [
|
||||||
|
'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id,
|
||||||
|
'line_items' => [[
|
||||||
|
'price' => $priceId,
|
||||||
|
'quantity' => 1,
|
||||||
|
]],
|
||||||
|
'customer_update' =>[
|
||||||
|
'name' => 'auto'
|
||||||
|
],
|
||||||
|
'tax_id_collection' => [
|
||||||
|
'enabled' => true,
|
||||||
|
],
|
||||||
|
'mode' => 'subscription',
|
||||||
|
'success_url' => route('subscription.success'),
|
||||||
|
'cancel_url' => route('subscription.show',['cancelled' => true]),
|
||||||
|
];
|
||||||
|
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
||||||
|
if ($customer) {
|
||||||
|
$payload['customer'] = $customer;
|
||||||
|
} else {
|
||||||
|
$payload['customer_email'] = auth()->user()->email;
|
||||||
|
}
|
||||||
|
$session = Session::create($payload);
|
||||||
|
return redirect($session->url, 303);
|
||||||
|
}
|
||||||
|
}
|
@ -15,12 +15,8 @@ class IsBoardingFlow
|
|||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
$allowed_paths = [
|
ray('IsBoardingFlow Middleware');
|
||||||
'subscription',
|
if (showBoarding() && !in_array($request->path(), allowedPaths())) {
|
||||||
'boarding',
|
|
||||||
'livewire/message/boarding'
|
|
||||||
];
|
|
||||||
if (showBoarding() && !in_array($request->path(), $allowed_paths)) {
|
|
||||||
return redirect('boarding');
|
return redirect('boarding');
|
||||||
}
|
}
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
@ -17,31 +17,17 @@ class SubscriptionValid
|
|||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInstanceAdmin()) {
|
if (isSubscriptionActive() && $request->path() === 'subscription') {
|
||||||
return $next($request);
|
// ray('active subscription Middleware');
|
||||||
}
|
|
||||||
|
|
||||||
if (is_subscription_active() && $request->path() === 'subscription') {
|
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
if (is_subscription_in_grace_period()) {
|
if (isSubscriptionOnGracePeriod()) {
|
||||||
|
// ray('is_subscription_in_grace_period Middleware');
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
if (!is_subscription_active() && !is_subscription_in_grace_period()) {
|
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
|
||||||
ray('SubscriptionValid Middleware');
|
// ray('SubscriptionValid Middleware');
|
||||||
|
if (!in_array($request->path(), allowedPaths())) {
|
||||||
$allowed_paths = [
|
|
||||||
'subscription',
|
|
||||||
'login',
|
|
||||||
'register',
|
|
||||||
'waitlist',
|
|
||||||
'force-password-reset',
|
|
||||||
'logout',
|
|
||||||
'livewire/message/force-password-reset',
|
|
||||||
'livewire/message/check-license',
|
|
||||||
'livewire/message/switch-team',
|
|
||||||
];
|
|
||||||
if (!in_array($request->path(), $allowed_paths)) {
|
|
||||||
return redirect('subscription');
|
return redirect('subscription');
|
||||||
} else {
|
} else {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
@ -22,6 +22,7 @@ class CheckResaleLicenseJob implements ShouldQueue
|
|||||||
try {
|
try {
|
||||||
resolve(CheckResaleLicense::class)();
|
resolve(CheckResaleLicense::class)();
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
|
send_internal_notification('CheckResaleLicenseJob failed with: ' . $th->getMessage());
|
||||||
ray($th);
|
ray($th);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ class CoolifyTask implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
$remote_process = resolve(RunRemoteProcess::class, [
|
$remote_process = resolve(RunRemoteProcess::class, [
|
||||||
'activity' => $this->activity,
|
'activity' => $this->activity,
|
||||||
'ignore_errors' => $this->ignore_errors,
|
'ignore_errors' => $this->ignore_errors,
|
||||||
|
@ -64,6 +64,7 @@ class DatabaseBackupJob implements ShouldQueue
|
|||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
if ($this->database_status !== 'running') {
|
if ($this->database_status !== 'running') {
|
||||||
ray('database not running');
|
ray('database not running');
|
||||||
return;
|
return;
|
||||||
@ -93,6 +94,12 @@ class DatabaseBackupJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
$this->save_backup_logs();
|
$this->save_backup_logs();
|
||||||
// TODO: Notify user
|
// TODO: Notify user
|
||||||
|
} catch (\Throwable $th) {
|
||||||
|
ray($th->getMessage());
|
||||||
|
send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage());
|
||||||
|
//throw $th;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function backup_standalone_postgresql(): void
|
private function backup_standalone_postgresql(): void
|
||||||
|
@ -46,6 +46,7 @@ class DatabaseContainerStatusJob implements ShouldQueue, ShouldBeUnique
|
|||||||
$this->database->save();
|
$this->database->save();
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
send_internal_notification('DatabaseContainerStatusJob failed with: ' . $e->getMessage());
|
||||||
ray($e->getMessage());
|
ray($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ class DockerCleanupJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
|
||||||
ray($e->getMessage());
|
ray($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ class ProxyCheckJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
ray($th->getMessage());
|
ray($th->getMessage());
|
||||||
|
send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage());
|
||||||
//throw $th;
|
//throw $th;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
|
|||||||
$this->server->proxy->status = 'exited';
|
$this->server->proxy->status = 'exited';
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
}
|
}
|
||||||
|
send_internal_notification('ProxyContainerStatusJob failed with: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,8 @@ class ProxyStartJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
resolve(StartProxy::class)($this->server);
|
resolve(StartProxy::class)($this->server);
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
|
send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage());
|
||||||
ray($th->getMessage());
|
ray($th->getMessage());
|
||||||
//throw $th;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,8 @@ class ResourceStatusJob implements ShouldQueue, ShouldBeUnique
|
|||||||
database: $postgresql,
|
database: $postgresql,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $th) {
|
||||||
ray($e->getMessage());
|
send_internal_notification('ResourceStatusJob failed with: ' . $th->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
app/Jobs/SubscriptionInvoiceFailedJob.php
Executable file
41
app/Jobs/SubscriptionInvoiceFailedJob.php
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Team;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Stripe\Stripe;
|
||||||
|
|
||||||
|
class SubscriptionInvoiceFailedJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(protected Team $team)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = getStripeCustomerPortalSession($this->team);
|
||||||
|
$mail = new MailMessage();
|
||||||
|
$mail->view('emails.subscription-invoice-failed', [
|
||||||
|
'stripeCustomerPortal' => $session->url,
|
||||||
|
]);
|
||||||
|
$mail->subject('Your last payment was failed for Coolify Cloud.');
|
||||||
|
$this->team->members()->each(function ($member) use ($mail) {
|
||||||
|
ray($member);
|
||||||
|
if ($member->isAdmin()) {
|
||||||
|
send_user_an_email($mail, $member->email);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (\Throwable $th) {
|
||||||
|
send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $th->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ class Webhook extends Model
|
|||||||
{
|
{
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'type' => 'string',
|
||||||
'payload' => 'encrypted',
|
'payload' => 'encrypted',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Team;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Stripe\Stripe;
|
||||||
|
|
||||||
function getSubscriptionLink($type)
|
function getSubscriptionLink($type)
|
||||||
{
|
{
|
||||||
@ -43,40 +45,79 @@ function getEndDate()
|
|||||||
return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||||
}
|
}
|
||||||
|
|
||||||
function is_subscription_active()
|
function isSubscriptionActive()
|
||||||
{
|
{
|
||||||
$team = currentTeam();
|
$team = currentTeam();
|
||||||
|
|
||||||
if (!$team) {
|
if (!$team) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isInstanceAdmin()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$subscription = $team?->subscription;
|
$subscription = $team?->subscription;
|
||||||
|
|
||||||
if (!$subscription) {
|
if (!$subscription) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$is_active = $subscription->lemon_status === 'active';
|
if (config('subscription.provider') === 'lemon') {
|
||||||
|
return $subscription->lemon_status === 'active';
|
||||||
|
}
|
||||||
|
if (config('subscription.provider') === 'stripe') {
|
||||||
|
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
// if (config('subscription.provider') === 'paddle') {
|
||||||
|
// return $subscription->paddle_status === 'active';
|
||||||
|
// }
|
||||||
|
|
||||||
return $is_active;
|
|
||||||
}
|
}
|
||||||
function is_subscription_in_grace_period()
|
function isSubscriptionOnGracePeriod()
|
||||||
{
|
{
|
||||||
|
|
||||||
$team = currentTeam();
|
$team = currentTeam();
|
||||||
if (!$team) {
|
if (!$team) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isInstanceAdmin()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$subscription = $team?->subscription;
|
$subscription = $team?->subscription;
|
||||||
if (!$subscription) {
|
if (!$subscription) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (config('subscription.provider') === 'lemon') {
|
||||||
$is_still_grace_period = $subscription->lemon_ends_at &&
|
$is_still_grace_period = $subscription->lemon_ends_at &&
|
||||||
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
|
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
|
||||||
|
|
||||||
return $is_still_grace_period;
|
return $is_still_grace_period;
|
||||||
|
}
|
||||||
|
if (config('subscription.provider') === 'stripe') {
|
||||||
|
return $subscription->stripe_cancel_at_period_end;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function subscriptionProvider()
|
||||||
|
{
|
||||||
|
return config('subscription.provider');
|
||||||
|
}
|
||||||
|
function getStripeCustomerPortalSession(Team $team)
|
||||||
|
{
|
||||||
|
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||||
|
$return_url = route('team.show');
|
||||||
|
$stripe_customer_id = $team->subscription->stripe_customer_id;
|
||||||
|
$session = \Stripe\BillingPortal\Session::create([
|
||||||
|
'customer' => $stripe_customer_id,
|
||||||
|
'return_url' => $return_url,
|
||||||
|
]);
|
||||||
|
return $session;
|
||||||
|
}
|
||||||
|
function allowedPaths()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'subscription',
|
||||||
|
'login',
|
||||||
|
'register',
|
||||||
|
'waitlist',
|
||||||
|
'force-password-reset',
|
||||||
|
'logout',
|
||||||
|
'boarding',
|
||||||
|
'livewire/message/boarding',
|
||||||
|
'livewire/message/force-password-reset',
|
||||||
|
'livewire/message/check-license',
|
||||||
|
'livewire/message/switch-team',
|
||||||
|
'livewire/message/subscription.pricing-plans'
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"spatie/laravel-ray": "^1.32.4",
|
"spatie/laravel-ray": "^1.32.4",
|
||||||
"spatie/laravel-schemaless-attributes": "^2.4",
|
"spatie/laravel-schemaless-attributes": "^2.4",
|
||||||
"spatie/url": "^2.2",
|
"spatie/url": "^2.2",
|
||||||
|
"stripe/stripe-php": "^12.0",
|
||||||
"symfony/yaml": "^6.2",
|
"symfony/yaml": "^6.2",
|
||||||
"visus/cuid2": "^2.0.0"
|
"visus/cuid2": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
63
composer.lock
generated
63
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "a4143cdb58c02a0490f9aa03d05b8e9a",
|
"content-hash": "da14dce99d76abcaaa6393166eda049a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
@ -6582,6 +6582,67 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-04-27T11:07:22+00:00"
|
"time": "2023-04-27T11:07:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "stripe/stripe-php",
|
||||||
|
"version": "v12.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stripe/stripe-php.git",
|
||||||
|
"reference": "732996be0714154716f19f73f956d77bafc99334"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/732996be0714154716f19f73f956d77bafc99334",
|
||||||
|
"reference": "732996be0714154716f19f73f956d77bafc99334",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=5.6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "3.5.0",
|
||||||
|
"php-coveralls/php-coveralls": "^2.5",
|
||||||
|
"phpstan/phpstan": "^1.2",
|
||||||
|
"phpunit/phpunit": "^5.7 || ^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stripe\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Stripe and contributors",
|
||||||
|
"homepage": "https://github.com/stripe/stripe-php/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Stripe PHP Library",
|
||||||
|
"homepage": "https://stripe.com/",
|
||||||
|
"keywords": [
|
||||||
|
"api",
|
||||||
|
"payment processing",
|
||||||
|
"stripe"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/stripe/stripe-php/issues",
|
||||||
|
"source": "https://github.com/stripe/stripe-php/tree/v12.0.0"
|
||||||
|
},
|
||||||
|
"time": "2023-08-18T18:55:28+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v6.3.2",
|
"version": "v6.3.2",
|
||||||
|
@ -1,6 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'provider'=> env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon
|
||||||
|
// Stripe
|
||||||
|
'stripe_api_key' => env('STRIPE_API_KEY', null),
|
||||||
|
'stripe_secret' => env('STRIPE_SECRET', null),
|
||||||
|
'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null),
|
||||||
|
'stripe_price_id_basic_monthly' => env('STRIPE_PRICE_ID_BASIC_MONTHLY', null),
|
||||||
|
'stripe_price_id_basic_yearly' => env('STRIPE_PRICE_ID_BASIC_YEARLY', null),
|
||||||
|
'stripe_price_id_pro_monthly' => env('STRIPE_PRICE_ID_PRO_MONTHLY', null),
|
||||||
|
'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null),
|
||||||
|
'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null),
|
||||||
|
'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null),
|
||||||
|
|
||||||
|
|
||||||
|
// Paddle
|
||||||
|
'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null),
|
||||||
|
'paddle_vendor_auth_code' => env('PADDLE_VENDOR_AUTH_CODE', null),
|
||||||
|
'paddle_public_key' => env('PADDLE_PUBLIC_KEY', null),
|
||||||
|
'paddle_price_id_basic_monthly' => env('PADDLE_PRICE_ID_BASIC_MONTHLY', null),
|
||||||
|
'paddle_price_id_basic_yearly' => env('PADDLE_PRICE_ID_BASIC_YEARLY', null),
|
||||||
|
'paddle_price_id_pro_monthly' => env('PADDLE_PRICE_ID_PRO_MONTHLY', null),
|
||||||
|
'paddle_price_id_pro_yearly' => env('PADDLE_PRICE_ID_PRO_YEARLY', null),
|
||||||
|
'paddle_price_id_ultimate_monthly' => env('PADDLE_PRICE_ID_ULTIMATE_MONTHLY', null),
|
||||||
|
'paddle_price_id_ultimate_yearly' => env('PADDLE_PRICE_ID_ULTIMATE_YEARLY', null),
|
||||||
|
'paddle_webhook_secret' => env('PADDLE_WEBHOOK_SECRET', null),
|
||||||
|
|
||||||
|
|
||||||
|
// Lemon
|
||||||
'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null),
|
'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null),
|
||||||
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
|
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
|
||||||
'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
|
'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('webhooks', function (Blueprint $table) {
|
||||||
|
$table->string('type')->change();
|
||||||
|
});
|
||||||
|
DB::statement("ALTER TABLE webhooks DROP CONSTRAINT webhooks_type_check");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('webhooks', function (Blueprint $table) {
|
||||||
|
$table->string('type')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->boolean('stripe_invoice_paid')->default(false);
|
||||||
|
$table->string('stripe_subscription_id')->nullable();
|
||||||
|
$table->string('stripe_customer_id')->nullable();
|
||||||
|
$table->boolean('stripe_cancel_at_period_end')->default(false);
|
||||||
|
$table->string('lemon_subscription_id')->nullable()->change();
|
||||||
|
$table->string('lemon_order_id')->nullable()->change();
|
||||||
|
$table->string('lemon_product_id')->nullable()->change();
|
||||||
|
$table->string('lemon_variant_id')->nullable()->change();
|
||||||
|
$table->string('lemon_variant_name')->nullable()->change();
|
||||||
|
$table->string('lemon_customer_id')->nullable()->change();
|
||||||
|
$table->string('lemon_status')->nullable()->change();
|
||||||
|
$table->string('lemon_renews_at')->nullable()->change();
|
||||||
|
$table->string('lemon_update_payment_menthod_url')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('stripe_invoice_paid');
|
||||||
|
$table->dropColumn('stripe_subscription_id');
|
||||||
|
$table->dropColumn('stripe_customer_id');
|
||||||
|
$table->dropColumn('stripe_cancel_at_period_end');
|
||||||
|
$table->string('lemon_subscription_id')->change();
|
||||||
|
$table->string('lemon_order_id')->change();
|
||||||
|
$table->string('lemon_product_id')->change();
|
||||||
|
$table->string('lemon_variant_id')->change();
|
||||||
|
$table->string('lemon_variant_name')->change();
|
||||||
|
$table->string('lemon_customer_id')->change();
|
||||||
|
$table->string('lemon_status')->change();
|
||||||
|
$table->string('lemon_renews_at')->change();
|
||||||
|
$table->string('lemon_update_payment_menthod_url')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -9,7 +9,7 @@
|
|||||||
@env('local')
|
@env('local')
|
||||||
<title>Coolify - localhost</title>
|
<title>Coolify - localhost</title>
|
||||||
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
||||||
@else
|
@else
|
||||||
<title>{{ $title ?? 'Coolify' }}</title>
|
<title>{{ $title ?? 'Coolify' }}</title>
|
||||||
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
|
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
|
||||||
@endenv
|
@endenv
|
||||||
@ -26,7 +26,7 @@
|
|||||||
<body>
|
<body>
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
<x-toaster-hub />
|
<x-toaster-hub />
|
||||||
@if (isInstanceAdmin() || is_subscription_in_grace_period())
|
@if (isSubscriptionOnGracePeriod())
|
||||||
<div class="fixed top-3 left-4" id="vue">
|
<div class="fixed top-3 left-4" id="vue">
|
||||||
<magic-bar></magic-bar>
|
<magic-bar></magic-bar>
|
||||||
</div>
|
</div>
|
||||||
@ -68,6 +68,18 @@
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Livewire.on('info', (message) => {
|
||||||
|
if (message) Toaster.info(message)
|
||||||
|
})
|
||||||
|
Livewire.on('error', (message) => {
|
||||||
|
if (message) Toaster.error(message)
|
||||||
|
})
|
||||||
|
Livewire.on('warning', (message) => {
|
||||||
|
if (message) Toaster.warning(message)
|
||||||
|
})
|
||||||
|
Livewire.on('success', (message) => {
|
||||||
|
if (message) Toaster.success(message)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
80
resources/views/components/paddle.blade.php
Normal file
80
resources/views/components/paddle.blade.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<x-slot:basic>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="w-full h-10 buyme"
|
||||||
|
x-on:click="subscribe('basic-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="w-full h-10 buyme"
|
||||||
|
x-on:click="subscribe('basic-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:basic>
|
||||||
|
<x-slot:pro>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||||
|
x-on:click="subscribe('pro-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||||
|
x-on:click="subscribe('pro-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:pro>
|
||||||
|
<x-slot:ultimate>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
|
||||||
|
x-on:click="subscribe('ultimate-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
|
||||||
|
x-on:click="subscribe('ultimate-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:ultimate>
|
||||||
|
<x-slot:other>
|
||||||
|
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
Paddle.Environment.set("{{ is_dev() ? 'sandbox' : 'production' }}");
|
||||||
|
Paddle.Setup({
|
||||||
|
seller: {{ config('subscription.paddle_vendor_id') }},
|
||||||
|
checkout: {
|
||||||
|
settings: {
|
||||||
|
displayMode: "overlay",
|
||||||
|
theme: "light",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function subscribe(type) {
|
||||||
|
let priceId = null
|
||||||
|
switch (type) {
|
||||||
|
case 'basic-monthly':
|
||||||
|
priceId = "{{ config('subscription.paddle_price_id_basic_monthly') }}"
|
||||||
|
break;
|
||||||
|
case 'basic-yearly':
|
||||||
|
priceId = "{{ config('subscription.paddle_price_id_basic_yearly') }}"
|
||||||
|
break;
|
||||||
|
case 'pro-monthly':
|
||||||
|
priceId = "{{ config('subscription.paddle_price_id_pro_monthly') }}"
|
||||||
|
break;
|
||||||
|
case 'pro-yearly':
|
||||||
|
priceId = "{{ config('subscription.paddle_price_id_pro_yearly') }}"
|
||||||
|
break;
|
||||||
|
case 'ultimate-monthly':
|
||||||
|
priceId = "{{ config('subscription.paddle_price_id_ultimate_monthly') }}"
|
||||||
|
break;
|
||||||
|
case 'ultimate-yearly':
|
||||||
|
priceId = "{{ config('subscription.paddle_price_id_ultimate_yearly') }}"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Paddle.Checkout.open({
|
||||||
|
customer: {
|
||||||
|
email: '{{ auth()->user()->email }}',
|
||||||
|
},
|
||||||
|
customData: {
|
||||||
|
"team_id": "{{ currentTeam()->id }}",
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
priceId,
|
||||||
|
quantity: 1
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</x-slot:other>
|
@ -105,10 +105,9 @@
|
|||||||
<span>billed annually</span>
|
<span>billed annually</span>
|
||||||
</span>
|
</span>
|
||||||
@if ($showSubscribeButtons)
|
@if ($showSubscribeButtons)
|
||||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
@isset($basic)
|
||||||
href="{{ getSubscriptionLink('monthly_basic') }}">Subscribe</a>
|
{{ $basic }}
|
||||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
@endisset
|
||||||
href="{{ getSubscriptionLink('yearly_basic') }}">Subscribe</a>
|
|
||||||
@endif
|
@endif
|
||||||
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in
|
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in
|
||||||
the cloud
|
the cloud
|
||||||
@ -168,10 +167,9 @@
|
|||||||
<span>billed annually</span>
|
<span>billed annually</span>
|
||||||
</span>
|
</span>
|
||||||
@if ($showSubscribeButtons)
|
@if ($showSubscribeButtons)
|
||||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro" class="buyme"
|
@isset($pro)
|
||||||
href="{{ getSubscriptionLink('monthly_pro') }}">Subscribe</a>
|
{{ $pro }}
|
||||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="buyme"
|
@endisset
|
||||||
href="{{ getSubscriptionLink('yearly_pro') }}">Subscribe</a>
|
|
||||||
@endif
|
@endif
|
||||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
|
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
|
||||||
</p>
|
</p>
|
||||||
@ -227,10 +225,9 @@
|
|||||||
<span>billed annually</span>
|
<span>billed annually</span>
|
||||||
</span>
|
</span>
|
||||||
@if ($showSubscribeButtons)
|
@if ($showSubscribeButtons)
|
||||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="buyme"
|
@isset($ultimate)
|
||||||
href="{{ getSubscriptionLink('monthly_ultimate') }}">Subscribe</a>
|
{{ $ultimate }}
|
||||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="buyme"
|
@endisset
|
||||||
href="{{ getSubscriptionLink('yearly_ultimate') }}">Subscribe</a>
|
|
||||||
@endif
|
@endif
|
||||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and
|
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and
|
||||||
manage them easily in one place.</p>
|
manage them easily in one place.</p>
|
||||||
@ -274,3 +271,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@isset($other)
|
||||||
|
{{ $other }}
|
||||||
|
@endisset
|
||||||
|
0
resources/views/components/stripe.blade.php
Normal file
0
resources/views/components/stripe.blade.php
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Your last invoice has failed to be paid for Coolify Cloud. Please <a href="{{$stripeCustomerPortal}}">update payment details on your Stripe Customer Portal</a>.
|
||||||
|
<br><br>
|
||||||
|
Thanks,<br>
|
||||||
|
Coolify Cloud
|
@ -1,4 +1,5 @@
|
|||||||
Congratulations!<br>
|
Congratulations!<br>
|
||||||
|
Congratulations!<br>
|
||||||
<br>
|
<br>
|
||||||
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a>
|
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a>
|
||||||
<br>
|
<br>
|
||||||
|
@ -1,4 +1,19 @@
|
|||||||
<div>
|
<div>
|
||||||
|
@if (subscriptionProvider() === 'stripe')
|
||||||
|
@if (currentTeam()->subscription->stripe_cancel_at_period_end)
|
||||||
|
<div>Subscription is active but on cancel period.</div>
|
||||||
|
@else
|
||||||
|
<div>Subscription is active. Last invoice is
|
||||||
|
{{ currentTeam()->subscription->stripe_invoice_paid ? 'paid' : 'not paid' }}.</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (currentTeam()->subscription->stripe_cancel_at_period_end)
|
||||||
|
<a class="hover:no-underline" href="{{ route('subscription') }}"><x-forms.button>Subscribe
|
||||||
|
again</x-forms.button></a>
|
||||||
|
@endif
|
||||||
|
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button>
|
||||||
|
@endif
|
||||||
|
@if (subscriptionProvider() === 'lemon')
|
||||||
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div>
|
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div>
|
||||||
<div>Type: {{ currentTeam()->subscription->lemon_variant_name }}</div>
|
<div>Type: {{ currentTeam()->subscription->lemon_variant_name }}</div>
|
||||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||||
@ -28,4 +43,6 @@
|
|||||||
Subscription</x-forms.button></a>
|
Subscription</x-forms.button></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
<x-pricing-plans>
|
||||||
|
@if (config('subscription.provider') === 'stripe')
|
||||||
|
<x-slot:basic>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
|
||||||
|
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
|
||||||
|
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:basic>
|
||||||
|
<x-slot:pro>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro"
|
||||||
|
class="w-full h-10 buyme" wire:click="subscribeStripe('pro-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||||
|
wire:click="subscribeStripe('pro-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:pro>
|
||||||
|
<x-slot:ultimate>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate"
|
||||||
|
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate"
|
||||||
|
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:ultimate>
|
||||||
|
@endif
|
||||||
|
@if (config('subscription.provider') === 'paddle')
|
||||||
|
<x-paddle />
|
||||||
|
@endif
|
||||||
|
@if (config('subscription.provider') === 'lemon')
|
||||||
|
<x-slot:basic>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
|
||||||
|
class="w-full h-10 buyme" wire:click="getSubscriptionLink('basic-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
|
||||||
|
class="w-full h-10 buyme" wire:click="getSubscriptionLink('basic-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:basic>
|
||||||
|
<x-slot:pro>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro"
|
||||||
|
class="w-full h-10 buyme" wire:click="getSubscriptionLink('pro-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||||
|
wire:click="getSubscriptionLink('pro-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:pro>
|
||||||
|
<x-slot:ultimate>
|
||||||
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate"
|
||||||
|
class="w-full h-10 buyme" wire:click="getSubscriptionLink('ultimate-monthly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
|
||||||
|
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate"
|
||||||
|
class="w-full h-10 buyme" wire:click="getSubscriptionLink('ultimate-yearly')"> Subscribe
|
||||||
|
</x-forms.button>
|
||||||
|
</x-slot:ultimate>
|
||||||
|
@endif
|
||||||
|
</x-pricing-plans>
|
3
resources/views/subscription/cancel.blade.php
Normal file
3
resources/views/subscription/cancel.blade.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<x-layout-subscription>
|
||||||
|
Cancel
|
||||||
|
</x-layout-subscription>
|
@ -1,7 +1,8 @@
|
|||||||
<x-layout-subscription>
|
<x-layout-subscription>
|
||||||
@if ($settings->is_resale_license_active)
|
@if ($settings->is_resale_license_active)
|
||||||
<div class="flex justify-center mx-10">
|
<div class="flex justify-center mx-10">
|
||||||
<div>
|
|
||||||
|
<div x-data>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<h2>Subscription</h2>
|
<h2>Subscription</h2>
|
||||||
<livewire:switch-team />
|
<livewire:switch-team />
|
||||||
@ -10,7 +11,12 @@
|
|||||||
<span>Currently active team: <span
|
<span>Currently active team: <span
|
||||||
class="text-warning">{{ session('currentTeam.name') }}</span></span>
|
class="text-warning">{{ session('currentTeam.name') }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<x-pricing-plans />
|
@if(request()->query->get('cancelled'))
|
||||||
|
<div class="text-xl text-center text-red-500">Something went wrong. Please try again.</div>
|
||||||
|
@endif
|
||||||
|
@if (config('subscription.provider') !== null)
|
||||||
|
<livewire:subscription.pricing-plans />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
3
resources/views/subscription/success.blade.php
Normal file
3
resources/views/subscription/success.blade.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<x-layout>
|
||||||
|
Success
|
||||||
|
</x-layout>
|
@ -97,7 +97,9 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::middleware(['throttle:force-password-reset'])->group(function() {
|
Route::middleware(['throttle:force-password-reset'])->group(function() {
|
||||||
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
|
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
|
||||||
});
|
});
|
||||||
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription');
|
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.show');
|
||||||
|
Route::get('/subscription/success', fn () => view('subscription.success'))->name('subscription.success');
|
||||||
|
Route::get('/subscription/cancel', fn () => view('profile'))->name('subscription.cancel');
|
||||||
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
|
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
|
||||||
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');
|
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');
|
||||||
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');
|
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\SubscriptionInvoiceFailedJob;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use App\Models\GithubApp;
|
use App\Models\GithubApp;
|
||||||
@ -206,7 +207,131 @@ Route::get('/waitlist/cancel', function () {
|
|||||||
return redirect()->route('dashboard');
|
return redirect()->route('dashboard');
|
||||||
}
|
}
|
||||||
})->name('webhooks.waitlist.cancel');
|
})->name('webhooks.waitlist.cancel');
|
||||||
Route::post('/payments/events', function () {
|
|
||||||
|
|
||||||
|
Route::post('/payments/stripe/events', function () {
|
||||||
|
try {
|
||||||
|
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||||
|
$signature = request()->header('Stripe-Signature');
|
||||||
|
|
||||||
|
$event = \Stripe\Webhook::constructEvent(
|
||||||
|
request()->getContent(),
|
||||||
|
$signature,
|
||||||
|
$webhookSecret
|
||||||
|
);
|
||||||
|
$webhook = Webhook::create([
|
||||||
|
'type' => 'stripe',
|
||||||
|
'payload' => request()->getContent()
|
||||||
|
]);
|
||||||
|
$type = data_get($event, 'type');
|
||||||
|
$data = data_get($event, 'data.object');
|
||||||
|
switch ($type) {
|
||||||
|
case 'checkout.session.completed':
|
||||||
|
$clientReferenceId = data_get($data, 'client_reference_id');
|
||||||
|
$userId = Str::before($clientReferenceId, ':');
|
||||||
|
$teamId = Str::after($clientReferenceId, ':');
|
||||||
|
$subscriptionId = data_get($data, 'subscription');
|
||||||
|
$customerId = data_get($data, 'customer');
|
||||||
|
$team = Team::find($teamId);
|
||||||
|
$found = $team->members->where('id', $userId)->first();
|
||||||
|
if (!$found->isAdmin()) {
|
||||||
|
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}.");
|
||||||
|
}
|
||||||
|
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||||
|
if ($subscription) {
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_subscription_id' => $subscriptionId,
|
||||||
|
'stripe_customer_id' => $customerId,
|
||||||
|
'stripe_invoice_paid' => true,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Subscription::create([
|
||||||
|
'team_id' => $teamId,
|
||||||
|
'stripe_subscription_id' => $subscriptionId,
|
||||||
|
'stripe_customer_id' => $customerId,
|
||||||
|
'stripe_invoice_paid' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'invoice.paid':
|
||||||
|
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||||
|
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_invoice_paid' => true,
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'invoice.payment_failed':
|
||||||
|
$customerId = data_get($data, 'customer');
|
||||||
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||||
|
SubscriptionInvoiceFailedJob::dispatch($subscription->team);
|
||||||
|
break;
|
||||||
|
case 'customer.subscription.updated':
|
||||||
|
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||||
|
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
|
||||||
|
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'customer.subscription.deleted':
|
||||||
|
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||||
|
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_cancel_at_period_end' => false,
|
||||||
|
'stripe_invoice_paid' => false,
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unhandled event type
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
ray($e->getMessage());
|
||||||
|
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
|
||||||
|
$webhook->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'failure_reason' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
return response($e->getMessage(), 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Route::post('/payments/paddle/events', function () {
|
||||||
|
try {
|
||||||
|
$payload = request()->all();
|
||||||
|
$signature = request()->header('Paddle-Signature');
|
||||||
|
$ts = Str::of($signature)->after('ts=')->before(';');
|
||||||
|
$h1 = Str::of($signature)->after('h1=');
|
||||||
|
$signedPayload = $ts->value . ':' . request()->getContent();
|
||||||
|
$verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret'));
|
||||||
|
ray($verify, $h1->value, hash_equals($verify, $h1->value));
|
||||||
|
if (!hash_equals($verify, $h1->value)) {
|
||||||
|
return response('Invalid signature.', 400);
|
||||||
|
}
|
||||||
|
$eventType = data_get($payload, 'event_type');
|
||||||
|
ray($eventType);
|
||||||
|
$webhook = Webhook::create([
|
||||||
|
'type' => 'paddle',
|
||||||
|
'payload' => $payload,
|
||||||
|
]);
|
||||||
|
// TODO - Handle events
|
||||||
|
switch ($eventType) {
|
||||||
|
case 'subscription.activated':
|
||||||
|
}
|
||||||
|
ray('Subscription event: ' . $eventType);
|
||||||
|
$webhook->update([
|
||||||
|
'status' => 'success',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
ray($e->getMessage());
|
||||||
|
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
|
||||||
|
$webhook->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'failure_reason' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
return response('OK');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Route::post('/payments/lemon/events', function () {
|
||||||
try {
|
try {
|
||||||
$secret = config('subscription.lemon_squeezy_webhook_secret');
|
$secret = config('subscription.lemon_squeezy_webhook_secret');
|
||||||
$payload = request()->collect();
|
$payload = request()->collect();
|
||||||
|
@ -47,9 +47,6 @@ function schedule:run {
|
|||||||
bash spin exec -u webuser coolify php artisan schedule:run
|
bash spin exec -u webuser coolify php artisan schedule:run
|
||||||
}
|
}
|
||||||
|
|
||||||
function db:reset {
|
|
||||||
bash spin exec -u webuser coolify php artisan migrate:fresh --seed
|
|
||||||
}
|
|
||||||
|
|
||||||
function db {
|
function db {
|
||||||
bash spin exec -u webuser coolify php artisan db
|
bash spin exec -u webuser coolify php artisan db
|
||||||
@ -59,6 +56,9 @@ function db:migrate {
|
|||||||
bash spin exec -u webuser coolify php artisan migrate
|
bash spin exec -u webuser coolify php artisan migrate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function db:reset {
|
||||||
|
bash spin exec -u webuser coolify php artisan migrate:fresh --seed
|
||||||
|
}
|
||||||
function db:reset-prod {
|
function db:reset-prod {
|
||||||
bash spin exec -u webuser coolify php artisan migrate:fresh --force --seed --seeder=ProductionSeeder ||
|
bash spin exec -u webuser coolify php artisan migrate:fresh --force --seed --seeder=ProductionSeeder ||
|
||||||
php artisan migrate:fresh --force --seed --seeder=ProductionSeeder
|
php artisan migrate:fresh --force --seed --seeder=ProductionSeeder
|
||||||
|
Loading…
x
Reference in New Issue
Block a user