diff --git a/config/docs.php b/config/docs.php
index 92c545a15..671d731a4 100644
--- a/config/docs.php
+++ b/config/docs.php
@@ -59,7 +59,7 @@
1 => ['1.0', '1.1'],
2 => ['2.0'],
3 => ['3.0', '3.1', '3.2', '3.3'],
- 4 => ['4.0', '4.1', '4.2'],
+ 4 => ['4.0', '4.1', '4.2', '4.5'],
],
],
diff --git a/resources/views/docs/mobile/4/getting-started/upgrade-guide.md b/resources/views/docs/mobile/4/getting-started/upgrade-guide.md
index ea88aa4a7..0ee11579d 100644
--- a/resources/views/docs/mobile/4/getting-started/upgrade-guide.md
+++ b/resources/views/docs/mobile/4/getting-started/upgrade-guide.md
@@ -3,6 +3,18 @@ title: Upgrade Guide
order: 3
---
+## Upgrading To 4.5 From 4.4
+
+### Public releases need `APP_ENV=production`
+
+From 4.5, only a build packaged with `APP_ENV=production` can be released to the public. Any other `APP_ENV` makes a
+testing build: Google Play holds it to the internal and closed testing tracks, and the App Store limits it to
+TestFlight's internal testers.
+
+If you've been packaging store releases with Laravel's default `APP_ENV=local`, set `APP_ENV=production` in your
+`.env` before you package your next release. See
+[Production and testing builds](../publishing/introduction#production-and-testing-builds).
+
## Upgrading To 4.0 From 3.x
v4's headline is [SuperNative](../architecture/super-native) — fully native UI. Most of the release is additive,
diff --git a/resources/views/docs/mobile/4/publishing/android.md b/resources/views/docs/mobile/4/publishing/android.md
index a36d43523..7889717f8 100644
--- a/resources/views/docs/mobile/4/publishing/android.md
+++ b/resources/views/docs/mobile/4/publishing/android.md
@@ -98,10 +98,19 @@ php artisan native:package android \
The `--play-store-track` option controls where the build is released:
- `internal` - Internal testing (default, fastest review)
-- `alpha` - Closed alpha testing
-- `beta` - Closed beta testing
+- `alpha` - Closed testing
+- `beta` - Open testing
- `production` - Production release
+### Releasing to production
+
+
+
+Only a build packaged with `APP_ENV=production` can be released on the `beta` or `production` tracks. A build packaged
+with any other `APP_ENV` is a [testing build](introduction#production-and-testing-builds), which Google Play holds to
+the `internal` and `alpha` tracks. If you ask for the `production` track, `native:package` stops before uploading and
+tells you which `APP_ENV` the build was made with.
+
## Testing Play Store Uploads
If you already have an AAB file and want to test uploading without rebuilding, use `--test-push`:
@@ -209,6 +218,7 @@ php artisan native:package android \
- Verify the Google Service Account has access to your app in Play Console
- Ensure the service account key file is valid and readable
- Check that your bundle ID matches your Play Console app ID
+- For the `beta` or `production` tracks, package with `APP_ENV=production` (see [Releasing to production](#releasing-to-production))
## Artifact Locations
diff --git a/resources/views/docs/mobile/4/publishing/introduction.md b/resources/views/docs/mobile/4/publishing/introduction.md
index a1d8a7530..c99b148ef 100644
--- a/resources/views/docs/mobile/4/publishing/introduction.md
+++ b/resources/views/docs/mobile/4/publishing/introduction.md
@@ -102,6 +102,38 @@ submit it to the stores for approval and distribution.
- [Google Play Store submission guidelines](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en-GB#zippy=%2Cmaximum-size-limit)
- [Apple App Store submission guidelines](https://developer.apple.com/ios/submit/)
+## Production and testing builds
+
+
+
+The `APP_ENV` in your `.env` when you package your app decides how far that build can be released. Only a build
+packaged with `APP_ENV=production` can be released to the public. Any other value, such as `local` or `staging`, makes
+it a testing build, and NativePHP tells both stores so:
+
+- **Play Store:** the build declares closed testing as the largest audience it may reach. It can go to the internal and
+ closed testing tracks, but Google Play won't release it on the open testing or production tracks.
+- **App Store:** the build is marked for TestFlight internal testing only. Your internal testers can install it, but it
+ can't be sent to external testers or submitted for App Store review.
+
+This is written into the build itself, so a testing build can't be promoted to a public release later.
+
+New Laravel apps start with `APP_ENV=local`, so set it before you package a release:
+
+```env
+APP_ENV=production
+```
+
+Your `.env` is bundled into the app, so it runs in the same environment on the device.
+
+
+
## Packaging Your App
The `native:package` command creates signed, production-ready apps for distribution to the App Store and Play Store.
@@ -118,3 +150,4 @@ Before you can package your app for distribution, ensure:
3. For Android: You have a signing keystore with a valid key alias
4. For iOS: You have the necessary signing certificates and provisioning profiles from Apple Developer
5. All configuration is complete (see the [configuration guide](../getting-started/configuration))
+6. For a public release, your `.env` has `APP_ENV=production` (see [Production and testing builds](#production-and-testing-builds))
diff --git a/resources/views/docs/mobile/4/publishing/ios.md b/resources/views/docs/mobile/4/publishing/ios.md
index 5d258e6ba..30e3cef69 100644
--- a/resources/views/docs/mobile/4/publishing/ios.md
+++ b/resources/views/docs/mobile/4/publishing/ios.md
@@ -55,6 +55,17 @@ php artisan native:package ios \
--team-id=ABC1234567
```
+### Testing builds
+
+
+
+An `app-store` build packaged with any `APP_ENV` other than `production` is a
+[testing build](introduction#production-and-testing-builds), marked for TestFlight internal testing only. It uploads to
+App Store Connect as usual and your internal testers can install it, but it can't be sent to external testers or
+submitted for App Store review. Package it again with `APP_ENV=production` when you're ready to release.
+
+Ad-hoc, enterprise and development builds don't go through TestFlight, so `APP_ENV` doesn't limit them.
+
## Building for Ad-Hoc Distribution
For distributing to specific devices without going through the App Store:
diff --git a/tests/Feature/Docs/PublishingAppEnvTest.php b/tests/Feature/Docs/PublishingAppEnvTest.php
new file mode 100644
index 000000000..f4b4ecce6
--- /dev/null
+++ b/tests/Feature/Docs/PublishingAppEnvTest.php
@@ -0,0 +1,66 @@
+ 'test-token']);
+ Http::fake([
+ '*' => Http::response(['blocks' => []], 200),
+ ]);
+ }
+
+ public function test_publishing_introduction_explains_that_only_production_builds_reach_the_public(): void
+ {
+ $this->withoutVite()
+ ->get('/docs/mobile/4/publishing/introduction')
+ ->assertOk()
+ ->assertSee('id="production-and-testing-builds"', false)
+ ->assertSee('>4.5<', false)
+ ->assertSee('APP_ENV=production')
+ ->assertSee('TestFlight internal testing only')
+ ->assertSee('#production-and-testing-builds', false);
+ }
+
+ public function test_android_page_keeps_testing_builds_off_the_open_and_production_tracks(): void
+ {
+ $this->withoutVite()
+ ->get('/docs/mobile/4/publishing/android')
+ ->assertOk()
+ ->assertSee('id="releasing-to-production"', false)
+ ->assertSee('introduction#production-and-testing-builds', false)
+ ->assertSee('Open testing')
+ ->assertDontSee('Closed beta testing');
+ }
+
+ public function test_ios_page_explains_that_app_store_testing_builds_stay_in_internal_testflight(): void
+ {
+ $this->withoutVite()
+ ->get('/docs/mobile/4/publishing/ios')
+ ->assertOk()
+ ->assertSee('id="testing-builds"', false)
+ ->assertSee('introduction#production-and-testing-builds', false)
+ ->assertSee('TestFlight internal testing only');
+ }
+
+ public function test_upgrade_guide_tells_4_4_apps_to_package_releases_with_production(): void
+ {
+ $this->withoutVite()
+ ->get('/docs/mobile/4/getting-started/upgrade-guide')
+ ->assertOk()
+ ->assertSeeInOrder(['Upgrading To 4.5 From 4.4', 'APP_ENV=production', 'Upgrading To 4.0 From 3.x'])
+ ->assertSee('publishing/introduction#production-and-testing-builds', false);
+ }
+}