From 6e2a750c4e21e4c7c9ea452e96c90acf99a6916f Mon Sep 17 00:00:00 2001 From: Gitea Date: Sat, 3 May 2025 06:13:19 +0530 Subject: [PATCH] added cashier subscription, subscription plans --- app/Filament/Resources/PlanResource.php | 100 ++++++ .../PlanResource/Pages/CreatePlan.php | 21 ++ .../Resources/PlanResource/Pages/EditPlan.php | 32 ++ .../PlanResource/Pages/ListPlans.php | 19 ++ app/Listeners/StripeEventListener.php | 37 ++ app/Livewire/Dashboard/Dashboard.php | 60 +++- app/Livewire/Dashboard/Pricing.php | 25 ++ app/Models/Plan.php | 23 ++ app/Models/User.php | 3 +- app/Providers/AppServiceProvider.php | 9 + bootstrap/app.php | 3 + composer.json | 5 +- composer.lock | 323 +++++++++++++++++- config/cashier.php | 127 +++++++ ...5_05_02_144314_create_customer_columns.php | 40 +++ ...5_02_144315_create_subscriptions_table.php | 37 ++ ...144316_create_subscription_items_table.php | 34 ++ .../2025_05_02_215351_create_plans_table.php | 34 ++ resources/css/boil.css | 8 + resources/views/flux/icon/circle-x.blade.php | 43 +++ .../livewire/dashboard/dashboard.blade.php | 52 ++- .../livewire/dashboard/pricing.blade.php | 46 +++ routes/web.php | 27 ++ 23 files changed, 1087 insertions(+), 21 deletions(-) create mode 100644 app/Filament/Resources/PlanResource.php create mode 100644 app/Filament/Resources/PlanResource/Pages/CreatePlan.php create mode 100644 app/Filament/Resources/PlanResource/Pages/EditPlan.php create mode 100644 app/Filament/Resources/PlanResource/Pages/ListPlans.php create mode 100644 app/Listeners/StripeEventListener.php create mode 100644 app/Livewire/Dashboard/Pricing.php create mode 100644 app/Models/Plan.php create mode 100644 config/cashier.php create mode 100644 database/migrations/2025_05_02_144314_create_customer_columns.php create mode 100644 database/migrations/2025_05_02_144315_create_subscriptions_table.php create mode 100644 database/migrations/2025_05_02_144316_create_subscription_items_table.php create mode 100644 database/migrations/2025_05_02_215351_create_plans_table.php create mode 100644 resources/views/flux/icon/circle-x.blade.php create mode 100644 resources/views/livewire/dashboard/pricing.blade.php diff --git a/app/Filament/Resources/PlanResource.php b/app/Filament/Resources/PlanResource.php new file mode 100644 index 0000000..79fbab7 --- /dev/null +++ b/app/Filament/Resources/PlanResource.php @@ -0,0 +1,100 @@ +schema([ + Section::make('Plan Information') + ->description('Add a new plan') + ->schema([ + TextInput::make('name')->label('Page Name') + ->required(), + TextInput::make('description'), + TextInput::make('product_id')->required(), + TextInput::make('pricing_id')->required(), + TextInput::make('price')->numeric()->required(), + Select::make('monthly_billing')->options([ + 1 => 'Monthly', + 0 => 'Yearly', + ])->default(1)->required(), + KeyValue::make('details') + ->label('Plan Details (Optional)') + ->keyPlaceholder('Name') + ->valuePlaceholder('Content') + ->reorderable(), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('name')->label('Name'), + TextColumn::make('product_id')->label('Product'), + TextColumn::make('pricing_id')->label('Pricing'), + TextColumn::make('price')->label('Price'), + BooleanColumn::make('monthly_billing')->label('Monthly Billing'), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPlans::route('/'), + 'create' => Pages\CreatePlan::route('/create'), + 'edit' => Pages\EditPlan::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/PlanResource/Pages/CreatePlan.php b/app/Filament/Resources/PlanResource/Pages/CreatePlan.php new file mode 100644 index 0000000..1d9a4b2 --- /dev/null +++ b/app/Filament/Resources/PlanResource/Pages/CreatePlan.php @@ -0,0 +1,21 @@ +success() + ->title('Plan created') + ->body('Plan created successfully'); + } +} diff --git a/app/Filament/Resources/PlanResource/Pages/EditPlan.php b/app/Filament/Resources/PlanResource/Pages/EditPlan.php new file mode 100644 index 0000000..0da8091 --- /dev/null +++ b/app/Filament/Resources/PlanResource/Pages/EditPlan.php @@ -0,0 +1,32 @@ +getResource()::getUrl('index'); + } + + protected function getSavedNotification(): ?Notification + { + return Notification::make() + ->success() + ->title('Plan updated') + ->body('Plan updated successfully'); + } +} diff --git a/app/Filament/Resources/PlanResource/Pages/ListPlans.php b/app/Filament/Resources/PlanResource/Pages/ListPlans.php new file mode 100644 index 0000000..73ab76d --- /dev/null +++ b/app/Filament/Resources/PlanResource/Pages/ListPlans.php @@ -0,0 +1,19 @@ +payload['type'] === 'invoice.payment_succeeded') { + session()->flash('alert', ['type' => 'success', 'message' => 'Payment completed successfully.']); + Log::info('Payment succeeded'); + } + + if ($event->payload['type'] === 'customer.subscription.deleted') { + session()->flash('alert', ['type' => 'error', 'message' => 'Subscription canceled.']); + Log::info('Subscription canceled'); + } + } +} diff --git a/app/Livewire/Dashboard/Dashboard.php b/app/Livewire/Dashboard/Dashboard.php index 1e0ef9a..06008ba 100644 --- a/app/Livewire/Dashboard/Dashboard.php +++ b/app/Livewire/Dashboard/Dashboard.php @@ -2,12 +2,70 @@ namespace App\Livewire\Dashboard; +use Illuminate\Http\Request; use Livewire\Component; class Dashboard extends Component { + public $message; + public $subscription; + public $plans; + + public function paymentStatus(Request $request) + { + $status = $request->route('status'); + $currentUrl = $request->fullUrl(); + if ($status == 'success') { + return redirect()->route('dashboard')->with('status', 'success'); + } elseif ($status == 'cancel') { + return redirect()->route('dashboard')->with('status', 'cancel'); + } + } + + public function mount(Request $request) + { + try { + $status = $request->session()->get('status'); + if (isset($status)) { + if ($status == 'success') { + $this->message = ['type' => 'success', 'message' => 'Order completed successfully.']; + } else { + $this->message = ['type' => 'error', 'message' => 'Order cancelled.']; + } + $request->session()->forget('status'); + } + } catch (\Exception $exception) { + + } + + if (auth()->user()->subscribedToProduct(config('app.plans')[0]['product_id'])) { + try { + $result = auth()->user()->subscriptions()->where(['stripe_status' => 'active'])->orderByDesc('updated_at')->first(); + $userPriceID = $result['items'][0]['stripe_price']; + $subscriptionEnd = $result['ends_at']; + + $planName = null; // Default value if not found + + foreach (config('app.plans') as $plan) { + if ($plan['pricing_id'] === $userPriceID) { + $planName = $plan['name']; + break; + } + } + $this->subscription['name'] = $planName; + $this->subscription['ends_at'] = $subscriptionEnd; + + } catch (\Exception $e) { + + \Log::error($e->getMessage()); + } + } + + + } + public function render() { - return view('livewire.dashboard.dashboard')->layout('components.layouts.dashboard'); + return view('livewire.dashboard.dashboard')->layout('components.layouts.dashboard')->with('message', $this->message); } } diff --git a/app/Livewire/Dashboard/Pricing.php b/app/Livewire/Dashboard/Pricing.php new file mode 100644 index 0000000..80e0e43 --- /dev/null +++ b/app/Livewire/Dashboard/Pricing.php @@ -0,0 +1,25 @@ +plans = config('app.plans'); + } + + public function choosePlan($pricing_id): void + { + $this->redirect(route('checkout', $pricing_id)); + } + + public function render() + { + return view('livewire.dashboard.pricing'); + } +} diff --git a/app/Models/Plan.php b/app/Models/Plan.php new file mode 100644 index 0000000..ea0ee84 --- /dev/null +++ b/app/Models/Plan.php @@ -0,0 +1,23 @@ + 'json', + 'monthly_billing' => 'boolean', + ]; +} diff --git a/app/Models/User.php b/app/Models/User.php index 3a0508e..92958c1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,11 +10,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Support\Str; +use Laravel\Cashier\Billable; class User extends Authenticatable implements FilamentUser, MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, Billable; /** * The attributes that are mass assignable. diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 42c815c..236bec2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,8 +4,10 @@ namespace App\Providers; use App\Models\Blog; use App\Models\Menu; +use App\Models\Plan; use DB; use Illuminate\Support\ServiceProvider; +use Laravel\Cashier\Cashier; class AppServiceProvider extends ServiceProvider { @@ -33,8 +35,15 @@ class AppServiceProvider extends ServiceProvider return Blog::where('is_published', 1)->get(); }); + $plans = cache()->remember('app_plans', now()->addHours(6), function () { + return Plan::all(); + }); + config(['app.settings' => (array) $settings]); config(['app.menus' => $menus]); config(['app.blogs' => $blogs]); + config(['app.plans' => $plans]); + + Cashier::calculateTaxes(); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 94f56e3..8ea7f1a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ \App\Http\Middleware\Locale::class, ]); + $middleware->validateCsrfTokens(except: [ + 'stripe/*', + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/composer.json b/composer.json index 73264b9..dccc01c 100644 --- a/composer.json +++ b/composer.json @@ -7,13 +7,14 @@ "license": "MIT", "require": { "php": "^8.2", + "ext-imap": "*", "ddeboer/imap": "^1.14", "filament/filament": "3.3", + "laravel/cashier": "^15.6", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "livewire/flux": "^2.1", - "livewire/livewire": "^3.6", - "ext-imap": "*" + "livewire/livewire": "^3.6" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.15", diff --git a/composer.lock b/composer.lock index 122a729..d444609 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b4dcaf149eb8c0291c1de5ccd42c8f94", + "content-hash": "f1d41505247807e937b78e4e92b1571e", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -2100,6 +2100,94 @@ }, "time": "2025-04-01T14:41:56+00:00" }, + { + "name": "laravel/cashier", + "version": "v15.6.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/cashier-stripe.git", + "reference": "8fe60cc71161ef06b6a1b23cffe886abf2a49b29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/cashier-stripe/zipball/8fe60cc71161ef06b6a1b23cffe886abf2a49b29", + "reference": "8fe60cc71161ef06b6a1b23cffe886abf2a49b29", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/log": "^10.0|^11.0|^12.0", + "illuminate/notifications": "^10.0|^11.0|^12.0", + "illuminate/pagination": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/view": "^10.0|^11.0|^12.0", + "moneyphp/money": "^4.0", + "nesbot/carbon": "^2.0|^3.0", + "php": "^8.1", + "stripe/stripe-php": "^16.2", + "symfony/console": "^6.0|^7.0", + "symfony/http-kernel": "^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.22.1" + }, + "require-dev": { + "dompdf/dompdf": "^2.0", + "mockery/mockery": "^1.0", + "orchestra/testbench": "^8.18|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.4|^11.5" + }, + "suggest": { + "dompdf/dompdf": "Required when generating and downloading invoice PDF's using Dompdf (^1.0.1|^2.0).", + "ext-intl": "Allows for more locales besides the default \"en\" when formatting money values." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Cashier\\CashierServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "15.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Cashier\\": "src/", + "Laravel\\Cashier\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Dries Vints", + "email": "dries@laravel.com" + } + ], + "description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.", + "keywords": [ + "billing", + "laravel", + "stripe" + ], + "support": { + "issues": "https://github.com/laravel/cashier/issues", + "source": "https://github.com/laravel/cashier" + }, + "time": "2025-04-22T13:59:36+00:00" + }, { "name": "laravel/framework", "version": "v12.10.2", @@ -3349,6 +3437,96 @@ }, "time": "2024-03-31T07:05:07+00:00" }, + { + "name": "moneyphp/money", + "version": "v4.7.0", + "source": { + "type": "git", + "url": "https://github.com/moneyphp/money.git", + "reference": "af048f0206d3b39b8fad9de6a230cedf765365fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moneyphp/money/zipball/af048f0206d3b39b8fad9de6a230cedf765365fa", + "reference": "af048f0206d3b39b8fad9de6a230cedf765365fa", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-filter": "*", + "ext-json": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "cache/taggable-cache": "^1.1.0", + "doctrine/coding-standard": "^12.0", + "doctrine/instantiator": "^1.5.0 || ^2.0", + "ext-gmp": "*", + "ext-intl": "*", + "florianv/exchanger": "^2.8.1", + "florianv/swap": "^4.3.0", + "moneyphp/crypto-currencies": "^1.1.0", + "moneyphp/iso-currencies": "^3.4", + "php-http/message": "^1.16.0", + "php-http/mock-client": "^1.6.0", + "phpbench/phpbench": "^1.2.5", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.9", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.9", + "psr/cache": "^1.0.1 || ^2.0 || ^3.0", + "ticketswap/phpstan-error-formatter": "^1.1" + }, + "suggest": { + "ext-gmp": "Calculate without integer limits", + "ext-intl": "Format Money objects with intl", + "florianv/exchanger": "Exchange rates library for PHP", + "florianv/swap": "Exchange rates library for PHP", + "psr/cache-implementation": "Used for Currency caching" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Money\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "http://verraes.net" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" + } + ], + "description": "PHP implementation of Fowler's Money pattern", + "homepage": "http://moneyphp.org", + "keywords": [ + "Value Object", + "money", + "vo" + ], + "support": { + "issues": "https://github.com/moneyphp/money/issues", + "source": "https://github.com/moneyphp/money/tree/v4.7.0" + }, + "time": "2025-04-03T08:26:36+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -5028,6 +5206,65 @@ ], "time": "2025-04-11T15:27:14+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v16.6.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "d6de0a536f00b5c5c74f36b8f4d0d93b035499ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/d6de0a536f00b5c5c74f36b8f4d0d93b035499ff", + "reference": "d6de0a536f00b5c5c74f36b8f4d0d93b035499ff", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "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/v16.6.0" + }, + "time": "2025-02-24T22:35:29+00:00" + }, { "name": "symfony/clock", "version": "v7.2.0", @@ -6204,6 +6441,90 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "763d2a91fea5681509ca01acbc1c5e450d127811" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/763d2a91fea5681509ca01acbc1c5e450d127811", + "reference": "763d2a91fea5681509ca01acbc1c5e450d127811", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-21T18:38:29+00:00" + }, { "name": "symfony/polyfill-intl-idn", "version": "v1.31.0", diff --git a/config/cashier.php b/config/cashier.php new file mode 100644 index 0000000..4a9b024 --- /dev/null +++ b/config/cashier.php @@ -0,0 +1,127 @@ + env('STRIPE_KEY'), + + 'secret' => env('STRIPE_SECRET'), + + /* + |-------------------------------------------------------------------------- + | Cashier Path + |-------------------------------------------------------------------------- + | + | This is the base URI path where Cashier's views, such as the payment + | verification screen, will be available from. You're free to tweak + | this path according to your preferences and application design. + | + */ + + 'path' => env('CASHIER_PATH', 'stripe'), + + /* + |-------------------------------------------------------------------------- + | Stripe Webhooks + |-------------------------------------------------------------------------- + | + | Your Stripe webhook secret is used to prevent unauthorized requests to + | your Stripe webhook handling controllers. The tolerance setting will + | check the drift between the current time and the signed request's. + | + */ + + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET'), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + 'events' => WebhookCommand::DEFAULT_EVENTS, + ], + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | This is the default currency that will be used when generating charges + | from your application. Of course, you are welcome to use any of the + | various world currencies that are currently supported via Stripe. + | + */ + + 'currency' => env('CASHIER_CURRENCY', 'usd'), + + /* + |-------------------------------------------------------------------------- + | Currency Locale + |-------------------------------------------------------------------------- + | + | This is the default locale in which your money values are formatted in + | for display. To utilize other locales besides the default en locale + | verify you have the "intl" PHP extension installed on the system. + | + */ + + 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Payment Confirmation Notification + |-------------------------------------------------------------------------- + | + | If this setting is enabled, Cashier will automatically notify customers + | whose payments require additional verification. You should listen to + | Stripe's webhooks in order for this feature to function correctly. + | + */ + + 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), + + /* + |-------------------------------------------------------------------------- + | Invoice Settings + |-------------------------------------------------------------------------- + | + | The following options determine how Cashier invoices are converted from + | HTML into PDFs. You're free to change the options based on the needs + | of your application or your preferences regarding invoice styling. + | + */ + + 'invoices' => [ + 'renderer' => env('CASHIER_INVOICE_RENDERER', DompdfInvoiceRenderer::class), + + 'options' => [ + // Supported: 'letter', 'legal', 'A4' + 'paper' => env('CASHIER_PAPER', 'letter'), + + 'remote_enabled' => env('CASHIER_REMOTE_ENABLED', false), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Stripe Logger + |-------------------------------------------------------------------------- + | + | This setting defines which logging channel will be used by the Stripe + | library to write log messages. You are free to specify any of your + | logging channels listed inside the "logging" configuration file. + | + */ + + 'logger' => env('CASHIER_LOGGER'), + +]; diff --git a/database/migrations/2025_05_02_144314_create_customer_columns.php b/database/migrations/2025_05_02_144314_create_customer_columns.php new file mode 100644 index 0000000..974b381 --- /dev/null +++ b/database/migrations/2025_05_02_144314_create_customer_columns.php @@ -0,0 +1,40 @@ +string('stripe_id')->nullable()->index(); + $table->string('pm_type')->nullable(); + $table->string('pm_last_four', 4)->nullable(); + $table->timestamp('trial_ends_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex([ + 'stripe_id', + ]); + + $table->dropColumn([ + 'stripe_id', + 'pm_type', + 'pm_last_four', + 'trial_ends_at', + ]); + }); + } +}; diff --git a/database/migrations/2025_05_02_144315_create_subscriptions_table.php b/database/migrations/2025_05_02_144315_create_subscriptions_table.php new file mode 100644 index 0000000..ccbcc6d --- /dev/null +++ b/database/migrations/2025_05_02_144315_create_subscriptions_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('user_id'); + $table->string('type'); + $table->string('stripe_id')->unique(); + $table->string('stripe_status'); + $table->string('stripe_price')->nullable(); + $table->integer('quantity')->nullable(); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + + $table->index(['user_id', 'stripe_status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; diff --git a/database/migrations/2025_05_02_144316_create_subscription_items_table.php b/database/migrations/2025_05_02_144316_create_subscription_items_table.php new file mode 100644 index 0000000..420e23f --- /dev/null +++ b/database/migrations/2025_05_02_144316_create_subscription_items_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('subscription_id'); + $table->string('stripe_id')->unique(); + $table->string('stripe_product'); + $table->string('stripe_price'); + $table->integer('quantity')->nullable(); + $table->timestamps(); + + $table->index(['subscription_id', 'stripe_price']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscription_items'); + } +}; diff --git a/database/migrations/2025_05_02_215351_create_plans_table.php b/database/migrations/2025_05_02_215351_create_plans_table.php new file mode 100644 index 0000000..f0f4ce0 --- /dev/null +++ b/database/migrations/2025_05_02_215351_create_plans_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('product_id')->collation('utf8_bin'); + $table->string('pricing_id')->collation('utf8_bin'); + $table->integer('price'); + $table->boolean('monthly_billing')->default(true); + $table->longText('details')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('plans'); + } +}; diff --git a/resources/css/boil.css b/resources/css/boil.css index 919f863..2c2d1f5 100644 --- a/resources/css/boil.css +++ b/resources/css/boil.css @@ -8,6 +8,14 @@ background-color: #f72a25; } +.app-primary-bg { + background-color: #F14743; +} + +.app-primary { + color: #F14743; +} + .btn-primary { color: white; background-color: #4361ee; diff --git a/resources/views/flux/icon/circle-x.blade.php b/resources/views/flux/icon/circle-x.blade.php new file mode 100644 index 0000000..bd216ed --- /dev/null +++ b/resources/views/flux/icon/circle-x.blade.php @@ -0,0 +1,43 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); +} + +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/livewire/dashboard/dashboard.blade.php b/resources/views/livewire/dashboard/dashboard.blade.php index a64fc45..701ce90 100644 --- a/resources/views/livewire/dashboard/dashboard.blade.php +++ b/resources/views/livewire/dashboard/dashboard.blade.php @@ -1,19 +1,11 @@ @section('title'){{ __('Dashboard') }}@endsection
-
+
+ +
- - - -
-

0

-

Balance

-
-
- -
- +
@@ -23,7 +15,7 @@
- +
@@ -34,7 +26,35 @@
-
- -
+ @if(auth()->user()->subscribedToProduct(config('app.plans')[0]['product_id'])) + +
+
+

Your {{ $subscription['name'] }} subscription is active and will be + @if($subscription['ends_at']) end at {{ \Carbon\Carbon::make($subscription['ends_at'])->toFormattedDayDateString() }}. + @else auto-renew as per the plan you chose. + @endif + To manage you subscription Click here

+
+
+ @else +
+ +
+ @endif + +
diff --git a/resources/views/livewire/dashboard/pricing.blade.php b/resources/views/livewire/dashboard/pricing.blade.php new file mode 100644 index 0000000..5513748 --- /dev/null +++ b/resources/views/livewire/dashboard/pricing.blade.php @@ -0,0 +1,46 @@ +
+
+

Purchase Subscription

+
+
+ + @if(isset($plans)) + @foreach($plans as $plan) +
+
+

{{ $plan->name }} @if(!$plan->monthly_billing) + 2 Months Free + @endif

+ +

+ ${{ $plan->price }} + /{{ $plan->monthly_billing ? 'month' : 'year' }} +

+
+ +
    + + @if($plan->details) + @forelse ($plan->details as $key => $value) + @if ($value) +
  • + @if($value == "true") + @else + @endif + {{ $key }} +
  • + @endif + @empty + @endforelse + @endif +
+ + + Choose Plan + +
+ @endforeach + @endif + +
+
diff --git a/routes/web.php b/routes/web.php index b89b755..f84ef8e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -47,6 +47,33 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('dashboard/bulk-email-generator', Dashboard::class)->name('dashboard.bulk'); Route::get('dashboard/compose-email', Dashboard::class)->name('dashboard.compose'); + // Checkout Routes + Route::get('checkout/{plan}', function ($pricing_id) { + $plans = config('app.plans'); + $pricingData = []; + foreach ($plans as $plan) { + $pricingData[] = $plan['pricing_id']; + } + + if (in_array($pricing_id, $pricingData)) { + return auth()->user() + ->newSubscription('default', $pricing_id) + ->allowPromotionCodes() + ->checkout([ + 'success_url' => route('checkout.success'), + 'cancel_url' => route('checkout.cancel'), + ]); + + } + abort(404); + })->name('checkout'); + + Route::get('dashboard/success', [Dashboard::class, 'paymentStatus'])->name('checkout.success')->defaults('status', 'success'); + Route::get('dashboard/cancel', [Dashboard::class, 'paymentStatus'])->name('checkout.cancel')->defaults('status', 'cancel'); + + Route::get('dashboard/billing', function () { + return auth()->user()->redirectToBillingPortal(route('dashboard')); + })->name('billing'); }); Route::middleware(['auth'])->group(function () {