Skip to content
Draft
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
2 changes: 1 addition & 1 deletion config/nativephp.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
'storage/framework/sessions',
'storage/framework/cache',
'storage/framework/testing',
'storage/logs/laravel.log'
'storage/logs/laravel.log',
],

/*
Expand Down
3 changes: 2 additions & 1 deletion src/Commands/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Native\Mobile\Support\AppIdValidator;
use Native\Mobile\Traits\DisplaysMarketingBanners;
use Native\Mobile\Traits\InstallsAndroid;
use Native\Mobile\Traits\InstallsIos;
Expand Down Expand Up @@ -209,6 +209,7 @@ protected function ensureAppIdIsSet(): void
placeholder: $suggestedAppId,
default: $suggestedAppId,
hint: 'This uniquely identifies your app on the App Store and Google Play',
validate: fn (string $value) => AppIdValidator::validateForPrompt($value),
);

$this->setEnvValue('NATIVEPHP_APP_ID', $appId);
Expand Down
17 changes: 17 additions & 0 deletions src/Commands/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use GuzzleHttp\Client;
use Illuminate\Console\Command;
use Native\Mobile\Plugins\PluginRegistry;
use Native\Mobile\Support\AppIdValidator;
use Native\Mobile\Traits\DisplaysMarketingBanners;
use Native\Mobile\Traits\ManagesViteDevServer;
use Native\Mobile\Traits\ManagesWatchman;
Expand Down Expand Up @@ -223,6 +224,22 @@ protected function ensureValidAppId(): void
if (str($appId)->startsWith('com.nativephp.')) {
warning('Please change your NATIVEPHP_APP_ID from the default value.');
}

$errors = AppIdValidator::validate($appId);

if ($errors['android']) {
error('Invalid app ID for Android: '.$errors['android']);
}

if ($errors['ios']) {
error('Invalid app ID for iOS: '.$errors['ios']);
}

if ($errors['android'] || $errors['ios']) {
note('Please fix NATIVEPHP_APP_ID in your .env file.');
note('A valid app ID looks like: com.yourcompany.yourapp');
exit(1);
}
}

protected function updateStartUrl(string $startUrl): void
Expand Down
2 changes: 1 addition & 1 deletion src/Plugins/Compilers/AndroidPluginCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ protected function buildRepositoryBlock(array $repo): ?string

$authBlock = '';
if ($authentication === 'basic') {
$authBlock = <<<KOTLIN
$authBlock = <<<'KOTLIN'

authentication {
create<BasicAuthentication>("basic")
Expand Down
86 changes: 86 additions & 0 deletions src/Support/AppIdValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Native\Mobile\Support;

class AppIdValidator
{
/**
* Validate an app ID for Android (Java package name rules).
* Each segment must start with a letter and contain only letters, digits, and underscores.
*/
public static function validateForAndroid(string $appId): ?string
{
if (trim($appId) === '') {
return 'App ID cannot be empty.';
}

$segments = explode('.', $appId);

if (count($segments) < 2) {
return 'App ID must have at least two segments (e.g. com.example).';
}

foreach ($segments as $segment) {
if ($segment === '') {
return 'App ID must not contain empty segments.';
}

if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $segment)) {
return "Invalid Android app ID segment \"{$segment}\". Each segment must start with a letter and contain only letters, digits, and underscores. Hyphens are not allowed.";
}
}

return null;
}

/**
* Validate an app ID for iOS (bundle identifier rules).
* Each segment must start and end with an alphanumeric character, and may contain hyphens in between.
*/
public static function validateForIos(string $appId): ?string
{
if (trim($appId) === '') {
return 'App ID cannot be empty.';
}

$segments = explode('.', $appId);

if (count($segments) < 2) {
return 'App ID must have at least two segments (e.g. com.example).';
}

foreach ($segments as $segment) {
if ($segment === '') {
return 'App ID must not contain empty segments.';
}

if (! preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/', $segment)) {
return "Invalid iOS bundle ID segment \"{$segment}\". Each segment must start and end with a letter or digit, and may contain hyphens in between.";
}
}

return null;
}

/**
* Validate an app ID for both platforms.
*
* @return array{android: string|null, ios: string|null}
*/
public static function validate(string $appId): array
{
return [
'android' => static::validateForAndroid($appId),
'ios' => static::validateForIos($appId),
];
}

/**
* Validate an app ID for use in a prompt validation closure.
* Returns the first error found (Android rules are strictest), or null if valid for both platforms.
*/
public static function validateForPrompt(string $appId): ?string
{
return static::validateForAndroid($appId) ?? static::validateForIos($appId);
}
}
18 changes: 18 additions & 0 deletions src/Traits/ValidatesAppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Native\Mobile\Traits;

use Native\Mobile\Support\AppIdValidator;

trait ValidatesAppConfig
{
private function validateAppVersion(string $buildType): void
Expand Down Expand Up @@ -41,5 +43,21 @@ protected function validateAppId(): void
if (str($appId)->startsWith('com.nativephp.')) {
\Laravel\Prompts\warning('Please change your NATIVEPHP_APP_ID. Must not contain "nativephp"');
}

$errors = AppIdValidator::validate($appId);

if ($errors['android']) {
\Laravel\Prompts\error('Invalid app ID for Android: '.$errors['android']);
}

if ($errors['ios']) {
\Laravel\Prompts\error('Invalid app ID for iOS: '.$errors['ios']);
}

if ($errors['android'] || $errors['ios']) {
$this->line('Please fix NATIVEPHP_APP_ID in your .env file.');
$this->line('A valid app ID looks like: com.yourcompany.yourapp');
exit(1);
}
}
}
154 changes: 154 additions & 0 deletions tests/Unit/AppIdValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

namespace Tests\Unit;

use Native\Mobile\Support\AppIdValidator;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;

class AppIdValidatorTest extends TestCase
{
// --- Android validation ---

#[DataProvider('validAndroidIds')]
public function test_valid_android_ids(string $appId): void
{
$this->assertNull(AppIdValidator::validateForAndroid($appId));
}

public static function validAndroidIds(): array
{
return [
'simple two segments' => ['com.example'],
'three segments' => ['com.example.myapp'],
'with underscore' => ['com.example.my_app'],
'with digits' => ['com.example.app123'],
'uppercase' => ['com.Example.MyApp'],
'many segments' => ['com.example.sub.package.app'],
];
}

#[DataProvider('invalidAndroidIds')]
public function test_invalid_android_ids(string $appId): void
{
$this->assertNotNull(AppIdValidator::validateForAndroid($appId));
}

public static function invalidAndroidIds(): array
{
return [
'empty string' => [''],
'single segment' => ['myapp'],
'hyphen in segment' => ['com.example.my-app'],
'segment starts with digit' => ['com.123example.app'],
'empty segment (double dot)' => ['com..example'],
'trailing dot' => ['com.example.'],
'leading dot' => ['.com.example'],
'spaces' => ['com .example'],
'special characters' => ['com.example.my@app'],
];
}

// --- iOS validation ---

#[DataProvider('validIosIds')]
public function test_valid_ios_ids(string $appId): void
{
$this->assertNull(AppIdValidator::validateForIos($appId));
}

public static function validIosIds(): array
{
return [
'simple two segments' => ['com.example'],
'three segments' => ['com.example.myapp'],
'with hyphen' => ['com.example.my-app'],
'with digits' => ['com.example.app123'],
'segment starts with digit' => ['com.1example.app'],
'uppercase' => ['com.Example.MyApp'],
'many segments' => ['com.example.sub.package.app'],
];
}

#[DataProvider('invalidIosIds')]
public function test_invalid_ios_ids(string $appId): void
{
$this->assertNotNull(AppIdValidator::validateForIos($appId));
}

public static function invalidIosIds(): array
{
return [
'empty string' => [''],
'single segment' => ['myapp'],
'empty segment (double dot)' => ['com..example'],
'trailing dot' => ['com.example.'],
'leading dot' => ['.com.example'],
'segment starts with hyphen' => ['com.-example.app'],
'segment ends with hyphen' => ['com.example-.app'],
'underscore' => ['com.example.my_app'],
];
}

// --- Cross-platform hyphens ---

public function test_hyphens_rejected_for_android_allowed_for_ios(): void
{
$appId = 'com.japseyz.ikast-musikliv';

$this->assertNotNull(AppIdValidator::validateForAndroid($appId));
$this->assertNull(AppIdValidator::validateForIos($appId));
}

// --- Cross-platform validate() ---

public function test_validate_returns_both_platforms(): void
{
$result = AppIdValidator::validate('com.example.myapp');

$this->assertArrayHasKey('android', $result);
$this->assertArrayHasKey('ios', $result);
$this->assertNull($result['android']);
$this->assertNull($result['ios']);
}

public function test_validate_returns_errors_for_both_platforms(): void
{
// Single segment is invalid for both
$result = AppIdValidator::validate('myapp');

$this->assertNotNull($result['android']);
$this->assertNotNull($result['ios']);
}

public function test_validate_returns_android_error_only_for_hyphens(): void
{
$result = AppIdValidator::validate('com.example.my-app');

$this->assertNotNull($result['android']);
$this->assertNull($result['ios']);
}

// --- validateForPrompt() ---

public function test_validate_for_prompt_returns_null_for_valid(): void
{
$this->assertNull(AppIdValidator::validateForPrompt('com.example.myapp'));
}

public function test_validate_for_prompt_returns_string_for_invalid(): void
{
$result = AppIdValidator::validateForPrompt('com.example.my-app');

$this->assertIsString($result);
$this->assertNotEmpty($result);
}

public function test_validate_for_prompt_returns_android_error_first(): void
{
// Hyphens fail Android but pass iOS — prompt should return the Android error
$result = AppIdValidator::validateForPrompt('com.example.my-app');

$this->assertStringContainsString('Android', $result);
}
}
Loading