Skip to content

Commit b2987da

Browse files
simonhampclaude
andauthored
Stop creating Anystack licenses on subscription events (#322)
* Stop creating Anystack licenses on subscription events License creation via Anystack is no longer needed for any subscription plan. Remove the createLicense() method and all call sites in HandleInvoicePaidJob, including fallbacks in legacy renewal and subscription cycle handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Grant Ultra subscribers access to claude-code repo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Enlarge email logo and fix Anystack license test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve support ticket email notifications Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 664123b commit b2987da

File tree

17 files changed

+425
-68
lines changed

17 files changed

+425
-68
lines changed

app/Filament/Resources/SupportTicketResource/RelationManagers/RepliesRelationManager.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ public function table(Table $table): Table
7373
}
7474

7575
$ticket = $this->getOwnerRecord();
76-
$ticket->user->notify(new SupportTicketReplied($ticket, $record));
76+
77+
if ($ticket->user_id !== auth()->id()) {
78+
$ticket->user->notify(new SupportTicketReplied($ticket, $record));
79+
}
7780
}),
7881
])
7982
->actions([

app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function sendReply(): void
3636
'note' => $this->isNote,
3737
]);
3838

39-
if (! $this->isNote) {
39+
if (! $this->isNote && $this->record->user_id !== auth()->id()) {
4040
$this->record->user->notify(new SupportTicketReplied($this->record, $reply));
4141
}
4242

app/Http/Controllers/GitHubIntegrationController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ public function requestClaudePluginsAccess(): RedirectResponse
167167
// Check if user has a Plugin Dev Kit license or is an Ultra team member
168168
$pluginDevKit = Product::where('slug', 'plugin-dev-kit')->first();
169169

170-
if (! $user->isUltraTeamMember() && (! $pluginDevKit || ! $user->hasProductLicense($pluginDevKit))) {
171-
return back()->with('error', 'You need a Plugin Dev Kit license or Ultra team membership to access the claude-code repository.');
170+
if (! $user->hasActiveUltraSubscription() && ! $user->isUltraTeamMember() && (! $pluginDevKit || ! $user->hasProductLicense($pluginDevKit))) {
171+
return back()->with('error', 'You need a Plugin Dev Kit license, Ultra subscription, or Ultra team membership to access the claude-code repository.');
172172
}
173173

174174
$github = GitHubOAuth::make();

app/Jobs/HandleInvoicePaidJob.php

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use App\Enums\PayoutStatus;
66
use App\Enums\Subscription;
7-
use App\Exceptions\InvalidStateException;
87
use App\Models\Cart;
98
use App\Models\CartItem;
109
use App\Models\License;
@@ -25,7 +24,6 @@
2524
use Illuminate\Queue\SerializesModels;
2625
use Illuminate\Support\Facades\Date;
2726
use Illuminate\Support\Facades\Log;
28-
use Illuminate\Support\Sleep;
2927
use Laravel\Cashier\Cashier;
3028
use Laravel\Cashier\SubscriptionItem;
3129
use Stripe\Invoice;
@@ -83,8 +81,6 @@ private function handleSubscriptionCreated(): void
8381
return;
8482
}
8583

86-
// Normal flow - create a new license
87-
$this->createLicense();
8884
$this->updateSubscriptionCompedStatus();
8985
}
9086

@@ -105,8 +101,6 @@ private function handleLegacyLicenseRenewal($subscription, string $licenseKey, s
105101
'user_id' => $user->id,
106102
'subscription_id' => $subscription->id,
107103
]);
108-
// Fallback to creating a new license
109-
$this->createLicense();
110104

111105
return;
112106
}
@@ -140,38 +134,6 @@ private function handleLegacyLicenseRenewal($subscription, string $licenseKey, s
140134
]);
141135
}
142136

143-
private function createLicense(): void
144-
{
145-
// Add some delay to allow all the Stripe events to come in
146-
Sleep::sleep(10);
147-
148-
// Assert the invoice line item is for a price_id that relates to a license plan.
149-
$plan = Subscription::fromStripePriceId($this->findPlanLineItem()->price->id);
150-
151-
// Assert the invoice line item relates to a subscription and has a subscription item id.
152-
if (blank($subscriptionItemId = $this->findPlanLineItem()->subscription_item)) {
153-
throw new UnexpectedValueException('Failed to retrieve the Stripe subscription item id from invoice lines.');
154-
}
155-
156-
// Assert we have a subscription item record for this subscription item id.
157-
$subscriptionItemModel = SubscriptionItem::query()->where('stripe_id', $subscriptionItemId)->firstOrFail();
158-
159-
// Assert we don't already have an existing license for this subscription item.
160-
if ($license = License::query()->whereBelongsTo($subscriptionItemModel)->first()) {
161-
throw new InvalidStateException("A license [{$license->id}] already exists for subscription item [{$subscriptionItemModel->id}].");
162-
}
163-
164-
$user = $this->billable();
165-
166-
dispatch(new CreateAnystackLicenseJob(
167-
$user,
168-
$plan,
169-
$subscriptionItemModel->id,
170-
$user->first_name,
171-
$user->last_name,
172-
));
173-
}
174-
175137
private function handleSubscriptionRenewal(): void
176138
{
177139
// Get the subscription item ID from the invoice line
@@ -186,9 +148,6 @@ private function handleSubscriptionRenewal(): void
186148
$license = License::query()->whereBelongsTo($subscriptionItemModel)->first();
187149

188150
if (! $license) {
189-
// No existing license found - this might be a new subscription, handle as create
190-
$this->createLicense();
191-
192151
return;
193152
}
194153

app/Listeners/StripeWebhookReceivedListener.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Jobs\CreateUserFromStripeCustomer;
66
use App\Jobs\HandleInvoicePaidJob;
77
use App\Jobs\RemoveDiscordMaxRoleJob;
8+
use App\Jobs\RevokeTeamUserAccessJob;
89
use App\Jobs\SuspendTeamJob;
910
use App\Jobs\UnsuspendTeamJob;
1011
use App\Models\User;
@@ -77,6 +78,7 @@ private function handleSubscriptionDeleted(WebhookReceived $event): void
7778
$this->removeDiscordRoleIfNoMaxLicense($user);
7879

7980
dispatch(new SuspendTeamJob($user->id));
81+
dispatch(new RevokeTeamUserAccessJob($user->id));
8082
}
8183

8284
private function handleSubscriptionUpdated(WebhookReceived $event): void
@@ -101,6 +103,7 @@ private function handleSubscriptionUpdated(WebhookReceived $event): void
101103
if (in_array($status, ['canceled', 'unpaid', 'past_due', 'incomplete_expired'])) {
102104
$this->removeDiscordRoleIfNoMaxLicense($user);
103105
dispatch(new SuspendTeamJob($user->id));
106+
dispatch(new RevokeTeamUserAccessJob($user->id));
104107
}
105108

106109
// Detect reactivation: status changed to active from a non-active state

app/Notifications/SupportTicketReplied.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Illuminate\Contracts\Queue\ShouldQueue;
99
use Illuminate\Notifications\Messages\MailMessage;
1010
use Illuminate\Notifications\Notification;
11-
use Illuminate\Support\Str;
1211

1312
class SupportTicketReplied extends Notification implements ShouldQueue
1413
{
@@ -30,11 +29,11 @@ public function via(object $notifiable): array
3029
public function toMail(object $notifiable): MailMessage
3130
{
3231
return (new MailMessage)
33-
->subject('Update on your support request: '.$this->ticket->subject)
32+
->subject('Update on your support request: '.$this->ticket->mask)
3433
->greeting("Hi {$notifiable->first_name},")
35-
->line('Your support ticket has received a new reply.')
36-
->line('**'.e($this->ticket->subject).'**')
37-
->line(Str::limit($this->reply->message, 500))
38-
->action('View Ticket', route('customer.support.tickets.show', $this->ticket));
34+
->line('Your support ticket **'.e($this->ticket->subject).'** has received a new reply.')
35+
->line('Please log in to your dashboard to view the message and respond.')
36+
->action('View Ticket', route('customer.support.tickets.show', $this->ticket))
37+
->line('*Please do not reply to this email — responses must be submitted through the support portal.*');
3938
}
4039
}

app/Notifications/SupportTicketSubmitted.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,39 @@ public function via(object $notifiable): array
2626
public function toMail(object $notifiable): MailMessage
2727
{
2828
$ticket = $this->ticket->loadMissing('user');
29+
$user = $ticket->user;
2930

3031
return (new MailMessage)
3132
->subject('New Support Ticket: '.$ticket->subject)
32-
->replyTo($ticket->user->email, $ticket->user->name)
3333
->greeting('New support ticket received!')
34+
->line("**Customer:** {$user->name}")
35+
->line('**Email:** '.$this->obfuscateEmail($user->email))
3436
->line("**Product:** {$ticket->product}")
3537
->line('**Issue Type:** '.($ticket->issue_type ?? 'N/A'))
3638
->line("**Subject:** {$ticket->subject}")
3739
->line('**Message:**')
3840
->line(Str::limit($ticket->message, 500))
3941
->action('View Ticket', SupportTicketResource::getUrl('view', ['record' => $ticket]));
4042
}
43+
44+
private function obfuscateEmail(string $email): string
45+
{
46+
$parts = explode('@', $email);
47+
$local = $parts[0];
48+
$domain = $parts[1] ?? '';
49+
50+
$visibleLocal = Str::length($local) > 2
51+
? Str::substr($local, 0, 2).str_repeat('*', Str::length($local) - 2)
52+
: $local;
53+
54+
$domainParts = explode('.', $domain);
55+
$domainName = $domainParts[0] ?? '';
56+
$tld = implode('.', array_slice($domainParts, 1));
57+
58+
$visibleDomain = Str::length($domainName) > 2
59+
? Str::substr($domainName, 0, 2).str_repeat('*', Str::length($domainName) - 2)
60+
: $domainName;
61+
62+
return "{$visibleLocal}@{$visibleDomain}.{$tld}";
63+
}
4164
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
10.5 KB
Loading

resources/views/livewire/claude-plugins-access-banner.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@php
33
$user = auth()->user();
44
$pluginDevKit = \App\Models\Product::where('slug', 'plugin-dev-kit')->first();
5-
$hasLicense = $pluginDevKit && $user->hasProductLicense($pluginDevKit);
5+
$hasLicense = ($pluginDevKit && $user->hasProductLicense($pluginDevKit)) || $user->hasActiveUltraSubscription();
66
@endphp
77

88
@if($hasLicense)

0 commit comments

Comments
 (0)