reis-mobile 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +24 -16
- package/agents/lead-mobile.md +5 -2
- package/agents/mobile-accessibility-auditor.md +63 -0
- package/agents/mobile-release-engineer.md +58 -0
- package/agents/mobile-staff-engineer.md +2 -2
- package/package.json +1 -1
- package/skills/mobile-accessibility/SKILL.md +69 -0
- package/skills/mobile-accessibility/references/android.md +54 -0
- package/skills/mobile-accessibility/references/flutter.md +59 -0
- package/skills/mobile-accessibility/references/ios.md +50 -0
- package/skills/mobile-accessibility/references/react-native.md +38 -0
- package/skills/mobile-debug/SKILL.md +2 -1
- package/skills/mobile-debug/references/android/anr.md +59 -0
- package/skills/mobile-debug/references/android/memory-leaks.md +43 -0
- package/skills/mobile-debug/references/android.md +6 -0
- package/skills/mobile-debug/references/deep-links.md +60 -0
- package/skills/mobile-release/SKILL.md +70 -0
- package/skills/mobile-release/references/android.md +47 -0
- package/skills/mobile-release/references/ci-cd.md +94 -0
- package/skills/mobile-release/references/flutter.md +37 -0
- package/skills/mobile-release/references/ios.md +48 -0
- package/skills/mobile-release/references/react-native.md +36 -0
- package/skills/mobile-release/references/store-copy.md +52 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Accessibility in React Native
|
|
2
|
+
|
|
3
|
+
React Native maps accessibility props to the native TalkBack and VoiceOver APIs. Core components such as `Button` and `Switch` fill in roles; `Pressable`, `TouchableOpacity` and `View` do not. Recent React Native versions also accept ARIA-style props (`role`, `aria-label`, `aria-checked`...) that map to the same native properties; follow whichever style the project already uses. For the broader guide, see `mobile-rn` → `references/ui-navigation.md`.
|
|
4
|
+
|
|
5
|
+
## Labels, roles and states
|
|
6
|
+
|
|
7
|
+
| Need | API |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Label an icon-only control | `<Pressable accessibilityRole="button" accessibilityLabel="Delete task">` |
|
|
10
|
+
| Hide decorative content | `accessible={false}` on images; for subtrees `accessibilityElementsHidden` (iOS) **and** `importantForAccessibility="no-hide-descendants"` (Android) |
|
|
11
|
+
| Group a row | `accessible` on the container, with a combined `accessibilityLabel` when children's text is not enough |
|
|
12
|
+
| State | `accessibilityState={{ checked, selected, disabled, expanded, busy }}` |
|
|
13
|
+
| Value | `accessibilityValue={{ min: 0, max: 100, now: 40 }}` or `{{ text: '3 of 5' }}` |
|
|
14
|
+
| Header | `accessibilityRole="header"` |
|
|
15
|
+
| Hint | `accessibilityHint="Opens the order details"` |
|
|
16
|
+
| Gesture alternative | `accessibilityActions={[{ name: 'archive', label: 'Archive' }]}` with `onAccessibilityAction` |
|
|
17
|
+
| Live updates | `accessibilityLiveRegion="polite"` (Android); `AccessibilityInfo.announceForAccessibility('Saved')` for both |
|
|
18
|
+
| Modal | `<Modal>` handles focus; custom overlays need `accessibilityViewIsModal` (iOS) and `importantForAccessibility="no-hide-descendants"` on the content behind (Android) |
|
|
19
|
+
| Move focus | `AccessibilityInfo.setAccessibilityFocus(findNodeHandle(ref.current))` |
|
|
20
|
+
|
|
21
|
+
Pitfalls:
|
|
22
|
+
|
|
23
|
+
- A `Pressable` without `accessibilityRole` is read as plain text: the user does not know it is tappable.
|
|
24
|
+
- A checkbox built from `Pressable` and an icon needs `accessibilityRole="checkbox"` and `accessibilityState={{ checked }}`.
|
|
25
|
+
- The iOS-only and Android-only props are silently ignored on the other platform; hiding and modal behavior need both.
|
|
26
|
+
- `TextInput` without a visible label: add `accessibilityLabel`; placeholders are not labels.
|
|
27
|
+
|
|
28
|
+
## Touch targets and text scaling
|
|
29
|
+
|
|
30
|
+
- Keep interactive elements at 44×44 pt / 48×48 dp. Use `hitSlop` or padding to extend the area of small icons; `hitSlop` does not extend beyond the parent's bounds on Android.
|
|
31
|
+
- Text scales with the system font size by default. Flag `allowFontScaling={false}` on body text; prefer `maxFontSizeMultiplier` when a layout has a real limit. Avoid fixed `height` on text containers.
|
|
32
|
+
- Respect reduced motion with `AccessibilityInfo.isReduceMotionEnabled()` or Reanimated's `useReducedMotion()`.
|
|
33
|
+
|
|
34
|
+
## Automated checks
|
|
35
|
+
|
|
36
|
+
- React Native Testing Library queries by role and label (`screen.getByRole('button', { name: 'Delete task' })`), so tests fail when a role or label disappears. `toBeChecked`, `toBeDisabled` and `toBeExpanded` matchers read the accessibility state.
|
|
37
|
+
- ESLint: `eslint-plugin-react-native-a11y` flags missing roles and labels, when the project uses ESLint.
|
|
38
|
+
- Manual: TalkBack and VoiceOver on devices, the Accessibility Scanner app on Android and Accessibility Inspector in Xcode on the iOS build.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mobile-debug
|
|
3
|
-
description: Finds the cause of failing builds and runtime errors in mobile apps on every stack — Flutter (pub version solving, Dart compilation, generated code, layout overflows, runtime exceptions), native Android (Gradle, AGP, JDK and Kotlin mismatches, dependency conflicts, manifest merger, R8, logcat crashes and
|
|
3
|
+
description: Finds the cause of failing builds and runtime errors in mobile apps on every stack — Flutter (pub version solving, Dart compilation, generated code, layout overflows, runtime exceptions), native Android (Gradle, AGP, JDK and Kotlin mismatches, dependency conflicts, manifest merger, R8, logcat crashes, ANRs and memory leaks), native iOS (Xcode signing and provisioning, deployment targets, CocoaPods, Swift Package Manager, crash logs) and React Native (Metro, native modules not found, Hermes, the Android and iOS builds underneath), plus deep links, App Links and Universal Links that do not open the app. Use whenever a build, `flutter run`, `pod install`, Gradle task, Xcode archive or app launch fails, when the user pastes a stack trace, "BUILD FAILED" or a red screen, or runs /reis-mobile:debug, even if they only paste the error.
|
|
4
4
|
intents: [debug, build, dependency, migration]
|
|
5
5
|
stacks: ["*"]
|
|
6
6
|
---
|
|
@@ -19,6 +19,7 @@ Choose by the stack **and** by the layer the error comes from — a Flutter or R
|
|
|
19
19
|
| Gradle, the Android build, logcat crashes or ANRs (native Android, or the `android/` folder of any app) | [references/android.md](references/android.md) |
|
|
20
20
|
| Xcode, signing, CocoaPods, Swift packages or iOS crash logs (native iOS, or the `ios/` folder of any app) | [references/ios.md](references/ios.md) |
|
|
21
21
|
| A React Native or Expo app: Metro, native modules, JavaScript errors | [references/react-native.md](references/react-native.md) |
|
|
22
|
+
| A deep link, App Link or Universal Link that opens the browser, the wrong screen or crashes (any stack) | [references/deep-links.md](references/deep-links.md) |
|
|
22
23
|
|
|
23
24
|
When `reis-mobile debug` (or `/reis-mobile:debug`) ran, its `area` line points at the layer: `gradle`, `manifest`, `proguard` → Android; `xcode`, `signing`, `cocoapods`, `spm` → iOS; `pub` → Flutter; `metro` → React Native.
|
|
24
25
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Investigating Android ANRs
|
|
2
|
+
|
|
3
|
+
An ANR (Application Not Responding) is reported when the main thread cannot process an input event for about five seconds, or when a broadcast receiver, service or job does not finish its callback in the time the system allows. The top frame of the main thread is often the victim, not the cause: find what the main thread was waiting for.
|
|
4
|
+
|
|
5
|
+
## 1. Get the evidence
|
|
6
|
+
|
|
7
|
+
| Source | What it gives |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Play Console → Android vitals → ANRs | Clusters with the main thread's stack, affected versions, devices and Android versions |
|
|
10
|
+
| Crashlytics or another reporter with ANR support | Main thread stack; other threads depend on the SDK |
|
|
11
|
+
| `ApplicationExitInfo` (Android 11+) | `REASON_ANR` exits of previous processes, with `getTraceInputStream()` returning the full thread dump — log or upload it on the next launch |
|
|
12
|
+
| `adb bugreport` | ANR traces of all threads when reproduced on a test device |
|
|
13
|
+
| Local reproduction | `adb logcat` shows `ANR in <package>` with the reason; `StrictMode` flags disk and network on the main thread before it becomes an ANR |
|
|
14
|
+
|
|
15
|
+
High confidence needs the **whole thread dump**, not only the main thread. With a single stack, say the conclusion is tentative and ask for the full trace.
|
|
16
|
+
|
|
17
|
+
## 2. Read the reason
|
|
18
|
+
|
|
19
|
+
- `Input dispatching timed out` → the main thread did not handle a touch or key event in time.
|
|
20
|
+
- `Broadcast of Intent { ... }` / `executing service` / `ContentProvider not responding` → a component callback ran too long on the main thread.
|
|
21
|
+
- `Context.startForegroundService() did not then call Service.startForeground()` → the service did not promote itself in time. On recent Android versions this is reported as a crash (`ForegroundServiceDidNotStartInTimeException`) as well.
|
|
22
|
+
|
|
23
|
+
## 3. Read the main thread's state
|
|
24
|
+
|
|
25
|
+
In the dump, find `"main" prio=5 tid=1 <State>`:
|
|
26
|
+
|
|
27
|
+
| State | Look for |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **Runnable** | Work on the main thread: JSON parsing, database queries, `SharedPreferences.commit()`, file I/O, bitmap decoding, crypto, class loading of large SDKs, a loop in layout or composition |
|
|
30
|
+
| **Blocked** | `- waiting to lock <0x...> held by thread N`: open thread N and see what it does while holding the lock |
|
|
31
|
+
| **Waiting / TimedWaiting** | `Object.wait`, `Future.get`, `CountDownLatch.await`, `Thread.join`, `runBlocking`, a `Semaphore` or `Mutex`: find who should release it |
|
|
32
|
+
| **Native** | Binder calls into system services (`BinderProxy.transact`), `SharedPreferences` `apply()` flushing in `QueuedWork.waitToFinish` during `onPause`/`onStop`, file system or graphics calls |
|
|
33
|
+
|
|
34
|
+
For a deadlock, write down the cycle: thread A holds lock 1 and waits for lock 2; thread B holds lock 2 and waits for lock 1.
|
|
35
|
+
|
|
36
|
+
## 4. Match the code
|
|
37
|
+
|
|
38
|
+
- `runBlocking` anywhere reachable from the main thread, including inside interceptors, `ContentProvider`s and DI factories.
|
|
39
|
+
- `@Synchronized` or `synchronized` blocks that also do network or disk work: the lock is held for the whole I/O.
|
|
40
|
+
- Eager initialization in `Application.onCreate`, `ContentProvider.onCreate` (including library initializers) or the first Activity.
|
|
41
|
+
- Room queries without `suspend`/`Flow`, or with `allowMainThreadQueries()`.
|
|
42
|
+
- `BroadcastReceiver.onReceive` doing work beyond starting a `WorkManager` job; `goAsync()` without calling `finish()`.
|
|
43
|
+
- `SharedPreferences.apply()` writes piling up and flushed on lifecycle transitions: move hot data to DataStore.
|
|
44
|
+
- Third-party SDK calls that are synchronous on first use (ads, analytics, feature flags).
|
|
45
|
+
|
|
46
|
+
## 5. Fix
|
|
47
|
+
|
|
48
|
+
- Remove the blocking path that the evidence proves, not the top frame. Moving work to `Dispatchers.IO` fixes main-thread I/O; it does not fix a lock held across I/O — shorten the critical section instead.
|
|
49
|
+
- Replace `runBlocking` with a suspending call chain, or with an in-memory value that is refreshed asynchronously.
|
|
50
|
+
- Defer SDK initialization (App Startup with lazy initializers, or initialization after the first frame).
|
|
51
|
+
- Keep receivers, services and job callbacks short; hand long work to `WorkManager`.
|
|
52
|
+
- Do not trade an ANR for lost work or a race: if the result is needed, expose loading state instead of blocking.
|
|
53
|
+
|
|
54
|
+
## 6. Verify
|
|
55
|
+
|
|
56
|
+
- Reproduce with the slow dependency made slower (a delay in the network call or the lock owner) on a low-end device or emulator with a cold start.
|
|
57
|
+
- `StrictMode.ThreadPolicy` with `detectAll()` in debug builds shows no violations on the path.
|
|
58
|
+
- A system trace (Perfetto, Android Studio's CPU profiler) shows the main thread free during the flow. See `mobile-android` → the profiler guide.
|
|
59
|
+
- After release, watch the cluster and the user-perceived ANR rate in Android vitals for the new version.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Investigating Android memory leaks
|
|
2
|
+
|
|
3
|
+
A leak is an object that should have been garbage collected — usually an Activity, Fragment view, View or Context — kept alive by a reference from something that lives longer. Symptoms: memory growing each time a screen opens, `OutOfMemoryError` after long sessions, and slow GC pauses.
|
|
4
|
+
|
|
5
|
+
## 1. Get the evidence
|
|
6
|
+
|
|
7
|
+
- **LeakCanary** (`debugImplementation("com.squareup.leakcanary:leakcanary-android:<version>")`, no code needed) detects retained Activities, Fragments, Fragment views, ViewModels and Services in debug builds and prints the **leak trace**: the chain of references from a GC root to the leaked object. Check the version in the project before adding it.
|
|
8
|
+
- **Android Studio Memory Profiler:** capture a heap dump after opening and closing the screen a few times and forcing GC; filter by the Activity class and look for more instances than screens on the back stack. *Show activity/fragment leaks* highlights destroyed ones.
|
|
9
|
+
- **`OutOfMemoryError` reports** show where the last allocation failed, which is rarely the leak itself; use them to know which flow to profile.
|
|
10
|
+
|
|
11
|
+
## 2. Read the leak trace
|
|
12
|
+
|
|
13
|
+
LeakCanary marks each reference as leaking, not leaking or unknown. The bug is in the references between the last **not leaking** object and the first **leaking** one. Typical shape:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
GC Root: static field
|
|
17
|
+
↓ SessionManager.INSTANCE
|
|
18
|
+
↓ SessionManager.listeners ← likely cause: never unregistered
|
|
19
|
+
↓ ProfileActivity$1 (anonymous listener)
|
|
20
|
+
↓ ProfileActivity ← destroyed but retained
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 3. Common causes and fixes
|
|
24
|
+
|
|
25
|
+
| Cause | Fix |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Singleton, `object` or static field holding an Activity, View or Context | Hold `applicationContext`, or do not store it; pass it per call |
|
|
28
|
+
| Listener or callback registered in `onStart`/`onCreate` and never removed | Unregister in the symmetric callback, or use lifecycle-aware APIs (`repeatOnLifecycle`, `LifecycleObserver`) |
|
|
29
|
+
| Fragment keeping a binding or views after `onDestroyView` | Clear the binding in `onDestroyView`, or use `viewLifecycleOwner` for observers |
|
|
30
|
+
| Coroutine launched in `GlobalScope` or a custom scope that captures the Activity | Use `lifecycleScope`/`viewModelScope`; never capture views in long-running work |
|
|
31
|
+
| ViewModel holding an Activity, Fragment, View or its Context | Keep only data; use `AndroidViewModel` or an injected application context when a Context is needed |
|
|
32
|
+
| Inner or anonymous classes (Handler, Runnable, AsyncTask) outliving the Activity | Static nested class with a weak reference, or cancel pending work in `onDestroy` |
|
|
33
|
+
| `Handler.postDelayed` pending when the screen closes | `removeCallbacksAndMessages(null)` in the teardown |
|
|
34
|
+
| Third-party SDK holding the Activity passed at init | Pass the application context, or report it upstream and add it to LeakCanary's known library leaks |
|
|
35
|
+
| Compose: objects captured in `remember` that hold an Activity, or `DisposableEffect` without cleanup in `onDispose` | Use `LocalContext` at call time; release in `onDispose` |
|
|
36
|
+
|
|
37
|
+
Not every growth is a leak: image caches and pools are bounded by design. Check whether memory stabilizes after several repetitions before calling it a leak.
|
|
38
|
+
|
|
39
|
+
## 4. Verify
|
|
40
|
+
|
|
41
|
+
- Run the flow again with LeakCanary: no retained instance reported.
|
|
42
|
+
- Heap dump after repeating the flow and forcing GC: one or zero instances of the Activity.
|
|
43
|
+
- For large bitmaps and caches, compare the Java and native heap in the profiler before and after.
|
|
@@ -24,3 +24,9 @@ In Flutter and React Native apps, run Gradle from `android/` to see the full out
|
|
|
24
24
|
- An ANR means the main thread was blocked for about five seconds (input) or a broadcast receiver or service ran too long.
|
|
25
25
|
- Get the main thread's stack from Play Console vitals, or from `adb bugreport` (the ANR traces are included), and look at what the main thread was waiting on: disk, network, a lock, `runBlocking`, a binder call.
|
|
26
26
|
- `StrictMode` in debug builds flags disk and network access on the main thread before it becomes an ANR.
|
|
27
|
+
|
|
28
|
+
Full investigation — evidence sources, reading thread states, locks and deadlocks, fixes and verification: [android/anr.md](android/anr.md).
|
|
29
|
+
|
|
30
|
+
## Memory leaks and OutOfMemoryError
|
|
31
|
+
|
|
32
|
+
Retained Activities, Fragments and Contexts, reading LeakCanary traces and heap dumps: [android/memory-leaks.md](android/memory-leaks.md).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Debugging deep links
|
|
2
|
+
|
|
3
|
+
Deep links fail in three places: the **domain verification** (the link opens the browser), the **app's registration** (the system does not know the app handles the URL) and the **in-app routing** (the app opens on the wrong screen or crashes). Find which one before changing anything. Security checks for deep links (validating parameters, not acting without confirmation) are in `mobile-security`.
|
|
4
|
+
|
|
5
|
+
## 1. Classify the symptom
|
|
6
|
+
|
|
7
|
+
| Symptom | Layer to check first |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Opens the browser instead of the app | Verification: `assetlinks.json` / `apple-app-site-association`, entitlement, fingerprint |
|
|
10
|
+
| Works in debug, not from the store build | Verification with the release signing key (Android) or the distribution Team ID (iOS) |
|
|
11
|
+
| Shows a chooser dialog on Android | App Links not verified; the link is treated as a plain intent filter |
|
|
12
|
+
| Opens the app on the home screen | In-app routing: the URL reaches the app but no route matches, or it is lost during cold start |
|
|
13
|
+
| Crashes on open | In-app routing: navigation before the navigator or the auth state is ready |
|
|
14
|
+
| Custom scheme (`myapp://`) does nothing | The scheme is not registered, or the test tool is quoting the URL wrong |
|
|
15
|
+
|
|
16
|
+
## 2. Android App Links
|
|
17
|
+
|
|
18
|
+
**Checklist**
|
|
19
|
+
- [ ] `<intent-filter android:autoVerify="true">` with `ACTION_VIEW`, categories `DEFAULT` and `BROWSABLE`, `android:scheme="https"` and the exact `android:host`.
|
|
20
|
+
- [ ] `https://<host>/.well-known/assetlinks.json` returns 200 over HTTPS without redirects, with `Content-Type: application/json`, for **every host** in the filters (including `www` and subdomains).
|
|
21
|
+
- [ ] The file lists the package name and the SHA-256 fingerprint of the certificate that signs the installed build. For Play builds that is the **app signing key** from the Play Console (App integrity), not the upload key; debug builds use the debug keystore fingerprint (`keytool -list -v -keystore ~/.android/debug.keystore`).
|
|
22
|
+
|
|
23
|
+
**Commands**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Verification state per domain (Android 12+)
|
|
27
|
+
adb shell pm get-app-links com.example.app
|
|
28
|
+
# Re-run verification after fixing the server file
|
|
29
|
+
adb shell pm verify-app-links --re-verify com.example.app
|
|
30
|
+
# Open a link as the system would
|
|
31
|
+
adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/product/42" com.example.app
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`verified` means the domain passed; `legacy_failure` or other states mean the server file or the fingerprint is wrong. Google's Statement List Generator and Tester checks a published `assetlinks.json`.
|
|
35
|
+
|
|
36
|
+
## 3. iOS Universal Links
|
|
37
|
+
|
|
38
|
+
**Checklist**
|
|
39
|
+
- [ ] The **Associated Domains** capability is on for the App ID and the target, with `applinks:example.com` (and each subdomain) in the entitlements of the build being tested.
|
|
40
|
+
- [ ] `https://<domain>/.well-known/apple-app-site-association` (no file extension) returns 200 over HTTPS without redirects, as JSON.
|
|
41
|
+
- [ ] The file's `applinks.details` contains `<TeamID>.<BundleID>` of that build and `components` (or legacy `paths`) that match the URL's path, query and fragment.
|
|
42
|
+
- [ ] Apple fetches the file through its CDN, not from the device: after changing it, check what the CDN serves at `https://app-site-association.cdn-apple.com/a/v1/<domain>`. To test against your server directly during development, use `applinks:example.com?mode=developer` with Developer Mode enabled on the device.
|
|
43
|
+
|
|
44
|
+
**Behavior that looks like a bug but is not**
|
|
45
|
+
- Typing or pasting a Universal Link in Safari's address bar opens the website. Test by tapping the link in Notes, Messages or a page on **another** domain.
|
|
46
|
+
- If the user chose to open the site in Safari from the banner, iOS remembers it for that domain; long-press the link and choose *Open in "App"* to reset.
|
|
47
|
+
|
|
48
|
+
**Tools:** Settings → Developer → Universal Links → Diagnostics on a device with Developer Mode checks a URL against the installed app. For custom schemes on the simulator: `xcrun simctl openurl booted "myapp://product/42"`.
|
|
49
|
+
|
|
50
|
+
## 4. In-app routing
|
|
51
|
+
|
|
52
|
+
- **Cold start vs warm start.** The URL arrives through different paths: Android `onCreate` intent vs `onNewIntent`; iOS `scene(_:willConnectTo:options:)` (`connectionOptions.userActivities`/`urlContexts`) vs `scene(_:continue:)`/`scene(_:openURLContexts:)`, or `.onOpenURL` in SwiftUI. A handler implemented only for the warm path loses links that launch the app.
|
|
53
|
+
- **Timing.** Links handled before the navigator, the auth state or remote config is ready either crash or are dropped. Store the pending link and route after initialization; send users to login first and resume the link afterward.
|
|
54
|
+
- **Route matching.** Log the received URL at the entry point and compare with the route patterns: trailing slashes, case, encoded characters and query parameters are frequent mismatches.
|
|
55
|
+
- **Flutter:** with `go_router`, the Flutter deep linking flag (`flutter_deeplinking_enabled` meta-data in `AndroidManifest.xml`, `FlutterDeepLinkingEnabled` in `Info.plist`) lets the router receive links; when a plugin such as `app_links` handles them instead, follow the plugin's instructions for that flag. Check the Flutter version's default before changing it.
|
|
56
|
+
- **React Native:** the `linking` config of React Navigation (`prefixes` and `config.screens`) or Expo Router's file routes decide the screen; test with `npx uri-scheme open "myapp://product/42" --android` (or `--ios`). Expo Go uses its own scheme; test real links in a development build.
|
|
57
|
+
|
|
58
|
+
## 5. Verify
|
|
59
|
+
|
|
60
|
+
Test on a device with the build type that will ship, for each link type and host, from cold start and from background, logged in and logged out.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-release
|
|
3
|
+
description: Prepares and checks mobile app releases on every stack — release readiness (go/no-go) checklist, version name and build number, signing, crash symbols (R8 mappings, dSYMs, Dart split debug info, JavaScript source maps), store requirements, staged rollout and rollback, release notes and store listing copy with the Google Play and App Store character limits, and CI/CD pipelines with GitHub Actions, fastlane and EAS — for Flutter, native Android, native iOS and React Native. Use when preparing a version for Google Play or the App Store, auditing whether a build is ready to ship, writing release notes or store descriptions from the git history, setting up or reviewing a build and deploy pipeline, or planning a rollout, even if the user only says "we ship on Friday".
|
|
4
|
+
intents: [release, deployment]
|
|
5
|
+
stacks: ["*"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mobile Release
|
|
9
|
+
|
|
10
|
+
One release process for every mobile stack. This file holds the process and what is true everywhere; each platform's versioning, signing, symbols and store details live in its reference.
|
|
11
|
+
|
|
12
|
+
## 1. Pick the references
|
|
13
|
+
|
|
14
|
+
| Need | Read |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Flutter app | [references/flutter.md](references/flutter.md) |
|
|
17
|
+
| Native Android, or the Android build of any app | [references/android.md](references/android.md) |
|
|
18
|
+
| Native iOS, or the iOS build of any app | [references/ios.md](references/ios.md) |
|
|
19
|
+
| React Native or Expo app | [references/react-native.md](references/react-native.md) |
|
|
20
|
+
| Release notes and store listing text | [references/store-copy.md](references/store-copy.md) |
|
|
21
|
+
| A CI/CD pipeline that builds, signs and uploads | [references/ci-cd.md](references/ci-cd.md) |
|
|
22
|
+
|
|
23
|
+
Flutter and React Native apps also need the Android and iOS references for signing and store details. Deeper platform guides live in the platform skills: Google Play policy and Play Billing in `mobile-android`, TestFlight and App Store Connect in `mobile-ios`, EAS in `mobile-rn`. Security hardening before release is in `mobile-security`.
|
|
24
|
+
|
|
25
|
+
## 2. Readiness checklist
|
|
26
|
+
|
|
27
|
+
Check each item against the project, not against memory. Mark each one **pass**, **fail** (with `file:line` or the missing artifact) or **unknown** (needs information only the team has, such as the store console state).
|
|
28
|
+
|
|
29
|
+
**Version**
|
|
30
|
+
- [ ] The version name follows the project's scheme and changed since the last release tag (`git describe --tags --abbrev=0`).
|
|
31
|
+
- [ ] The build number is higher than any build already uploaded. Both stores reject a build number they have seen.
|
|
32
|
+
- [ ] Android, iOS and any cross-platform manifest (`pubspec.yaml`, `app.json`) agree on the version.
|
|
33
|
+
|
|
34
|
+
**Build quality**
|
|
35
|
+
- [ ] CI is green on the release commit: lint or analysis, tests, and a release build for each platform.
|
|
36
|
+
- [ ] The release build is the one tested: release configuration, production environment, real signing — not a debug build.
|
|
37
|
+
- [ ] No debug leftovers: logging of sensitive data, debug menus, test endpoints, `debuggable` or development entitlements, feature flags pointing at staging.
|
|
38
|
+
- [ ] Crash symbols for this exact build are uploaded or archived: R8 mapping, dSYMs, Dart debug info, JavaScript source maps.
|
|
39
|
+
|
|
40
|
+
**Store and compliance**
|
|
41
|
+
- [ ] The target SDK, minimum OS and build toolchain meet the store's current requirements. These change every year: tell the user to confirm them in the Play Console and Apple's upload requirements instead of stating dates from memory.
|
|
42
|
+
- [ ] New permissions, tracking or data collection are reflected in the Play Data safety form, the App Store privacy details, privacy manifests and the privacy policy.
|
|
43
|
+
- [ ] Reviewers can reach every feature: a demo account and notes for anything behind login, hardware or location.
|
|
44
|
+
- [ ] Release notes and, when features changed, screenshots and description are ready in every listed language.
|
|
45
|
+
|
|
46
|
+
**Rollout**
|
|
47
|
+
- [ ] Staged rollout (Play) or phased release (App Store) planned for updates, with the crash-free and ANR metrics to watch and the threshold that halts it.
|
|
48
|
+
- [ ] Rollback plan: stores do not roll back a binary. Halting a rollout stops new installs; users who updated need a new build with a higher build number, a server-side kill switch or a remote config flag. Know which one applies before shipping.
|
|
49
|
+
- [ ] The release commit is tagged and the tag matches the version.
|
|
50
|
+
|
|
51
|
+
## 3. Report
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## Release readiness — <app> <version> (<build>)
|
|
55
|
+
Verdict: GO | GO WITH RISKS | NO-GO
|
|
56
|
+
|
|
57
|
+
### Blockers
|
|
58
|
+
- item — evidence (file:line or missing artifact) — fix
|
|
59
|
+
|
|
60
|
+
### Risks
|
|
61
|
+
- item — why it matters — mitigation
|
|
62
|
+
|
|
63
|
+
### Unknown
|
|
64
|
+
- item — who or what can confirm it
|
|
65
|
+
|
|
66
|
+
### Rollout plan
|
|
67
|
+
Stages, metrics to watch, halt threshold, rollback path.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A single blocker makes the verdict NO-GO. Do not bump versions, create tags, upload builds or change store settings unless the user asks; propose the exact commands instead.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Releasing on Android
|
|
2
|
+
|
|
3
|
+
Google Play specifics: policy compliance and Play Billing live in `mobile-android` → `references/play.md`; R8 keep rules in its build and performance guides.
|
|
4
|
+
|
|
5
|
+
## Version
|
|
6
|
+
|
|
7
|
+
- `versionCode` (integer) must increase with every upload to Play, across all tracks. `versionName` is what users see.
|
|
8
|
+
- Set them in `defaultConfig` of `app/build.gradle(.kts)`, or from CI (`-PversionCode=...` read in the build script). Flutter and Expo generate them from their own manifests; see their references.
|
|
9
|
+
- When several APKs or bundles share a version (Wear OS, TV, different ABIs), each needs a distinct `versionCode`.
|
|
10
|
+
|
|
11
|
+
## Build and signing
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
./gradlew bundleRelease # app/build/outputs/bundle/release/app-release.aab
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- Google Play requires an Android App Bundle (`.aab`) for new apps. Use APKs only for other stores or direct distribution.
|
|
18
|
+
- With **Play App Signing**, Google holds the app signing key and you sign uploads with the **upload key**. A lost upload key can be reset through the Play Console; a lost app signing key outside Play App Signing cannot. Check in *Test and release → App integrity* which setup the app uses.
|
|
19
|
+
- Keystore passwords and the keystore itself never live in the repository. Read them from environment variables or an untracked `keystore.properties`, and check `.gitignore`.
|
|
20
|
+
- Fingerprints for App Links (`assetlinks.json`), Google Sign-In and Firebase come from the **app signing key** shown in the Play Console for Play builds, not from the upload or debug key.
|
|
21
|
+
|
|
22
|
+
## Symbols
|
|
23
|
+
|
|
24
|
+
- **R8 mapping** (`app/build/outputs/mapping/release/mapping.txt`) deobfuscates stack traces. Check in the Play Console's App bundle explorer that the deobfuscation file is attached to the version, and upload it when it is not. Crashlytics uploads it through its Gradle plugin. Archive it with the build either way.
|
|
25
|
+
- **Native symbols** for apps with C/C++ code: `android { buildTypes { release { ndk { debugSymbolLevel = "FULL" } } } }` (or `"SYMBOL_TABLE"`) adds them to the bundle.
|
|
26
|
+
|
|
27
|
+
## Before uploading
|
|
28
|
+
|
|
29
|
+
- [ ] `targetSdk` meets Google Play's current target API level requirement (it moves every year; confirm in the Play Console or the Play policy guide).
|
|
30
|
+
- [ ] `isMinifyEnabled` and `isShrinkResources` enabled for release, and the release build tested with them.
|
|
31
|
+
- [ ] `android:debuggable` not set, no `usesCleartextTraffic="true"` without a scoped network security config, `android:allowBackup` decided on purpose.
|
|
32
|
+
- [ ] New permissions justified; sensitive ones (location in background, SMS, call log, all files access, exact alarms) have the Play declaration they need.
|
|
33
|
+
- [ ] Data safety form updated for new SDKs or data collection.
|
|
34
|
+
- [ ] Release notes (500 characters per language) ready — see [store-copy.md](store-copy.md).
|
|
35
|
+
|
|
36
|
+
## Tracks and rollout
|
|
37
|
+
|
|
38
|
+
- **Internal testing** for fast team builds, **closed testing** for a group of testers, **open testing** for a public beta, **production**.
|
|
39
|
+
- New personal developer accounts must run a closed test with a minimum number of testers for a minimum period before applying for production access; check the current numbers in the Play Console.
|
|
40
|
+
- Production updates go out as a **staged rollout** (a percentage of users). Increase it while the Android vitals (user-perceived crash rate and ANR rate) stay at the baseline of the previous version; **halt** the rollout when they regress. Halting stops new installs but does not revert users who already updated: the fix is a new build with a higher `versionCode`.
|
|
41
|
+
- **Managed publishing** holds approved changes until you publish them, useful to coordinate with a backend release.
|
|
42
|
+
|
|
43
|
+
## Automation
|
|
44
|
+
|
|
45
|
+
- `fastlane supply` (`upload_to_play_store`) uploads bundles, notes and listing text with a service account JSON key.
|
|
46
|
+
- The Gradle Play Publisher plugin (`com.github.triplet.play`) does the same from Gradle.
|
|
47
|
+
- The service account needs access granted in the Play Console (*Users and permissions*), limited to the app and the tracks it publishes to.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# CI/CD for mobile apps
|
|
2
|
+
|
|
3
|
+
## Pipeline shape
|
|
4
|
+
|
|
5
|
+
| Trigger | Jobs |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Pull request | Static analysis or lint, unit tests, a debug or release build per platform that compiles (no signing, no upload) |
|
|
8
|
+
| Merge to the main branch | The above, plus a signed build uploaded to internal testing (Play internal track, TestFlight internal) |
|
|
9
|
+
| Release tag (`v1.4.0`) | Signed release builds, symbol upload, upload to the release track, artifacts attached to the release |
|
|
10
|
+
|
|
11
|
+
Keep Android and iOS in separate jobs: Android runs on Linux runners, iOS needs macOS runners, which cost more and are slower — do not run iOS builds on every push if the team does not need them.
|
|
12
|
+
|
|
13
|
+
## Principles
|
|
14
|
+
|
|
15
|
+
- **Reproducible toolchain.** Pin the Flutter version, Node version, JDK, Xcode version (`sudo xcode-select -s /Applications/Xcode_<version>.app` or the runner image's documented selector) and Ruby/fastlane through `Gemfile.lock`. CI that builds with a different toolchain than developers is how "works on my machine" reaches the store.
|
|
16
|
+
- **Caching.** Gradle (`~/.gradle/caches`, or `gradle/actions/setup-gradle`), pub cache, `node_modules` keyed by the lock file, CocoaPods `Pods/` keyed by `Podfile.lock`. Never cache build outputs across different commits for release builds.
|
|
17
|
+
- **Secrets.** Keystore as a base64 CI secret decoded to a temporary file; keystore passwords, the Play service account JSON, the App Store Connect API key (`.p8`, key ID, issuer ID) and `match` passwords as secrets. Nothing written to logs; files deleted at the end of the job. Release jobs run only on protected branches or tags, so a pull request from a fork cannot read them.
|
|
18
|
+
- **Build numbers from CI.** Derive the build number from the run number or the store's latest build (fastlane `latest_testflight_build_number`, `google_play_track_version_codes`) instead of committing a bump on every build.
|
|
19
|
+
- **Artifacts.** Keep the `.aab`/`.ipa`, the R8 mapping, dSYMs, Dart symbols and source maps for every release build.
|
|
20
|
+
- **One way to ship.** Once CI publishes, nobody uploads from a laptop: a manual upload skips the checks and breaks the build number sequence.
|
|
21
|
+
|
|
22
|
+
## GitHub Actions sketch (Flutter, Android)
|
|
23
|
+
|
|
24
|
+
Adapt names and versions to the project; check each action's current major version in its repository.
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
name: android-release
|
|
28
|
+
on:
|
|
29
|
+
push:
|
|
30
|
+
tags: ['v*']
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
android:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: actions/setup-java@v4
|
|
38
|
+
with:
|
|
39
|
+
distribution: temurin
|
|
40
|
+
java-version: '17'
|
|
41
|
+
- uses: subosito/flutter-action@v2
|
|
42
|
+
with:
|
|
43
|
+
flutter-version-file: pubspec.yaml # or a pinned flutter-version
|
|
44
|
+
cache: true
|
|
45
|
+
- run: flutter pub get
|
|
46
|
+
- run: flutter analyze
|
|
47
|
+
- run: flutter test
|
|
48
|
+
- name: Decode keystore
|
|
49
|
+
run: |
|
|
50
|
+
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload.jks
|
|
51
|
+
cat > android/key.properties <<EOF
|
|
52
|
+
storeFile=upload.jks
|
|
53
|
+
storePassword=$ANDROID_KEYSTORE_PASSWORD
|
|
54
|
+
keyAlias=$ANDROID_KEY_ALIAS
|
|
55
|
+
keyPassword=$ANDROID_KEY_PASSWORD
|
|
56
|
+
EOF
|
|
57
|
+
env:
|
|
58
|
+
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
59
|
+
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
60
|
+
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
61
|
+
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
62
|
+
- run: flutter build appbundle --release --build-number=${{ github.run_number }} --obfuscate --split-debug-info=build/symbols
|
|
63
|
+
- uses: actions/upload-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: android-release
|
|
66
|
+
path: |
|
|
67
|
+
build/app/outputs/bundle/release/app-release.aab
|
|
68
|
+
build/app/outputs/mapping/release/mapping.txt
|
|
69
|
+
build/symbols
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`flutter-version-file` reads the version from the `environment.flutter` constraint only when it is an exact version; otherwise pin `flutter-version`. Upload to Play in a following step with fastlane `supply` or an upload action, using the service account secret.
|
|
73
|
+
|
|
74
|
+
## fastlane
|
|
75
|
+
|
|
76
|
+
- `Fastfile` lanes per platform: `beta` (build, upload to internal testing or TestFlight) and `release` (upload to production or submit for review).
|
|
77
|
+
- iOS signing: `match` (certificates and profiles in an encrypted store) with `readonly: true` in CI; `app_store_connect_api_key` for authentication instead of an Apple ID with two-factor authentication.
|
|
78
|
+
- Android upload: `upload_to_play_store(track: 'internal', aab: ...)` with `json_key_data` from a secret.
|
|
79
|
+
- Run with `bundle exec fastlane <lane>` so CI uses the fastlane version in `Gemfile.lock`.
|
|
80
|
+
|
|
81
|
+
## Other services
|
|
82
|
+
|
|
83
|
+
- **EAS (Expo):** `eas build` and `eas submit` run on Expo's builders; trigger them from CI with `EXPO_TOKEN`, or use EAS Workflows. `eas update` in CI publishes over-the-air updates — gate it like a release.
|
|
84
|
+
- **Codemagic, Bitrise, Xcode Cloud, Azure DevOps, GitLab CI:** the same pipeline shape applies; prefer the service's managed code signing when the team already uses it, and keep secrets in the service's encrypted variables.
|
|
85
|
+
|
|
86
|
+
## Reviewing an existing pipeline
|
|
87
|
+
|
|
88
|
+
- [ ] Toolchain versions pinned and matching local development.
|
|
89
|
+
- [ ] Secrets never printed (`set -x`, `cat` of decoded files, verbose tool flags) and not available to untrusted pull requests.
|
|
90
|
+
- [ ] Release jobs restricted to tags or protected branches.
|
|
91
|
+
- [ ] Tests run before the signed build, and a failure stops the upload.
|
|
92
|
+
- [ ] Symbols and mapping files uploaded or archived for every release build.
|
|
93
|
+
- [ ] Build numbers cannot collide between parallel runs or re-runs.
|
|
94
|
+
- [ ] Third-party actions pinned to a version (or a commit SHA for sensitive jobs).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Releasing a Flutter app
|
|
2
|
+
|
|
3
|
+
A Flutter release is an Android release and an iOS release produced by the Flutter tool. Store details, signing and rollout follow [android.md](android.md) and [ios.md](ios.md); this guide covers what Flutter changes.
|
|
4
|
+
|
|
5
|
+
## Version
|
|
6
|
+
|
|
7
|
+
- `pubspec.yaml` holds both numbers: `version: 1.4.0+42` → version name `1.4.0`, build number `42`.
|
|
8
|
+
- On Android it becomes `versionName`/`versionCode` through `flutter.versionName` and `flutter.versionCode` in `android/app/build.gradle(.kts)`. On iOS it becomes `FLUTTER_BUILD_NAME`/`FLUTTER_BUILD_NUMBER`, which `Info.plist` must reference (`$(FLUTTER_BUILD_NAME)`, `$(FLUTTER_BUILD_NUMBER)`). Check both files: a hard-coded value there silently ignores `pubspec.yaml`.
|
|
9
|
+
- CI can override without editing the file: `--build-name=1.4.0 --build-number=$GITHUB_RUN_NUMBER`.
|
|
10
|
+
|
|
11
|
+
## Build
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
flutter build appbundle --release --obfuscate --split-debug-info=build/symbols
|
|
15
|
+
flutter build ipa --release --obfuscate --split-debug-info=build/symbols \
|
|
16
|
+
--export-options-plist=ios/ExportOptions.plist
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `--obfuscate` requires `--split-debug-info`. The symbol files in that folder are per build and per architecture: archive them with the build or stack traces from that release cannot be read.
|
|
20
|
+
- Symbolize a stack trace with `flutter symbolize -i trace.txt -d build/symbols/app.android-arm64.symbols`. Crashlytics needs the symbols uploaded (`firebase crashlytics:symbols:upload --app=<firebase-app-id> build/symbols`).
|
|
21
|
+
- Obfuscation renames Dart types: code that relies on `runtimeType.toString()` or type names in logs and analytics changes behavior in release.
|
|
22
|
+
- Pass environment configuration with `--dart-define` or `--dart-define-from-file`, and check which file the release command uses. Anything passed this way is in the binary: no secrets.
|
|
23
|
+
- Android signing: `android/key.properties` (untracked) read by the `signingConfigs.release` block. A release build that falls back to `signingConfig = signingConfigs.debug` — the default in new projects — is rejected by Play and must be caught before upload.
|
|
24
|
+
|
|
25
|
+
## Before uploading
|
|
26
|
+
|
|
27
|
+
- [ ] `flutter analyze` and `flutter test` pass on the release commit.
|
|
28
|
+
- [ ] The app runs in `--release` (or `--profile`) on a real device: assertions, `kDebugMode` branches and the debug banner behave differently in debug.
|
|
29
|
+
- [ ] `debugPrint` and logging do not print sensitive data in release.
|
|
30
|
+
- [ ] Plugins that need platform setup for release are configured: Android permissions and R8 keep rules some plugins document, iOS usage descriptions, `Podfile` platform version.
|
|
31
|
+
- [ ] `flutter build ipa` warnings about the app icon, launch image or bundle identifier are resolved.
|
|
32
|
+
- [ ] App size checked (`--analyze-size`) when the release adds assets or packages.
|
|
33
|
+
|
|
34
|
+
## Automation
|
|
35
|
+
|
|
36
|
+
- fastlane lanes in `android/` and `ios/` run after `flutter build`, or call it themselves.
|
|
37
|
+
- Codemagic and GitHub Actions with `subosito/flutter-action` are common CI setups; pin the Flutter version to the one the team uses locally (`.fvmrc` or a documented version) so CI builds what was tested. See [ci-cd.md](ci-cd.md).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Releasing on iOS
|
|
2
|
+
|
|
3
|
+
App Store Connect, TestFlight and signing basics also appear in `mobile-ios` → `references/performance-release.md`; this guide focuses on getting a version out safely.
|
|
4
|
+
|
|
5
|
+
## Version
|
|
6
|
+
|
|
7
|
+
- **Version** (`CFBundleShortVersionString`, build setting `MARKETING_VERSION`) is what users see.
|
|
8
|
+
- **Build** (`CFBundleVersion`, build setting `CURRENT_PROJECT_VERSION`) identifies the upload. App Store Connect rejects a build number already used for the same version; incrementing it for every upload avoids the problem entirely.
|
|
9
|
+
- Keep both in build settings, not hard-coded in `Info.plist`, so every target (app, extensions, widgets) shares them. Extensions whose version differs from the app produce an upload warning or error.
|
|
10
|
+
- `agvtool new-version -all <n>` and `agvtool new-marketing-version <x.y.z>` update them when the project uses Apple Generic versioning; fastlane's `increment_build_number` wraps it.
|
|
11
|
+
|
|
12
|
+
## Archive, export and upload
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
xcodebuild -workspace App.xcworkspace -scheme App -configuration Release \
|
|
16
|
+
-archivePath build/App.xcarchive archive
|
|
17
|
+
xcodebuild -exportArchive -archivePath build/App.xcarchive \
|
|
18
|
+
-exportOptionsPlist ExportOptions.plist -exportPath build/export
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Use `-project App.xcodeproj` when there is no workspace (no CocoaPods).
|
|
22
|
+
- `ExportOptions.plist` sets the method (App Store Connect distribution), team and signing style.
|
|
23
|
+
- Upload with Xcode's Organizer, the Transporter app, or fastlane (`upload_to_testflight`, `upload_to_app_store`) using an App Store Connect API key.
|
|
24
|
+
- Automatic signing in CI needs the API key or an account with access; manual signing needs the distribution certificate and provisioning profile installed. fastlane `match` keeps them in an encrypted repository or bucket.
|
|
25
|
+
|
|
26
|
+
## Symbols
|
|
27
|
+
|
|
28
|
+
- Release builds need **Debug Information Format = DWARF with dSYM File**.
|
|
29
|
+
- The dSYMs are inside the archive (`App.xcarchive/dSYMs`). Upload them to the crash reporter (Crashlytics `upload-symbols`, Sentry, others) and keep the archive: Xcode Organizer symbolicates its own crash reports only when it has the matching dSYM.
|
|
30
|
+
|
|
31
|
+
## Before uploading
|
|
32
|
+
|
|
33
|
+
- [ ] The build uses the current minimum Xcode and iOS SDK Apple requires for uploads (confirm on Apple's upcoming requirements page).
|
|
34
|
+
- [ ] Every permission the app requests has its `NS...UsageDescription` string, written for the user.
|
|
35
|
+
- [ ] `PrivacyInfo.xcprivacy` declares required-reason APIs and collected data, and third-party SDKs ship their own manifests and signatures.
|
|
36
|
+
- [ ] `ITSAppUsesNonExemptEncryption` set in `Info.plist` when the app uses only exempt encryption (HTTPS), so each build does not ask for export compliance.
|
|
37
|
+
- [ ] Capabilities and entitlements (push, associated domains, App Groups, Sign in with Apple) enabled for the App ID and present in the distribution profile. Push needs `aps-environment` = `production` in distribution builds, which the profile sets.
|
|
38
|
+
- [ ] No `NSAllowsArbitraryLoads` without a justification Apple will accept.
|
|
39
|
+
- [ ] App Privacy details in App Store Connect match the data the app and its SDKs collect.
|
|
40
|
+
- [ ] Review notes and a demo account when features need login; release notes ready — see [store-copy.md](store-copy.md).
|
|
41
|
+
|
|
42
|
+
## TestFlight and rollout
|
|
43
|
+
|
|
44
|
+
- **Internal testers** (members of the App Store Connect team) get builds after processing, without review. **External testers** need the first build of a version to pass Beta App Review.
|
|
45
|
+
- TestFlight builds expire after 90 days.
|
|
46
|
+
- For updates, **phased release** distributes the version over seven days to users with automatic updates on; it can be paused (for a limited total time) or released to everyone. Anyone can still update manually from the App Store during the phase.
|
|
47
|
+
- Choose manual or scheduled release after approval when the launch must match a backend change or an announcement.
|
|
48
|
+
- A bad version cannot be withdrawn to the previous binary: remove it from sale only in extreme cases, and ship a fix with a new build (expedited review can be requested for critical bugs).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Releasing a React Native app
|
|
2
|
+
|
|
3
|
+
A React Native release is an Android and an iOS release plus a JavaScript bundle. Store details, signing and rollout follow [android.md](android.md) and [ios.md](ios.md); EAS Build and Submit commands are also in `mobile-rn` → `references/performance-release.md`.
|
|
4
|
+
|
|
5
|
+
## Version
|
|
6
|
+
|
|
7
|
+
- **Bare React Native:** `versionCode`/`versionName` in `android/app/build.gradle`, and `MARKETING_VERSION`/`CURRENT_PROJECT_VERSION` in the Xcode project. `package.json`'s `version` is not used by either store unless a script copies it.
|
|
8
|
+
- **Expo:** `expo.version` in `app.json`/`app.config.(js|ts)` is the version name; `android.versionCode` and `ios.buildNumber` are the build numbers. With EAS, `"cli": { "appVersionSource": "remote" }` in `eas.json` lets EAS keep the build numbers, and `"autoIncrement": true` on the production profile increments them on each build.
|
|
9
|
+
|
|
10
|
+
## Build
|
|
11
|
+
|
|
12
|
+
- **Expo / EAS:** `eas build --platform all --profile production`, then `eas submit`. Credentials (keystore, certificates, provisioning) are managed by EAS unless the project configured local credentials.
|
|
13
|
+
- **Bare:** `./gradlew bundleRelease` in `android/`, and an Xcode archive for iOS.
|
|
14
|
+
- Environment: values in `EXPO_PUBLIC_*`, `react-native-config` and similar end up in the JavaScript bundle. Confirm the release build points at production and holds no secrets.
|
|
15
|
+
|
|
16
|
+
## Symbols and source maps
|
|
17
|
+
|
|
18
|
+
- JavaScript stack traces from release builds need the **source map** of that exact bundle. With Hermes, the map must be the composed one (Metro map combined with the Hermes bytecode map); crash reporters' React Native integrations (Sentry, Bugsnag, Crashlytics tooling) document how to generate and upload it.
|
|
19
|
+
- The native side still needs the R8 mapping and dSYMs, as in the Android and iOS guides.
|
|
20
|
+
|
|
21
|
+
## Over-the-air updates
|
|
22
|
+
|
|
23
|
+
- EAS Update (or another OTA service) changes JavaScript and assets without a store review. It cannot change native code, permissions or native dependencies: those need a store build.
|
|
24
|
+
- The **runtime version** decides which binaries can receive an update. A policy such as `fingerprint` or `appVersion` prevents shipping JavaScript that calls native code the installed binary does not have; a manual runtime version that nobody bumps is the classic cause of crashes after an update.
|
|
25
|
+
- Publish to a **channel** mapped to the production build (`eas update --channel production`), test it first on a preview channel with a build from the same commit, and know how to roll back (republish a previous update or roll back to the embedded bundle — check `eas update --help` for the commands in the installed CLI version).
|
|
26
|
+
- Store rules still apply: an OTA update must not change the app's purpose or add features that would need review.
|
|
27
|
+
|
|
28
|
+
## Before uploading
|
|
29
|
+
|
|
30
|
+
- [ ] `tsc --noEmit`, lint and tests pass on the release commit.
|
|
31
|
+
- [ ] Release build tested on real devices: Hermes, minification and the absence of the dev menu change behavior.
|
|
32
|
+
- [ ] `console.log` removed from release bundles (a Babel plugin such as `transform-remove-console`) when logs can contain user data.
|
|
33
|
+
- [ ] Config plugins and `expo prebuild` output match the committed native folders, when the project commits them.
|
|
34
|
+
- [ ] `expo-doctor` (Expo) or the React Native upgrade helper's checklist shows no version mismatches for native dependencies.
|
|
35
|
+
|
|
36
|
+
See [ci-cd.md](ci-cd.md) for pipelines.
|