diff --git a/config/nativephp.php b/config/nativephp.php index 7ae7ca05..f3ab13f5 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -128,7 +128,7 @@ 'storage/framework/sessions', 'storage/framework/cache', 'storage/framework/testing', - 'storage/logs/laravel.log' + 'storage/logs/laravel.log', ], /* diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index 186f1dbb..9733dff7 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -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; @@ -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); diff --git a/src/Commands/RunCommand.php b/src/Commands/RunCommand.php index 0cdc0ac0..09ae2759 100644 --- a/src/Commands/RunCommand.php +++ b/src/Commands/RunCommand.php @@ -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; @@ -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 diff --git a/src/Plugins/Compilers/AndroidPluginCompiler.php b/src/Plugins/Compilers/AndroidPluginCompiler.php index 07ea6822..bcabb52b 100644 --- a/src/Plugins/Compilers/AndroidPluginCompiler.php +++ b/src/Plugins/Compilers/AndroidPluginCompiler.php @@ -936,7 +936,7 @@ protected function buildRepositoryBlock(array $repo): ?string $authBlock = ''; if ($authentication === 'basic') { - $authBlock = <<("basic") diff --git a/src/Support/AppIdValidator.php b/src/Support/AppIdValidator.php new file mode 100644 index 00000000..4b817b59 --- /dev/null +++ b/src/Support/AppIdValidator.php @@ -0,0 +1,86 @@ + 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); + } +} diff --git a/src/Traits/ValidatesAppConfig.php b/src/Traits/ValidatesAppConfig.php index 7dae8238..120db08b 100644 --- a/src/Traits/ValidatesAppConfig.php +++ b/src/Traits/ValidatesAppConfig.php @@ -2,6 +2,8 @@ namespace Native\Mobile\Traits; +use Native\Mobile\Support\AppIdValidator; + trait ValidatesAppConfig { private function validateAppVersion(string $buildType): void @@ -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); + } } } diff --git a/tests/Unit/AppIdValidatorTest.php b/tests/Unit/AppIdValidatorTest.php new file mode 100644 index 00000000..19d377ea --- /dev/null +++ b/tests/Unit/AppIdValidatorTest.php @@ -0,0 +1,154 @@ +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); + } +}