diff --git a/app/Filament/Admin/Resources/PolydockStoreAppResource.php b/app/Filament/Admin/Resources/PolydockStoreAppResource.php index e821b98..9087e50 100644 --- a/app/Filament/Admin/Resources/PolydockStoreAppResource.php +++ b/app/Filament/Admin/Resources/PolydockStoreAppResource.php @@ -52,6 +52,11 @@ public static function form(Form $form): Form Forms\Components\TextInput::make('name') ->required() ->maxLength(255), + Forms\Components\Select::make('mail_theme') + ->label('Email Theme') + ->options(fn() => array_merge(['null' => 'Default'], array_combine(app('mail.themes'), app('mail.themes')))) + ->nullable() + ->dehydrateStateUsing(fn(?string $state) => $state === 'null' ? null : $state), Forms\Components\Textarea::make('description') ->required() ->columnSpanFull(), diff --git a/app/Mail/AppInstanceMidtrialMail.php b/app/Mail/AppInstanceMidtrialMail.php index aae8f65..a4ff7b1 100644 --- a/app/Mail/AppInstanceMidtrialMail.php +++ b/app/Mail/AppInstanceMidtrialMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Mail\Traits\ResolvesThemeTemplate; use App\Models\PolydockAppInstance; use App\Models\User; use Illuminate\Bus\Queueable; @@ -10,23 +11,22 @@ class AppInstanceMidtrialMail extends Mailable { - use Queueable, SerializesModels; + use Queueable, SerializesModels, ResolvesThemeTemplate; - public PolydockAppInstance $appInstance; - public User $toUser; - - public function __construct(PolydockAppInstance $appInstance, User $toUser) - { - $this->appInstance = $appInstance; - $this->toUser = $toUser; - } + public function __construct( + public PolydockAppInstance $appInstance, + public User $toUser, + public string $markdownTemplate = 'emails.app-instance.midtrial' + ) {} public function build() { + $this->resolveThemeTemplate($this->appInstance->storeApp->mail_theme, $this->markdownTemplate); + $subject = $this->appInstance->storeApp->midtrial_email_subject ?? 'Halfway Through Your Trial'; $subject .= " [" . $this->appInstance->name . "]"; - return $this->markdown('emails.app-instance.midtrial') + return $this->markdown($this->markdownTemplate) ->subject($subject); } } \ No newline at end of file diff --git a/app/Mail/AppInstanceOneDayLeftMail.php b/app/Mail/AppInstanceOneDayLeftMail.php index 76b0e58..29d39be 100644 --- a/app/Mail/AppInstanceOneDayLeftMail.php +++ b/app/Mail/AppInstanceOneDayLeftMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Mail\Traits\ResolvesThemeTemplate; use App\Models\PolydockAppInstance; use App\Models\User; use Illuminate\Bus\Queueable; @@ -10,23 +11,23 @@ class AppInstanceOneDayLeftMail extends Mailable { - use Queueable, SerializesModels; + use Queueable, SerializesModels, ResolvesThemeTemplate; - public PolydockAppInstance $appInstance; - public User $toUser; - - public function __construct(PolydockAppInstance $appInstance, User $toUser) - { - $this->appInstance = $appInstance; - $this->toUser = $toUser; + public function __construct( + public PolydockAppInstance $appInstance, + public User $toUser, + public string $markdownTemplate = 'emails.app-instance.one-day-left' + ) { } public function build() { + $this->resolveThemeTemplate($this->appInstance->storeApp->mail_theme, $this->markdownTemplate); + $subject = $this->appInstance->storeApp->one_day_left_email_subject ?? 'One Day Left in Your Trial'; $subject .= " [" . $this->appInstance->name . "]"; - return $this->markdown('emails.app-instance.one-day-left') + return $this->markdown($this->markdownTemplate) ->subject($subject); } } \ No newline at end of file diff --git a/app/Mail/AppInstanceReadyMail.php b/app/Mail/AppInstanceReadyMail.php index 304e207..ce9ed48 100644 --- a/app/Mail/AppInstanceReadyMail.php +++ b/app/Mail/AppInstanceReadyMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Mail\Traits\ResolvesThemeTemplate; use App\Models\PolydockAppInstance; use App\Models\User; use Illuminate\Bus\Queueable; @@ -12,21 +13,25 @@ class AppInstanceReadyMail extends Mailable { - use Queueable, SerializesModels; + use Queueable, SerializesModels, ResolvesThemeTemplate; /** * Create a new message instance. */ public function __construct( public PolydockAppInstance $appInstance, - public User $toUser - ) {} + public User $toUser, + public string $markdownTemplate = 'emails.app-instance.ready' + ) { + } /** * Get the message envelope. */ public function envelope(): Envelope { + $this->resolveThemeTemplate($this->appInstance->storeApp->mail_theme, $this->markdownTemplate); + $subject = $this->appInstance->storeApp->email_subject_line; if (empty($subject)) { @@ -46,7 +51,7 @@ public function envelope(): Envelope public function content(): Content { return new Content( - markdown: 'emails.app-instance.ready', + markdown: $this->markdownTemplate, ); } diff --git a/app/Mail/AppInstanceTrialCompleteMail.php b/app/Mail/AppInstanceTrialCompleteMail.php index 07fd74e..62994ec 100644 --- a/app/Mail/AppInstanceTrialCompleteMail.php +++ b/app/Mail/AppInstanceTrialCompleteMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Mail\Traits\ResolvesThemeTemplate; use App\Models\PolydockAppInstance; use App\Models\User; use Illuminate\Bus\Queueable; @@ -10,23 +11,22 @@ class AppInstanceTrialCompleteMail extends Mailable { - use Queueable, SerializesModels; + use Queueable, SerializesModels, ResolvesThemeTemplate; - public PolydockAppInstance $appInstance; - public User $toUser; - - public function __construct(PolydockAppInstance $appInstance, User $toUser) - { - $this->appInstance = $appInstance; - $this->toUser = $toUser; - } + public function __construct( + public PolydockAppInstance $appInstance, + public User $toUser, + public string $markdownTemplate = 'emails.app-instance.trial-complete' + ) {} public function build() { + $this->resolveThemeTemplate($this->appInstance->storeApp->mail_theme, $this->markdownTemplate); + $subject = $this->appInstance->storeApp->trial_complete_email_subject ?? 'Your Trial Has Ended'; $subject .= " [" . $this->appInstance->name . "]"; - return $this->markdown('emails.app-instance.trial-complete') + return $this->markdown($this->markdownTemplate) ->subject($subject); } } \ No newline at end of file diff --git a/app/Mail/Traits/ResolvesThemeTemplate.php b/app/Mail/Traits/ResolvesThemeTemplate.php new file mode 100644 index 0000000..4164882 --- /dev/null +++ b/app/Mail/Traits/ResolvesThemeTemplate.php @@ -0,0 +1,35 @@ +theme and $this->markdownTemplate accordingly. + * + * @param string $themeBase The theme namespace (e.g., 'promet') + * @param string $markdownTemplate The default markdown template path + * @throws \Exception + */ + protected function resolveThemeTemplate(string|null $themeBase, string $markdownTemplate): void + { + if (empty($themeBase)) { + return; + } + + $this->theme = sprintf("%s::%s", $themeBase, "emails.theme"); + + $themedTemplate = sprintf("%s::%s", $themeBase, $markdownTemplate); + + if (View::exists($themedTemplate)) { + $this->markdownTemplate = $themedTemplate; + } else { + if (!View::exists($markdownTemplate)) { + throw new \Exception("Unable to find any template corresponding to " . $markdownTemplate); + } + } + } +} diff --git a/app/Models/PolydockStoreApp.php b/app/Models/PolydockStoreApp.php index 8579ef4..84ecc62 100644 --- a/app/Models/PolydockStoreApp.php +++ b/app/Models/PolydockStoreApp.php @@ -34,6 +34,7 @@ class PolydockStoreApp extends Model 'lagoon_remove_script', 'email_subject_line', 'email_body_markdown', + 'mail_theme', 'status', 'uuid', 'available_for_trials', diff --git a/app/Providers/CustomTemplateProvider.php b/app/Providers/CustomTemplateProvider.php new file mode 100644 index 0000000..53882f7 --- /dev/null +++ b/app/Providers/CustomTemplateProvider.php @@ -0,0 +1,72 @@ +discoverThemes($templatePath); + + foreach ($themes as $themeName => $themePath) { + $this->loadViewsFrom($themePath, $themeName); + Log::info('Custom theme registered', [ + 'theme' => $themeName, + 'path' => $themePath, + ]); + } + + Log::info('Custom email themes loaded', [ + 'path' => $templatePath, + 'themes_found' => count($themes), + 'theme_names' => array_keys($themes) + ]); + } else { + Log::debug('Custom templates directory not found', ['path' => $templatePath]); + } + + // Make themes available globally via service container + $this->app->singleton('mail.themes', fn() => array_keys($themes)); + } + + /** + * Discover theme directories and return them as an array. + * + * @param string $templatePath + * @return array Array of theme name => path + */ + private function discoverThemes(string $templatePath): array + { + $themes = []; + + $directories = array_filter( + scandir($templatePath), + fn($item) => is_dir($templatePath . DIRECTORY_SEPARATOR . $item) && !str_starts_with($item, '.') + ); + + foreach ($directories as $dir) { + $themes[$dir] = $templatePath . DIRECTORY_SEPARATOR . $dir; + } + + return $themes; + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 9977dbd..60ff037 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,6 +2,7 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\CustomTemplateProvider::class, App\Providers\Filament\AdminPanelProvider::class, App\Providers\Filament\AppPanelProvider::class, App\Providers\HorizonServiceProvider::class, diff --git a/database/migrations/2025_11_17_232329_add_markdown_template_to_app.php b/database/migrations/2025_11_17_232329_add_markdown_template_to_app.php new file mode 100644 index 0000000..0a0c0b3 --- /dev/null +++ b/database/migrations/2025_11_17_232329_add_markdown_template_to_app.php @@ -0,0 +1,28 @@ +string('mail_theme')->nullable()->after('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('polydock_store_apps', function (Blueprint $table) { + $table->dropColumn('mail_theme'); + }); + } +}; diff --git a/docs/MAIL_THEMING.md b/docs/MAIL_THEMING.md new file mode 100644 index 0000000..59d9169 --- /dev/null +++ b/docs/MAIL_THEMING.md @@ -0,0 +1,67 @@ +# Mail Theming System + +## Overview + +The Mail Theming System allows you to create and manage multiple email templates and themes for transactional emails sent by the Polydock Engine. Each theme can override the default email layout, styling, and content while maintaining fallback to default templates if themed versions don't exist. + +## Directory Structure + +``` +storage/app/private/templates/ +├── theme-name/ # Theme name (directory name = theme key) +│ ├── emails/ +│ │ └── app-instance/ +│ │ ├── ready.blade.php (optional) +│ │ ├── midtrial.blade.php (optional) +│ │ ├── one-day-left.blade.php (optional) +│ │ └── trial-complete.blade.php (optional) +│ └── css/ +│ └── theme.css # Optional theme-specific styles +├── another-theme/ +│ └── emails/ +│ └── ... +``` + +## Creating a New Theme + +### Step 1: Create Theme Directory + +```bash +mkdir -p storage/app/private/templates/your-theme-name/emails/app-instance +``` + +### Step 2: create your theme + +Add a theme.css file with your mail's css + +### Step 3: Create Email Templates (Optional) + +If you want to override specific email templates, create them in the theme directory: + +- `storage/app/private/templates/your-theme-name/emails/app-instance/ready.blade.php` +- `storage/app/private/templates/your-theme-name/emails/app-instance/midtrial.blade.php` +- `storage/app/private/templates/your-theme-name/emails/app-instance/one-day-left.blade.php` +- `storage/app/private/templates/your-theme-name/emails/app-instance/trial-complete.blade.php` + +If these files don't exist, the system falls back to the default templates in `resources/views/emails/app-instance/`. + +## Using Themes + +### In the Admin UI + +1. Navigate to Apps → Apps +2. Create or edit a store app +3. Select a theme from the **Email Theme** dropdown +4. Save the record + +The theme name will be stored in the `polydock_store_apps.mail_theme` column. + +## Theme Resolution Logic + +When an email is sent: + +1. Check if a `mail_theme` is set on the store app +2. If set, resolve the themed template path (e.g., `promet::emails.app-instance.ready`) +3. If the themed template exists, use it +4. If the themed template doesn't exist, fall back to the default template (e.g., `emails.app-instance.ready`) +5. If no fallback exists, throw an exception