Skip to content
Open
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
63 changes: 63 additions & 0 deletions app/Enums/AndroidPermission.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum AndroidPermission: string
{
case Camera = 'android.permission.CAMERA';
case RecordAudio = 'android.permission.RECORD_AUDIO';
case AccessFineLocation = 'android.permission.ACCESS_FINE_LOCATION';
case AccessCoarseLocation = 'android.permission.ACCESS_COARSE_LOCATION';
case AccessBackgroundLocation = 'android.permission.ACCESS_BACKGROUND_LOCATION';
case ReadContacts = 'android.permission.READ_CONTACTS';
case WriteContacts = 'android.permission.WRITE_CONTACTS';
case ReadCalendar = 'android.permission.READ_CALENDAR';
case WriteCalendar = 'android.permission.WRITE_CALENDAR';
case BluetoothConnect = 'android.permission.BLUETOOTH_CONNECT';
case BluetoothScan = 'android.permission.BLUETOOTH_SCAN';
case ReadMediaImages = 'android.permission.READ_MEDIA_IMAGES';
case ReadExternalStorage = 'android.permission.READ_EXTERNAL_STORAGE';
case BodySensors = 'android.permission.BODY_SENSORS';
case ActivityRecognition = 'android.permission.ACTIVITY_RECOGNITION';
case Internet = 'android.permission.INTERNET';

public function label(): string
{
return match ($this) {
self::Camera => 'Camera',
self::RecordAudio => 'Microphone',
self::AccessFineLocation => 'Precise Location',
self::AccessCoarseLocation => 'Approximate Location',
self::AccessBackgroundLocation => 'Background Location',
self::ReadContacts => 'Read Contacts',
self::WriteContacts => 'Write Contacts',
self::ReadCalendar => 'Read Calendar',
self::WriteCalendar => 'Write Calendar',
self::BluetoothConnect => 'Bluetooth',
self::BluetoothScan => 'Bluetooth Scanning',
self::ReadMediaImages => 'Photo Library',
self::ReadExternalStorage => 'Device Storage',
self::BodySensors => 'Body Sensors',
self::ActivityRecognition => 'Motion & Fitness',
self::Internet => 'Internet Access',
};
}

public function iosUsageDescriptionKey(): ?string
{
return match ($this) {
self::Camera => 'NSCameraUsageDescription',
self::RecordAudio => 'NSMicrophoneUsageDescription',
self::AccessFineLocation, self::AccessCoarseLocation => 'NSLocationWhenInUseUsageDescription',
self::AccessBackgroundLocation => 'NSLocationAlwaysAndWhenInUseUsageDescription',
self::ReadContacts, self::WriteContacts => 'NSContactsUsageDescription',
self::ReadCalendar, self::WriteCalendar => 'NSCalendarsUsageDescription',
self::BluetoothConnect, self::BluetoothScan => 'NSBluetoothAlwaysUsageDescription',
self::ReadMediaImages, self::ReadExternalStorage => 'NSPhotoLibraryUsageDescription',
self::BodySensors, self::ActivityRecognition => 'NSMotionUsageDescription',
self::Internet => null,
};
}
}
29 changes: 29 additions & 0 deletions app/Enums/IosBackgroundMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum IosBackgroundMode: string
{
case Audio = 'audio';
case Fetch = 'fetch';
case Processing = 'processing';
case Location = 'location';
case RemoteNotification = 'remote-notification';
case BluetoothCentral = 'bluetooth-central';
case BluetoothPeripheral = 'bluetooth-peripheral';

public function label(): string
{
return match ($this) {
self::Audio => 'Background Audio',
self::Fetch => 'Background Fetch',
self::Processing => 'Background Processing',
self::Location => 'Background Location',
self::RemoteNotification => 'Push Notifications',
self::BluetoothCentral => 'Bluetooth (Central)',
self::BluetoothPeripheral => 'Bluetooth (Peripheral)',
};
}
}
11 changes: 11 additions & 0 deletions app/Enums/PermissionExpansionMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum PermissionExpansionMode: string
{
case Flag = 'flag';
case Gate = 'gate';
}
5 changes: 5 additions & 0 deletions app/Enums/PluginActivityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum PluginActivityType: string
case ReturnedToDraft = 'returned_to_draft';
case MessageToDeveloper = 'message_to_developer';
case MessageFromDeveloper = 'message_from_developer';
case PermissionsExpanded = 'permissions_expanded';

/**
* Types that represent a message in the admin <-> developer conversation.
Expand All @@ -38,6 +39,7 @@ public function label(): string
self::ReturnedToDraft => 'Returned to Draft',
self::MessageToDeveloper => 'Message Sent',
self::MessageFromDeveloper => 'Developer Reply',
self::PermissionsExpanded => 'Permissions Expanded',
};
}

Expand Down Expand Up @@ -65,6 +67,7 @@ public function color(): string
self::ReturnedToDraft => 'warning',
self::MessageToDeveloper => 'primary',
self::MessageFromDeveloper => 'info',
self::PermissionsExpanded => 'warning',
};
}

Expand All @@ -79,6 +82,7 @@ public function badgeColor(): string
self::Rejected => 'red',
self::MessageToDeveloper => 'purple',
self::MessageFromDeveloper => 'sky',
self::PermissionsExpanded => 'amber',
default => 'zinc',
};
}
Expand All @@ -95,6 +99,7 @@ public function icon(): string
self::ReturnedToDraft => 'heroicon-o-arrow-uturn-left',
self::MessageToDeveloper => 'heroicon-o-chat-bubble-left-right',
self::MessageFromDeveloper => 'heroicon-o-chat-bubble-left-ellipsis',
self::PermissionsExpanded => 'heroicon-o-shield-exclamation',
};
}

Expand Down
15 changes: 15 additions & 0 deletions app/Filament/Resources/PluginResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ public static function form(Schema $schema): Schema
->content(fn (?Plugin $record) => ($record?->review_checks['has_android_min_version'] ?? false)
? '✅ '.($record->review_checks['android_min_version'] ?? '')
: '❌ Missing'),

Forms\Components\Placeholder::make('review_permission_parity')
->label('App Store Permission Parity')
->content(function (?Plugin $record) {
$missing = $record?->review_checks['missing_permission_parity'] ?? [];

if (empty($missing)) {
return '✅ All Android permissions have a matching iOS usage description';
}

return '❌ Missing: '.collect($missing)
->map(fn (array $issue) => "{$issue['permission']} → {$issue['expected_key']}")
->implode(', ');
}),
])
->headerActions([
Action::make('emailReviewChecks')
Expand Down Expand Up @@ -553,6 +567,7 @@ public static function getRelations(): array
RelationManagers\PricesRelationManager::class,
RelationManagers\LicensesRelationManager::class,
RelationManagers\ActivitiesRelationManager::class,
RelationManagers\VersionsRelationManager::class,
RelationManagers\RatingsRelationManager::class,
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\PluginResource\RelationManagers;

use App\Models\PluginVersion;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Auth;

final class VersionsRelationManager extends RelationManager
{
protected static string $relationship = 'versions';

protected static ?string $title = 'Versions';

public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('version')
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('tag_name')
->label('Tag')
->searchable(),

Tables\Columns\IconColumn::make('permissions_expanded')
->label('Permissions Expanded')
->boolean(),

Tables\Columns\IconColumn::make('requires_review')
->label('Pending Review')
->boolean()
->trueColor('danger'),

Tables\Columns\TextColumn::make('approvedBy.email')
->label('Approved By')
->placeholder('—'),

Tables\Columns\TextColumn::make('published_at')
->dateTime()
->sortable(),
])
->actions([
Actions\Action::make('viewManifest')
->label('View Permissions')
->icon('heroicon-o-shield-check')
->color('gray')
->visible(fn (PluginVersion $record): bool => filled($record->manifest_permissions))
->modalHeading(fn (PluginVersion $record): string => "Permissions — {$record->version}")
->modalContent(fn (PluginVersion $record) => view(
'filament.resources.plugin-resource.manifest-permissions',
['manifest' => $record->manifest_permissions]
))
->modalSubmitAction(false)
->modalCancelActionLabel('Close'),

Actions\Action::make('approve')
->label('Approve')
->icon('heroicon-o-check-circle')
->color('success')
->visible(fn (PluginVersion $record): bool => $record->requires_review)
->requiresConfirmation()
->modalDescription('This version will become the plugin\'s current visible version.')
->action(function (PluginVersion $record): void {
$record->approve(Auth::user());

$latest = $record->plugin->versions()->visible()->latest('published_at')->first();

if ($latest) {
$record->plugin->update(['latest_version' => $latest->version]);
}

Notification::make()
->title("Version {$record->version} approved")
->success()
->send();
}),
])
->bulkActions([])
->defaultSort('published_at', 'desc')
->paginated([10, 25, 50]);
}
}
5 changes: 5 additions & 0 deletions app/Http/Controllers/PluginDirectoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public function show(string $vendor, string $package): View
$bestPrice = $plugin->getBestPriceForUser($user);
$regularPrice = $plugin->getRegularPrice();

$currentManifest = $plugin->versions()->visible()->latest('published_at')->first()?->manifest_permissions;
$versionHistory = $plugin->versions()->visible()->orderByDesc('published_at')->limit(15)->get();

$this->setPluginSeo($plugin);

return view('plugin-show', [
Expand All @@ -81,6 +84,8 @@ public function show(string $vendor, string $package): View
'regularPrice' => $regularPrice,
'hasDiscount' => $bestPrice && $regularPrice && $bestPrice->id !== $regularPrice->id,
'isAdminPreview' => (! $plugin->isApproved() || ! $plugin->is_active) && ($isAdmin || $isOwner),
'currentManifest' => $currentManifest,
'versionHistory' => $versionHistory,
]);
}

Expand Down
40 changes: 25 additions & 15 deletions app/Jobs/ReviewPluginRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Jobs;

use App\Models\Plugin;
use App\Services\PluginManifestParser;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
Expand All @@ -21,7 +22,7 @@ class ReviewPluginRepository implements ShouldQueue

public function __construct(public Plugin $plugin) {}

public function handle(): array
public function handle(PluginManifestParser $manifestParser): array
{
$repo = $this->plugin->getRepositoryOwnerAndName();

Expand All @@ -38,6 +39,8 @@ public function handle(): array
'ios_min_version' => null,
'has_android_min_version' => false,
'android_min_version' => null,
'permission_parity_complete' => true,
'missing_permission_parity' => [],
];

if (! $repo) {
Expand Down Expand Up @@ -70,11 +73,13 @@ public function handle(): array
$composerJson = $this->fetchComposerJson($owner, $repoName, $token);
$nativephpJson = $this->fetchNativephpJson($owner, $repoName, $token);

$supportsIos = $this->checkDirectoryHasFiles($tree, 'resources/ios/');

$checks = [
'has_license_file' => $this->checkHasLicenseFile($tree),
'has_release_version' => false,
'release_version' => null,
'supports_ios' => $this->checkDirectoryHasFiles($tree, 'resources/ios/'),
'supports_ios' => $supportsIos,
'supports_android' => $this->checkDirectoryHasFiles($tree, 'resources/android/'),
'supports_js' => $this->checkDirectoryHasFiles($tree, 'resources/js/'),
'requires_mobile_sdk' => false,
Expand All @@ -83,6 +88,8 @@ public function handle(): array
'ios_min_version' => null,
'has_android_min_version' => false,
'android_min_version' => null,
'permission_parity_complete' => true,
'missing_permission_parity' => [],
];

$latestTag = $this->fetchLatestTag($owner, $repoName, $token);
Expand All @@ -91,21 +98,24 @@ public function handle(): array
$checks['release_version'] = $latestTag;
}

if ($composerJson) {
$mobileConstraint = $composerJson['require']['nativephp/mobile'] ?? null;
$checks['requires_mobile_sdk'] = $mobileConstraint !== null;
$checks['mobile_sdk_constraint'] = $mobileConstraint;
}
$mobileConstraint = $manifestParser->sdkConstraint($composerJson);
$checks['requires_mobile_sdk'] = $mobileConstraint !== null;
$checks['mobile_sdk_constraint'] = $mobileConstraint;

if ($nativephpJson) {
$iosMinVersion = $nativephpJson['ios']['min_version'] ?? null;
$checks['has_ios_min_version'] = $iosMinVersion !== null;
$checks['ios_min_version'] = $iosMinVersion;
$iosMinVersion = $manifestParser->iosMinVersion($nativephpJson);
$checks['has_ios_min_version'] = $iosMinVersion !== null;
$checks['ios_min_version'] = $iosMinVersion;

$androidMinVersion = $nativephpJson['android']['min_version'] ?? null;
$checks['has_android_min_version'] = $androidMinVersion !== null;
$checks['android_min_version'] = $androidMinVersion;
}
$androidMinVersion = $manifestParser->androidMinVersion($nativephpJson);
$checks['has_android_min_version'] = $androidMinVersion !== null;
$checks['android_min_version'] = $androidMinVersion;

$missingParity = $manifestParser->missingUsageDescriptions(
$manifestParser->permissionManifest($nativephpJson),
$supportsIos
);
$checks['permission_parity_complete'] = $missingParity === [];
$checks['missing_permission_parity'] = $missingParity;

$this->plugin->update([
'review_checks' => $checks,
Expand Down
Loading