Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1d3d628
wip
simonhamp Dec 5, 2025
0b20c8f
Update seeding command in databases.md (#233)
MasoodRehman Dec 5, 2025
5507089
Updates vite config
shanerbaner82 Dec 5, 2025
f8563c3
Updates // [tl! focus]
shanerbaner82 Dec 5, 2025
2ca9841
Mobile release 2025-12-05 (#243)
simonhamp Dec 5, 2025
5704583
Showcase! (#244)
simonhamp Dec 6, 2025
af96933
Changelog
simonhamp Dec 6, 2025
64b23c7
Minor tweaks
shanerbaner82 Dec 6, 2025
c5229f4
Improve showcase galleries for mobile apps
simonhamp Dec 8, 2025
e9536b7
Update mobile and desktop links in platform switcher (#246)
codingwithrk Dec 8, 2025
e9b9949
Add lightbox
simonhamp Dec 8, 2025
e43a63e
Update Haptics documentation to remove deprecated notice
simonhamp Dec 8, 2025
e58f284
Updates changelog mobile 2.1.1 (#248)
shanerbaner82 Dec 8, 2025
779a4fc
Updates changelog
shanerbaner82 Dec 8, 2025
4b8e4eb
Comparisons
simonhamp Dec 9, 2025
4a44c12
Add features
simonhamp Dec 9, 2025
306921e
Updates API docs for mobile (JS) (#249)
shanerbaner82 Dec 10, 2025
7acf8ac
Update top-bar.md for top-bar-actions
shanerbaner82 Dec 10, 2025
1a5edb0
Leads
simonhamp Dec 11, 2025
cae308b
Mobile 2.2.0 (#247)
shanerbaner82 Dec 13, 2025
d3600b5
GitHub access (#253)
simonhamp Dec 21, 2025
4325b76
OpenCollective (#252)
simonhamp Dec 21, 2025
40e9558
Discord integration (#254)
simonhamp Dec 21, 2025
128c4a7
Add note for Hot Reloading on real test devices (#251)
michaelishri Dec 21, 2025
322f7e1
Update documentation links in footer component (#250)
codingwithrk Dec 21, 2025
2234aa6
Disable the renewal reminder
simonhamp Jan 5, 2026
91e028d
Installs nightwatch
shanerbaner82 Jan 6, 2026
cd29826
wip
simonhamp Dec 5, 2025
89d154b
Add plugin detail page with GitHub webhook sync
simonhamp Jan 7, 2026
53082de
Satis and Bundles
simonhamp Jan 16, 2026
9f503ea
Improve
simonhamp Jan 16, 2026
c01b9c1
Chart
simonhamp Jan 17, 2026
63b28e4
Merge branch 'main' into plugin-directory
simonhamp Jan 17, 2026
46bfcf2
Fix tests, remove unused functionality
simonhamp Jan 17, 2026
701d2ee
Bump deps
simonhamp Jan 18, 2026
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
89 changes: 89 additions & 0 deletions app/Console/Commands/RetryFailedPayouts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace App\Console\Commands;

use App\Enums\PayoutStatus;
use App\Models\PluginPayout;
use App\Services\StripeConnectService;
use Illuminate\Console\Command;

class RetryFailedPayouts extends Command
{
protected $signature = 'payouts:retry-failed {--payout-id= : Retry a specific payout}';

protected $description = 'Retry failed plugin payouts';

public function handle(StripeConnectService $stripeConnectService): int
{
$payoutId = $this->option('payout-id');

if ($payoutId) {
$payout = PluginPayout::find($payoutId);

if (! $payout) {
$this->error("Payout #{$payoutId} not found.");

return self::FAILURE;
}

if (! $payout->isFailed()) {
$this->error("Payout #{$payoutId} is not in failed status.");

return self::FAILURE;
}

return $this->retryPayout($payout, $stripeConnectService);
}

$failedPayouts = PluginPayout::failed()
->with(['pluginLicense', 'developerAccount'])
->get();

if ($failedPayouts->isEmpty()) {
$this->info('No failed payouts to retry.');

return self::SUCCESS;
}

$this->info("Found {$failedPayouts->count()} failed payout(s) to retry.");

$succeeded = 0;
$failed = 0;

foreach ($failedPayouts as $payout) {
// Reset status to pending before retrying
$payout->update(['status' => PayoutStatus::Pending]);

if ($stripeConnectService->processTransfer($payout)) {
$this->info("Payout #{$payout->id} succeeded.");
$succeeded++;
} else {
$this->error("Payout #{$payout->id} failed again.");
$failed++;
}
}

$this->newLine();
$this->info("Results: {$succeeded} succeeded, {$failed} failed.");

return $failed > 0 ? self::FAILURE : self::SUCCESS;
}

protected function retryPayout(PluginPayout $payout, StripeConnectService $stripeConnectService): int
{
$this->info("Retrying payout #{$payout->id}...");

// Reset status to pending before retrying
$payout->update(['status' => PayoutStatus::Pending]);

if ($stripeConnectService->processTransfer($payout)) {
$this->info('Payout succeeded!');

return self::SUCCESS;
}

$this->error('Payout failed again.');

return self::FAILURE;
}
}
76 changes: 76 additions & 0 deletions app/Console/Commands/SatisBuild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace App\Console\Commands;

use App\Services\SatisService;
use Illuminate\Console\Command;

class SatisBuild extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'satis:build
{--plugin= : Build only a specific plugin by name (e.g., vendor/package)}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Trigger a Satis repository build';

/**
* Execute the console command.
*/
public function handle(SatisService $satisService): int
{
$pluginName = $this->option('plugin');

if ($pluginName) {
$plugin = \App\Models\Plugin::where('name', $pluginName)->first();

if (! $plugin) {
$this->error("Plugin '{$pluginName}' not found.");

return self::FAILURE;
}

if (! $plugin->isApproved()) {
$this->error("Plugin '{$pluginName}' is not approved.");

return self::FAILURE;
}

$this->info("Triggering Satis build for: {$pluginName}");
$result = $satisService->build([$plugin]);
} else {
$this->info('Triggering Satis build for all approved plugins...');
$result = $satisService->buildAll();
}

if ($result['success']) {
$this->info('Build triggered successfully!');
$this->line("Job ID: {$result['job_id']}");

if (isset($result['plugins_count'])) {
$this->line("Plugins: {$result['plugins_count']}");
}

return self::SUCCESS;
}

$this->error('Build trigger failed: '.$result['error']);

if (isset($result['status'])) {
$this->line("HTTP Status: {$result['status']}");
}

$this->line('API URL: '.config('services.satis.url'));
$this->line('API Key configured: '.(config('services.satis.api_key') ? 'Yes' : 'No'));

return self::FAILURE;
}
}
12 changes: 9 additions & 3 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
// Send license expiry warnings daily at 9 AM UTC
$schedule->command('licenses:send-expiry-warnings')
->dailyAt('09:00')
// Remove GitHub access for users with expired Max licenses
$schedule->command('github:remove-expired-access')
->dailyAt('10:00')
->onOneServer()
->runInBackground();

// Remove Discord Max role for users with expired Max licenses
$schedule->command('discord:remove-expired-roles')
->dailyAt('10:30')
->onOneServer()
->runInBackground();

Expand Down
48 changes: 48 additions & 0 deletions app/Enums/GrandfatheringTier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Enums;

use App\Models\Plugin;

enum GrandfatheringTier: string
{
case None = 'none';
case Discounted = 'discounted';
case FreeOfficialPlugins = 'free_official_plugins';

public function label(): string
{
return match ($this) {
self::None => 'No Discount',
self::Discounted => 'Legacy Discount',
self::FreeOfficialPlugins => 'Free Official Plugins',
};
}

public function color(): string
{
return match ($this) {
self::None => 'gray',
self::Discounted => 'blue',
self::FreeOfficialPlugins => 'green',
};
}

public function getDiscountPercent(): int
{
return match ($this) {
self::None => 0,
self::Discounted => 20,
self::FreeOfficialPlugins => 100,
};
}

public function appliesToPlugin(Plugin $plugin): bool
{
return match ($this) {
self::None => false,
self::Discounted => true,
self::FreeOfficialPlugins => $plugin->is_official,
};
}
}
28 changes: 28 additions & 0 deletions app/Enums/PayoutStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Enums;

enum PayoutStatus: string
{
case Pending = 'pending';
case Transferred = 'transferred';
case Failed = 'failed';

public function label(): string
{
return match ($this) {
self::Pending => 'Pending',
self::Transferred => 'Transferred',
self::Failed => 'Failed',
};
}

public function color(): string
{
return match ($this) {
self::Pending => 'yellow',
self::Transferred => 'green',
self::Failed => 'red',
};
}
}
45 changes: 45 additions & 0 deletions app/Enums/PluginActivityType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Enums;

enum PluginActivityType: string
{
case Submitted = 'submitted';
case Resubmitted = 'resubmitted';
case Approved = 'approved';
case Rejected = 'rejected';
case DescriptionUpdated = 'description_updated';

public function label(): string
{
return match ($this) {
self::Submitted => 'Submitted',
self::Resubmitted => 'Resubmitted',
self::Approved => 'Approved',
self::Rejected => 'Rejected',
self::DescriptionUpdated => 'Description Updated',
};
}

public function color(): string
{
return match ($this) {
self::Submitted => 'info',
self::Resubmitted => 'info',
self::Approved => 'success',
self::Rejected => 'danger',
self::DescriptionUpdated => 'gray',
};
}

public function icon(): string
{
return match ($this) {
self::Submitted => 'heroicon-o-paper-airplane',
self::Resubmitted => 'heroicon-o-arrow-path',
self::Approved => 'heroicon-o-check-circle',
self::Rejected => 'heroicon-o-x-circle',
self::DescriptionUpdated => 'heroicon-o-pencil-square',
};
}
}
28 changes: 28 additions & 0 deletions app/Enums/PluginStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Enums;

enum PluginStatus: string
{
case Pending = 'pending';
case Approved = 'approved';
case Rejected = 'rejected';

public function label(): string
{
return match ($this) {
self::Pending => 'Pending Review',
self::Approved => 'Approved',
self::Rejected => 'Rejected',
};
}

public function color(): string
{
return match ($this) {
self::Pending => 'yellow',
self::Approved => 'green',
self::Rejected => 'red',
};
}
}
17 changes: 17 additions & 0 deletions app/Enums/PluginType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Enums;

enum PluginType: string
{
case Free = 'free';
case Paid = 'paid';

public function label(): string
{
return match ($this) {
self::Free => 'Free',
self::Paid => 'Paid',
};
}
}
33 changes: 33 additions & 0 deletions app/Enums/StripeConnectStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Enums;

enum StripeConnectStatus: string
{
case Pending = 'pending';
case Active = 'active';
case Disabled = 'disabled';

public function label(): string
{
return match ($this) {
self::Pending => 'Pending Onboarding',
self::Active => 'Active',
self::Disabled => 'Disabled',
};
}

public function color(): string
{
return match ($this) {
self::Pending => 'yellow',
self::Active => 'green',
self::Disabled => 'red',
};
}

public function canReceivePayouts(): bool
{
return $this === self::Active;
}
}
Loading
Loading