Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions app/Console/Commands/SendUltraFreeUserPromotion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Notifications\UltraFreeUserPromotion;
use Illuminate\Console\Command;

class SendUltraFreeUserPromotion extends Command
{
protected $signature = 'ultra:send-free-user-promo
{--dry-run : Show what would be sent without actually sending}';

protected $description = 'Send a promotional email to users who signed up but never purchased a license or subscription, encouraging them to try Ultra';

public function handle(): int
{
$dryRun = $this->option('dry-run');

if ($dryRun) {
$this->info('DRY RUN - No emails will be sent');
}

$users = User::query()
->whereDoesntHave('licenses')
->whereDoesntHave('subscriptions')
->get();

$sent = 0;

foreach ($users as $user) {
if ($dryRun) {
$this->line("Would send to: {$user->email}");
} else {
$user->notify(new UltraFreeUserPromotion);
$this->line("Sent to: {$user->email}");
}

$sent++;
}

$this->newLine();
$this->info("Found {$sent} eligible user(s)");
$this->info($dryRun ? "Would send: {$sent} email(s)" : "Sent: {$sent} email(s)");

return Command::SUCCESS;
}
}
43 changes: 43 additions & 0 deletions app/Notifications/UltraFreeUserPromotion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class UltraFreeUserPromotion extends Notification implements ShouldQueue
{
use Queueable;

public function via($notifiable): array
{
return ['mail'];
}

public function toMail($notifiable): MailMessage
{
$firstName = $notifiable->name ? explode(' ', $notifiable->name)[0] : null;
$greeting = $firstName ? "Hi {$firstName}," : 'Hi there,';

return (new MailMessage)
->subject('NativePHP is Free — And Ultra Takes It Further')
->greeting($greeting)
->line('We wanted to make sure you heard the news: **[NativePHP for Mobile is now completely free and open source!](https://nativephp.com/blog/nativephp-for-mobile-is-now-free)**')
->line('That means you can build native iOS and Android apps with Laravel and PHP — no license required. Just install and go.')
->line('But if you want to take things to the next level, **NativePHP Ultra** gives you some incredible benefits:')
->line('- **Teams** - up to 5 seats (you + 4 collaborators) to share your plugin access')
->line('- **Free official plugins** - every NativePHP-published plugin, included with your subscription')
->line('- **Plugin Dev Kit** - tools and resources to build and publish your own plugins')
->line('- **90% Marketplace revenue** - keep up to 90% of earnings on paid plugins you publish')
->line('- **Priority support** - get help faster when you need it')
->line('- **Early access** - be first to try new features and plugins')
->line('- **Exclusive content** - tutorials, guides, and deep dives just for Ultra members')
->line('- **Shape the roadmap** - your feedback directly influences what we build next')
->line('---')
->line('Ultra is available with **annual or monthly billing** - choose what works best for you.')
->action('See Ultra Plans', route('pricing'))
->salutation("Cheers,\n\nThe NativePHP Team");
}
}
189 changes: 189 additions & 0 deletions tests/Feature/SendUltraFreeUserPromotionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

namespace Tests\Feature;

use App\Enums\Subscription;
use App\Models\License;
use App\Models\User;
use App\Notifications\UltraFreeUserPromotion;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Laravel\Cashier\SubscriptionItem;
use Tests\TestCase;

class SendUltraFreeUserPromotionTest extends TestCase
{
use RefreshDatabase;

private function createLegacyLicense(User $user, string $policyName = 'mini'): License
{
return License::factory()
->for($user)
->withoutSubscriptionItem()
->state(['policy_name' => $policyName])
->create();
}

private function createActiveSubscription(User $user, string $priceId): \Laravel\Cashier\Subscription
{
$user->update(['stripe_id' => 'cus_'.uniqid()]);

$subscription = \Laravel\Cashier\Subscription::factory()
->for($user)
->active()
->create([
'stripe_price' => $priceId,
'is_comped' => false,
]);

SubscriptionItem::factory()
->for($subscription, 'subscription')
->create([
'stripe_price' => $priceId,
'quantity' => 1,
]);

return $subscription;
}

public function test_sends_to_user_with_no_licenses_and_no_subscriptions(): void
{
Notification::fake();

$user = User::factory()->create();

$this->artisan('ultra:send-free-user-promo')
->expectsOutputToContain('Found 1 eligible user(s)')
->expectsOutputToContain('Sent: 1 email(s)')
->assertSuccessful();

Notification::assertSentTo($user, UltraFreeUserPromotion::class);
}

public function test_skips_user_with_license(): void
{
Notification::fake();

$user = User::factory()->create();
$this->createLegacyLicense($user, 'mini');

$this->artisan('ultra:send-free-user-promo')
->expectsOutputToContain('Found 0 eligible user(s)')
->assertSuccessful();

Notification::assertNotSentTo($user, UltraFreeUserPromotion::class);
}

public function test_skips_user_with_active_subscription(): void
{
Notification::fake();

$user = User::factory()->create();
$this->createActiveSubscription($user, Subscription::Pro->stripePriceId());

$this->artisan('ultra:send-free-user-promo')
->expectsOutputToContain('Found 0 eligible user(s)')
->assertSuccessful();

Notification::assertNotSentTo($user, UltraFreeUserPromotion::class);
}

public function test_sends_to_multiple_eligible_users(): void
{
Notification::fake();

$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user3 = User::factory()->create();

// Give user3 a license so they should be excluded
$this->createLegacyLicense($user3, 'pro');

$this->artisan('ultra:send-free-user-promo')
->expectsOutputToContain('Found 2 eligible user(s)')
->expectsOutputToContain('Sent: 2 email(s)')
->assertSuccessful();

Notification::assertSentTo($user1, UltraFreeUserPromotion::class);
Notification::assertSentTo($user2, UltraFreeUserPromotion::class);
Notification::assertNotSentTo($user3, UltraFreeUserPromotion::class);
}

public function test_dry_run_does_not_send(): void
{
Notification::fake();

$user = User::factory()->create();

$this->artisan('ultra:send-free-user-promo --dry-run')
->expectsOutputToContain('DRY RUN')
->expectsOutputToContain("Would send to: {$user->email}")
->expectsOutputToContain('Would send: 1 email(s)')
->assertSuccessful();

Notification::assertNotSentTo($user, UltraFreeUserPromotion::class);
}

public function test_notification_has_correct_subject(): void
{
$user = User::factory()->create(['name' => 'Jane Doe']);

$notification = new UltraFreeUserPromotion;
$mail = $notification->toMail($user);

$this->assertEquals('NativePHP is Free — And Ultra Takes It Further', $mail->subject);
}

public function test_notification_greeting_uses_first_name(): void
{
$user = User::factory()->create(['name' => 'Jane Doe']);

$notification = new UltraFreeUserPromotion;
$mail = $notification->toMail($user);

$this->assertEquals('Hi Jane,', $mail->greeting);
}

public function test_notification_greeting_fallback_when_no_name(): void
{
$user = User::factory()->create(['name' => null]);

$notification = new UltraFreeUserPromotion;
$mail = $notification->toMail($user);

$this->assertEquals('Hi there,', $mail->greeting);
}

public function test_notification_contains_ultra_benefits(): void
{
$user = User::factory()->create(['name' => 'Test']);

$notification = new UltraFreeUserPromotion;
$mail = $notification->toMail($user);

$rendered = $mail->render()->__toString();

$this->assertStringContainsString('free and open source', $rendered);
$this->assertStringContainsString('Teams', $rendered);
$this->assertStringContainsString('Free official plugins', $rendered);
$this->assertStringContainsString('Plugin Dev Kit', $rendered);
$this->assertStringContainsString('Priority support', $rendered);
$this->assertStringContainsString('Early access', $rendered);
$this->assertStringContainsString('Exclusive content', $rendered);
$this->assertStringContainsString('Shape the roadmap', $rendered);
$this->assertStringContainsString('monthly billing', $rendered);
}

public function test_notification_mentions_nativephp_is_free(): void
{
$user = User::factory()->create(['name' => 'Test']);

$notification = new UltraFreeUserPromotion;
$mail = $notification->toMail($user);

$rendered = $mail->render()->__toString();

$this->assertStringContainsString('free and open source', $rendered);
$this->assertStringContainsString('no license required', $rendered);
}
}
Loading