Merge pull request #2678 from coollabsio/next

v4.0.0-beta.306
This commit is contained in:
Andras Bacsai 2024-06-25 14:04:44 +02:00 committed by GitHub
commit e9158b7305
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 193 additions and 517 deletions

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Actions\Server\RunCommand;
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
@ -137,7 +138,7 @@ public function runCommand()
} else { } else {
$exec = "docker exec {$container_name} {$cmd}"; $exec = "docker exec {$container_name} {$cmd}";
} }
$activity = remote_process([$exec], $server, ignore_errors: true); $activity = RunCommand::run(server: $server, command: $exec);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@ -3,7 +3,6 @@
namespace App\Livewire\Subscription; namespace App\Livewire\Subscription;
use App\Models\Team; use App\Models\Team;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class Actions extends Component class Actions extends Component
@ -15,70 +14,6 @@ public function mount()
$this->server_limits = Team::serverLimit(); $this->server_limits = Team::serverLimit();
} }
public function cancel()
{
try {
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
if (! $subscription_id) {
throw new \Exception('No subscription found');
}
$response = Http::withHeaders([
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'),
])->delete('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id);
$json = $response->json();
if ($response->failed()) {
$error = data_get($json, 'errors.0.status');
if ($error === '404') {
throw new \Exception('Subscription not found.');
}
throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
} else {
$this->dispatch('success', 'Subscription cancelled successfully. Reloading in 5s.');
$this->dispatch('reloadWindow', 5000);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function resume()
{
try {
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
if (! $subscription_id) {
throw new \Exception('No subscription found');
}
$response = Http::withHeaders([
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'),
])->patch('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id, [
'data' => [
'type' => 'subscriptions',
'id' => $subscription_id,
'attributes' => [
'cancelled' => false,
],
],
]);
$json = $response->json();
if ($response->failed()) {
$error = data_get($json, 'errors.0.status');
if ($error === '404') {
throw new \Exception('Subscription not found.');
}
throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
} else {
$this->dispatch('success', 'Subscription resumed successfully. Reloading in 5s.');
$this->dispatch('reloadWindow', 5000);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stripeCustomerPortal() public function stripeCustomerPortal()
{ {
$session = getStripeCustomerPortalSession(currentTeam()); $session = getStripeCustomerPortalSession(currentTeam());

View File

@ -15,22 +15,7 @@ public function team()
public function type() public function type()
{ {
if (isLemon()) { if (isStripe()) {
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
$subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) {
return 'basic';
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $ultimate)) {
return 'ultimate';
}
} elseif (isStripe()) {
if (! $this->stripe_plan_id) { if (! $this->stripe_plan_id) {
return 'zero'; return 'zero';
} }

View File

@ -1,51 +1,8 @@
<?php <?php
use App\Models\Team; use App\Models\Team;
use Illuminate\Support\Carbon;
use Stripe\Stripe; use Stripe\Stripe;
function getSubscriptionLink($type)
{
$checkout_id = config("subscription.lemon_squeezy_checkout_id_$type");
if (! $checkout_id) {
return null;
}
$user_id = auth()->user()->id;
$team_id = currentTeam()->id ?? null;
$email = auth()->user()->email ?? null;
$name = auth()->user()->name ?? null;
$url = "https://store.coollabs.io/checkout/buy/$checkout_id?";
if ($user_id) {
$url .= "&checkout[custom][user_id]={$user_id}";
}
if (isset($team_id)) {
$url .= "&checkout[custom][team_id]={$team_id}";
}
if ($email) {
$url .= "&checkout[email]={$email}";
}
if ($name) {
$url .= "&checkout[name]={$name}";
}
return $url;
}
function getPaymentLink()
{
return currentTeam()->subscription->lemon_update_payment_menthod_url;
}
function getRenewDate()
{
return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
}
function getEndDate()
{
return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
}
function isSubscriptionActive() function isSubscriptionActive()
{ {
if (! isCloud()) { if (! isCloud()) {
@ -60,12 +17,6 @@ function isSubscriptionActive()
if (is_null($subscription)) { if (is_null($subscription)) {
return false; return false;
} }
if (isLemon()) {
return $subscription->lemon_status === 'active';
}
// if (isPaddle()) {
// return $subscription->paddle_status === 'active';
// }
if (isStripe()) { if (isStripe()) {
return $subscription->stripe_invoice_paid === true; return $subscription->stripe_invoice_paid === true;
} }
@ -82,12 +33,6 @@ function isSubscriptionOnGracePeriod()
if (! $subscription) { if (! $subscription) {
return false; return false;
} }
if (isLemon()) {
$is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period;
}
if (isStripe()) { if (isStripe()) {
return $subscription->stripe_cancel_at_period_end; return $subscription->stripe_cancel_at_period_end;
} }
@ -98,18 +43,10 @@ function subscriptionProvider()
{ {
return config('subscription.provider'); return config('subscription.provider');
} }
function isLemon()
{
return config('subscription.provider') === 'lemon';
}
function isStripe() function isStripe()
{ {
return config('subscription.provider') === 'stripe'; return config('subscription.provider') === 'stripe';
} }
function isPaddle()
{
return config('subscription.provider') === 'paddle';
}
function getStripeCustomerPortalSession(Team $team) function getStripeCustomerPortalSession(Team $team)
{ {
Stripe::setApiKey(config('subscription.stripe_api_key')); Stripe::setApiKey(config('subscription.stripe_api_key'));

View File

@ -24,6 +24,7 @@
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-sftp-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0",
"livewire/livewire": "3.4.9", "livewire/livewire": "3.4.9",
"log1x/laravel-webfonts": "^1.0",
"lorisleiva/laravel-actions": "^2.7", "lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2", "nubs/random-name-generator": "^2.2",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0",

64
composer.lock generated
View File

@ -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": "dbce9f366320f4d58392673fe25c69f6", "content-hash": "168e351cec87acbea9c1c745b83eead2",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@ -4522,6 +4522,68 @@
], ],
"time": "2024-03-14T14:03:32+00:00" "time": "2024-03-14T14:03:32+00:00"
}, },
{
"name": "log1x/laravel-webfonts",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/Log1x/laravel-webfonts.git",
"reference": "0d38122aa7f5501394006a6715f7d97dac223507"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Log1x/laravel-webfonts/zipball/0d38122aa7f5501394006a6715f7d97dac223507",
"reference": "0d38122aa7f5501394006a6715f7d97dac223507",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^7.8",
"laravel/prompts": "^0.1.15",
"php": ">=8.1"
},
"require-dev": {
"illuminate/console": "^10.41",
"illuminate/http": "^10.41",
"illuminate/support": "^10.41",
"laravel/pint": "^1.13"
},
"type": "package",
"extra": {
"laravel": {
"providers": [
"Log1x\\LaravelWebfonts\\WebfontsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Log1x\\LaravelWebfonts\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brandon Nifong",
"email": "brandon@tendency.me",
"homepage": "https://github.com/log1x"
}
],
"description": "Download, install, and preload over 1500 Google fonts locally in your Laravel project",
"support": {
"issues": "https://github.com/Log1x/laravel-webfonts/issues",
"source": "https://github.com/Log1x/laravel-webfonts/tree/v1.0.1"
},
"funding": [
{
"url": "https://github.com/Log1x",
"type": "github"
}
],
"time": "2024-03-28T11:53:11+00:00"
},
{ {
"name": "lorisleiva/laravel-actions", "name": "lorisleiva/laravel-actions",
"version": "v2.8.0", "version": "v2.8.0",

View File

@ -7,7 +7,7 @@
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.305', 'release' => '4.0.0-beta.306',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@ -1,7 +1,8 @@
<?php <?php
return [ return [
'provider' => env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon 'provider' => env('SUBSCRIPTION_PROVIDER', null), // stripe
// Stripe // Stripe
'stripe_api_key' => env('STRIPE_API_KEY', null), 'stripe_api_key' => env('STRIPE_API_KEY', null),
'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null),
@ -22,29 +23,4 @@
'stripe_price_id_dynamic_monthly' => env('STRIPE_PRICE_ID_DYNAMIC_MONTHLY', null), 'stripe_price_id_dynamic_monthly' => env('STRIPE_PRICE_ID_DYNAMIC_MONTHLY', null),
'stripe_price_id_dynamic_yearly' => env('STRIPE_PRICE_ID_DYNAMIC_YEARLY', null), 'stripe_price_id_dynamic_yearly' => env('STRIPE_PRICE_ID_DYNAMIC_YEARLY', null),
// Paddle
'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null),
'paddle_vendor_auth_code' => env('PADDLE_VENDOR_AUTH_CODE', null),
'paddle_webhook_secret' => env('PADDLE_WEBHOOK_SECRET', 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),
// Lemon
'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null),
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
'lemon_squeezy_checkout_id_basic_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_BASIC_MONTHLY', null),
'lemon_squeezy_checkout_id_basic_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_BASIC_YEARLY', null),
'lemon_squeezy_checkout_id_pro_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_MONTHLY', null),
'lemon_squeezy_checkout_id_pro_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_YEARLY', null),
'lemon_squeezy_checkout_id_ultimate_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_MONTHLY', null),
'lemon_squeezy_checkout_id_ultimate_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_YEARLY', null),
'lemon_squeezy_basic_plan_ids' => env('LEMON_SQUEEZY_BASIC_PLAN_IDS', ''),
'lemon_squeezy_pro_plan_ids' => env('LEMON_SQUEEZY_PRO_PLAN_IDS', ''),
'lemon_squeezy_ultimate_plan_ids' => env('LEMON_SQUEEZY_ULTIMATE_PLAN_IDS', ''),
]; ];

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.305'; return '4.0.0-beta.306';

View File

@ -69,27 +69,6 @@ services:
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD - STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD
- STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD - STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD
- STRIPE_EXCLUDED_PLANS - STRIPE_EXCLUDED_PLANS
- PADDLE_VENDOR_ID
- PADDLE_WEBHOOK_SECRET
- PADDLE_VENDOR_AUTH_CODE
- PADDLE_PUBLIC_KEY
- PADDLE_PRICE_ID_BASIC_MONTHLY
- PADDLE_PRICE_ID_BASIC_YEARLY
- PADDLE_PRICE_ID_PRO_MONTHLY
- PADDLE_PRICE_ID_PRO_YEARLY
- PADDLE_PRICE_ID_ULTIMATE_MONTHLY
- PADDLE_PRICE_ID_ULTIMATE_YEARLY
- LEMON_SQUEEZY_API_KEY
- LEMON_SQUEEZY_WEBHOOK_SECRET
- LEMON_SQUEEZY_CHECKOUT_ID_BASIC_MONTHLY
- LEMON_SQUEEZY_CHECKOUT_ID_BASIC_YEARLY
- LEMON_SQUEEZY_CHECKOUT_ID_PRO_MONTHLY
- LEMON_SQUEEZY_CHECKOUT_ID_PRO_YEARLY
- LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_MONTHLY
- LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_YEARLY
- LEMON_SQUEEZY_BASIC_PLAN_IDS
- LEMON_SQUEEZY_PRO_PLAN_IDS
- LEMON_SQUEEZY_ULTIMATE_PLAN_IDS
ports: ports:
- "${APP_PORT:-8000}:80" - "${APP_PORT:-8000}:80"
expose: expose:

16
public/js/apexcharts.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,11 @@
@import 'fonts';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html, html,
body { body {
@apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400; @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400;

72
resources/css/fonts.css Normal file
View File

@ -0,0 +1,72 @@
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 100;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-100.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 200;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-200.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-300.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-500.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-600.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-700.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 800;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-800.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 900;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-900.woff2') format('woff2');
}
@font-face {
font-display: swap;
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
}

View File

@ -1,80 +0,0 @@
<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("{{ isDev() ? '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>

View File

@ -34,15 +34,16 @@ class="font-bold dark:text-warning">{{ config('constants.limits.trial_period') }
<div> <div>
</div> </div>
</div> </div>
{{-- <div class="p-4 rounded bg-coolgray-400"> <div class="p-4 rounded bg-coolgray-400">
<h2 id="tier-hobby" class="flex items-start gap-4 text-4xl font-bold tracking-tight">Unlimited Trial <h2 id="tier-hobby" class="flex items-start gap-4 text-4xl font-bold tracking-tight">Unlimited Trial
<x-forms.button><a class="font-bold dark:text-white hover:no-underline" <x-forms.button><a class="font-bold dark:text-white hover:no-underline"
href="https://github.com/coollabsio/coolify">Get Started</a></x-forms.button> href="https://github.com/coollabsio/coolify">Get Started</a></x-forms.button>
</h2> </h2>
<p class="mt-4 text-sm leading-6">Start self-hosting <span class="dark:text-warning">without limits</span> with <p class="mt-4 text-sm leading-6">Start self-hosting <span class="dark:text-warning">without limits</span>
with
our our
OSS version. Same features as the paid version, but you have to manage by yourself.</p> OSS version. Same features as the paid version, but you have to manage by yourself.</p>
</div> --}} </div>
<div class="flow-root mt-12"> <div class="flow-root mt-12">
<div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a <div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a
@ -78,8 +79,8 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-
</p> </p>
<ul role="list" class="space-y-3 text-sm leading-6 "> <ul role="list" class="space-y-3 text-sm leading-6 ">
<li class="flex"> <li class="flex">
<svg class="flex-none w-5 h-6 mr-3 dark:text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 mr-3 dark:text-warning" viewBox="0 0 20 20"
aria-hidden="true"> fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.775 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.775 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
@ -141,13 +142,14 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-
{{ $pro }} {{ $pro }}
@endisset @endisset
@endif @endif
<p class="h-20 mt-10 text-sm leading-6 dark:text-white">Expand your business or set up your own hosting <p class="h-20 mt-10 text-sm leading-6 dark:text-white">Expand your business or set up your own
hosting
environment. environment.
</p> </p>
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 "> <ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
<li class="flex "> <li class="flex ">
<svg class="flex-none w-5 h-6 mr-3 dark:text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 mr-3 dark:text-warning" viewBox="0 0 20 20"
aria-hidden="true"> fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
@ -188,7 +190,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-
</div> </div>
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-12"> <div class="pt-16 lg:px-8 lg:pt-0 xl:px-12">
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 dark:text-white">Ultimate</h3> <h3 id="tier-ultimate" class="text-base font-semibold leading-7 dark:text-white">Ultimate</h3>
<p class="flex items-baseline mt-6 gap-x-1"> <p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span class="text-4xl font-bold tracking-tight dark:text-white">Custom</span> <span class="text-4xl font-bold tracking-tight dark:text-white">Custom</span>
{{-- <span class="text-sm font-semibold leading-6 ">pay-as-you-go</span> --}} {{-- <span class="text-sm font-semibold leading-6 ">pay-as-you-go</span> --}}
@ -198,7 +200,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-
{{-- <span class="text-sm font-semibold leading-6 ">/month + VAT</span> --}} {{-- <span class="text-sm font-semibold leading-6 ">/month + VAT</span> --}}
</span> </span>
</p> </p>
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span>pay-as-you-go</span> <span>pay-as-you-go</span>
</span> </span>
<span x-show="selected === 'yearly'" x-cloak> <span x-show="selected === 'yearly'" x-cloak>
@ -213,8 +215,8 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-
single location.</p> single location.</p>
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 "> <ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
<li class="flex "> <li class="flex ">
<svg class="flex-none w-5 h-6 mr-3 dark:text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 mr-3 dark:text-warning" viewBox="0 0 20 20"
aria-hidden="true"> fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />

View File

@ -46,13 +46,6 @@
stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
{{-- <div class="flex-1 text-xl font-bold leading-6 dark:text-white">Dashboard</div> --}}
{{-- <a href="#">
<span class="sr-only">Your profile</span>
<img class="w-8 h-8 rounded-full bg-gray-50"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt="">
</a> --}}
</div> </div>
<main class="lg:pl-48"> <main class="lg:pl-48">

View File

@ -4,32 +4,22 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link rel="dns-prefetch" href="https://api.fonts.coollabs.io" />
<link rel="preload" href="https://api.fonts.coollabs.io/css2?family=Inter:wght@400;500;600;700;800&display=swap"
as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/400.woff2" as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/500.woff2" as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/600.woff2" as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/700.woff2" as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/800.woff2" as="style" />
<link href="https://api.fonts.coollabs.io/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
@use('App\Models\InstanceSettings') @use('App\Models\InstanceSettings')
@php @php
$instanceSettings = InstanceSettings::first(); $instanceSettings = InstanceSettings::first();
$name = null; $name = null;
if($instanceSettings) { if ($instanceSettings) {
$displayName = $instanceSettings->getTitleDisplayName(); $displayName = $instanceSettings->getTitleDisplayName();
if(strlen($displayName) > 0) { if (strlen($displayName) > 0) {
$name = $displayName . ' '; $name = $displayName . ' ';
}
} }
}
@endphp @endphp
<title>{{ $name }}{{ $title ?? 'Coolify' }}</title> <title>{{ $name }}{{ $title ?? 'Coolify' }}</title>
@env('local') @env('local')
<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
@ -46,13 +36,9 @@
<script defer data-domain="app.coolify.io" src="https://analytics.coollabs.io/js/plausible.js"></script> <script defer data-domain="app.coolify.io" src="https://analytics.coollabs.io/js/plausible.js"></script>
@endif @endif
@auth @auth
<script src="https://cdnjs.cloudflare.com/ajax/libs/laravel-echo/1.15.3/echo.iife.min.js" <script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
integrity="sha512-aPAh2oRUr3ALz2MwVWkd6lmdgBQC0wSr0R++zclNjXZreT/JrwDPZQwA/p6R3wOCTcXKIHgA9pQGEQBWQmdLaA==" <script type="text/javascript" src="{{ URL::asset('js/pusher.js') }}"></script>
crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script type="text/javascript" src="{{ URL::asset('js/apexcharts.js') }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/8.3.0/pusher.min.js"
integrity="sha512-tXL5mrkSoP49uQf2jO0LbvzMyFgki//znmq0wYXGq94gVF6TU0QlrSbwGuPpKTeN1mIjReeqKZ4/NJPjHN1d2Q=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
@endauth @endauth
</head> </head>
@section('body') @section('body')

View File

@ -2,12 +2,11 @@
@if (subscriptionProvider() === 'stripe') @if (subscriptionProvider() === 'stripe')
<div class="pt-4"> <div class="pt-4">
<h2>Your current plan</h2> <h2>Your current plan</h2>
<div class="pb-4">Tier: <strong <div class="pb-4">Tier: <strong class="dark:text-warning">
class="dark:text-warning">
@if (data_get(currentTeam(), 'subscription')->type() == 'dynamic') @if (data_get(currentTeam(), 'subscription')->type() == 'dynamic')
Pay-as-you-go Pay-as-you-go
@else @else
{{ data_get(currentTeam(), 'subscription')->type() }} {{ data_get(currentTeam(), 'subscription')->type() }}
@endif @endif
</strong></div> </strong></div>
@ -50,35 +49,4 @@ class="dark:text-warning">
target="_blank">contact us.</a> target="_blank">contact us.</a>
</div> </div>
@endif @endif
{{-- @if (subscriptionProvider() === 'lemon')
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div>
<div>Type: {{ currentTeam()->subscription->lemon_variant_name }}</div>
@if (currentTeam()->subscription->lemon_status === 'cancelled')
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
class="underline dark:text-white" href="{{ config('coolify.contact') }}" target="_blank">please
contact
us.</a></div>
@else
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
@endif
<div class="flex flex-col gap-2">
<div class="flex gap-2">
@if (currentTeam()->subscription->lemon_status === 'cancelled')
<x-forms.button class="bg-coollabs-gradient" wire:click='resume'>Resume Subscription
</x-forms.button>
@else
<x-forms.button wire:click='cancel'>Cancel Subscription</x-forms.button>
@endif
</div>
<div>
<x-forms.button><a class="dark:text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment
Details</a>
</x-forms.button>
<a class="dark:text-white hover:no-underline"
href="https://app.lemonsqueezy.com/my-orders"><x-forms.button>Manage My
Subscription</x-forms.button></a>
</div>
</div>
@endif --}}
</div> </div>

View File

@ -1,67 +0,0 @@
<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')">
{{ $isTrial ? 'Start Trial' : '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')">
{{ $isTrial ? 'Start Trial' : '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')">
{{ $isTrial ? 'Start Trial' : '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')"> {{ $isTrial ? 'Start Trial' : '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')">
{{ $isTrial ? 'Start Trial' : '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')"> {{ $isTrial ? 'Start Trial' : '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>

View File

@ -25,7 +25,7 @@
<div>This is the default team. You can't delete it.</div> <div>This is the default team. You can't delete it.</div>
@elseif(auth()->user()->teams()->get()->count() === 1 || auth()->user()->currentTeam()->personal_team) @elseif(auth()->user()->teams()->get()->count() === 1 || auth()->user()->currentTeam()->personal_team)
<div>You can't delete your last / personal team.</div> <div>You can't delete your last / personal team.</div>
@elseif(currentTeam()->subscription && currentTeam()->subscription?->lemon_status !== 'cancelled') @elseif(currentTeam()->subscription)
<div>Please cancel your subscription <a class="underline dark:text-white" <div>Please cancel your subscription <a class="underline dark:text-white"
href="{{ route('subscription.show') }}">here</a> before delete this team.</div> href="{{ route('subscription.show') }}">here</a> before delete this team.</div>
@else @else

View File

@ -1,62 +0,0 @@
<?php
use App\Actions\CoolifyTask\RunRemoteProcess;
use App\Actions\CoolifyTask\TidyOutput;
use App\Models\Server;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
uses(DatabaseMigrations::class);
beforeEach(function () {
$this->seed(DatabaseSeeder::class);
});
it('starts a docker container correctly', function () {
test()->actingAs(User::factory([
'uuid' => Str::uuid(),
'email' => Str::uuid() . '@example.com',
])->create());
$coolifyNamePrefix = 'coolify_test_';
$format = '{"ID":"{{ .ID }}", "Image": "{{ .Image }}", "Names":"{{ .Names }}"}';
$areThereCoolifyTestContainers = "docker ps --filter=\"name={$coolifyNamePrefix}*\" --format '{$format}' ";
// Generate a known name
$containerName = 'coolify_test_' . now()->format('Ymd_his');
$host = Server::where('name', 'testing-local-docker-container')->first();
remote_process([
"docker rm -f $(docker ps --filter='name={$coolifyNamePrefix}*' -aq) > /dev/null 2>&1"
], $host);
// Assert there's no containers start with coolify_test_*
$activity = remote_process([$areThereCoolifyTestContainers], $host);
$tidyOutput = RunRemoteProcess::decodeOutput($activity);
$containers = format_docker_command_output_to_json($tidyOutput);
expect($containers)->toBeEmpty();
// start a container nginx -d --name = $containerName
$activity = remote_process(["docker run -d --rm --name {$containerName} nginx"], $host);
expect($activity->getExtraProperty('exitCode'))->toBe(0);
// docker ps name = $container
$activity = remote_process([$areThereCoolifyTestContainers], $host);
$tidyOutput = RunRemoteProcess::decodeOutput($activity);
$containers = format_docker_command_output_to_json($tidyOutput);
expect($containers->where('Names', $containerName)->count())->toBe(1);
// Stop testing containers
$activity = remote_process([
"docker ps --filter='name={$coolifyNamePrefix}*' -aq && " .
"docker rm -f $(docker ps --filter='name={$coolifyNamePrefix}*' -aq)"
], $host);
expect($activity->getExtraProperty('exitCode'))->toBe(0);
});

View File

@ -1,32 +0,0 @@
<?php
use App\Actions\CoolifyTask\RunRemoteProcess;
use App\Models\Server;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
uses(DatabaseMigrations::class);
beforeEach(function () {
$this->seed(DatabaseSeeder::class);
});
it('outputs correctly', function () {
$host = Server::where('name', 'testing-local-docker-container')->first();
$activity = remote_process([
'pwd',
'x=1; while [ $x -le 3 ]; do sleep 0.1 && echo "Welcome $x times" $(( x++ )); done',
], $host);
$tidyOutput = RunRemoteProcess::decodeOutput($activity);
expect($tidyOutput)
->toContain('Welcome 1 times')
->toContain('Welcome 3 times')
->not()->toBeJson();
});

View File

@ -1,7 +1,7 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.305" "version": "4.0.0-beta.306"
} }
} }
} }