@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.
- package/LICENSE +178 -0
- package/README.md +714 -0
- package/android/build.gradle +90 -0
- package/android/consumer-rules.pro +1 -0
- package/android/gradle.properties +3 -0
- package/android/settings.gradle +9 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +384 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/AmplyPackage.kt +39 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +30 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +296 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/AmplyInitializationOptions.kt +10 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/DataSetType.kt +42 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/DataSetTypeMapper.kt +38 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/DeepLinkPayload.kt +8 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/EventEnvelope.kt +9 -0
- package/android/src/main/jni/AmplyTurboModule.cpp +29 -0
- package/android/src/main/jni/CMakeLists.txt +76 -0
- package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +75 -0
- package/android/src/newarch/jni/AmplyReactNative-generated.cpp +77 -0
- package/android/src/newarch/jni/AmplyReactNative.h +31 -0
- package/android/src/newarch/jni/CMakeLists.txt +40 -0
- package/app.plugin.js +1 -0
- package/dist/index.js +272 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +234 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin/index.d.ts +6 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +186 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/index.mjs +169 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/dist/plugin/src/index.d.ts +6 -0
- package/dist/plugin/src/index.d.ts.map +1 -0
- package/dist/plugin/src/index.js +3 -0
- package/dist/plugin/src/withAmply.d.ts +30 -0
- package/dist/plugin/src/withAmply.d.ts.map +1 -0
- package/dist/plugin/src/withAmply.js +51 -0
- package/dist/plugin/withAmply.d.ts +12 -0
- package/dist/plugin/withAmply.d.ts.map +1 -0
- package/dist/src/__tests__/index.test.d.ts +2 -0
- package/dist/src/__tests__/index.test.d.ts.map +1 -0
- package/dist/src/__tests__/index.test.js +70 -0
- package/dist/src/hooks/useAmplySystemEvents.d.ts +12 -0
- package/dist/src/hooks/useAmplySystemEvents.d.ts.map +1 -0
- package/dist/src/hooks/useAmplySystemEvents.js +56 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +80 -0
- package/dist/src/nativeModule.d.ts +5 -0
- package/dist/src/nativeModule.d.ts.map +1 -0
- package/dist/src/nativeModule.js +48 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +75 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.js +2 -0
- package/dist/src/systemEventUtils.d.ts +3 -0
- package/dist/src/systemEventUtils.d.ts.map +1 -0
- package/dist/src/systemEventUtils.js +30 -0
- package/dist/src/systemEvents.d.ts +6 -0
- package/dist/src/systemEvents.d.ts.map +1 -0
- package/dist/src/systemEvents.js +8 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/docs/ARCHITECTURE.md +1115 -0
- package/expo-module.config.json +11 -0
- package/ios/AmplyReactNative.podspec +32 -0
- package/ios/README.md +11 -0
- package/ios/Sources/AmplyReactNative/AmplyModule.mm +332 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +111 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +152 -0
- package/package.json +71 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +8 -0
- package/plugin/build/withAmply.d.ts +29 -0
- package/plugin/build/withAmply.js +53 -0
- package/plugin/src/index.ts +7 -0
- package/plugin/src/withAmply.ts +68 -0
- package/plugin/tsconfig.json +8 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/react-native.config.js +34 -0
- package/scripts/codegen.js +212 -0
- package/src/__tests__/index.test.ts +92 -0
- package/src/hooks/useAmplySystemEvents.ts +75 -0
- package/src/index.ts +115 -0
- package/src/nativeModule.ts +65 -0
- package/src/nativeSpecs/NativeAmplyModule.ts +80 -0
- package/src/systemEventUtils.ts +35 -0
- 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.
|