@amplytools/react-native-amply-sdk 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +178 -0
  2. package/README.md +714 -0
  3. package/android/build.gradle +90 -0
  4. package/android/consumer-rules.pro +1 -0
  5. package/android/gradle.properties +3 -0
  6. package/android/settings.gradle +9 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +384 -0
  9. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyPackage.kt +39 -0
  10. package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +30 -0
  11. package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +296 -0
  12. package/android/src/main/java/tools/amply/sdk/reactnative/model/AmplyInitializationOptions.kt +10 -0
  13. package/android/src/main/java/tools/amply/sdk/reactnative/model/DataSetType.kt +42 -0
  14. package/android/src/main/java/tools/amply/sdk/reactnative/model/DataSetTypeMapper.kt +38 -0
  15. package/android/src/main/java/tools/amply/sdk/reactnative/model/DeepLinkPayload.kt +8 -0
  16. package/android/src/main/java/tools/amply/sdk/reactnative/model/EventEnvelope.kt +9 -0
  17. package/android/src/main/jni/AmplyTurboModule.cpp +29 -0
  18. package/android/src/main/jni/CMakeLists.txt +76 -0
  19. package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +75 -0
  20. package/android/src/newarch/jni/AmplyReactNative-generated.cpp +77 -0
  21. package/android/src/newarch/jni/AmplyReactNative.h +31 -0
  22. package/android/src/newarch/jni/CMakeLists.txt +40 -0
  23. package/app.plugin.js +1 -0
  24. package/dist/index.js +272 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/index.mjs +234 -0
  27. package/dist/index.mjs.map +1 -0
  28. package/dist/plugin/index.d.ts +6 -0
  29. package/dist/plugin/index.d.ts.map +1 -0
  30. package/dist/plugin/index.js +186 -0
  31. package/dist/plugin/index.js.map +1 -0
  32. package/dist/plugin/index.mjs +169 -0
  33. package/dist/plugin/index.mjs.map +1 -0
  34. package/dist/plugin/src/index.d.ts +6 -0
  35. package/dist/plugin/src/index.d.ts.map +1 -0
  36. package/dist/plugin/src/index.js +3 -0
  37. package/dist/plugin/src/withAmply.d.ts +30 -0
  38. package/dist/plugin/src/withAmply.d.ts.map +1 -0
  39. package/dist/plugin/src/withAmply.js +51 -0
  40. package/dist/plugin/withAmply.d.ts +12 -0
  41. package/dist/plugin/withAmply.d.ts.map +1 -0
  42. package/dist/src/__tests__/index.test.d.ts +2 -0
  43. package/dist/src/__tests__/index.test.d.ts.map +1 -0
  44. package/dist/src/__tests__/index.test.js +70 -0
  45. package/dist/src/hooks/useAmplySystemEvents.d.ts +12 -0
  46. package/dist/src/hooks/useAmplySystemEvents.d.ts.map +1 -0
  47. package/dist/src/hooks/useAmplySystemEvents.js +56 -0
  48. package/dist/src/index.d.ts +32 -0
  49. package/dist/src/index.d.ts.map +1 -0
  50. package/dist/src/index.js +80 -0
  51. package/dist/src/nativeModule.d.ts +5 -0
  52. package/dist/src/nativeModule.d.ts.map +1 -0
  53. package/dist/src/nativeModule.js +48 -0
  54. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +75 -0
  55. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -0
  56. package/dist/src/nativeSpecs/NativeAmplyModule.js +2 -0
  57. package/dist/src/systemEventUtils.d.ts +3 -0
  58. package/dist/src/systemEventUtils.d.ts.map +1 -0
  59. package/dist/src/systemEventUtils.js +30 -0
  60. package/dist/src/systemEvents.d.ts +6 -0
  61. package/dist/src/systemEvents.d.ts.map +1 -0
  62. package/dist/src/systemEvents.js +8 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -0
  64. package/docs/ARCHITECTURE.md +1115 -0
  65. package/expo-module.config.json +11 -0
  66. package/ios/AmplyReactNative.podspec +32 -0
  67. package/ios/README.md +11 -0
  68. package/ios/Sources/AmplyReactNative/AmplyModule.mm +332 -0
  69. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +111 -0
  70. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +152 -0
  71. package/package.json +71 -0
  72. package/plugin/build/index.d.ts +5 -0
  73. package/plugin/build/index.js +8 -0
  74. package/plugin/build/withAmply.d.ts +29 -0
  75. package/plugin/build/withAmply.js +53 -0
  76. package/plugin/src/index.ts +7 -0
  77. package/plugin/src/withAmply.ts +68 -0
  78. package/plugin/tsconfig.json +8 -0
  79. package/plugin/tsconfig.tsbuildinfo +1 -0
  80. package/react-native.config.js +34 -0
  81. package/scripts/codegen.js +212 -0
  82. package/src/__tests__/index.test.ts +92 -0
  83. package/src/hooks/useAmplySystemEvents.ts +75 -0
  84. package/src/index.ts +115 -0
  85. package/src/nativeModule.ts +65 -0
  86. package/src/nativeSpecs/NativeAmplyModule.ts +80 -0
  87. package/src/systemEventUtils.ts +35 -0
  88. package/src/systemEvents.ts +13 -0
@@ -0,0 +1,1115 @@
1
+ # Amply React Native SDK – Architecture Guide
2
+
3
+ This document provides a comprehensive overview of the Amply React Native SDK's architectural design, implementation approaches, integration patterns, system event lifecycle, and development workflow. It consolidates technical decisions and serves as the single source of truth for how the SDK is built and maintained.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [Architectural Pattern](#architectural-pattern)
11
+ 3. [Bare React Native vs Expo Implementation](#bare-react-native-vs-expo-implementation)
12
+ 4. [System Events Lifecycle](#system-events-lifecycle)
13
+ 5. [Module Loading Strategy](#module-loading-strategy)
14
+ 6. [Build & Development Workflow](#build--development-workflow)
15
+ 7. [Script Commands Reference](#script-commands-reference)
16
+ 8. [Future Roadmap](#future-roadmap)
17
+
18
+ ---
19
+
20
+ ## Overview
21
+
22
+ ### What is the Amply React Native SDK?
23
+
24
+ The Amply React Native SDK is a **TurboModule bridge** that connects React Native applications to the **Amply Kotlin Multiplatform (KMP) SDK**. It enables:
25
+
26
+ - **Event tracking** – Custom and system event collection
27
+ - **Deeplink campaign handling** – Parse and respond to deep link campaigns
28
+ - **Real-time data access** – Query device, user, and session datasets
29
+ - **Event inspection** – Retrieve recent tracked events for debugging
30
+
31
+ ### Architecture in One Diagram
32
+
33
+ ```
34
+ ┌─────────────────────────────────────────────────────────────┐
35
+ │ React Native Application (JS/TypeScript) │
36
+ │ - import { initialize, track, addSystemEventsListener } │
37
+ └──────────────────────┬──────────────────────────────────────┘
38
+
39
+ ┌──────────────┴──────────────────┐
40
+ │ TurboModule Bridge │
41
+ │ (New Architecture Only) │
42
+ │ - Codegen-generated spec │
43
+ │ - EventEmitter channels │
44
+ └──────────────┬──────────────────┘
45
+
46
+ ┌──────────────┴──────────────────┐
47
+ │ Android Native (Kotlin) │
48
+ │ - AmplyModule (TurboModule) │
49
+ │ - DefaultAmplyClient wrapper │
50
+ │ - Lifecycle management │
51
+ └──────────────┬──────────────────┘
52
+
53
+ ┌──────────────┴──────────────────┐
54
+ │ Amply KMP SDK │
55
+ │ (tools.amply.sdk.Amply) │
56
+ │ - Event tracking │
57
+ │ - Dataset management │
58
+ │ - Deeplink routing │
59
+ └─────────────────────────────────┘
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Architectural Pattern
65
+
66
+ ### 1. **Layered Architecture**
67
+
68
+ The SDK follows a clean, maintainable layered design:
69
+
70
+ | Layer | Location | Responsibility |
71
+ |-------|----------|---|
72
+ | **TypeScript API** | `src/` | Public API, type exports, event emitter helpers, React hooks |
73
+ | **TurboModule Spec** | `src/nativeSpecs/` | Authoritative contract for codegen (generates C++/Java/ObjC bindings) |
74
+ | **Android Native** | `android/src/main/` | Kotlin wrapper managing lifecycle and bridging to KMP SDK |
75
+ | **Expo Plugin** | `plugin/src/` | Configuration plugin for managed workflow (registers packages automatically) |
76
+
77
+ ### 2. **TurboModule Pattern (New Architecture Only)**
78
+
79
+ React Native's **New Architecture** is the only supported integration path. This simplifies maintenance and unlocks JSI-backed performance.
80
+
81
+ **Why New Architecture only?**
82
+ - Eliminates double maintenance burden of legacy bridge + New Architecture
83
+ - Enables type-safe, codegen-driven API contract
84
+ - Leverages `EventEmitter` for reliable event delivery
85
+ - Supports iOS when the Amply iOS SDK ships
86
+
87
+ **How it works:**
88
+
89
+ ```
90
+ 1. Developer writes TypeScript spec (NativeAmplyModule.ts)
91
+
92
+ 2. React Native Codegen reads spec → generates language bindings
93
+ - Java stubs (Android)
94
+ - C++ glue code (both platforms)
95
+ - ObjC++ headers (iOS)
96
+
97
+ 3. Native code extends generated stubs
98
+ - Implements actual functionality
99
+ - Calls parent `emit*()` methods to send events to JS
100
+
101
+ 4. JS calls NativeAmply methods via TurboModule registry
102
+ ```
103
+
104
+ ### 3. **Event Streaming Pattern**
105
+
106
+ Events flow through **TurboModule EventEmitters**, NOT DeviceEventEmitter:
107
+
108
+ ```
109
+ Amply KMP SDK
110
+ ├─ SharedFlow<SystemEvent> collector started in AmplyModule
111
+ ├─ Each event → AmplyModule.emitOnSystemEvent(payload)
112
+ └─ JS: NativeAmply.onSystemEvent(listener) receives updates
113
+
114
+ Amply KMP SDK
115
+ ├─ DeepLink listeners registered in DefaultAmplyClient
116
+ ├─ Each deeplink → AmplyModule.emitOnDeepLink(payload)
117
+ └─ JS: NativeAmply.onDeepLink(listener) receives updates
118
+ ```
119
+
120
+ **Why TurboModule EventEmitters?**
121
+ - Works identically in Legacy Bridge and New Architecture
122
+ - Type-safe – payloads defined in spec
123
+ - Single integration point – no DeviceEventEmitter fallback needed
124
+ - Events propagate deterministically
125
+
126
+ ### 4. **Client Wrapper Pattern**
127
+
128
+ The `DefaultAmplyClient` (Kotlin) isolates the KMP SDK and manages:
129
+ - Lifecycle (initialization, shutdown)
130
+ - Coroutine scopes
131
+ - `SharedFlow` for event streaming
132
+ - Activity lifecycle priming
133
+ - Thread safety
134
+
135
+ **Why wrap the KMP SDK?**
136
+ - Decouples the React Native layer from KMP implementation details
137
+ - Allows async-to-promise bridging
138
+ - Manages native resources cleanly
139
+ - Makes testing easier (mock `AmplyClient` interface)
140
+
141
+ ### 5. **Spec-Driven Codegen**
142
+
143
+ The **NativeAmplyModule.ts** file is the authoritative contract:
144
+
145
+ ```typescript
146
+ export interface Spec extends TurboModule {
147
+ initialize(config: AmplyInitializationConfig): Promise<void>;
148
+ isInitialized(): boolean;
149
+ track(payload: TrackEventPayload): Promise<void>;
150
+ getRecentEvents(limit: number): Promise<EventRecord[]>;
151
+ getDataSetSnapshot(type: DataSetType): Promise<DataSetSnapshot>;
152
+ readonly onSystemEvent: EventEmitter<SystemEventPayload>;
153
+ readonly onDeepLink: EventEmitter<DeepLinkEvent>;
154
+ }
155
+ ```
156
+
157
+ This spec:
158
+ - Defines every method, parameter, and return type
159
+ - Maps to Java, C++, and ObjC++ via codegen
160
+ - Serves as the foundation for future Contract Fabric automation (sync with KMP SDK schema)
161
+ - Is version-controlled so changes are visible in git history
162
+
163
+ ---
164
+
165
+ ## Bare React Native vs Expo Implementation
166
+
167
+ Both integration paths use the **same SDK code** but differ in **discovery and package registration**.
168
+
169
+ ### Bare React Native Integration
170
+
171
+ **How it works:**
172
+
173
+ 1. **Autolinking via `react-native.config.js`**
174
+
175
+ React Native's autolinking system discovers native modules automatically:
176
+
177
+ ```javascript
178
+ // react-native.config.js
179
+ module.exports = {
180
+ dependency: {
181
+ platforms: {
182
+ android: {
183
+ sourceDir: androidProjectDir,
184
+ packageImportPath: 'import com.amply.reactnative.AmplyPackage;',
185
+ packageInstance: 'new AmplyPackage()',
186
+ cmakeListsPath: path.join(androidJniDir, 'CMakeLists.txt'),
187
+ },
188
+ },
189
+ },
190
+ codegenConfig: {
191
+ name: 'AmplyReactNative',
192
+ type: 'modules',
193
+ jsSrcsDir: path.join(__dirname, 'src'),
194
+ android: { sourceDir: codegenSourceDirAndroid, packageName: 'com.amply.reactnative' },
195
+ },
196
+ };
197
+ ```
198
+
199
+ 2. **React Native applies the config**
200
+ - Reads `react-native.config.js` from `@amply/amply-react-native` package
201
+ - Auto-imports `AmplyPackage` in `MainApplication.java`
202
+ - Auto-registers it: `new AmplyPackage()` in the packages list
203
+ - Runs codegen to generate TurboModule bindings
204
+
205
+ 3. **Codegen generates native stubs**
206
+ - Java spec base class in `android/src/newarch/java/`
207
+ - C++ glue code in `android/src/newarch/jni/`
208
+ - Helper methods: `emitOnSystemEvent()`, `emitOnDeepLink()`
209
+
210
+ 4. **Developer rebuilds**
211
+ ```bash
212
+ yarn react-native run-android
213
+ ```
214
+ - Gradle compiles generated + hand-written Kotlin code
215
+ - React Native bundle includes JS layer
216
+ - Module is immediately available
217
+
218
+ **Advantages:**
219
+ - Zero manual configuration
220
+ - Fully automatic package discovery
221
+ - Works with standard React Native tooling
222
+ - Explicit in git (easy to audit changes)
223
+
224
+ **Limitations:**
225
+ - Requires React Native >= 0.79
226
+ - Requires New Architecture enabled in `gradle.properties`
227
+ - No CLI setup needed, but build setup must be done upfront
228
+
229
+ ### Expo Integration
230
+
231
+ **How it works:**
232
+
233
+ 1. **User adds plugin to `app.json`**
234
+
235
+ ```json
236
+ {
237
+ "expo": {
238
+ "plugins": ["@amply/amply-react-native"]
239
+ }
240
+ }
241
+ ```
242
+
243
+ 2. **Expo Config Plugin runs during `expo prebuild`**
244
+
245
+ The `withAmply.ts` plugin:
246
+ - Reads the generated `MainApplication.kt`
247
+ - Adds import: `import com.amply.reactnative.AmplyPackage`
248
+ - Adds registration: `add(AmplyPackage())` in the packages block
249
+ - Ensures idempotency (doesn't duplicate imports)
250
+
251
+ 3. **Expo prebuild completes**
252
+ - Merges autolinking config from `react-native.config.js`
253
+ - Merges plugin config from `withAmply`
254
+ - Generates native code in `android/app/src/main/`
255
+ - Runs codegen
256
+ - Prepares Gradle for compilation
257
+
258
+ 4. **Developer runs the app**
259
+ ```bash
260
+ expo prebuild --clean # Generate native code
261
+ expo run:android # Compile and deploy
262
+ ```
263
+
264
+ **Why both paths are needed:**
265
+
266
+ | Scenario | Path |
267
+ |----------|------|
268
+ | Custom Gradle/Kotlin build | Bare RN (react-native.config.js only) |
269
+ | Managed development (Expo CLI) | Expo (config plugin + react-native.config.js) |
270
+ | No native code edits planned | Expo (faster iteration) |
271
+ | Custom native modules needed | Bare RN (full control) |
272
+
273
+ **Integration schematic:**
274
+
275
+ ```
276
+ ┌─────────────────────────────────────────┐
277
+ │ Developer Code & Config │
278
+ │ - package.json (dependencies) │
279
+ │ - app.json (plugins) [Expo only] │
280
+ └────────┬────────────────────────────────┘
281
+
282
+ ├─→ [Bare RN Path]
283
+ │ react-native run-android
284
+ │ ↓
285
+ │ React Native CLI reads react-native.config.js
286
+ │ ↓
287
+ │ Autolinking injects AmplyPackage
288
+ │ ↓
289
+ │ Gradle compiles
290
+
291
+ └─→ [Expo Path]
292
+ expo prebuild --clean
293
+
294
+ Expo reads app.json plugins
295
+
296
+ withAmply plugin modifies MainApplication
297
+
298
+ Autolinking also applies (react-native.config.js)
299
+
300
+ Gradle compiles
301
+ ```
302
+
303
+ ---
304
+
305
+ ## System Events Lifecycle
306
+
307
+ ### Event Flow End-to-End
308
+
309
+ System events travel from the KMP SDK → Kotlin wrapper → React Native → JS application in these steps:
310
+
311
+ #### **Step 1: KMP SDK Emits Event**
312
+
313
+ The Amply Kotlin Multiplatform SDK internally tracks system events (e.g., session start, app open, deeplink received). When an event occurs:
314
+
315
+ ```kotlin
316
+ // Inside Amply KMP SDK (tools.amply.sdk.Amply)
317
+ internal val systemEvents = MutableSharedFlow<SystemEvent>()
318
+
319
+ fun trackSystemEvent(event: SystemEvent) {
320
+ viewModelScope.launch {
321
+ systemEvents.emit(event) // ← Events flow here
322
+ }
323
+ }
324
+ ```
325
+
326
+ #### **Step 2: Kotlin Wrapper Collects Events**
327
+
328
+ The `DefaultAmplyClient` subscribes to the KMP's event stream:
329
+
330
+ ```kotlin
331
+ // android/src/main/java/com/amply/reactnative/core/DefaultAmplyClient.kt
332
+ private val amplyClient = Amply.getInstance()
333
+
334
+ init {
335
+ scope.launch {
336
+ amplyClient.systemEvents.collect { event ->
337
+ // Event received, forward to AmplyModule
338
+ onSystemEvent?.invoke(event)
339
+ }
340
+ }
341
+ }
342
+ ```
343
+
344
+ #### **Step 3: TurboModule Receives and Emits**
345
+
346
+ The `AmplyModule` receives the event from `DefaultAmplyClient` and pushes it through the TurboModule EventEmitter:
347
+
348
+ ```kotlin
349
+ // android/src/main/java/com/amply/reactnative/AmplyModule.kt
350
+ class AmplyModule(reactContext: ReactApplicationContext) : NativeAmplyModuleSpec(reactContext) {
351
+
352
+ private val client = DefaultAmplyClient(...)
353
+
354
+ init {
355
+ // Register callback from client
356
+ client.onSystemEvent = { event ->
357
+ // Convert Kotlin object to WritableMap
358
+ val payload = event.toWritableMap()
359
+ // Emit to JS via codegen-generated method
360
+ emitOnSystemEvent(payload)
361
+ }
362
+ }
363
+ }
364
+ ```
365
+
366
+ The `emitOnSystemEvent(payload)` method is **generated by React Native Codegen**:
367
+
368
+ ```java
369
+ // Generated by codegen (android/src/newarch/java/com/.../NativeAmplyModuleSpec.java)
370
+ protected void emitOnSystemEvent(WritableMap payload) {
371
+ // Internal RN plumbing routes this to all registered listeners
372
+ }
373
+ ```
374
+
375
+ #### **Step 4: Event Arrives in JS**
376
+
377
+ The event propagates through the TurboModule EventEmitter to all registered JS listeners:
378
+
379
+ ```javascript
380
+ // src/systemEvents.ts
381
+ import NativeAmply from './nativeModule';
382
+
383
+ export function addSystemEventsListener(listener) {
384
+ const subscription = NativeAmply.onSystemEvent((payload) => {
385
+ listener(payload); // ← JS application receives event
386
+ });
387
+ return () => subscription?.remove?.();
388
+ }
389
+ ```
390
+
391
+ #### **Step 5: Application Consumes Event**
392
+
393
+ A React component can listen using the JavaScript API:
394
+
395
+ ```typescript
396
+ // Consumer code
397
+ import { addSystemEventsListener } from '@amply/amply-react-native';
398
+
399
+ addSystemEventsListener((event) => {
400
+ console.log('System event:', event);
401
+ // Update UI, trigger analytics, etc.
402
+ });
403
+ ```
404
+
405
+ Or using the React hook:
406
+
407
+ ```typescript
408
+ import { useAmplySystemEvents } from '@amply/amply-react-native';
409
+
410
+ function MyComponent() {
411
+ const events = useAmplySystemEvents();
412
+ // events is a live array of recent system events
413
+ return <EventLog events={events} />;
414
+ }
415
+ ```
416
+
417
+ ### Event Stream Architecture Details
418
+
419
+ **Why SharedFlow?**
420
+
421
+ The `SharedFlow` in Kotlin is a **hot stream**:
422
+ - Events are emitted whether or not collectors are listening
423
+ - Perfect for system-level events (don't want to miss app-open because listener wasn't registered yet)
424
+ - Supports multiple collectors simultaneously
425
+
426
+ **Timing guarantees:**
427
+
428
+ ```
429
+ Event emitted from KMP SDK
430
+ ↓ (immediate, on KMP dispatcher thread)
431
+ Kotlin collector receives it
432
+ ↓ (coroutine context switch if needed)
433
+ emitOnSystemEvent() called from TurboModule
434
+ ↓ (queued in RN event loop)
435
+ JS listener callback fired
436
+ ↓ (may be batched with other JS events)
437
+ Application receives event
438
+ ```
439
+
440
+ Latency is typically < 50ms for simple events, but is subject to:
441
+ - Thread dispatcher availability
442
+ - React Native event loop congestion
443
+ - JS execution time
444
+
445
+ **Listener Lifecycle:**
446
+
447
+ ```javascript
448
+ // Listener added
449
+ const unsubscribe = addSystemEventsListener((event) => {
450
+ console.log(event);
451
+ });
452
+
453
+ // After this point, all emitted events reach the listener
454
+ // ...
455
+
456
+ // Listener removed
457
+ unsubscribe();
458
+
459
+ // After this point, emitted events do NOT reach this listener
460
+ // (but other listeners still receive them)
461
+ ```
462
+
463
+ ### Event Types
464
+
465
+ **System Events** are emitted by the Amply KMP SDK for:
466
+ - **Session lifecycle** – User opens app, closes app, session expires
467
+ - **User identification** – User logs in, logs out
468
+ - **App lifecycle** – App enters foreground, enters background
469
+ - **Location events** – If location tracking is enabled
470
+ - **Custom tracked events** – If the app calls `track()`
471
+
472
+ Each event carries:
473
+ ```typescript
474
+ type SystemEventPayload = {
475
+ id?: string; // Unique event ID
476
+ name: string; // Event name: "session_start", "app_open", etc.
477
+ type: 'custom' | 'system'; // Always 'system' for these
478
+ timestamp: number; // Unix timestamp (ms)
479
+ properties: JsonMap; // Event metadata
480
+ };
481
+ ```
482
+
483
+ ---
484
+
485
+ ## Module Loading Strategy
486
+
487
+ ### Why Dynamic Loading?
488
+
489
+ The SDK doesn't hardcode `require('NativeModules').Amply` because:
490
+ 1. Module may not be available (development, wrong RN version)
491
+ 2. Want to support both New Architecture (TurboModule) and legacy bridge (future)
492
+ 3. Need clear error messages if module is missing
493
+
494
+ ### Loading Process
495
+
496
+ **Step 1: Attempt TurboModule Registry**
497
+
498
+ ```typescript
499
+ // src/nativeModule.ts
500
+ import { TurboModuleRegistry } from 'react-native';
501
+
502
+ let cachedModule: any = null;
503
+
504
+ try {
505
+ cachedModule = TurboModuleRegistry.getEnforcing<Spec>('Amply');
506
+ } catch (error) {
507
+ // TurboModule not found or not properly linked
508
+ }
509
+ ```
510
+
511
+ `getEnforcing()` means:
512
+ - Fail fast if module is registered but broken
513
+ - Don't silently fall back to legacy bridge (we don't want that)
514
+
515
+ **Step 2: Fallback to Legacy Bridge (if enabled)**
516
+
517
+ ```typescript
518
+ if (!cachedModule) {
519
+ try {
520
+ cachedModule = NativeModules.Amply;
521
+ } catch (error) {
522
+ throw new Error('Amply native module not found...');
523
+ }
524
+ }
525
+ ```
526
+
527
+ Currently, this fallback is **not recommended** but is prepared for future compatibility.
528
+
529
+ **Step 3: Cache and Return**
530
+
531
+ ```typescript
532
+ export function getNativeModule(): Spec {
533
+ if (!cachedModule) {
534
+ throw new Error('Amply module not initialized');
535
+ }
536
+ return cachedModule;
537
+ }
538
+ ```
539
+
540
+ ### When Module Loading Fails
541
+
542
+ **If `TurboModuleRegistry.getEnforcing()` throws:**
543
+ - Module is **registered** but **broken** (native code crash, type mismatch)
544
+ - Error message is specific → helps debugging
545
+ - App initialization will fail
546
+
547
+ **Common causes:**
548
+ - `react-native.config.js` not applied (didn't rebuild)
549
+ - Codegen stubs missing (didn't run codegen)
550
+ - Kotlin code has syntax error (didn't compile)
551
+ - AmplyPackage not registered in MainApplication
552
+
553
+ ---
554
+
555
+ ## Build & Development Workflow
556
+
557
+ ### Why Multiple Build Commands?
558
+
559
+ The SDK has three distinct build steps:
560
+
561
+ #### **1. Codegen**
562
+
563
+ ```bash
564
+ yarn codegen
565
+ ```
566
+
567
+ **What it does:**
568
+ - Reads `codegen.config.json` to find the TypeScript spec
569
+ - Finds `src/nativeSpecs/NativeAmplyModule.ts`
570
+ - Generates language-specific bindings
571
+
572
+ **Outputs:**
573
+ - `android/src/newarch/java/com/amply/reactnative/**` – Java spec base + helper methods
574
+ - `android/src/newarch/jni/**` – C++ glue code
575
+ - `ios/Sources/AmplyReactNative/**` – ObjC++ (placeholder until iOS SDK ships)
576
+
577
+ **When to run:**
578
+ - After changing the `Spec` interface
579
+ - After changing type definitions in `NativeAmplyModule.ts`
580
+ - Before committing spec changes (artifacts must be in sync)
581
+
582
+ **Why it's separate:**
583
+ - Codegen takes several seconds (compiles and runs Gradle tasks)
584
+ - Only needed when the contract changes
585
+ - Not needed for implementation changes (Kotlin code edits, JS logic, etc.)
586
+
587
+ #### **2. JavaScript Bundling (TypeScript → JS)**
588
+
589
+ ```bash
590
+ yarn build
591
+ # Internally runs: expo-module build (which uses tsup)
592
+ ```
593
+
594
+ **What it does:**
595
+ - Compiles TypeScript to JavaScript using `tsup`
596
+ - Generates ES Modules (`.mjs`) and CommonJS (`.js`)
597
+ - Type checks via `tsc` → generates `.d.ts` files
598
+ - Outputs to `dist/` directory
599
+
600
+ **Outputs:**
601
+ - `dist/index.js` – CommonJS entry point (consumers importing from Node)
602
+ - `dist/index.mjs` – ES Module entry point (bundlers like Metro)
603
+ - `dist/src/**/*.d.ts` – TypeScript declarations
604
+
605
+ **When to run:**
606
+ - After changing any TypeScript/JavaScript code
607
+ - Before running example apps locally (they import from dist/)
608
+ - Before publishing to npm
609
+ - When you need to link locally and test changes
610
+
611
+ **Why the rebuild matters:**
612
+ - React Native Metro bundler requires compiled output
613
+ - When using `yarn link` or `file:../` paths, Metro resolves the `main` field in package.json
614
+ - package.json points to `dist/index.js`, so it must be built
615
+
616
+ #### **3. Plugin Build**
617
+
618
+ ```bash
619
+ yarn build:plugin
620
+ ```
621
+
622
+ **What it does:**
623
+ - Compiles the Expo config plugin TypeScript code
624
+ - Outputs to `plugin/build/index.js`
625
+ - Makes it executable by Expo CLI during `expo prebuild`
626
+
627
+ **When to run:**
628
+ - After changing `plugin/src/withAmply.ts`
629
+ - Before testing with Expo apps
630
+ - Before publishing
631
+
632
+ ### Complete Build Sequence
633
+
634
+ When publishing or setting up for local testing:
635
+
636
+ ```bash
637
+ # 1. Check spec hasn't changed (if it has, run codegen)
638
+ # yarn codegen # Only if you edited NativeAmplyModule.ts
639
+
640
+ # 2. Build JavaScript and types
641
+ yarn build
642
+
643
+ # 3. Build plugin (Expo only)
644
+ yarn build:plugin
645
+
646
+ # 4. Run tests
647
+ yarn test
648
+
649
+ # 5. Link to example app and rebuild it
650
+ cd example/bare
651
+ npm install # Picks up new dist/ contents
652
+ yarn react-native run-android
653
+ ```
654
+
655
+ ---
656
+
657
+ ## Script Commands Reference
658
+
659
+ All scripts are defined in `package.json`. Here's what each does and when to use it.
660
+
661
+ ### Development & Testing
662
+
663
+ | Command | What it does | When to use |
664
+ |---------|------------|-----------|
665
+ | `yarn build` | Compile TS → JS, generate types | After code changes, before testing |
666
+ | `yarn build:plugin` | Compile Expo plugin | After editing plugin code |
667
+ | `yarn clean` | Remove `dist/` and build artifacts | If you get strange errors, or before fresh build |
668
+ | `yarn test` | Run Jest unit tests | Before committing, in CI |
669
+ | `yarn prepare` | Runs `build` (called by npm/yarn automatically) | Before `npm install` of this package |
670
+
671
+ ### Linting & Type Checking
672
+
673
+ | Command | What it does | When to use |
674
+ |---------|------------|-----------|
675
+ | `yarn lint` | Check code style (ESLint) | Before committing |
676
+ | `yarn typecheck` | Check TypeScript types | Before committing |
677
+
678
+ ### Publishing
679
+
680
+ | Command | What it does | When to use |
681
+ |---------|------------|-----------|
682
+ | `yarn prepublishOnly` | Runs final checks before npm publish | Before `npm publish` |
683
+ | `npm publish` | Upload package to npm registry | Release to public (requires npm access token) |
684
+
685
+ ### Hidden Scripts (used by example apps)
686
+
687
+ When you link the SDK to example apps, these are called automatically:
688
+
689
+ | Command | Internal use |
690
+ |---------|---|
691
+ | `yarn codegen` | Generate TurboModule stubs (called during example app build) |
692
+
693
+ ### Script Dependency Chain
694
+
695
+ ```
696
+ developer runs yarn build
697
+
698
+ expo-module build (wrapper script)
699
+
700
+ tsup (TypeScript bundler)
701
+ ├─→ tsc (compile TS)
702
+ ├─→ writes dist/index.js (CommonJS)
703
+ └─→ writes dist/index.mjs (ES Module)
704
+
705
+ tsc --project tsconfig.build.json
706
+ └─→ writes dist/src/**/*.d.ts (type declarations)
707
+
708
+ dist/ is ready for consumption
709
+ ```
710
+
711
+ ### Why So Many Compile Steps?
712
+
713
+ 1. **Codegen** – Generates native stubs (Java, C++, ObjC++)
714
+ 2. **JS bundling** – Converts TS to JS for Node and Metro
715
+ 3. **Type generation** – Creates `.d.ts` for consumers
716
+ 4. **Plugin build** – Separate build for Expo plugin code
717
+
718
+ Each step is separate because:
719
+ - They operate on different file types
720
+ - They have different output locations
721
+ - They have different dependencies and tools
722
+ - Separating them makes CI faster (can cache between steps)
723
+
724
+ ---
725
+
726
+ ## Future Roadmap
727
+
728
+ ### Contract Fabric Automation
729
+
730
+ **Goal:** Keep TypeScript types in sync with the Amply KMP SDK schema automatically.
731
+
732
+ **Current state:** NativeAmplyModule.ts is hand-written.
733
+
734
+ **Future state:**
735
+ 1. KMP SDK emits a JSON schema of its public API
736
+ 2. React Native SDK has a CLI tool that downloads the schema
737
+ 3. CLI regenerates `NativeAmplyModule.ts`, types, Kotlin adapters
738
+ 4. CI enforces schema doesn't drift (schema:check fails the build)
739
+
740
+ **Implementation steps (documented in CONTRACT_FABRIC_PIPELINE.md):**
741
+ - Add `schema-emitter` Gradle plugin to KMP repository
742
+ - Create `contract-fabric` CLI tool (Node.js)
743
+ - Add schema pull/generate/check commands to SDK
744
+ - Integrate into CI pipeline
745
+
746
+ **Benefits:**
747
+ - Breaking changes in KMP SDK are caught immediately
748
+ - New KMP API additions are available in React Native within days
749
+ - No manual type sync needed
750
+ - Self-documenting (types are ground truth)
751
+
752
+ ### iOS Support
753
+
754
+ **Current state:** iOS placeholder with stub implementation
755
+
756
+ **Future steps:**
757
+ 1. Amply ships iOS SDK (Swift + Objective-C++)
758
+ 2. Mirror Kotlin implementation in Swift
759
+ 3. Implement EventEmitter event streaming (Obj-C++)
760
+ 4. Wire up deep link detection
761
+ 5. Test with iOS example app
762
+
763
+ **No changes needed to:**
764
+ - JavaScript API (same for both platforms)
765
+ - TypeScript spec (works for all platforms)
766
+ - Expo plugin (already platform-agnostic)
767
+
768
+ ### Advanced Features
769
+
770
+ These are deferred until the Amply KMP SDK exposes corresponding methods:
771
+
772
+ | Feature | What it does | Status |
773
+ |---------|-------------|--------|
774
+ | `identify(userId)` | Associate user ID with events | TODO (KMP SDK support needed) |
775
+ | `setUserProperty(key, value)` | Store persistent user attributes | TODO (KMP SDK support needed) |
776
+ | `flush()` | Send events immediately (don't wait for batch) | TODO (KMP SDK support needed) |
777
+ | `setLogLevel()` | Control SDK logging verbosity | TODO (KMP SDK support needed) |
778
+ | `trackSystemEvent()` | Manually emit system events | TODO (KMP SDK support needed) |
779
+
780
+ ---
781
+
782
+ ## Key Design Decisions Explained
783
+
784
+ ### Decision 1: New Architecture Only
785
+
786
+ **Why?**
787
+ - Legacy Bridge is being phased out by React Native
788
+ - Reduces maintenance burden (one integration path, not two)
789
+ - TurboModule EventEmitters are more reliable than DeviceEventEmitter
790
+ - Unblocks iOS when the Amply iOS SDK arrives
791
+
792
+ **Tradeoff:**
793
+ - Requires React Native >= 0.79
794
+ - New Architecture must be enabled (one-time setup)
795
+ - Can't support older React Native projects
796
+
797
+ ### Decision 2: Spec-Driven Codegen
798
+
799
+ **Why?**
800
+ - TypeScript is the source of truth for the contract
801
+ - Codegen ensures Java/C++/ObjC++ stubs are always in sync
802
+ - Type safety across JS-native boundary
803
+ - Makes it easy to add new methods (edit spec, run codegen, done)
804
+
805
+ **Tradeoff:**
806
+ - Must run codegen after any spec change
807
+ - Generates a lot of boilerplate code (OK, it's committed to git)
808
+
809
+ ### Decision 3: TurboModule EventEmitters Only
810
+
811
+ **Why?**
812
+ - Works identically in Legacy Bridge and New Architecture
813
+ - Type-safe (event types defined in spec)
814
+ - No DeviceEventEmitter complexity
815
+ - Simpler testing (mock TurboModule event emitters)
816
+
817
+ **Tradeoff:**
818
+ - Can't use legacy patterns (no `NativeEventEmitter`)
819
+
820
+ ### Decision 4: Wrapper Pattern (DefaultAmplyClient)
821
+
822
+ **Why?**
823
+ - Decouples React Native from KMP internals
824
+ - Allows adding RN-specific logic (lifecycle, permissions, etc.)
825
+ - Makes testing easier (mock the wrapper, not the KMP SDK)
826
+ - Isolates thread safety concerns
827
+
828
+ **Tradeoff:**
829
+ - Extra layer of indirection (minimal performance cost)
830
+ - Must maintain two interfaces (`AmplyClient`, `DefaultAmplyClient`)
831
+
832
+ ### Decision 5: Separate Bare RN + Expo Paths
833
+
834
+ **Why?**
835
+ - Bare RN: Standard autolinking, full control
836
+ - Expo: Managed workflow, faster development, config plugin integration
837
+ - Same codebase works for both
838
+
839
+ **Tradeoff:**
840
+ - Must test both paths in CI
841
+ - Plugin code must handle Expo-specific concerns
842
+
843
+ ---
844
+
845
+ ## Maintenance & Contribution Guide
846
+
847
+ ### Adding a New Method to the SDK
848
+
849
+ 1. Add method to the `Spec` interface in `src/nativeSpecs/NativeAmplyModule.ts`
850
+ 2. Update TypeScript API in `src/index.ts` (export wrapper)
851
+ 3. Add type exports to spec file
852
+ 4. Run `yarn codegen` to generate native stubs
853
+ 5. Implement method in `android/src/main/java/com/amply/reactnative/AmplyModule.kt`
854
+ 6. Write tests in `src/__tests__/`
855
+ 7. Commit generated files + new code
856
+
857
+ ### Adding System Event Type
858
+
859
+ 1. Update `SystemEventPayload` type in spec
860
+ 2. Update `DefaultAmplyClient` Kotlin code (if needed)
861
+ 3. Run `yarn codegen`
862
+ 4. Update `useAmplySystemEvents` hook if new filtering logic needed
863
+ 5. Test in example app
864
+
865
+ ### Diagnosing Module Loading Issues
866
+
867
+ ```typescript
868
+ // If you get "Amply module not found":
869
+
870
+ // 1. Check react-native.config.js is in package root
871
+ // 2. Check MainApplication.java has import:
872
+ // import com.amply.reactnative.AmplyPackage;
873
+ // 3. Check MainApplication.java has registration:
874
+ // new AmplyPackage() in packages list
875
+ // 4. If using Expo, check app.json has plugin:
876
+ // "plugins": ["@amply/amply-react-native"]
877
+ // 5. Rebuild completely:
878
+ // yarn clean && yarn build && yarn react-native run-android
879
+ ```
880
+
881
+ ---
882
+
883
+ ## Quick Reference
884
+
885
+ ### Environment Requirements
886
+
887
+ - **Node.js:** >= 18
888
+ - **React Native:** >= 0.79 with New Architecture enabled
889
+ - **Expo:** >= 53 (for Expo apps)
890
+ - **Android API:** >= 24
891
+ - **Kotlin:** >= 1.9
892
+ - **Gradle:** >= 8.0
893
+
894
+ ### File Organization Quick Guide
895
+
896
+ ```
897
+ src/
898
+ ├─ index.ts # Public API (what users import)
899
+ ├─ nativeModule.ts # Module loader
900
+ ├─ systemEvents.ts # Event listener helper
901
+ ├─ hooks/
902
+ │ └─ useAmplySystemEvents.ts # React hook
903
+ └─ nativeSpecs/
904
+ └─ NativeAmplyModule.ts # TurboModule spec (authoritative!)
905
+
906
+ android/
907
+ ├─ src/main/java/
908
+ │ └─ com/amply/reactnative/
909
+ │ ├─ AmplyModule.kt # TurboModule implementation
910
+ │ ├─ AmplyPackage.kt # Package registration
911
+ │ └─ core/
912
+ │ └─ DefaultAmplyClient.kt # KMP SDK wrapper
913
+ └─ src/newarch/ # Generated by codegen
914
+
915
+ plugin/
916
+ └─ src/withAmply.ts # Expo config plugin
917
+
918
+ example/
919
+ ├─ bare/ # Bare React Native example
920
+ └─ expo/ # Expo example with expo-router
921
+ ```
922
+
923
+ ### Common Commands Cheat Sheet
924
+
925
+ ```bash
926
+ # Development
927
+ yarn build # Build JS + types
928
+ yarn build:plugin # Build Expo plugin
929
+ yarn test # Run tests
930
+ yarn lint # Check style
931
+
932
+ # Codegen (only if you edited the spec!)
933
+ yarn codegen # Generate native stubs
934
+
935
+ # Local testing
936
+ cd example/bare
937
+ npm install # Pick up new dist/
938
+ yarn react-native run-android
939
+
940
+ # Publishing
941
+ yarn build && yarn test && yarn publish
942
+ ```
943
+
944
+ ---
945
+
946
+ ## Development Credentials & Private Repositories
947
+
948
+ ### GitHub Packages for Private SDK Versions
949
+
950
+ For specific development scenarios, the Amply React Native SDK can be published to GitHub Packages instead of npm. This is useful for:
951
+
952
+ - **Private patches**: Test bug fixes before public release
953
+ - **Pre-release versions**: Alpha/beta versions for early adopters
954
+ - **Team-only builds**: Restricted distribution to team members
955
+ - **SDK updates**: Testing updates that depend on unreleased KMP SDK versions
956
+
957
+ #### Configuration
958
+
959
+ To use a private GitHub Packages version, update your app's `package.json`:
960
+
961
+ ```json
962
+ {
963
+ "dependencies": {
964
+ "@amply/amply-react-native": "^0.2.0-beta.1"
965
+ },
966
+ "resolutions": {
967
+ "@amply/amply-react-native": "github:amply/react-native-sdk#main"
968
+ }
969
+ }
970
+ ```
971
+
972
+ Or install directly from a GitHub branch/tag:
973
+
974
+ ```bash
975
+ yarn add @amply/amply-react-native@github:amply/react-native-sdk#feature/my-feature
976
+ ```
977
+
978
+ #### GitHub Packages Authentication
979
+
980
+ To access private GitHub Packages, create a Personal Access Token (PAT):
981
+
982
+ 1. **Generate Token** at https://github.com/settings/tokens
983
+ - Scope: `read:packages`
984
+ - Note: "Amply SDK Development"
985
+
986
+ 2. **Configure npm/yarn** – Create `~/.npmrc`:
987
+ ```
988
+ //npm.pkg.github.com/:_authToken=ghp_xxxxxxxxxxxxxxxxxxxx
989
+ @amply:registry=https://npm.pkg.github.com
990
+ ```
991
+
992
+ 3. **Or use environment variable**:
993
+ ```bash
994
+ export NPM_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
995
+ yarn install
996
+ ```
997
+
998
+ #### Publishing Private Versions
999
+
1000
+ From the SDK repository:
1001
+
1002
+ ```bash
1003
+ # Build the package
1004
+ yarn build && yarn test
1005
+
1006
+ # Publish to GitHub Packages (requires GitHub credentials)
1007
+ npm publish --registry https://npm.pkg.github.com
1008
+
1009
+ # Or set in .npmrc and use:
1010
+ npm publish
1011
+ ```
1012
+
1013
+ The package is published with scope `@amply` to GitHub Packages registry.
1014
+
1015
+ #### CI/CD Integration
1016
+
1017
+ For automated publishing to GitHub Packages in CI:
1018
+
1019
+ 1. **Create GitHub Secret**: `GITHUB_TOKEN` (automatically available in Actions)
1020
+
1021
+ 2. **Publish step** in GitHub Actions:
1022
+ ```yaml
1023
+ - name: Publish to GitHub Packages
1024
+ run: npm publish --registry https://npm.pkg.github.com
1025
+ env:
1026
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1027
+ ```
1028
+
1029
+ 3. **Version strategy**:
1030
+ - Development: `0.x.x-dev.{timestamp}`
1031
+ - Pre-release: `0.x.x-alpha.1`, `0.x.x-beta.2`
1032
+ - Stable: `0.x.x` (published to npm instead)
1033
+
1034
+ #### Working with Private Versions
1035
+
1036
+ Example app using a development version:
1037
+
1038
+ ```bash
1039
+ # Install from a specific branch
1040
+ yarn add @amply/amply-react-native@github:amply/react-native-sdk#main
1041
+
1042
+ # Or update to latest from branch
1043
+ yarn upgrade @amply/amply-react-native@github:amply/react-native-sdk#develop
1044
+
1045
+ # Rebuild example
1046
+ cd example/bare
1047
+ yarn install
1048
+ yarn react-native run-android
1049
+ ```
1050
+
1051
+ #### Troubleshooting GitHub Packages
1052
+
1053
+ **"401 Unauthorized" errors:**
1054
+ - Verify PAT token is valid and hasn't expired
1055
+ - Check `~/.npmrc` has correct token format
1056
+ - Token must have `read:packages` scope minimum
1057
+
1058
+ **"404 Not Found":**
1059
+ - Confirm package name is `@amply/amply-react-native`
1060
+ - Package must be published to GitHub Packages registry
1061
+ - Check repository visibility (private packages require authentication)
1062
+
1063
+ **Version not found:**
1064
+ - Ensure version was published with `npm publish`
1065
+ - Check GitHub Packages page for the version
1066
+ - May need to wait a few seconds for replication
1067
+
1068
+ #### Advantages of GitHub Packages
1069
+
1070
+ | Aspect | npm | GitHub Packages |
1071
+ |--------|-----|-----------------|
1072
+ | **Stability** | Stable releases | Pre-release, experimental |
1073
+ | **Access** | Public | Team/authenticated only |
1074
+ | **Automation** | Manual workflow | Integrated with GitHub Actions |
1075
+ | **Testing** | Production environment | CI/CD integration tests |
1076
+ | **Rollback** | Unpublish restrictions | Easy branch switching |
1077
+
1078
+ #### When to Use Each Registry
1079
+
1080
+ **Use npm (public):**
1081
+ - Production releases
1082
+ - Stable versions
1083
+ - Public samples
1084
+ - Long-term support versions
1085
+
1086
+ **Use GitHub Packages (private):**
1087
+ - Testing breaking changes
1088
+ - Pre-release builds
1089
+ - Patches for unreleased KMP SDK versions
1090
+ - Team collaboration on features
1091
+ - Early access for selected users
1092
+
1093
+ ---
1094
+
1095
+ ## Summary
1096
+
1097
+ The Amply React Native SDK demonstrates a modern, maintainable approach to React Native native modules:
1098
+
1099
+ ✅ **Clean architecture** – Layered design with clear responsibilities
1100
+ ✅ **Type safety** – Spec-driven codegen ensures JS-native contract
1101
+ ✅ **Event streaming** – TurboModule EventEmitters, no fallbacks
1102
+ ✅ **Dual integration** – Works for Bare RN and Expo
1103
+ ✅ **Forward-looking** – Prepared for iOS, Contract Fabric automation
1104
+ ✅ **Developer experience** – Simple API, comprehensive types, good errors
1105
+
1106
+ The system events lifecycle is reliable and predictable, flowing from Kotlin → TurboModule → JavaScript with minimal latency. The build system is structured to be fast and modular, with clear separation between codegen, bundling, and testing.
1107
+
1108
+ For new contributors, understand these layers:
1109
+ 1. **TypeScript spec** is the contract
1110
+ 2. **Codegen** creates stubs
1111
+ 3. **Kotlin wrapper** does the work
1112
+ 4. **Event emitters** transport data
1113
+ 5. **Expo plugin** configures everything
1114
+
1115
+ For maintenance, keep the spec up-to-date, always run codegen after spec changes, and test both Bare and Expo paths.