reis-mobile 0.7.2 → 0.8.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 +23 -0
- package/README.md +54 -27
- package/agents/android-architect.md +3 -3
- package/agents/android-performance-engineer.md +1 -1
- package/agents/flutter-architect.md +3 -3
- package/agents/flutter-performance-engineer.md +1 -1
- package/agents/ios-architect.md +3 -3
- package/agents/ios-performance-engineer.md +1 -1
- package/agents/kmp-architect.md +3 -3
- package/agents/kmp-performance-engineer.md +1 -1
- package/agents/lead-mobile.md +3 -0
- package/agents/mobile-release-engineer.md +4 -4
- package/agents/mobile-staff-engineer.md +1 -1
- package/agents/rn-architect.md +3 -3
- package/agents/rn-performance-engineer.md +1 -1
- package/bin/reis-mobile.mjs +24 -1
- package/commands/reis-mobile.md +7 -2
- package/commands/release.md +46 -0
- package/core/release/context.mjs +11 -0
- package/core/router/intents.mjs +15 -0
- package/package.json +1 -1
- package/skills/mobile-accessibility/SKILL.md +3 -0
- package/skills/mobile-accessibility/references/localization.md +77 -0
- package/skills/mobile-observability/SKILL.md +137 -0
- package/skills/mobile-observability/references/android.md +105 -0
- package/skills/mobile-observability/references/flutter.md +86 -0
- package/skills/mobile-observability/references/ios.md +88 -0
- package/skills/mobile-observability/references/kotlin-multiplatform.md +76 -0
- package/skills/mobile-observability/references/react-native.md +62 -0
- package/skills/mobile-offline-sync/SKILL.md +132 -0
- package/skills/mobile-offline-sync/references/android.md +123 -0
- package/skills/mobile-offline-sync/references/flutter.md +103 -0
- package/skills/mobile-offline-sync/references/ios.md +103 -0
- package/skills/mobile-offline-sync/references/kotlin-multiplatform.md +114 -0
- package/skills/mobile-offline-sync/references/react-native.md +108 -0
- package/skills/mobile-performance/SKILL.md +88 -0
- package/skills/mobile-performance/references/android.md +96 -0
- package/skills/mobile-performance/references/background.md +87 -0
- package/skills/mobile-performance/references/flutter.md +101 -0
- package/skills/mobile-performance/references/ios.md +82 -0
- package/skills/mobile-performance/references/kotlin-multiplatform.md +62 -0
- package/skills/mobile-performance/references/react-native.md +85 -0
- package/skills/mobile-release/SKILL.md +11 -22
- package/skills/mobile-release/references/billing.md +92 -0
- package/skills/mobile-release/references/readiness.md +88 -0
- package/skills/mobile-release/scripts/verdict.mjs +59 -0
- package/skills/mobile-security/SKILL.md +3 -0
- package/skills/mobile-security/references/auth.md +92 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Performance on native iOS
|
|
2
|
+
|
|
3
|
+
iOS performance work is mostly main-thread work. The main thread renders, handles touch and runs SwiftUI/UIKit updates; anything that blocks it becomes a hitch, a hang or a watchdog termination. Instruments explains a single run, MetricKit and Xcode Organizer explain the installed base.
|
|
4
|
+
|
|
5
|
+
Related material already in this plugin: [`mobile-ios/references/performance-release.md`](../../mobile-ios/references/performance-release.md).
|
|
6
|
+
|
|
7
|
+
## Measure
|
|
8
|
+
|
|
9
|
+
**Release configuration, physical device.** The Simulator has a different GPU path and no thermal or memory pressure; debug builds skip optimization and enable extra checks.
|
|
10
|
+
|
|
11
|
+
| Question | Tool |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Where CPU time goes | Instruments → Time Profiler, with "Record Waiting Threads" to catch blocking |
|
|
14
|
+
| Why a scroll drops frames | Instruments → Animation Hitches (hitch time per frame, scroll and commit phases) |
|
|
15
|
+
| What SwiftUI is doing and why a view updated | Instruments → SwiftUI template; `Self._printChanges()` inside `body` in debug |
|
|
16
|
+
| Main thread blocked | Instruments → Hangs; the Thread Performance Checker in a debug run |
|
|
17
|
+
| Allocations, growth and leaks | Instruments → Allocations (mark generations around the journey) and Leaks; the Memory Graph Debugger for retain cycles |
|
|
18
|
+
| Launch cost | Instruments → App Launch; `DYLD_PRINT_STATISTICS=1` for pre-main dynamic linking time |
|
|
19
|
+
| Field data from real users | MetricKit (`MXMetricManager`), and Xcode Organizer → Launch Time, Hangs, Memory, Disk Writes, Battery |
|
|
20
|
+
| Download size | App Store Connect → App Store → the build's app size report, per device variant |
|
|
21
|
+
|
|
22
|
+
Apple treats the main thread being blocked for more than about 250 ms as a hang; Organizer reports it as a hang rate per hour of use. A scroll hitch is measured as *hitch time ratio* — milliseconds of hitch per second of scrolling.
|
|
23
|
+
|
|
24
|
+
## Launch
|
|
25
|
+
|
|
26
|
+
Launch splits into pre-main (dynamic linker, runtime setup) and post-main (your `AppDelegate`/`App` initialization up to the first usable frame). Apple's guidance is to finish launch in roughly 400 ms, before the system's launch animation ends.
|
|
27
|
+
|
|
28
|
+
- **Pre-main:** the number of dynamically linked frameworks dominates. Merge small dynamic frameworks into static libraries or use mergeable libraries; remove `+load` methods and heavy static initializers (`__attribute__((constructor))`, non-lazy Swift globals with side effects).
|
|
29
|
+
- **Post-main:** every SDK initialized synchronously in `application(_:didFinishLaunchingWithOptions:)` is launch cost. Defer analytics, remote config, crash-symbol upload and network prefetch to after the first frame. Read persisted state asynchronously; `UserDefaults` is cheap but a Core Data or SwiftData stack opened on the main queue is not.
|
|
30
|
+
- **Measure the right end point.** `os_signpost` a span from `didFinishLaunching` to the moment content is on screen, and query it in a UI test with `XCTOSSignpostMetric`.
|
|
31
|
+
|
|
32
|
+
```swift
|
|
33
|
+
import XCTest
|
|
34
|
+
|
|
35
|
+
final class LaunchTests: XCTestCase {
|
|
36
|
+
func testLaunchPerformance() {
|
|
37
|
+
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
|
38
|
+
XCUIApplication().launch()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`measure(metrics:)` records a baseline in the test bundle and fails the test when a later run regresses past the configured deviation — this is the cheapest regression gate on iOS.
|
|
45
|
+
|
|
46
|
+
## SwiftUI updates
|
|
47
|
+
|
|
48
|
+
- **Identity drives everything.** A view whose identity changes is torn down and rebuilt; a `ForEach` over indices instead of stable `id`s rebuilds every row on any change. Use `Identifiable` with an id that survives a refresh.
|
|
49
|
+
- **Narrow what `body` observes.** With `@Observable`, SwiftUI tracks the exact properties read inside `body`; reading a whole model object where one property is needed widens the dependency. With `ObservableObject`, every `@Published` change invalidates every observer — split the object instead.
|
|
50
|
+
- `Self._printChanges()` at the top of `body` prints which dependency caused the update. It is the fastest way to answer "why did this redraw".
|
|
51
|
+
- **Lazy containers for long content:** `LazyVStack`/`LazyHStack` inside a `ScrollView`, or `List`. A plain `VStack` in a `ScrollView` builds every row.
|
|
52
|
+
- **`AnyView` and heavy type erasure** defeat structural identity and the diffing fast path; prefer `@ViewBuilder` and generics.
|
|
53
|
+
- **Expensive work in `body`.** `body` may run many times per frame. Format dates, sort and filter outside it, in the model.
|
|
54
|
+
|
|
55
|
+
For UIKit: cell reuse (`dequeueReusableCell`), self-sizing cells with cached heights, `UICollectionViewDiffableDataSource` over `reloadData`, image decoding off the main thread (`UIImage.prepareForDisplay`/`preparingForDisplay`), and avoiding `layoutIfNeeded` inside `layoutSubviews`.
|
|
56
|
+
|
|
57
|
+
## Memory
|
|
58
|
+
|
|
59
|
+
- Retain cycles are the dominant leak: a closure capturing `self` strongly in a long-lived object, a delegate declared `strong`, a `Timer` or `NotificationCenter` observer never invalidated, a `Task` that outlives its owner. `[weak self]` in escaping closures and the Memory Graph Debugger's cycle detection cover most of it.
|
|
60
|
+
- Images: decode at display size (`UIGraphicsImageRenderer`, `prepareThumbnail(of:)`, or `ImageIO` with `kCGImageSourceThumbnailMaxPixelSize`). Full-resolution decoding into a small view is the standard memory spike.
|
|
61
|
+
- `MXAppExitMetric` from MetricKit tells you how often users' apps were terminated for memory pressure in the field — the only reliable signal that the footprint is too large on the devices you actually ship to.
|
|
62
|
+
|
|
63
|
+
## Concurrency and the main thread
|
|
64
|
+
|
|
65
|
+
- `@MainActor` isolates UI state; work that is not UI should not be on it. An `actor` or a detached `Task` with an explicit priority keeps parsing and I/O off the main thread.
|
|
66
|
+
- A synchronous call into a `DispatchQueue.sync`, a `NSLock`, or an `await` on an actor already busy on the main thread all produce a hang. The Hangs instrument names the blocking frame.
|
|
67
|
+
- Background `URLSession` configurations continue transfers after suspension; ordinary sessions do not. See [background.md](background.md).
|
|
68
|
+
|
|
69
|
+
## Regression detection in CI
|
|
70
|
+
|
|
71
|
+
- `XCTMetric` based tests (`XCTApplicationLaunchMetric`, `XCTClockMetric`, `XCTMemoryMetric`, `XCTOSSignpostMetric`) with committed baselines fail the build on regression. Run them on a pinned simulator or device model; baselines are per-device.
|
|
72
|
+
- Track binary size per build from the archive and compare against the previous baseline.
|
|
73
|
+
- Feed MetricKit payloads into your backend so launch, hang and memory metrics have a per-release series. See [mobile-observability](../../mobile-observability/SKILL.md).
|
|
74
|
+
|
|
75
|
+
## Official documentation
|
|
76
|
+
|
|
77
|
+
- Improving your app's performance: https://developer.apple.com/documentation/xcode/improving-your-app-s-performance
|
|
78
|
+
- Reducing your app's launch time: https://developer.apple.com/documentation/xcode/reducing-your-app-s-launch-time
|
|
79
|
+
- Understanding hangs in your app: https://developer.apple.com/documentation/xcode/understanding-hangs-in-your-app
|
|
80
|
+
- Analyzing responsiveness issues: https://developer.apple.com/documentation/xcode/analyzing-responsiveness-issues-in-your-shipping-app
|
|
81
|
+
- MetricKit: https://developer.apple.com/documentation/metrickit
|
|
82
|
+
- Performance tests with XCTest: https://developer.apple.com/documentation/xctest/performance-tests
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Performance in Kotlin Multiplatform
|
|
2
|
+
|
|
3
|
+
A KMP performance problem is either **shared Kotlin cost**, which shows up on both targets, or a **target-specific consequence** — the Kotlin/Native binary on iOS, the interop boundary, or the host UI. Deciding which comes first is the whole job: optimizing shared code that is not the bottleneck is wasted work, and so is profiling the Android host for a cost that lives in `commonMain`.
|
|
4
|
+
|
|
5
|
+
Related material already in this plugin: [`mobile-kmp/references/build-performance.md`](../../mobile-kmp/references/build-performance.md) and [`source-sets.md`](../../mobile-kmp/references/source-sets.md).
|
|
6
|
+
|
|
7
|
+
## 1. Split shared cost from host cost
|
|
8
|
+
|
|
9
|
+
Run the same journey on both hosts and compare:
|
|
10
|
+
|
|
11
|
+
| Observation | Where the cost lives |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Slow on Android and iOS, similar magnitude | Shared Kotlin: algorithm, serialization, database access in `commonMain` |
|
|
14
|
+
| Slow only on iOS | Kotlin/Native: memory management, the Swift/Kotlin bridge, a suspend function crossing into Swift, framework initialization |
|
|
15
|
+
| Slow only on Android | The Android host: Compose recomposition, main-thread work in the ViewModel, `ContentProvider` initializers |
|
|
16
|
+
| Both slow, different symptoms | Two problems. Fix them separately |
|
|
17
|
+
|
|
18
|
+
Then profile with the host's own tools — there is no KMP profiler. Android Studio Profiler and Macrobenchmark for the Android host ([android.md](android.md)); Instruments for the iOS host ([ios.md](ios.md)), where Kotlin frames appear once the framework's dSYM is available to the trace.
|
|
19
|
+
|
|
20
|
+
Benchmark the shared code directly where you can: `kotlinx-benchmark` runs the same benchmark on JVM and Native targets, which is the only way to compare the two compilers' output for your algorithm rather than guessing.
|
|
21
|
+
|
|
22
|
+
## 2. The interop boundary
|
|
23
|
+
|
|
24
|
+
Every call from Swift into the generated framework crosses a bridge. One call per frame is free; one call per list row per frame is not.
|
|
25
|
+
|
|
26
|
+
- **Batch the crossing.** Return a whole page of prepared view state in one call instead of exposing a getter per field. The shared layer should expose data already shaped for the screen.
|
|
27
|
+
- **Suspend functions and `Flow` become callbacks in Swift.** Each emission crosses the boundary and hops a dispatcher. A `Flow` that emits per keystroke from shared code into SwiftUI is a per-keystroke bridge crossing plus a SwiftUI update — debounce or conflate in shared code, not in Swift.
|
|
28
|
+
- **Collections cross by conversion.** A Kotlin `List` handed to Swift is bridged; a large list converted on every access is a hidden O(n) per call. Hold the reference, do not re-fetch.
|
|
29
|
+
- **Objective-C interop erases generics and sealed hierarchies**, which pushes Swift callers into exhaustive `switch`es over erased types and sometimes into defensive copies. Design a flat, explicitly-typed API surface at the boundary.
|
|
30
|
+
|
|
31
|
+
## 3. Kotlin/Native specifics
|
|
32
|
+
|
|
33
|
+
- The current Kotlin/Native memory manager removes the old freezing rules, so shared mutable state across threads is allowed — but objects still cross into Objective-C autorelease pools, and retain cycles between a Kotlin object and a Swift closure leak exactly like they do in pure Swift. Break them with `[weak self]` on the Swift side and explicit cancellation on the Kotlin side.
|
|
34
|
+
- **Framework initialization is launch cost on iOS.** Anything in a Kotlin top-level property with an initializer, or in a Koin/DI module built at framework load, runs before your first screen. Keep it lazy.
|
|
35
|
+
- **Framework size** counts against the app's download size. Check `isStatic`, dead-code elimination and whether you export more modules than the host actually calls (`export(...)` in the framework block pulls whole modules into the public API and prevents their elimination).
|
|
36
|
+
- Release builds only. Kotlin/Native debug binaries are dramatically slower than release ones, and a "KMP is slow" report measured on a debug framework is a measurement bug.
|
|
37
|
+
|
|
38
|
+
## 4. Shared code that is usually the bottleneck
|
|
39
|
+
|
|
40
|
+
- **Serialization.** `kotlinx.serialization` over a large payload on the main dispatcher blocks both hosts. Parse on `Dispatchers.Default`, and parse once — not per screen.
|
|
41
|
+
- **Database access.** SQLDelight queries on the calling thread; map rows lazily and do not materialize a whole table to display twenty rows.
|
|
42
|
+
- **Flow operators.** An unconfined chain of `map`/`combine` re-running on every upstream emission multiplies work across the boundary. `distinctUntilChanged`, `conflate` and `stateIn` with a sharing policy bound to the host lifecycle keep it bounded.
|
|
43
|
+
- **`expect`/`actual` asymmetry.** A cheap JVM implementation and an expensive Native one for the same declaration produces the "slow only on iOS" case above. Measure both actuals.
|
|
44
|
+
|
|
45
|
+
## 5. Compose Multiplatform UI
|
|
46
|
+
|
|
47
|
+
When the UI is shared, the Compose performance rules in [android.md](android.md) apply unchanged — stability of parameters, deferred state reads, lazy layouts, `derivedStateOf`. What changes is measurement: the Android host gives you Layout Inspector recomposition counts and composition tracing; on iOS you have Instruments' time profile with Kotlin frames and no recomposition counter, so validate recomposition behaviour on Android and confirm the frame result on iOS.
|
|
48
|
+
|
|
49
|
+
## 6. Regression detection in CI
|
|
50
|
+
|
|
51
|
+
- Macrobenchmark on the Android host guards startup and frame time for the shared stack ([android.md](android.md)).
|
|
52
|
+
- `XCTApplicationLaunchMetric` on the iOS host guards framework initialization cost ([ios.md](ios.md)).
|
|
53
|
+
- `kotlinx-benchmark` on JVM and Native guards the shared algorithms themselves, and is the only gate that fails for a shared-code regression before either host notices.
|
|
54
|
+
- Track the produced framework size and the Android AAB size per build; an `export(...)` added to the framework block can move both without any UI change.
|
|
55
|
+
|
|
56
|
+
## Official documentation
|
|
57
|
+
|
|
58
|
+
- Kotlin Multiplatform documentation: https://kotlinlang.org/docs/multiplatform.html
|
|
59
|
+
- Kotlin/Native memory management: https://kotlinlang.org/docs/native-memory-manager.html
|
|
60
|
+
- Swift/Objective-C interop: https://kotlinlang.org/docs/native-objc-interop.html
|
|
61
|
+
- kotlinx-benchmark: https://github.com/Kotlin/kotlinx-benchmark
|
|
62
|
+
- Compose Multiplatform: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-and-jetpack-compose.html
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Performance in React Native and Expo
|
|
2
|
+
|
|
3
|
+
A React Native frame crosses three execution contexts: the **JavaScript thread** (your React code), the **native UI thread**, and — with the New Architecture — the shadow tree and Fabric's rendering pipeline. A slow app is slow in one of them, and the fix differs. Identify the thread first.
|
|
4
|
+
|
|
5
|
+
Related material already in this plugin: [`mobile-rn/references/performance-release.md`](../../mobile-rn/references/performance-release.md).
|
|
6
|
+
|
|
7
|
+
## Measure
|
|
8
|
+
|
|
9
|
+
**Release build, physical device, Hermes enabled.** A Metro dev build carries the dev bundle, React strict-mode double renders, and the bridge dev listeners; its numbers are not the shipped app's.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx react-native run-android --mode=release
|
|
13
|
+
npx expo run:ios --configuration Release
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Question | Tool |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Which React components render, and why | React DevTools Profiler (React Native DevTools), "Highlight updates" |
|
|
19
|
+
| JavaScript CPU time | Hermes sampling profiler via React Native DevTools → Performance, or `.cpuprofile` capture |
|
|
20
|
+
| Frame drops and native thread cost | The platform tools: Perfetto/system trace on Android, Instruments on iOS (see [android.md](android.md) and [ios.md](ios.md)) |
|
|
21
|
+
| Startup cost breakdown | `performance.now()` marks around module require and first render; on Android, `adb shell am start -W` for the whole process |
|
|
22
|
+
| Bundle size and what is in it | `npx react-native bundle --platform android --dev false --sourcemap-output bundle.map` then a source-map explorer; `npx expo export` for Expo |
|
|
23
|
+
|
|
24
|
+
Instrument your own spans, and keep them in release behind a flag, so the same marks exist in the field:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const start = performance.now();
|
|
28
|
+
const parsed = parseOrders(payload);
|
|
29
|
+
track('parse_orders_ms', performance.now() - start);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The JavaScript thread
|
|
33
|
+
|
|
34
|
+
- **Re-renders are the default problem.** A context value recreated each render, an inline object/array/function prop, or a parent that re-renders on every keystroke will cascade. `React.memo` on the child plus `useCallback`/`useMemo` on the props, or moving the state down so the parent is not involved at all.
|
|
35
|
+
- **Selectors, not whole stores.** Subscribing to an entire Zustand/Redux store re-renders on every change; subscribe to the slice. With React Query/TanStack Query, `select` narrows what a component observes.
|
|
36
|
+
- **Never block JS.** JSON parsing of a large payload, sorting thousands of rows, crypto, image manipulation — these freeze every touch handler. Move them to the native side, a worklet, or chunk them across frames with `InteractionManager.runAfterInteractions` and batched state updates.
|
|
37
|
+
- **Text input and animation state** should not live in React state when they change per frame. Use `Animated` with `useNativeDriver: true`, or Reanimated shared values, so the animation runs on the UI thread and never touches JS.
|
|
38
|
+
|
|
39
|
+
## Lists
|
|
40
|
+
|
|
41
|
+
`FlatList`/`SectionList` are virtualized but still render through React. The props that matter:
|
|
42
|
+
|
|
43
|
+
| Prop | Why |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `keyExtractor` | Stable keys; without them React remounts rows on any change |
|
|
46
|
+
| `getItemLayout` | Skips measurement, enables instant `scrollToIndex` — only valid for fixed-height rows |
|
|
47
|
+
| `initialNumToRender` | Keep it to what a screen actually shows; the default renders more |
|
|
48
|
+
| `maxToRenderPerBatch` / `updateCellsBatchingPeriod` | Trade blank space during fast scrolls against JS work per batch |
|
|
49
|
+
| `windowSize` | How many screens are kept mounted around the viewport |
|
|
50
|
+
| `removeClippedSubviews` | Detaches offscreen native views; helps on Android, test it — it has edge cases with absolute positioning |
|
|
51
|
+
|
|
52
|
+
Memoize the row component (`React.memo`) and make `renderItem` stable. If the list is long and rows are heavy, `@shopify/flash-list` recycles native views instead of mounting new ones and is usually a larger win than tuning the props above.
|
|
53
|
+
|
|
54
|
+
## Images
|
|
55
|
+
|
|
56
|
+
- Size the remote image on the server or CDN; downloading a 3000 px JPEG for a 120 px avatar costs bandwidth, decode time and memory on every row.
|
|
57
|
+
- `expo-image` (or `react-native-fast-image` on bare projects) adds disk/memory caching and progressive loading that the core `Image` does not.
|
|
58
|
+
- Set explicit `width`/`height` so layout does not reflow when the image resolves.
|
|
59
|
+
|
|
60
|
+
## Startup
|
|
61
|
+
|
|
62
|
+
- **Enable Hermes** (the default in current React Native). Hermes precompiles to bytecode, which removes JS parse time at launch.
|
|
63
|
+
- **Enable the New Architecture** where the project's dependencies support it: Fabric and TurboModules remove the asynchronous bridge serialization, and TurboModules load lazily instead of all at startup.
|
|
64
|
+
- **Audit top-level requires.** Every module imported at the entry point is evaluated at launch. Lazy-import screens through the navigator's lazy options, and defer SDK initialization until after the first screen.
|
|
65
|
+
- **Native side still counts.** Time to first JS frame includes the Android `Application.onCreate` and the iOS `didFinishLaunching` path; see the native references.
|
|
66
|
+
|
|
67
|
+
## Bundle and app size
|
|
68
|
+
|
|
69
|
+
- Inspect the release source map with a source-map explorer to find the heavy dependency. Moment/lodash-style full imports, duplicated polyfills and an icon font shipped whole are the usual entries.
|
|
70
|
+
- On Android, enable Proguard/R8 for the release variant and ship an App Bundle. On iOS, check the App Store Connect size report per device variant.
|
|
71
|
+
- Expo: `expo-updates` OTA payloads are downloaded by every user — size matters there too, and an update that grows the bundle grows every launch that has to apply it.
|
|
72
|
+
|
|
73
|
+
## Regression detection in CI
|
|
74
|
+
|
|
75
|
+
- Wrap a scripted journey in Detox or Maestro on a pinned device/emulator, capture the platform's frame data (see [android.md](android.md) and [ios.md](ios.md)), and compare percentiles against a baseline.
|
|
76
|
+
- Assert bundle size in CI: build the release bundle, read its byte size, fail past a percentage delta.
|
|
77
|
+
- `react-native-performance` exposes native startup marks (`nativeLaunchStart`, `runJsBundleStart/End`) as `PerformanceEntry`s, which gives a startup number you can assert on and also report from production.
|
|
78
|
+
|
|
79
|
+
## Official documentation
|
|
80
|
+
|
|
81
|
+
- React Native performance overview: https://reactnative.dev/docs/performance
|
|
82
|
+
- Optimizing FlatList configuration: https://reactnative.dev/docs/optimizing-flatlist-configuration
|
|
83
|
+
- Profiling with Hermes and React Native DevTools: https://reactnative.dev/docs/react-native-devtools
|
|
84
|
+
- The New Architecture: https://reactnative.dev/architecture/landing-page
|
|
85
|
+
- Expo performance guidance: https://docs.expo.dev/guides/analyzing-bundles/
|
|
@@ -13,23 +13,27 @@ One release process for every mobile stack. This file holds the process and what
|
|
|
13
13
|
|
|
14
14
|
| Need | Read |
|
|
15
15
|
|---|---|
|
|
16
|
+
| Evidence-backed readiness audit, verdict and report | [references/readiness.md](references/readiness.md) |
|
|
16
17
|
| Flutter app | [references/flutter.md](references/flutter.md) |
|
|
17
18
|
| Native Android, or the Android build of any app | [references/android.md](references/android.md) |
|
|
18
19
|
| Native iOS, or the iOS build of any app | [references/ios.md](references/ios.md) |
|
|
19
20
|
| React Native or Expo app | [references/react-native.md](references/react-native.md) |
|
|
20
21
|
| Kotlin Multiplatform shared module and both host apps | [references/kotlin-multiplatform.md](references/kotlin-multiplatform.md) |
|
|
22
|
+
| In-app purchases and subscription lifecycle | [references/billing.md](references/billing.md) |
|
|
21
23
|
| Release notes and store listing text | [references/store-copy.md](references/store-copy.md) |
|
|
22
24
|
| A CI/CD pipeline that builds, signs and uploads | [references/ci-cd.md](references/ci-cd.md) |
|
|
23
25
|
|
|
24
|
-
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
|
|
26
|
+
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`, and production crash, error and release-health signals are in [mobile-observability](../mobile-observability/SKILL.md).
|
|
25
27
|
|
|
26
28
|
## 2. Readiness checklist
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
For readiness audits, first follow [the readiness contract](references/readiness.md): resolve each artifact identity, collect per-target evidence and apply its required gates and deterministic verdict helper.
|
|
31
|
+
|
|
32
|
+
Check each item against the project, not against memory. For the readiness verdict, a missing artifact or unavailable check is **unknown**; use **fail** only for an observed failure with evidence. Record a source reference and next action for every gate.
|
|
29
33
|
|
|
30
34
|
**Version**
|
|
31
|
-
- [ ] The version name follows the
|
|
32
|
-
- [ ] The build number
|
|
35
|
+
- [ ] The version name follows the app's actual versioning scheme and is verified in the artifact and applicable CI overrides.
|
|
36
|
+
- [ ] The build number satisfies the destination store’s numbering scope, verified against upload history: Android versionCode increases across tracks; iOS build numbers must be unused within the applicable version train. See the native references.
|
|
33
37
|
- [ ] Android, iOS and any cross-platform manifest (`pubspec.yaml`, `app.json`) agree on the version.
|
|
34
38
|
|
|
35
39
|
**Build quality**
|
|
@@ -47,25 +51,10 @@ Check each item against the project, not against memory. Mark each one **pass**,
|
|
|
47
51
|
**Rollout**
|
|
48
52
|
- [ ] 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.
|
|
49
53
|
- [ ] 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.
|
|
50
|
-
- [ ] The release commit is tagged and the tag matches the version.
|
|
54
|
+
- [ ] The release commit is tagged and the tag matches the version, when this is the project's release convention.
|
|
51
55
|
|
|
52
56
|
## 3. Report
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
## Release readiness — <app> <version> (<build>)
|
|
56
|
-
Verdict: GO | GO WITH RISKS | NO-GO
|
|
57
|
-
|
|
58
|
-
### Blockers
|
|
59
|
-
- item — evidence (file:line or missing artifact) — fix
|
|
60
|
-
|
|
61
|
-
### Risks
|
|
62
|
-
- item — why it matters — mitigation
|
|
63
|
-
|
|
64
|
-
### Unknown
|
|
65
|
-
- item — who or what can confirm it
|
|
66
|
-
|
|
67
|
-
### Rollout plan
|
|
68
|
-
Stages, metrics to watch, halt threshold, rollback path.
|
|
69
|
-
```
|
|
58
|
+
Use the [readiness report and decision rules](references/readiness.md#report). Any failed or unknown required gate means **NO-GO**. **GO WITH RISKS** requires all gates to pass with explicitly documented non-blocking risks; **GO** requires no outstanding risks.
|
|
70
59
|
|
|
71
|
-
|
|
60
|
+
Readiness audits inspect existing evidence without running builds/tests or modifying the app. Version bumps, tags, uploads and store changes follow the user's explicit execution scope and existing authorization.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# In-app purchases and subscription lifecycle
|
|
2
|
+
|
|
3
|
+
Applies to every stack. Billing bugs are expensive in a way other mobile bugs are not: a user who paid and did not receive the entitlement files a store complaint, a chargeback, and a one-star review, and the store holds *you* responsible for the reconciliation. The design that avoids this is the same on both platforms.
|
|
4
|
+
|
|
5
|
+
## The rule everything follows from
|
|
6
|
+
|
|
7
|
+
**The client never decides entitlement.** The app's job is to start a purchase and to hand the resulting receipt/token to your backend. The backend verifies it with the store's server, records the entitlement, and answers "what does this user own?". Every screen reads that answer.
|
|
8
|
+
|
|
9
|
+
An app that grants a feature because a local purchase callback returned success can be defeated with a patched binary, and — more commonly — simply gets it wrong: the callback is missed because the app was killed mid-purchase, or the subscription was cancelled from the store's settings and the app never hears about it.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
app ──buy──▶ store ──token/receipt──▶ app ──▶ your backend ──verify──▶ store server
|
|
13
|
+
│
|
|
14
|
+
app ◀────── entitlement state ─────────────────────┘
|
|
15
|
+
▲
|
|
16
|
+
store server ──server notifications──┘ (renewals, cancellations, refunds, billing retries)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Server notifications are not optional
|
|
20
|
+
|
|
21
|
+
The purchase callback covers the moment of purchase. Everything afterwards — a renewal at 3am, a cancellation, a refund, a failed payment entering a retry window, an upgrade, a family-sharing change — happens with the app closed and reaches you only through the store's server-to-server notifications:
|
|
22
|
+
|
|
23
|
+
- **Google Play:** Real-time developer notifications (Pub/Sub), plus the Google Play Developer API for the authoritative subscription state.
|
|
24
|
+
- **Apple:** App Store Server Notifications V2, plus the App Store Server API and signed transactions.
|
|
25
|
+
|
|
26
|
+
Without them, a cancelled subscription keeps working until the app next happens to check, and a refunded purchase is never revoked. "We poll on app launch" is not equivalent: the user who cancelled may not open the app for weeks, and a refunded purchase must be revoked immediately.
|
|
27
|
+
|
|
28
|
+
## The states to model
|
|
29
|
+
|
|
30
|
+
A subscription is not a boolean. At minimum, entitlement must distinguish:
|
|
31
|
+
|
|
32
|
+
| State | Behaviour |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Active | Full access |
|
|
35
|
+
| Active, cancelled (will not renew) | Full access until the period ends; a good place to ask why |
|
|
36
|
+
| In a billing retry / grace period | **Keep access** and prompt the user to fix payment — revoking here loses users who would have paid |
|
|
37
|
+
| On hold / suspended | Access revoked, recoverable if payment succeeds |
|
|
38
|
+
| Paused (Android) | Access revoked, scheduled to resume |
|
|
39
|
+
| Expired | No access |
|
|
40
|
+
| Refunded / revoked | Access removed immediately, on the notification |
|
|
41
|
+
| Trial / introductory offer | Active, but the eligibility rules differ and are per-account, not per-device |
|
|
42
|
+
|
|
43
|
+
Treating anything other than "active" as "no access" is the standard way to break grace periods and lose revenue.
|
|
44
|
+
|
|
45
|
+
## Purchase acknowledgement
|
|
46
|
+
|
|
47
|
+
Both stores require the purchase to be consumed or acknowledged, and both **refund automatically** if you do not.
|
|
48
|
+
|
|
49
|
+
- **Google Play:** a purchase not acknowledged (or consumed, for consumables) within three days is automatically refunded and revoked. Acknowledge only **after** your backend has recorded the entitlement, so a failure to record cannot produce a user who paid and owns nothing.
|
|
50
|
+
- **Apple:** a transaction that is not finished is re-delivered on every launch. Finish it only after the entitlement is recorded — the re-delivery is the recovery mechanism, and finishing early discards it.
|
|
51
|
+
|
|
52
|
+
The crash-in-the-middle case is the one to test: purchase succeeds, the app is killed before the backend call. On the next launch, the pending purchase must still be visible, sent, recorded and only then acknowledged.
|
|
53
|
+
|
|
54
|
+
## Restore and multi-device
|
|
55
|
+
|
|
56
|
+
- A **restore** path is mandatory on iOS (App Review rejects apps without one for non-consumables and subscriptions) and expected everywhere. It must be reachable without being logged in to *your* account if the purchase is tied to the store account.
|
|
57
|
+
- Entitlement follows the **store account**, but your product follows **your** user account. Decide explicitly what happens when they disagree: one store account buying for two of your users, or one of your users signing in on a device with a different store account. This is where fraud and support tickets concentrate; the backend must own the mapping, and it must be enforced there.
|
|
58
|
+
- Prices, currencies and availability come from the store per region. Never hard-code a price string; read it from the product query and display the localized formatted price the store returns.
|
|
59
|
+
|
|
60
|
+
## Testing
|
|
61
|
+
|
|
62
|
+
- **Google Play:** licence testers with test payment methods, internal testing track, and shortened subscription periods for renewal testing. Purchases must come from a build uploaded to a track with the same package name and signing key.
|
|
63
|
+
- **Apple:** StoreKit Testing in Xcode (a local `.storekit` configuration file, with time acceleration and error injection — the only practical way to test renewals, billing retry and refunds) and Sandbox accounts for the end-to-end path with real server notifications.
|
|
64
|
+
- Test on the backend, not only in the app: replay a server notification payload and assert the entitlement change. That is the code that actually decides access.
|
|
65
|
+
- Cases worth a test each: purchase interrupted by process death; a refund notification; a cancellation followed by a resubscribe; a user who already owns the product buying again; an expired subscription that renews after a grace period.
|
|
66
|
+
|
|
67
|
+
## Per-stack notes
|
|
68
|
+
|
|
69
|
+
| Stack | Client library |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Native Android | Google Play Billing Library — the deep migration and version material is in [`mobile-android/references/play.md`](../../mobile-android/references/play.md) |
|
|
72
|
+
| Native iOS | StoreKit 2 (`Product`, `Transaction`, `Transaction.updates`, `Transaction.currentEntitlements`) — the `Transaction.updates` listener must be started at launch, or transactions completed outside the app are missed |
|
|
73
|
+
| Flutter | `in_app_purchase` (federated, both stores) or a vendor SDK; the purchase stream must be subscribed before the first purchase and on every launch |
|
|
74
|
+
| React Native / Expo | `react-native-iap`, `expo-in-app-purchases` where available, or a vendor SDK; Expo managed projects need a development build — in-app purchases do not work in Expo Go |
|
|
75
|
+
| Kotlin Multiplatform | Billing is host-specific. Keep the entitlement *model* and the backend calls in `commonMain`, and the store client in each host |
|
|
76
|
+
|
|
77
|
+
A subscription infrastructure vendor (RevenueCat, Adapty and similar) exists precisely because the server-side half above is substantial. Using one is a legitimate decision; it does not remove the need to decide entitlement on a server rather than in the app — it moves that server.
|
|
78
|
+
|
|
79
|
+
## Store policy
|
|
80
|
+
|
|
81
|
+
Both stores require that digital content consumed in the app is sold through their billing system, with a narrow and changing set of exceptions that vary by region and by court ruling. Alternative payment links have specific, enforced requirements. Before designing anything that bypasses store billing, read the current policy for the target markets — this is the most volatile area of store policy and the most expensive one to get wrong, since the penalty is removal.
|
|
82
|
+
|
|
83
|
+
Release-time checks for the store submission itself are in [readiness.md](readiness.md).
|
|
84
|
+
|
|
85
|
+
## Official documentation
|
|
86
|
+
|
|
87
|
+
- Google Play Billing: https://developer.android.com/google/play/billing
|
|
88
|
+
- Real-time developer notifications: https://developer.android.com/google/play/billing/getting-ready#configure-rtdn
|
|
89
|
+
- Google Play Developer API, subscriptions: https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2
|
|
90
|
+
- StoreKit: https://developer.apple.com/documentation/storekit
|
|
91
|
+
- App Store Server Notifications V2: https://developer.apple.com/documentation/appstoreservernotifications
|
|
92
|
+
- Testing in-app purchases with StoreKit Test: https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Evidence-backed readiness audit
|
|
2
|
+
|
|
3
|
+
Use this contract for `/reis-mobile:release` and any readiness request. Other release tasks (store copy, pipeline implementation, deployment) retain their requested scope.
|
|
4
|
+
|
|
5
|
+
## Release identity and target coverage
|
|
6
|
+
|
|
7
|
+
For every intended artifact, record **app/package or bundle ID → flavor/scheme/environment → version/build → source revision → store/track → built artifact** with evidence. Inspect build overrides, CI parameters and artifact metadata; distinguish configured values from values verified in the binary. A directory or configured KMP target does not prove a shippable host exists. A shared framework is not an iOS app archive.
|
|
8
|
+
|
|
9
|
+
Use detection as an inventory, then confirm actual Android/iOS hosts and requested destinations. Flutter, React Native/Expo and KMP shipping to both stores need separate gate rows for both artifacts. A prompt focusing on Android does not silently approve the iOS artifact. If the user explicitly limits the audit to one store, state the exclusion and do not issue a combined verdict. Expo can generate native folders: inspect app configuration and EAS profiles without running prebuild. Unknown targets or missing binaries stay unknown.
|
|
10
|
+
|
|
11
|
+
Read the stack reference plus [android.md](android.md) and/or [ios.md](ios.md) for the actual destinations. KMP also needs [kotlin-multiplatform.md](kotlin-multiplatform.md). Do not substitute a shared-code test for a native host test.
|
|
12
|
+
|
|
13
|
+
## Required gates
|
|
14
|
+
|
|
15
|
+
Each target has these gate IDs. Every gate is **pass**, **fail**, or **unknown**, with a source reference and an exact next action/owner when unresolved. A failed check needs observed failure evidence; unavailable evidence is unknown. A local config file proves configuration only, not an executed check, successful upload, current console state or store approval.
|
|
16
|
+
|
|
17
|
+
| ID | Evidence required |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `identity` | All identity fields above, including artifact provenance and agreement with the intended source revision. Record dirty/untracked source and unknown CI overrides. |
|
|
20
|
+
| `build` | Successful release build log/CI job for this revision and artifact, with the actual variant and production environment. A workflow YAML or debug build is insufficient. |
|
|
21
|
+
| `tests` | Actual results for applicable unit/integration/device checks on this revision, with counts and skipped/failed checks. Resolve required suites from project policy and the changed behavior. |
|
|
22
|
+
| `analysis` | Actual lint/static-analysis results for the release revision and applicable targets. Missing tools or logs are unknown, not pass. |
|
|
23
|
+
| `version` | Artifact version/build matched to source/CI inputs and store upload history, applying the platform's numbering scope. Verify the previous release using the project's convention, not merely the closest tag. See [Android versioning](https://developer.android.com/studio/publish/versioning) and [Apple's build identification](https://developer.apple.com/help/app-store-connect/reference/app-information/app-information). |
|
|
24
|
+
| `signing` | Signed artifact verification, intended distribution identity, and applicable entitlements/profile or upload-key setup. Never read or print private keys, passwords or tokens. |
|
|
25
|
+
| `symbols` | Matching R8/native symbols, dSYMs, Dart debug info and/or JS source maps, with archive/upload evidence as required by the app. Verify build IDs/UUIDs or provenance. |
|
|
26
|
+
| `store` | Current official requirement URL and access date, target SDK/toolchain evidence, and any account/track eligibility or review state needed for the intended action. Unavailable portal confirmation stays unknown. |
|
|
27
|
+
| `privacy` | Permissions/data collection and third-party SDK inventory reconciled with manifests, usage descriptions, privacy policy and store disclosures. Local declarations alone do not prove console forms agree. |
|
|
28
|
+
| `listing` | Required locales, release notes/screenshots/metadata, and reviewer access where applicable. Account secrets never enter the report. |
|
|
29
|
+
| `rollout` | Owner, stages, monitoring signals and halt thresholds, plus a verified halt/kill-switch/higher-build hotfix path. Mark proposed thresholds as proposals until accepted. A halt does not revert installed binaries. See [Play staged rollouts](https://support.google.com/googleplay/android-developer/answer/6346149) and [Apple phased releases](https://developer.apple.com/help/app-store-connect/update-your-app/release-a-version-update-in-phases); confirm their current behavior before planning recovery. |
|
|
30
|
+
|
|
31
|
+
If a requirement within a gate does not apply (for example no R8 mapping when R8 is disabled), cite evidence and the reason; do not omit the gate. A gate can pass with a justified non-applicability statement only after verifying all of its applicable requirements. No gate is waived simply to obtain GO.
|
|
32
|
+
|
|
33
|
+
Current store rules must be checked against official sources, such as [Google Play target API requirements](https://support.google.com/googleplay/android-developer/answer/11926878) and [Apple's upcoming requirements](https://developer.apple.com/news/upcoming-requirements/). State the exact unresolved confirmation if browsing or store access is unavailable. Do not reuse a date or minimum SDK from memory.
|
|
34
|
+
|
|
35
|
+
## Decision rule
|
|
36
|
+
|
|
37
|
+
- **NO-GO:** any required gate fails, is unknown, is missing, or has no supporting evidence. Missing targets also block. Distinguish observed failures from missing verification.
|
|
38
|
+
- **GO WITH RISKS:** every required gate passes and explicitly documented non-blocking risks remain. A risk cannot waive an unresolved required gate.
|
|
39
|
+
- **GO:** every required gate passes and there are no outstanding risks.
|
|
40
|
+
|
|
41
|
+
The combined verdict covers every included target; one platform passing cannot approve another.
|
|
42
|
+
|
|
43
|
+
Use [the verdict helper](../scripts/verdict.mjs) to check the report's decision. It validates completeness and consistency, **not the truth of the evidence or whether a risk is truly non-blocking**. Supply JSON on stdin using a quoted heredoc delimiter (no shell expansion):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/mobile-release/scripts/verdict.mjs" <<'REIS_RELEASE_EVIDENCE'
|
|
47
|
+
{"targets":[{"target":"android:production","gates":[{"id":"build","status":"unknown","evidence":[],"nextAction":"Release owner: provide the release CI job and artifact for the requested revision."}]}],"risks":[]}
|
|
48
|
+
REIS_RELEASE_EVIDENCE
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This intentionally incomplete example returns NO-GO. Replace it with the actual targets and all 11 gates; use one target name per artifact/store/track combination. `evidence` is an array of non-empty references (`file:line`, command plus result/log, artifact metadata, CI URL or dated portal confirmation). `nextAction` explains what resolves each fail/unknown. `risks` is an optional array of objects with `id`, `description`, evidence references, `mitigation` and `owner`; include only evidenced issues whose impact truly does not block the release. The auditor is responsible for that classification. Do not place secrets in JSON. Choose a heredoc delimiter absent from the payload. The helper writes only stdout; exit 0 means evaluation succeeded, **not** GO. Malformed input exits 1 and must be corrected before reporting a verdict. Preserve returned unknowns and blockers; do not manually upgrade the result.
|
|
52
|
+
|
|
53
|
+
## Report
|
|
54
|
+
|
|
55
|
+
Write in the requested language; keep gate IDs and verdict identifiers stable.
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
## Release readiness — <app> <version/build>
|
|
59
|
+
Verdict: GO | GO WITH RISKS | NO-GO
|
|
60
|
+
Scope: <included artifacts/stores/tracks and explicit exclusions>
|
|
61
|
+
|
|
62
|
+
### Release identity
|
|
63
|
+
| Target | App ID | Variant/environment | Version/build | Revision | Store/track | Artifact | Evidence |
|
|
64
|
+
|---|---|---|---|---|---|---|---|
|
|
65
|
+
|
|
66
|
+
### Gates
|
|
67
|
+
| Target | Gate | Status | Evidence / missing confirmation | Next action / owner |
|
|
68
|
+
|---|---|---|---|---|
|
|
69
|
+
|
|
70
|
+
### Blockers
|
|
71
|
+
Observed failures and unverified required gates, separately identified.
|
|
72
|
+
|
|
73
|
+
### Risks
|
|
74
|
+
Non-blocking risks, mitigation and owner.
|
|
75
|
+
|
|
76
|
+
### Unknowns
|
|
77
|
+
Missing evidence and the exact external confirmation needed.
|
|
78
|
+
|
|
79
|
+
### Next actions
|
|
80
|
+
Ordered resolution steps; commands prepared versus commands actually executed.
|
|
81
|
+
|
|
82
|
+
### Rollout and rollback
|
|
83
|
+
Stages, signals, thresholds, owner and recovery for users already updated.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Read-only boundary
|
|
87
|
+
|
|
88
|
+
A readiness audit inspects files, existing artifacts/logs and accessible CI/store records. Do not build, run tests, install dependencies, execute Fastlane lanes, prebuild, bump versions, tag, upload, change signing or store settings by default. Inspect scripts before considering execution. Prepare exact commands for missing checks and label them **not executed**. An explicit request to execute a release authorizes only that requested scope; reuse existing authorization, and complete reviewable preparation before any additional approval that is actually needed. Never change an app merely to make the audit green.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_GATES = ['identity', 'build', 'tests', 'analysis', 'version', 'signing', 'symbols', 'store', 'privacy', 'listing', 'rollout'];
|
|
5
|
+
const text = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
6
|
+
|
|
7
|
+
/** Validate decision consistency. Evidence truth and applicability remain the auditor's job. */
|
|
8
|
+
export function evaluateReadiness(report) {
|
|
9
|
+
if (!report || !Array.isArray(report.targets)) throw new Error('targets must be an array');
|
|
10
|
+
const risks = report.risks ?? [];
|
|
11
|
+
if (!Array.isArray(risks) || !risks.every((risk) => risk && text(risk.id) && text(risk.description)
|
|
12
|
+
&& Array.isArray(risk.evidence) && risk.evidence.length > 0 && risk.evidence.every(text)
|
|
13
|
+
&& text(risk.mitigation) && text(risk.owner))) {
|
|
14
|
+
throw new Error('risks must include id, description, evidence, mitigation and owner');
|
|
15
|
+
}
|
|
16
|
+
const targets = new Set();
|
|
17
|
+
const gates = [];
|
|
18
|
+
for (const item of report.targets) {
|
|
19
|
+
if (!item || !text(item.target) || !Array.isArray(item.gates)) throw new Error('each target needs a name and gates array');
|
|
20
|
+
if (targets.has(item.target)) throw new Error(`duplicate target: ${item.target}`);
|
|
21
|
+
targets.add(item.target);
|
|
22
|
+
const byId = new Map();
|
|
23
|
+
for (const gate of item.gates) {
|
|
24
|
+
if (!gate || !REQUIRED_GATES.includes(gate.id)) throw new Error('unknown gate id');
|
|
25
|
+
if (byId.has(gate.id)) throw new Error(`duplicate gate: ${gate.id}`);
|
|
26
|
+
if (!['pass', 'fail', 'unknown'].includes(gate.status)) throw new Error(`invalid status for ${gate.id}`);
|
|
27
|
+
if (gate.evidence !== undefined && (!Array.isArray(gate.evidence) || !gate.evidence.every((value) => typeof value === 'string'))) {
|
|
28
|
+
throw new Error(`evidence for ${gate.id} must be a string array`);
|
|
29
|
+
}
|
|
30
|
+
byId.set(gate.id, gate);
|
|
31
|
+
}
|
|
32
|
+
for (const id of REQUIRED_GATES) {
|
|
33
|
+
const supplied = byId.get(id);
|
|
34
|
+
const evidence = (supplied?.evidence ?? []).filter(text);
|
|
35
|
+
const status = !supplied || (supplied.status !== 'unknown' && !evidence.length) ? 'unknown' : supplied.status;
|
|
36
|
+
gates.push({
|
|
37
|
+
target: item.target, id, status, evidence,
|
|
38
|
+
nextAction: status === 'pass' ? 'None.' : (text(supplied?.nextAction) && supplied.nextAction !== 'None.'
|
|
39
|
+
? supplied.nextAction : `Provide and verify ${id} evidence for ${item.target}.`),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!targets.size) gates.push({ target: 'unresolved', id: 'targets', status: 'unknown', evidence: [], nextAction: 'Identify the app artifacts and intended store targets.' });
|
|
44
|
+
const blockers = gates.filter((gate) => gate.status !== 'pass');
|
|
45
|
+
const unknowns = gates.filter((gate) => gate.status === 'unknown');
|
|
46
|
+
return { verdict: blockers.length ? 'NO-GO' : risks.length ? 'GO WITH RISKS' : 'GO', gates, blockers, risks, unknowns };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// stdin avoids creating an audit file in the inspected project. Exit zero means evaluated, not GO.
|
|
50
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
51
|
+
try {
|
|
52
|
+
let input = '';
|
|
53
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
54
|
+
console.log(JSON.stringify(evaluateReadiness(JSON.parse(input)), null, 2));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(`readiness: ${error.message}`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -18,6 +18,7 @@ Organized by the OWASP MASVS categories. Apply the general checks below, then th
|
|
|
18
18
|
| Native iOS (Swift/Objective-C), or `ios/` in any app | [references/ios.md](references/ios.md) |
|
|
19
19
|
| React Native or Expo | [references/react-native.md](references/react-native.md) |
|
|
20
20
|
| Kotlin Multiplatform shared code and platform implementations | [references/kotlin-multiplatform.md](references/kotlin-multiplatform.md) |
|
|
21
|
+
| OAuth 2.0 / OIDC login, PKCE, tokens and biometric gating, on any stack | [references/auth.md](references/auth.md) |
|
|
21
22
|
|
|
22
23
|
Firebase-specific protection (security rules, App Check) is covered by `mobile-firebase`.
|
|
23
24
|
|
|
@@ -44,6 +45,8 @@ Every finding needs evidence in the code or configuration at `file:line`. Severi
|
|
|
44
45
|
- [ ] Sessions without expiration, or refresh tokens never invalidated on logout.
|
|
45
46
|
- [ ] Biometrics used only as a local boolean, without unlocking a key in the Keychain or Keystore.
|
|
46
47
|
|
|
48
|
+
For anything involving a login flow — OAuth 2.0, OpenID Connect, PKCE, redirect URIs, token rotation or biometric-gated keys — read [references/auth.md](references/auth.md) before reviewing the stack reference.
|
|
49
|
+
|
|
47
50
|
**MASVS-NETWORK**
|
|
48
51
|
- [ ] `http://` URLs for APIs.
|
|
49
52
|
- [ ] Certificate validation disabled or overridden to accept everything.
|