diff --git a/app/Filament/Resources/SupportTicketResource/RelationManagers/RepliesRelationManager.php b/app/Filament/Resources/SupportTicketResource/RelationManagers/RepliesRelationManager.php
index 2c4d5ae4..f7e33017 100644
--- a/app/Filament/Resources/SupportTicketResource/RelationManagers/RepliesRelationManager.php
+++ b/app/Filament/Resources/SupportTicketResource/RelationManagers/RepliesRelationManager.php
@@ -73,7 +73,10 @@ public function table(Table $table): Table
}
$ticket = $this->getOwnerRecord();
- $ticket->user->notify(new SupportTicketReplied($ticket, $record));
+
+ if ($ticket->user_id !== auth()->id()) {
+ $ticket->user->notify(new SupportTicketReplied($ticket, $record));
+ }
}),
])
->actions([
diff --git a/app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php b/app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php
index 97836c7f..99910c0d 100644
--- a/app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php
+++ b/app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php
@@ -36,7 +36,7 @@ public function sendReply(): void
'note' => $this->isNote,
]);
- if (! $this->isNote) {
+ if (! $this->isNote && $this->record->user_id !== auth()->id()) {
$this->record->user->notify(new SupportTicketReplied($this->record, $reply));
}
diff --git a/app/Http/Controllers/GitHubIntegrationController.php b/app/Http/Controllers/GitHubIntegrationController.php
index 8298de3b..403c9a7a 100644
--- a/app/Http/Controllers/GitHubIntegrationController.php
+++ b/app/Http/Controllers/GitHubIntegrationController.php
@@ -167,8 +167,8 @@ public function requestClaudePluginsAccess(): RedirectResponse
// Check if user has a Plugin Dev Kit license or is an Ultra team member
$pluginDevKit = Product::where('slug', 'plugin-dev-kit')->first();
- if (! $user->isUltraTeamMember() && (! $pluginDevKit || ! $user->hasProductLicense($pluginDevKit))) {
- return back()->with('error', 'You need a Plugin Dev Kit license or Ultra team membership to access the claude-code repository.');
+ if (! $user->hasActiveUltraSubscription() && ! $user->isUltraTeamMember() && (! $pluginDevKit || ! $user->hasProductLicense($pluginDevKit))) {
+ return back()->with('error', 'You need a Plugin Dev Kit license, Ultra subscription, or Ultra team membership to access the claude-code repository.');
}
$github = GitHubOAuth::make();
diff --git a/app/Jobs/HandleInvoicePaidJob.php b/app/Jobs/HandleInvoicePaidJob.php
index d3f13e45..b618b1e1 100644
--- a/app/Jobs/HandleInvoicePaidJob.php
+++ b/app/Jobs/HandleInvoicePaidJob.php
@@ -4,7 +4,6 @@
use App\Enums\PayoutStatus;
use App\Enums\Subscription;
-use App\Exceptions\InvalidStateException;
use App\Models\Cart;
use App\Models\CartItem;
use App\Models\License;
@@ -25,7 +24,6 @@
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Sleep;
use Laravel\Cashier\Cashier;
use Laravel\Cashier\SubscriptionItem;
use Stripe\Invoice;
@@ -83,8 +81,6 @@ private function handleSubscriptionCreated(): void
return;
}
- // Normal flow - create a new license
- $this->createLicense();
$this->updateSubscriptionCompedStatus();
}
@@ -105,8 +101,6 @@ private function handleLegacyLicenseRenewal($subscription, string $licenseKey, s
'user_id' => $user->id,
'subscription_id' => $subscription->id,
]);
- // Fallback to creating a new license
- $this->createLicense();
return;
}
@@ -140,38 +134,6 @@ private function handleLegacyLicenseRenewal($subscription, string $licenseKey, s
]);
}
- private function createLicense(): void
- {
- // Add some delay to allow all the Stripe events to come in
- Sleep::sleep(10);
-
- // Assert the invoice line item is for a price_id that relates to a license plan.
- $plan = Subscription::fromStripePriceId($this->findPlanLineItem()->price->id);
-
- // Assert the invoice line item relates to a subscription and has a subscription item id.
- if (blank($subscriptionItemId = $this->findPlanLineItem()->subscription_item)) {
- throw new UnexpectedValueException('Failed to retrieve the Stripe subscription item id from invoice lines.');
- }
-
- // Assert we have a subscription item record for this subscription item id.
- $subscriptionItemModel = SubscriptionItem::query()->where('stripe_id', $subscriptionItemId)->firstOrFail();
-
- // Assert we don't already have an existing license for this subscription item.
- if ($license = License::query()->whereBelongsTo($subscriptionItemModel)->first()) {
- throw new InvalidStateException("A license [{$license->id}] already exists for subscription item [{$subscriptionItemModel->id}].");
- }
-
- $user = $this->billable();
-
- dispatch(new CreateAnystackLicenseJob(
- $user,
- $plan,
- $subscriptionItemModel->id,
- $user->first_name,
- $user->last_name,
- ));
- }
-
private function handleSubscriptionRenewal(): void
{
// Get the subscription item ID from the invoice line
@@ -186,9 +148,6 @@ private function handleSubscriptionRenewal(): void
$license = License::query()->whereBelongsTo($subscriptionItemModel)->first();
if (! $license) {
- // No existing license found - this might be a new subscription, handle as create
- $this->createLicense();
-
return;
}
diff --git a/app/Listeners/StripeWebhookReceivedListener.php b/app/Listeners/StripeWebhookReceivedListener.php
index 7544b010..c293fb54 100644
--- a/app/Listeners/StripeWebhookReceivedListener.php
+++ b/app/Listeners/StripeWebhookReceivedListener.php
@@ -5,6 +5,7 @@
use App\Jobs\CreateUserFromStripeCustomer;
use App\Jobs\HandleInvoicePaidJob;
use App\Jobs\RemoveDiscordMaxRoleJob;
+use App\Jobs\RevokeTeamUserAccessJob;
use App\Jobs\SuspendTeamJob;
use App\Jobs\UnsuspendTeamJob;
use App\Models\User;
@@ -77,6 +78,7 @@ private function handleSubscriptionDeleted(WebhookReceived $event): void
$this->removeDiscordRoleIfNoMaxLicense($user);
dispatch(new SuspendTeamJob($user->id));
+ dispatch(new RevokeTeamUserAccessJob($user->id));
}
private function handleSubscriptionUpdated(WebhookReceived $event): void
@@ -101,6 +103,7 @@ private function handleSubscriptionUpdated(WebhookReceived $event): void
if (in_array($status, ['canceled', 'unpaid', 'past_due', 'incomplete_expired'])) {
$this->removeDiscordRoleIfNoMaxLicense($user);
dispatch(new SuspendTeamJob($user->id));
+ dispatch(new RevokeTeamUserAccessJob($user->id));
}
// Detect reactivation: status changed to active from a non-active state
diff --git a/app/Notifications/SupportTicketReplied.php b/app/Notifications/SupportTicketReplied.php
index 49236beb..de0d901e 100644
--- a/app/Notifications/SupportTicketReplied.php
+++ b/app/Notifications/SupportTicketReplied.php
@@ -8,7 +8,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Str;
class SupportTicketReplied extends Notification implements ShouldQueue
{
@@ -30,11 +29,11 @@ public function via(object $notifiable): array
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
- ->subject('Update on your support request: '.$this->ticket->subject)
+ ->subject('Update on your support request: '.$this->ticket->mask)
->greeting("Hi {$notifiable->first_name},")
- ->line('Your support ticket has received a new reply.')
- ->line('**'.e($this->ticket->subject).'**')
- ->line(Str::limit($this->reply->message, 500))
- ->action('View Ticket', route('customer.support.tickets.show', $this->ticket));
+ ->line('Your support ticket **'.e($this->ticket->subject).'** has received a new reply.')
+ ->line('Please log in to your dashboard to view the message and respond.')
+ ->action('View Ticket', route('customer.support.tickets.show', $this->ticket))
+ ->line('*Please do not reply to this email — responses must be submitted through the support portal.*');
}
}
diff --git a/app/Notifications/SupportTicketSubmitted.php b/app/Notifications/SupportTicketSubmitted.php
index e1e72a36..a72d27e8 100644
--- a/app/Notifications/SupportTicketSubmitted.php
+++ b/app/Notifications/SupportTicketSubmitted.php
@@ -26,11 +26,13 @@ public function via(object $notifiable): array
public function toMail(object $notifiable): MailMessage
{
$ticket = $this->ticket->loadMissing('user');
+ $user = $ticket->user;
return (new MailMessage)
->subject('New Support Ticket: '.$ticket->subject)
- ->replyTo($ticket->user->email, $ticket->user->name)
->greeting('New support ticket received!')
+ ->line("**Customer:** {$user->name}")
+ ->line('**Email:** '.$this->obfuscateEmail($user->email))
->line("**Product:** {$ticket->product}")
->line('**Issue Type:** '.($ticket->issue_type ?? 'N/A'))
->line("**Subject:** {$ticket->subject}")
@@ -38,4 +40,25 @@ public function toMail(object $notifiable): MailMessage
->line(Str::limit($ticket->message, 500))
->action('View Ticket', SupportTicketResource::getUrl('view', ['record' => $ticket]));
}
+
+ private function obfuscateEmail(string $email): string
+ {
+ $parts = explode('@', $email);
+ $local = $parts[0];
+ $domain = $parts[1] ?? '';
+
+ $visibleLocal = Str::length($local) > 2
+ ? Str::substr($local, 0, 2).str_repeat('*', Str::length($local) - 2)
+ : $local;
+
+ $domainParts = explode('.', $domain);
+ $domainName = $domainParts[0] ?? '';
+ $tld = implode('.', array_slice($domainParts, 1));
+
+ $visibleDomain = Str::length($domainName) > 2
+ ? Str::substr($domainName, 0, 2).str_repeat('*', Str::length($domainName) - 2)
+ : $domainName;
+
+ return "{$visibleLocal}@{$visibleDomain}.{$tld}";
+ }
}
diff --git a/package-lock.json b/package-lock.json
index 0d04291b..9b900603 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "cyan-salmon",
+ "name": "curious-dragon",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/public/brand-assets/logo/nativephp-for-light-background@2x.png b/public/brand-assets/logo/nativephp-for-light-background@2x.png
new file mode 100644
index 00000000..22bfb08e
Binary files /dev/null and b/public/brand-assets/logo/nativephp-for-light-background@2x.png differ
diff --git a/resources/views/livewire/claude-plugins-access-banner.blade.php b/resources/views/livewire/claude-plugins-access-banner.blade.php
index efbd6339..948239c2 100644
--- a/resources/views/livewire/claude-plugins-access-banner.blade.php
+++ b/resources/views/livewire/claude-plugins-access-banner.blade.php
@@ -2,7 +2,7 @@
@php
$user = auth()->user();
$pluginDevKit = \App\Models\Product::where('slug', 'plugin-dev-kit')->first();
- $hasLicense = $pluginDevKit && $user->hasProductLicense($pluginDevKit);
+ $hasLicense = ($pluginDevKit && $user->hasProductLicense($pluginDevKit)) || $user->hasActiveUltraSubscription();
@endphp
@if($hasLicense)
diff --git a/resources/views/livewire/customer/integrations.blade.php b/resources/views/livewire/customer/integrations.blade.php
index 313d3aef..f44a9d33 100644
--- a/resources/views/livewire/customer/integrations.blade.php
+++ b/resources/views/livewire/customer/integrations.blade.php
@@ -28,7 +28,7 @@
nativephp/mobile repository. Plugin Dev Kit license holders can access nativephp/claude-code.nativephp/mobile repository. Plugin Dev Kit license holders and Ultra subscribers can access nativephp/claude-code.diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php index df932926..78e0dd09 100644 --- a/resources/views/vendor/mail/html/header.blade.php +++ b/resources/views/vendor/mail/html/header.blade.php @@ -2,7 +2,7 @@
+