diff --git a/extensions/Others/Onboarding/Admin/Resources/OnboardingResource.php b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource.php new file mode 100644 index 000000000..622de3a17 --- /dev/null +++ b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource.php @@ -0,0 +1,162 @@ +columns(1) + ->components([ + TextInput::make('title') + ->label('Title') + ->required() + ->maxLength(255) + ->placeholder('Welcome to Our Platform'), + + Textarea::make('description') + ->label('Description') + ->required() + ->rows(4) + ->placeholder('A brief description of this onboarding step...'), + + FileUpload::make('image') + ->label('Image') + ->image() + ->disk('public') + ->directory('onboarding') + ->visibility('public') + ->imageEditor() + ->imageEditorAspectRatios([ + '16:9', + '4:3', + '1:1', + ]) + ->maxSize(2048) + ->helperText('Upload an image for this slide (max 2MB). Recommended aspect ratio: 16:9'), + + TextInput::make('sort') + ->label('Sort Order') + ->numeric() + ->default(0) + ->required() + ->helperText('Lower numbers appear first'), + + Toggle::make('enabled') + ->label('Enabled') + ->default(true) + ->helperText('Only enabled slides will be shown to users'), + + Toggle::make('is_guided') + ->label('Guided Slide') + ->default(false) + ->helperText('If enabled, this slide will redirect the user to a specific page') + ->live(), + + Select::make('page_route') + ->label('Page to Guide To') + ->visible(fn (Get $get): bool => $get('is_guided')) + ->required(fn (Get $get): bool => $get('is_guided')) + ->options([ + 'dashboard' => 'Dashboard', + 'services' => 'Services', + 'services.first' => 'First Active Service', + 'invoices' => 'Invoices', + 'tickets' => 'Tickets', + 'tickets.create' => 'Create Ticket', + 'account' => 'Account Settings', + 'account.security' => 'Account Security', + 'account.payment-methods' => 'Payment Methods', + 'account.notifications' => 'Notifications', + 'products' => 'Browse Products', + ]) + ->helperText('Select which page the user should be guided to'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + ImageColumn::make('image') + ->label('Image') + ->disk('public') + ->square() + ->defaultImageUrl(url('/images/placeholder.png')), + + TextColumn::make('title') + ->label('Title') + ->searchable() + ->sortable() + ->limit(50), + + TextColumn::make('description') + ->label('Description') + ->limit(80) + ->wrap(), + + TextColumn::make('sort') + ->label('Order') + ->sortable() + ->alignCenter(), + + ToggleColumn::make('enabled') + ->label('Enabled') + ->alignCenter(), + + TextColumn::make('is_guided') + ->label('Guided') + ->badge() + ->color(fn (bool $state): string => $state ? 'success' : 'gray') + ->formatStateUsing(fn (bool $state): string => $state ? 'Yes' : 'No') + ->alignCenter(), + + TextColumn::make('created_at') + ->label('Created') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('sort', 'asc') + ->reorderable('sort'); + } + + public static function getPages(): array + { + return [ + 'index' => ListOnboardingSlides::route('/'), + 'create' => CreateOnboardingSlide::route('/create'), + 'edit' => EditOnboardingSlide::route('/{record}/edit'), + ]; + } +} diff --git a/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/CreateOnboardingSlide.php b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/CreateOnboardingSlide.php new file mode 100644 index 000000000..85dfb3953 --- /dev/null +++ b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/CreateOnboardingSlide.php @@ -0,0 +1,16 @@ +getResource()::getUrl('index'); + } +} diff --git a/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/EditOnboardingSlide.php b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/EditOnboardingSlide.php new file mode 100644 index 000000000..27dd5e84d --- /dev/null +++ b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/EditOnboardingSlide.php @@ -0,0 +1,24 @@ +getResource()::getUrl('index'); + } +} diff --git a/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/ListOnboardingSlides.php b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/ListOnboardingSlides.php new file mode 100644 index 000000000..4d5398882 --- /dev/null +++ b/extensions/Others/Onboarding/Admin/Resources/OnboardingResource/Pages/ListOnboardingSlides.php @@ -0,0 +1,18 @@ +config('test_mode', false); + $perSession = $extension->config('per_session', false); + + $userId = Auth::id(); + + // Check if user has seen onboarding before + if ($perSession) { + // Use cookie for per-session tracking + $hasSeenOnboarding = $testMode ? false : \Illuminate\Support\Facades\Cookie::has('onboarding_completed'); + $this->currentSlide = (int) \Illuminate\Support\Facades\Cookie::get('onboarding_current_slide', 0); + } else { + // Use database session for persistent tracking + $hasSeenOnboarding = $testMode ? false : session()->get("onboarding_seen_{$userId}", false); + $this->currentSlide = session()->get("onboarding_current_slide_{$userId}", 0); + } + + // Only show if user hasn't seen it and there are slides + $this->slides = OnboardingSlide::where('enabled', true) + ->orderBy('sort', 'asc') + ->get() + ->toArray(); + + $this->showModal = !$hasSeenOnboarding && count($this->slides) > 0 && $this->currentSlide < count($this->slides); + } + + public function getShowBackdropProperty() + { + $extension = ExtensionHelper::getExtension('other', 'Onboarding'); + return $extension->config('show_backdrop', false); + } + + public function nextSlide() + { + $currentSlideData = $this->slides[$this->currentSlide] ?? null; + $extension = ExtensionHelper::getExtension('other', 'Onboarding'); + $perSession = $extension->config('per_session', false); + + // If current slide is guided, redirect to the page + if ($currentSlideData && $currentSlideData['is_guided'] && $currentSlideData['page_route']) { + // Save current slide progress before redirecting + $this->currentSlide++; + + if ($perSession) { + \Illuminate\Support\Facades\Cookie::queue('onboarding_current_slide', $this->currentSlide, 60 * 24 * 7); // 7 days + } else { + $userId = Auth::id(); + session()->put("onboarding_current_slide_{$userId}", $this->currentSlide); + } + + $this->redirectToPage($currentSlideData['page_route']); + return; + } + + if ($this->currentSlide < count($this->slides) - 1) { + $this->currentSlide++; + // Save progress + if ($perSession) { + \Illuminate\Support\Facades\Cookie::queue('onboarding_current_slide', $this->currentSlide, 60 * 24 * 7); // 7 days + } else { + $userId = Auth::id(); + session()->put("onboarding_current_slide_{$userId}", $this->currentSlide); + } + } else { + $this->finish(); + } + } + + public function previousSlide() + { + if ($this->currentSlide > 0) { + $this->currentSlide--; + // Save progress + $extension = ExtensionHelper::getExtension('other', 'Onboarding'); + $perSession = $extension->config('per_session', false); + + if ($perSession) { + \Illuminate\Support\Facades\Cookie::queue('onboarding_current_slide', $this->currentSlide, 60 * 24 * 7); // 7 days + } else { + $userId = Auth::id(); + session()->put("onboarding_current_slide_{$userId}", $this->currentSlide); + } + } + } + + public function finish() + { + // Check if test mode and per session mode are enabled + $extension = ExtensionHelper::getExtension('other', 'Onboarding'); + $testMode = $extension->config('test_mode', false); + $perSession = $extension->config('per_session', false); + + if ($perSession) { + // Clear current slide progress cookie + \Illuminate\Support\Facades\Cookie::queue(\Illuminate\Support\Facades\Cookie::forget('onboarding_current_slide')); + + // Only mark as completed if not in test mode (30 days expiration) + if (!$testMode) { + \Illuminate\Support\Facades\Cookie::queue('onboarding_completed', '1', 60 * 24 * 30); + } + } else { + $userId = Auth::id(); + + // Clear current slide progress + session()->forget("onboarding_current_slide_{$userId}"); + + // Only mark as seen if not in test mode + if (!$testMode) { + session()->put("onboarding_seen_{$userId}", true); + } + } + + $this->showModal = false; + } + + protected function redirectToPage($pageRoute) + { + $url = match ($pageRoute) { + 'dashboard' => route('dashboard'), + 'services' => route('services'), + 'services.first' => $this->getFirstServiceUrl(), + 'invoices' => route('invoices'), + 'tickets' => route('tickets'), + 'tickets.create' => route('tickets.create'), + 'account' => route('account'), + 'account.security' => route('account.security'), + 'account.payment-methods' => route('account.payment-methods'), + 'account.notifications' => route('account.notifications'), + 'products' => route('home') . '#shop', + default => route('dashboard'), + }; + + if ($url) { + return $this->redirect($url, navigate: true); + } + } + + protected function getFirstServiceUrl() + { + $service = Auth::user()->services()->where('status', 'active')->first(); + return $service ? route('services.show', $service) : route('services'); + } + + public function render() + { + return view('onboarding::modal'); + } +} diff --git a/extensions/Others/Onboarding/Models/OnboardingSlide.php b/extensions/Others/Onboarding/Models/OnboardingSlide.php new file mode 100644 index 000000000..a71e13ca2 --- /dev/null +++ b/extensions/Others/Onboarding/Models/OnboardingSlide.php @@ -0,0 +1,26 @@ + 'boolean', + 'is_guided' => 'boolean', + 'sort' => 'integer', + ]; +} diff --git a/extensions/Others/Onboarding/Onboarding.php b/extensions/Others/Onboarding/Onboarding.php new file mode 100644 index 000000000..b84cc589c --- /dev/null +++ b/extensions/Others/Onboarding/Onboarding.php @@ -0,0 +1,111 @@ + 'Onboarding', + 'version' => '1.0.0', + 'author' => 'Paymenter', + 'website' => 'https://paymenter.org', + ]; + } + + public function getConfig($values = []) + { + return [ + [ + 'name' => 'test_mode', + 'type' => 'checkbox', + 'label' => 'Test Mode', + 'description' => 'Enable test mode to always show onboarding modal (bypass session tracking). Useful for testing while setting up slides.', + 'required' => false, + 'default' => false, + ], + [ + 'name' => 'show_backdrop', + 'type' => 'checkbox', + 'label' => 'Show Backdrop', + 'description' => 'Display a dark backdrop behind the onboarding modal.', + 'required' => false, + 'default' => false, + ], + [ + 'name' => 'per_session', + 'type' => 'checkbox', + 'label' => 'Per Session Onboarding', + 'description' => 'Use cookies instead of database session. Onboarding will show again if user clears cookies or uses a different browser/device.', + 'required' => false, + 'default' => false, + ], + ]; + } + + public function installed() + { + + ExtensionHelper::runMigrations('extensions/Others/Onboarding/database/migrations'); + } + + public function uninstalled() + { + + ExtensionHelper::rollbackMigrations('extensions/Others/Onboarding/database/migrations'); + } + + public function boot() + { + + if (app()->runningInConsole() === false) { + \Filament\Facades\Filament::getCurrentPanel()?->resources([ + \Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource::class, + ]); + } + + + app('view')->addNamespace('onboarding', __DIR__ . '/resources/views'); + + + \Livewire\Livewire::component('onboarding.modal', \Paymenter\Extensions\Others\Onboarding\Livewire\Onboarding\Modal::class); + + + \Illuminate\Support\Facades\Gate::policy( + \Paymenter\Extensions\Others\Onboarding\Models\OnboardingSlide::class, + \Paymenter\Extensions\Others\Onboarding\Policies\OnboardingSlidePolicy::class + ); + + + \Illuminate\Support\Facades\Event::listen('permissions', function () { + return [ + 'admin.onboarding.view' => 'View Onboarding Slides', + 'admin.onboarding.create' => 'Create Onboarding Slides', + 'admin.onboarding.update' => 'Update Onboarding Slides', + 'admin.onboarding.delete' => 'Delete Onboarding Slides', + ]; + }); + + + \Illuminate\Support\Facades\Event::listen('body', function () { + + if (!request()->is('admin/*') && + !request()->is('login') && + !request()->is('register') && + !request()->is('2fa') && + !request()->is('forgot-password') && + !request()->is('reset-password/*') && + !request()->is('verify-email') && + !request()->is('/') && + \Illuminate\Support\Facades\Auth::check()) { + return [ + 'view' => view('onboarding::widget'), + ]; + } + }); + } +} diff --git a/extensions/Others/Onboarding/Policies/OnboardingSlidePolicy.php b/extensions/Others/Onboarding/Policies/OnboardingSlidePolicy.php new file mode 100644 index 000000000..878af4d5e --- /dev/null +++ b/extensions/Others/Onboarding/Policies/OnboardingSlidePolicy.php @@ -0,0 +1,46 @@ +hasPermission('admin.onboarding.view'); + } + + + public function view(User $user, OnboardingSlide $onboardingSlide): bool + { + return $user->hasPermission('admin.onboarding.view'); + } + + + public function create(User $user): bool + { + return $user->hasPermission('admin.onboarding.create'); + } + + + public function update(User $user, OnboardingSlide $onboardingSlide): bool + { + return $user->hasPermission('admin.onboarding.update'); + } + + + public function delete(User $user, OnboardingSlide $onboardingSlide): bool + { + return $user->hasPermission('admin.onboarding.delete'); + } + + + public function deleteAny(User $user): bool + { + return $user->hasPermission('admin.onboarding.delete'); + } +} diff --git a/extensions/Others/Onboarding/composer.json b/extensions/Others/Onboarding/composer.json new file mode 100644 index 000000000..5e3913d42 --- /dev/null +++ b/extensions/Others/Onboarding/composer.json @@ -0,0 +1,16 @@ +{ + "name": "paymenter/onboarding", + "description": "Onboarding extension for Paymenter", + "type": "paymenter-extension", + "autoload": { + "psr-4": { + "Paymenter\\Extensions\\Others\\Onboarding\\": "" + } + }, + "extra": { + "paymenter": { + "extension": "Onboarding", + "type": "other" + } + } +} diff --git a/extensions/Others/Onboarding/database/migrations/2024_11_30_000002_create_ext_onboarding_slides_table.php b/extensions/Others/Onboarding/database/migrations/2024_11_30_000002_create_ext_onboarding_slides_table.php new file mode 100644 index 000000000..a11aafc59 --- /dev/null +++ b/extensions/Others/Onboarding/database/migrations/2024_11_30_000002_create_ext_onboarding_slides_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('title'); + $table->text('description'); + $table->string('image')->nullable(); + $table->integer('sort')->default(0); + $table->boolean('enabled')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ext_onboarding_slides'); + } +}; diff --git a/extensions/Others/Onboarding/database/migrations/2024_11_30_000003_add_guided_fields_to_ext_onboarding_slides_table.php b/extensions/Others/Onboarding/database/migrations/2024_11_30_000003_add_guided_fields_to_ext_onboarding_slides_table.php new file mode 100644 index 000000000..6242ffc52 --- /dev/null +++ b/extensions/Others/Onboarding/database/migrations/2024_11_30_000003_add_guided_fields_to_ext_onboarding_slides_table.php @@ -0,0 +1,23 @@ +boolean('is_guided')->default(false)->after('enabled'); + $table->string('page_route')->nullable()->after('is_guided'); + }); + } + + public function down() + { + Schema::table('ext_onboarding_slides', function (Blueprint $table) { + $table->dropColumn(['is_guided', 'page_route']); + }); + } +}; diff --git a/extensions/Others/Onboarding/resources/views/modal.blade.php b/extensions/Others/Onboarding/resources/views/modal.blade.php new file mode 100644 index 000000000..995547767 --- /dev/null +++ b/extensions/Others/Onboarding/resources/views/modal.blade.php @@ -0,0 +1,104 @@ +
+ {{ $slide['description'] }} +
+