added cashier subscription, subscription plans
This commit is contained in:
100
app/Filament/Resources/PlanResource.php
Normal file
100
app/Filament/Resources/PlanResource.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PlanResource\Pages;
|
||||||
|
use App\Filament\Resources\PlanResource\RelationManagers;
|
||||||
|
use App\Models\Plan;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Filament\Forms\Components\KeyValue;
|
||||||
|
use Filament\Forms\Components\RichEditor;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Forms\Set;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\BooleanColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use phpDocumentor\Reflection\Types\Boolean;
|
||||||
|
|
||||||
|
class PlanResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Plan::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'Web Master';
|
||||||
|
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Filament/Resources/PlanResource/Pages/CreatePlan.php
Normal file
21
app/Filament/Resources/PlanResource/Pages/CreatePlan.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PlanResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PlanResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreatePlan extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PlanResource::class;
|
||||||
|
|
||||||
|
protected function getCreatedNotification(): ?Notification
|
||||||
|
{
|
||||||
|
return Notification::make()
|
||||||
|
->success()
|
||||||
|
->title('Plan created')
|
||||||
|
->body('Plan created successfully');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Filament/Resources/PlanResource/Pages/EditPlan.php
Normal file
32
app/Filament/Resources/PlanResource/Pages/EditPlan.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PlanResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PlanResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditPlan extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PlanResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
protected function getRedirectUrl(): ?string
|
||||||
|
{
|
||||||
|
return $this->getResource()::getUrl('index');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSavedNotification(): ?Notification
|
||||||
|
{
|
||||||
|
return Notification::make()
|
||||||
|
->success()
|
||||||
|
->title('Plan updated')
|
||||||
|
->body('Plan updated successfully');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/PlanResource/Pages/ListPlans.php
Normal file
19
app/Filament/Resources/PlanResource/Pages/ListPlans.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PlanResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PlanResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListPlans extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = PlanResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Listeners/StripeEventListener.php
Normal file
37
app/Listeners/StripeEventListener.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Laravel\Cashier\Events\WebhookReceived;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
class StripeEventListener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*/
|
||||||
|
public function handle(WebhookReceived $event): void
|
||||||
|
{
|
||||||
|
if ($event->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,70 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Dashboard;
|
namespace App\Livewire\Dashboard;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Dashboard extends 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()
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
app/Livewire/Dashboard/Pricing.php
Normal file
25
app/Livewire/Dashboard/Pricing.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Dashboard;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Pricing extends Component
|
||||||
|
{
|
||||||
|
public $plans;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->plans = config('app.plans');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function choosePlan($pricing_id): void
|
||||||
|
{
|
||||||
|
$this->redirect(route('checkout', $pricing_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.dashboard.pricing');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/Plan.php
Normal file
23
app/Models/Plan.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Plan extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'product_id',
|
||||||
|
'pricing_id',
|
||||||
|
'price',
|
||||||
|
'monthly_billing',
|
||||||
|
'details'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'details' => 'json',
|
||||||
|
'monthly_billing' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -10,11 +10,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
|||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Laravel\Cashier\Billable;
|
||||||
|
|
||||||
class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, Billable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ namespace App\Providers;
|
|||||||
|
|
||||||
use App\Models\Blog;
|
use App\Models\Blog;
|
||||||
use App\Models\Menu;
|
use App\Models\Menu;
|
||||||
|
use App\Models\Plan;
|
||||||
use DB;
|
use DB;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Laravel\Cashier\Cashier;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -33,8 +35,15 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
return Blog::where('is_published', 1)->get();
|
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.settings' => (array) $settings]);
|
||||||
config(['app.menus' => $menus]);
|
config(['app.menus' => $menus]);
|
||||||
config(['app.blogs' => $blogs]);
|
config(['app.blogs' => $blogs]);
|
||||||
|
config(['app.plans' => $plans]);
|
||||||
|
|
||||||
|
Cashier::calculateTaxes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\Locale::class,
|
\App\Http\Middleware\Locale::class,
|
||||||
]);
|
]);
|
||||||
|
$middleware->validateCsrfTokens(except: [
|
||||||
|
'stripe/*',
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -7,13 +7,14 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"ext-imap": "*",
|
||||||
"ddeboer/imap": "^1.14",
|
"ddeboer/imap": "^1.14",
|
||||||
"filament/filament": "3.3",
|
"filament/filament": "3.3",
|
||||||
|
"laravel/cashier": "^15.6",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"livewire/flux": "^2.1",
|
"livewire/flux": "^2.1",
|
||||||
"livewire/livewire": "^3.6",
|
"livewire/livewire": "^3.6"
|
||||||
"ext-imap": "*"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.15",
|
"barryvdh/laravel-debugbar": "^3.15",
|
||||||
|
|||||||
323
composer.lock
generated
323
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": "b4dcaf149eb8c0291c1de5ccd42c8f94",
|
"content-hash": "f1d41505247807e937b78e4e92b1571e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@@ -2100,6 +2100,94 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-04-01T14:41:56+00:00"
|
"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",
|
"name": "laravel/framework",
|
||||||
"version": "v12.10.2",
|
"version": "v12.10.2",
|
||||||
@@ -3349,6 +3437,96 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-03-31T07:05:07+00:00"
|
"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",
|
"name": "monolog/monolog",
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
@@ -5028,6 +5206,65 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-04-11T15:27:14+00:00"
|
"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",
|
"name": "symfony/clock",
|
||||||
"version": "v7.2.0",
|
"version": "v7.2.0",
|
||||||
@@ -6204,6 +6441,90 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-09-09T11:45:10+00:00"
|
"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",
|
"name": "symfony/polyfill-intl-idn",
|
||||||
"version": "v1.31.0",
|
"version": "v1.31.0",
|
||||||
|
|||||||
127
config/cashier.php
Normal file
127
config/cashier.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Cashier\Console\WebhookCommand;
|
||||||
|
use Laravel\Cashier\Invoices\DompdfInvoiceRenderer;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stripe Keys
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The Stripe publishable key and secret key give you access to Stripe's
|
||||||
|
| API. The "publishable" key is typically used when interacting with
|
||||||
|
| Stripe.js while the "secret" key accesses private API endpoints.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 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'),
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?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('users', function (Blueprint $table) {
|
||||||
|
$table->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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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::create('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?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::create('subscription_items', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
34
database/migrations/2025_05_02_215351_create_plans_table.php
Normal file
34
database/migrations/2025_05_02_215351_create_plans_table.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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::create('plans', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,6 +8,14 @@
|
|||||||
background-color: #f72a25;
|
background-color: #f72a25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-primary-bg {
|
||||||
|
background-color: #F14743;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-primary {
|
||||||
|
color: #F14743;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #4361ee;
|
background-color: #4361ee;
|
||||||
|
|||||||
43
resources/views/flux/icon/circle-x.blade.php
Normal file
43
resources/views/flux/icon/circle-x.blade.php
Normal file
@@ -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
|
||||||
|
|
||||||
|
<svg
|
||||||
|
{{ $attributes->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"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="m15 9-6 6" />
|
||||||
|
<path d="m9 9 6 6" />
|
||||||
|
</svg>
|
||||||
@@ -1,19 +1,11 @@
|
|||||||
@section('title'){{ __('Dashboard') }}@endsection
|
@section('title'){{ __('Dashboard') }}@endsection
|
||||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
<div class="grid auto-rows-min gap-4 md:grid-cols-2">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<article class="flex items-center gap-4 rounded-lg border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
|
<article class="flex items-center gap-4 rounded-lg border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||||
<span class="rounded-full bg-blue-100 p-3 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400">
|
<span class="rounded-full bg-[#F04743]/20 p-3 text-[#F04743] dark:bg-[#F04743]/20 dark:text-[#F04743]">
|
||||||
<flux:icon.circle-dollar-sign />
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<p class="text-2xl font-medium text-gray-900 dark:text-white">0</p>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Balance</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="flex items-center gap-4 rounded-lg border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<span class="rounded-full bg-blue-100 p-3 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400">
|
|
||||||
<flux:icon.at-sign />
|
<flux:icon.at-sign />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -23,7 +15,7 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="flex items-center gap-4 rounded-lg border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
|
<article class="flex items-center gap-4 rounded-lg border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||||
<span class="rounded-full bg-blue-100 p-3 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400">
|
<span class="rounded-full bg-[#F04743]/20 p-3 text-[#F04743] dark:bg-[#F04743]/20 dark:text-[#F04743]">
|
||||||
<flux:icon.mails />
|
<flux:icon.mails />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -34,7 +26,35 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
@if(auth()->user()->subscribedToProduct(config('app.plans')[0]['product_id']))
|
||||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
|
||||||
|
<article class="flex items-center gap-4 rounded-lg border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Your <span class="text-accent-content font-bold">{{ $subscription['name'] }}</span> subscription is active and will be
|
||||||
|
@if($subscription['ends_at']) end at<span class="app-primary"> {{ \Carbon\Carbon::make($subscription['ends_at'])->toFormattedDayDateString() }}.</span>
|
||||||
|
@else auto-renew as per the plan you chose.
|
||||||
|
@endif
|
||||||
|
To manage you subscription <a href="{{ route('billing') }}" target="_blank" class="text-blue-500">Click here</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
</article>
|
||||||
|
@else
|
||||||
|
<div class="flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 dark:bg-white/[0.03]">
|
||||||
|
<livewire:dashboard.pricing />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Check if session flash data exists
|
||||||
|
@if(session()->has('alert'))
|
||||||
|
setTimeout(function() {
|
||||||
|
// Emitting showAlert event with type and message from session
|
||||||
|
Livewire.emit('showAlert', {
|
||||||
|
type: '{{ session('alert')['type'] }}',
|
||||||
|
message: '{{ session('alert')['message'] }}'
|
||||||
|
});
|
||||||
|
}, 2000); // 2000ms = 2 seconds delay
|
||||||
|
@endif
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
46
resources/views/livewire/dashboard/pricing.blade.php
Normal file
46
resources/views/livewire/dashboard/pricing.blade.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-12 lg:px-8 ">
|
||||||
|
<div class="w-full mb-8 items-center flex justify-center">
|
||||||
|
<h1 class="text-center text-3xl text-gray-900 dark:text-gray-200">Purchase Subscription</h1>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:items-center md:gap-8">
|
||||||
|
|
||||||
|
@if(isset($plans))
|
||||||
|
@foreach($plans as $plan)
|
||||||
|
<div class="rounded-2xl border dark:border-white/[0.1] border-black/[0.3] p-6 shadow-xs ring-1 ring-white/[0.5] sm:px-8 lg:p-12">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-400">{{ $plan->name }} @if(!$plan->monthly_billing)
|
||||||
|
<flux:badge variant="solid" size="sm" color="emerald">2 Months Free</flux:badge>
|
||||||
|
@endif</h2>
|
||||||
|
|
||||||
|
<p class="mt-2 sm:mt-4">
|
||||||
|
<strong class="text-3xl font-bold text-gray-900 dark:text-gray-200 sm:text-4xl">${{ $plan->price }}</strong>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">/{{ $plan->monthly_billing ? 'month' : 'year' }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="mt-6 space-y-2">
|
||||||
|
|
||||||
|
@if($plan->details)
|
||||||
|
@forelse ($plan->details as $key => $value)
|
||||||
|
@if ($value)
|
||||||
|
<li class="flex items-center gap-1">
|
||||||
|
@if($value == "true")<flux:icon.check-circle />
|
||||||
|
@else <flux:icon.circle-x />
|
||||||
|
@endif
|
||||||
|
<span class="text-gray-700 dark:text-gray-400 "> {{ $key }} </span>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
@empty
|
||||||
|
@endforelse
|
||||||
|
@endif
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<flux:button variant="primary" class="w-full mt-6 cursor-pointer" wire:click="choosePlan('{{ $plan->pricing_id }}')">
|
||||||
|
Choose Plan
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -47,6 +47,33 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::get('dashboard/bulk-email-generator', Dashboard::class)->name('dashboard.bulk');
|
Route::get('dashboard/bulk-email-generator', Dashboard::class)->name('dashboard.bulk');
|
||||||
Route::get('dashboard/compose-email', Dashboard::class)->name('dashboard.compose');
|
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 () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user