mergn-react-native 1.0.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 (41) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +131 -0
  3. package/android/build.gradle +66 -0
  4. package/android/src/main/java/com/mergn/reactnative/MergnModule.java +152 -0
  5. package/android/src/main/java/com/mergn/reactnative/MergnPackage.java +31 -0
  6. package/app.plugin.js +2 -0
  7. package/docs/ANDROID.md +147 -0
  8. package/docs/IOS.md +156 -0
  9. package/docs/MIGRATION.md +365 -0
  10. package/ios/MergnModule.swift +149 -0
  11. package/ios/MergnModuleBridge.m +14 -0
  12. package/ios/MergnNotificationHandler.swift +160 -0
  13. package/ios/NotificationService/Info.plist +31 -0
  14. package/ios/NotificationService/NotificationService.swift +82 -0
  15. package/mergn-react-native.podspec +32 -0
  16. package/package.json +49 -0
  17. package/plugin/index.js +219 -0
  18. package/react-native.config.js +19 -0
  19. package/scripts/release-ios.sh +79 -0
  20. package/scripts/verify-package.js +58 -0
  21. package/sdk/MergnSDK.podspec +28 -0
  22. package/sdk/mergn_ios.xcframework/Info.plist +44 -0
  23. package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Info.plist +0 -0
  24. package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.abi.json +2517 -0
  25. package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.private.swiftinterface +82 -0
  26. package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  27. package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.swiftinterface +82 -0
  28. package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/mergn_ios +0 -0
  29. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Info.plist +0 -0
  30. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios-simulator.abi.json +2517 -0
  31. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +82 -0
  32. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  33. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios-simulator.swiftinterface +82 -0
  34. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/x86_64-apple-ios-simulator.abi.json +2517 -0
  35. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +82 -0
  36. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  37. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +82 -0
  38. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/_CodeSignature/CodeResources +101 -0
  39. package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/mergn_ios +0 -0
  40. package/src/index.d.ts +37 -0
  41. package/src/index.js +34 -0
package/docs/IOS.md ADDED
@@ -0,0 +1,156 @@
1
+ # iOS setup
2
+
3
+ ```sh
4
+ npm install mergn-react-native
5
+ ```
6
+
7
+ ```json
8
+ { "expo": { "plugins": ["mergn-react-native"] } }
9
+ ```
10
+
11
+ ```sh
12
+ npx expo prebuild --platform ios
13
+ cd ios && pod install && cd ..
14
+ npx expo run:ios
15
+ ```
16
+
17
+ ## What the package does for you
18
+
19
+ | | |
20
+ | --- | --- |
21
+ | Native module registration | Autolinking (`mergn-react-native.podspec`) |
22
+ | MERGN iOS SDK | `mergn_ios.xcframework`, bundled — nothing to download |
23
+ | `ios.deploymentTarget` raised to 14.0 | Config plugin (only ever raised, never lowered) |
24
+ | `ios.useFrameworks: "static"` | Config plugin |
25
+ | `pod 'MergnSDK'` in your Podfile | Config plugin |
26
+ | Notification view / click tracking | Automatic (see below) |
27
+
28
+ You do **not** edit your `Podfile` or `AppDelegate`.
29
+
30
+ ### Why those two build settings are needed
31
+
32
+ - **iOS 14.0** — the framework is built for iOS 14. Expo's template defaults to
33
+ 13.4, which fails with *"they required a higher minimum deployment target"*.
34
+ - **`useFrameworks: "static"`** — the SDK ships a Swift module with no
35
+ Objective-C headers, so without `use_frameworks!` the build fails with
36
+ *"Unable to resolve module dependency: 'mergn_ios'"*. `static` is chosen
37
+ because Firebase's Swift pods also require it.
38
+
39
+ Both are applied automatically. If your app already targets a newer iOS, that is
40
+ left alone.
41
+
42
+ ## Push notifications
43
+
44
+ ```sh
45
+ npx expo install @react-native-firebase/app @react-native-firebase/messaging
46
+ ```
47
+
48
+ 1. Register your app's **bundle ID** in the Firebase console and download
49
+ `GoogleService-Info.plist`.
50
+ 2. Wire it up in `app.json`:
51
+ ```json
52
+ {
53
+ "expo": {
54
+ "ios": {
55
+ "googleServicesFile": "./GoogleService-Info.plist",
56
+ "entitlements": { "aps-environment": "development" }
57
+ },
58
+ "plugins": [
59
+ "mergn-react-native",
60
+ "@react-native-firebase/app"
61
+ ]
62
+ }
63
+ }
64
+ ```
65
+ 3. Upload an **APNs key** (`.p8`) to Firebase → Project settings → Cloud
66
+ Messaging. This needs a paid Apple Developer account.
67
+ 4. Pass the token to MERGN:
68
+ ```js
69
+ const token = await messaging().getToken();
70
+ await Mergn.setFirebaseToken(token);
71
+ ```
72
+
73
+ Without `GoogleService-Info.plist`, `messaging()` throws
74
+ `No Firebase App '[DEFAULT]' has been created`. Everything else in this package
75
+ keeps working — only FCM is affected.
76
+
77
+ > APNs cannot be used on the iOS Simulator. Real push delivery requires a
78
+ > physical device. To test *handling* without APNs, inject a payload locally:
79
+ > ```sh
80
+ > xcrun simctl push booted <your.bundle.id> payload.json
81
+ > ```
82
+ > The payload needs `aps` at the top level (raw APNs shape). An FCM v1 request —
83
+ > the JSON you POST to Firebase, with a top-level `message` — will be rejected
84
+ > with *"Notification payload is missing the aps key"*, because only Firebase's
85
+ > servers unwrap that envelope.
86
+
87
+ ### Notification view / click tracking
88
+
89
+ Automatic — no JS API and no AppDelegate changes.
90
+
91
+ MERGN's iOS SDK expects the host app to call `notificationViewed` and
92
+ `notificationTapped` from the `UNUserNotificationCenter` delegate. A React Native
93
+ app cannot simply assign that delegate: `expo-notifications` and
94
+ `@react-native-firebase` both claim it, and the last one to load wins.
95
+
96
+ So this package **wraps** whichever delegate your app ends up with, forwards the
97
+ two events to MERGN, then calls the original implementation. Your own
98
+ notification handling is untouched, and tracking is additive.
99
+
100
+ It also refreshes the SDK's current view controller whenever the app comes to the
101
+ foreground, which is what in-app popups need.
102
+
103
+ ## In-app popups
104
+
105
+ Triggered by MERGN campaigns in response to events:
106
+
107
+ ```js
108
+ await Mergn.performEvent("Collection Viewed", { title: "Beauty" });
109
+ ```
110
+
111
+ The package keeps `SDKManager.shared.setCurrentViewController(...)` up to date,
112
+ so popups have somewhere to present. If none appears, confirm a campaign is
113
+ configured for that event name in your MERGN dashboard.
114
+
115
+ ## Rich push images (optional)
116
+
117
+ iOS renders a notification image only if your app contains a **Notification
118
+ Service Extension** — a separate binary that must be a target of *your* app, so
119
+ no library can supply it. Without one, a push carrying `mutable-content: 1` and
120
+ an `image` URL still arrives, just as text.
121
+
122
+ To add it:
123
+
124
+ 1. **Xcode → File → New → Target → Notification Service Extension**, name it
125
+ `NotificationService`.
126
+ 2. Replace the generated `NotificationService.swift` with the reference
127
+ implementation:
128
+ `node_modules/mergn-react-native/ios/NotificationService/NotificationService.swift`
129
+ 3. Set the extension's **iOS Deployment Target to match your app** — Xcode
130
+ defaults it to the latest SDK, and an extension must not require a newer OS
131
+ than its host.
132
+
133
+ Because `expo prebuild --clean` regenerates the Xcode project, commit your `ios/`
134
+ directory (as you would `android/`) or re-add the target after a clean prebuild.
135
+
136
+ Android needs none of this — the MERGN SDK handles images itself there.
137
+
138
+ ## Troubleshooting
139
+
140
+ **`Unable to resolve module dependency: 'mergn_ios'`** — `use_frameworks!` is not
141
+ active. Ensure `mergn-react-native` is in your `app.json` plugins, then
142
+ `npx expo prebuild --platform ios` and `pod install` again.
143
+
144
+ **`they required a higher minimum deployment target`** — same cause; the plugin
145
+ sets iOS 14.0 for you.
146
+
147
+ **`Unable to find compatibility version string for object version 70`** —
148
+ Xcode 16/26 wrote a project format your CocoaPods cannot read. The plugin pins it
149
+ back on prebuild; alternatively update CocoaPods.
150
+
151
+ **`Cycle inside <YourApp>`** — `@react-native-firebase`'s script phase declares
152
+ your built `Info.plist` as an input, which cycles once an app extension is
153
+ embedded. The plugin clears that input; re-run prebuild.
154
+
155
+ **`MergnModule.<method> is unavailable`** — native module not loaded. Expo Go
156
+ cannot load native code; use `npx expo run:ios` or an EAS build.
@@ -0,0 +1,365 @@
1
+ # Migrating from a manual MERGN integration
2
+
3
+ If you wired MERGN in by hand — copying `MergnModule.java` into your app,
4
+ registering `MergnPackage()` in `MainApplication`, and dropping the `.aar` into
5
+ `android/app/libs/` — you **must remove those pieces** before installing this
6
+ package.
7
+
8
+ Read this page top to bottom before you start. The removal is not optional and
9
+ it is not all loud: one of the four steps fails your build immediately, and two
10
+ of them fail silently while the app still looks healthy.
11
+
12
+ - [Is removing the `.aar` enough?](#is-removing-the-aar-enough)
13
+ - [Android: removing the old integration, step by step](#android-removing-the-old-integration-step-by-step)
14
+ - [Android: installing the new package](#android-installing-the-new-package)
15
+ - [iOS: what to remove](#ios-what-to-remove)
16
+ - [JavaScript: update imports](#javascript-update-imports)
17
+ - [Verifying the migration worked](#verifying-the-migration-worked)
18
+ - [Rollback](#rollback)
19
+
20
+ ---
21
+
22
+ ## Is removing the `.aar` enough?
23
+
24
+ **No.** Removing the `.aar` fixes the *build failure*. Two further problems
25
+ remain, and neither announces itself:
26
+
27
+ | Step skipped | What happens | Do you notice? |
28
+ | --- | --- | --- |
29
+ | `.aar` left in `libs/` | `Duplicate class com.mergn.insights.*` — build fails | **Yes, immediately** |
30
+ | `MergnModule.java` / `MergnPackage.java` left in your app | Two native modules **both named `MergnModule`**. React Native registers one and warns; which one wins is not guaranteed | **No** — builds and runs |
31
+ | `packages.add(MergnPackage())` left in `MainApplication` | `MergnPackage` resolves to your *old* class, so this package's module is bypassed entirely | **No** |
32
+
33
+ Both old and new modules return the identical name from `getName()`:
34
+
35
+ ```java
36
+ @Override
37
+ public String getName() {
38
+ return "MergnModule"; // identical in com.android.test.MergnModule
39
+ } // and com.mergn.reactnative.MergnModule
40
+ ```
41
+
42
+ `NativeModules.MergnModule` is resolved by that string, so with both classes
43
+ present your JS call may reach either one. The old one references SDK classes
44
+ that no longer come from an `.aar`, so you get a working-looking app with calls
45
+ landing in the wrong place.
46
+
47
+ Do all four steps below.
48
+
49
+ ---
50
+
51
+ ## Android: removing the old integration, step by step
52
+
53
+ ### Step 1 — Find what you actually have
54
+
55
+ Run this from your project root. It prints every trace of the old integration:
56
+
57
+ ```sh
58
+ echo "--- copied native sources ---"
59
+ find android/app/src/main/java -name 'Mergn*.java' -o -name 'Mergn*.kt'
60
+
61
+ echo "--- bundled SDK binaries ---"
62
+ find android -name '*.aar' | grep -i mergn
63
+
64
+ echo "--- gradle references ---"
65
+ grep -rn -i 'mergn\|libs/.*\.aar\|fileTree' android/app/build.gradle
66
+
67
+ echo "--- MainApplication registration ---"
68
+ grep -rn -i 'mergn' android/app/src/main/java --include='MainApplication.*'
69
+
70
+ echo "--- manifest service ---"
71
+ grep -n -i 'mergn' android/app/src/main/AndroidManifest.xml
72
+
73
+ echo "--- JS imports ---"
74
+ grep -rn 'MergnModule' --include='*.js' --include='*.jsx' --include='*.ts' --include='*.tsx' . \
75
+ | grep -v node_modules
76
+ ```
77
+
78
+ Everything it lists gets deleted. Keep the output — you will use it to confirm
79
+ you got all of it.
80
+
81
+ > **If `android/` is not committed to git** (a pure Expo CNG project where
82
+ > `npx expo prebuild` generates it), your changes live in `app.json`, a config
83
+ > plugin, or a `patch-package` patch instead. In that case delete the plugin or
84
+ > patch that injected MERGN, and run `npx expo prebuild --clean`. Steps 2–5 then
85
+ > apply to whatever your plugin was writing.
86
+
87
+ ### Step 2 — Delete the copied native sources
88
+
89
+ ```sh
90
+ rm -f android/app/src/main/java/<your/package/path>/MergnModule.java
91
+ rm -f android/app/src/main/java/<your/package/path>/MergnPackage.java
92
+ ```
93
+
94
+ For example, if your package is `com.android.test`:
95
+
96
+ ```sh
97
+ rm -f android/app/src/main/java/com/android/test/MergnModule.java
98
+ rm -f android/app/src/main/java/com/android/test/MergnPackage.java
99
+ ```
100
+
101
+ If you translated them to Kotlin, delete the `.kt` files instead. If you also
102
+ added a custom `FirebaseMessagingService` subclass purely for MERGN, delete that
103
+ too — the SDK ships its own.
104
+
105
+ **This step is silent if you skip it.** Do not skip it.
106
+
107
+ ### Step 3 — Un-register the package in `MainApplication`
108
+
109
+ `android/app/src/main/java/<your/package/path>/MainApplication.kt`:
110
+
111
+ ```diff
112
+ override fun getPackages(): List<ReactPackage> =
113
+ PackageList(this).packages.apply {
114
+ - add(MergnPackage())
115
+ }
116
+ ```
117
+
118
+ Or, if it's still Java:
119
+
120
+ ```diff
121
+ @Override
122
+ protected List<ReactPackage> getPackages() {
123
+ List<ReactPackage> packages = new PackageList(this).getPackages();
124
+ - packages.add(new MergnPackage());
125
+ return packages;
126
+ }
127
+ ```
128
+
129
+ Also delete the now-unused import if you had an explicit one:
130
+
131
+ ```diff
132
+ - import com.android.test.MergnPackage;
133
+ ```
134
+
135
+ Anything you added to `onCreate` for MERGN comes out as well:
136
+
137
+ ```diff
138
+ override fun onCreate() {
139
+ super.onCreate()
140
+ - MergnSDK.initialize(this)
141
+ }
142
+ ```
143
+
144
+ This package initializes the SDK for you on the first `registerApi` call.
145
+
146
+ **This step is also silent if you skip it.**
147
+
148
+ ### Step 4 — Remove the bundled `.aar`
149
+
150
+ In `android/app/build.gradle`:
151
+
152
+ ```diff
153
+ dependencies {
154
+ - implementation files('libs/MERGN_SDK_KOTLIN_REACT_NATIVE_5.2.4.aar')
155
+ }
156
+ ```
157
+
158
+ Some integrations used a `fileTree` instead — remove the MERGN part of it, or
159
+ the whole line if MERGN was the only thing in `libs/`:
160
+
161
+ ```diff
162
+ - implementation fileTree(dir: 'libs', include: ['*.aar'])
163
+ ```
164
+
165
+ Then delete the binaries:
166
+
167
+ ```sh
168
+ rm -f android/app/libs/*mergn*.aar android/app/libs/*MERGN*.aar
169
+ rmdir android/app/libs 2>/dev/null # only succeeds if now empty
170
+ ```
171
+
172
+ If your old integration also declared the SDK's dependencies by hand (Room,
173
+ WorkManager, coroutines, gson…) *solely for MERGN*, remove those too — this
174
+ package declares them itself. Leave anything your own code uses.
175
+
176
+ **Skipping this step fails the build:**
177
+
178
+ ```
179
+ Duplicate class com.mergn.insights.classes.EventManager found in modules
180
+ jetified-MERGN_SDK_KOTLIN_REACT_NATIVE_5.2.4.aar and
181
+ jetified-Mergn_sdk_android-5.2.4.aar
182
+ ```
183
+
184
+ ### Step 5 — Remove the hand-added manifest service
185
+
186
+ The config plugin declares MERGN's messaging service, so a hand-written
187
+ duplicate for the same class is a manifest-merger error. In
188
+ `android/app/src/main/AndroidManifest.xml`:
189
+
190
+ ```diff
191
+ - <service
192
+ - android:name="com.mergn.insights.firebaseservices.FireBaseMessagingService"
193
+ - android:exported="false">
194
+ - <intent-filter>
195
+ - <action android:name="com.google.firebase.MESSAGING_EVENT" />
196
+ - </intent-filter>
197
+ - </service>
198
+ ```
199
+
200
+ Leave `INTERNET`, `POST_NOTIFICATIONS` and `VIBRATE` alone if they're there —
201
+ the plugin adds them idempotently, and duplicate `<uses-permission>` entries
202
+ merge cleanly.
203
+
204
+ ### Step 6 — Confirm the old integration is gone
205
+
206
+ Re-run the Step 1 audit. Every section should now be empty except your JS
207
+ imports, which Step 8 handles:
208
+
209
+ ```sh
210
+ find android/app/src/main/java -name 'Mergn*' ; \
211
+ find android -iname '*mergn*.aar' ; \
212
+ grep -rn -i mergn android/app/build.gradle android/app/src/main/AndroidManifest.xml ; \
213
+ grep -rn -i mergn android/app/src/main/java --include='MainApplication.*' ; \
214
+ echo "audit complete — no output above this line means clean"
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Android: installing the new package
220
+
221
+ ### Step 7 — Install
222
+
223
+ ```sh
224
+ npm install mergn-react-native
225
+ ```
226
+
227
+ Add the plugin to `app.json`:
228
+
229
+ ```json
230
+ {
231
+ "expo": {
232
+ "plugins": ["mergn-react-native"]
233
+ }
234
+ }
235
+ ```
236
+
237
+ That is the entire native setup. Autolinking registers the module; the config
238
+ plugin adds the manifest service and permissions; the package's own
239
+ `build.gradle` pulls the SDK from JitPack and declares its transitive
240
+ dependencies. You do **not** touch `MainApplication`, `settings.gradle`, or
241
+ `app/build.gradle` again.
242
+
243
+ See [ANDROID.md](./ANDROID.md) for Firebase/push setup and the Android 13+
244
+ runtime permission, which are unchanged from your old integration.
245
+
246
+ ### Step 8 — Update your JavaScript
247
+
248
+ ```diff
249
+ - import MergnModule from "../MergnModule";
250
+ + import Mergn from "mergn-react-native";
251
+ ```
252
+
253
+ **Method names, arguments and return values are identical**, so the call sites
254
+ do not change:
255
+
256
+ ```js
257
+ await Mergn.registerApi(key);
258
+ await Mergn.performEvent("Collection Viewed", { title: "Beauty" });
259
+ await Mergn.setAttribute("Email", "user@example.com");
260
+ await Mergn.login("user@example.com");
261
+ await Mergn.setFirebaseToken(token);
262
+ await Mergn.setAppContext(token);
263
+ ```
264
+
265
+ The action strings on the bridge (`register_api`, `perform_event`,
266
+ `add_attribute`, `login_mergn`, `firebase_token_mergn`, `mergn_app_context`) are
267
+ byte-identical to the old module's, so anything you built on top still lines up.
268
+
269
+ Delete your local `MergnModule.js`.
270
+
271
+ ### Step 9 — Rebuild from scratch
272
+
273
+ Stale native artefacts survive an ordinary rebuild and will hide your work:
274
+
275
+ ```sh
276
+ rm -rf android/app/build android/build android/.gradle
277
+ npx expo prebuild --platform android --clean
278
+ npx expo run:android
279
+ ```
280
+
281
+ > `prebuild --clean` regenerates `android/`. If you hand-edited anything else in
282
+ > there that is *not* committed as a config plugin, it is lost — commit or note
283
+ > those changes first.
284
+
285
+ ---
286
+
287
+ ## iOS: what to remove
288
+
289
+ If you had a hand-rolled iOS integration:
290
+
291
+ 1. Delete any hand-added bridge sources from `ios/<YourApp>/`:
292
+ ```sh
293
+ rm -f ios/<YourApp>/MergnModule.swift ios/<YourApp>/MergnModuleBridge.m
294
+ ```
295
+ 2. Remove a manual pod declaration from your `Podfile` — the config plugin
296
+ injects it:
297
+ ```diff
298
+ - pod 'MergnSDK', :path => '...'
299
+ ```
300
+ 3. Delete a vendored framework from your project. Keeping it alongside the
301
+ package's copy produces duplicate-symbol link errors:
302
+ ```sh
303
+ rm -rf ios/Frameworks/mergn_ios.xcframework
304
+ ```
305
+ Also remove it from the target's *Link Binary With Libraries* and *Embed
306
+ Frameworks* phases in Xcode.
307
+ 4. Rebuild clean:
308
+ ```sh
309
+ rm -rf ios/Pods ios/Podfile.lock ios/build
310
+ npx expo prebuild --platform ios --clean
311
+ npx expo run:ios
312
+ ```
313
+
314
+ See [IOS.md](./IOS.md) for the full iOS setup.
315
+
316
+ ---
317
+
318
+ ## Verifying the migration worked
319
+
320
+ After `npx expo run:android`, watch the native log:
321
+
322
+ ```sh
323
+ adb logcat -s MergnModule:D ReactNativeJS:V
324
+ ```
325
+
326
+ Call each method once from your app. You should see exactly one line per call:
327
+
328
+ ```
329
+ D MergnModule: Registered API key
330
+ D MergnModule: User Logged In
331
+ D MergnModule: Attribute Set: Email
332
+ D MergnModule: Event Sent: Collection Viewed
333
+ D MergnModule: Firebase Token Set
334
+ ```
335
+
336
+ Then confirm the *new* class is the one running:
337
+
338
+ ```sh
339
+ adb logcat -d | grep -i 'MergnModule\|mergn.reactnative'
340
+ ```
341
+
342
+ Two warning signs:
343
+
344
+ - **`Native module MergnModule tried to override …`** in logcat — both modules
345
+ are still registered. Step 2 or Step 3 was missed.
346
+ - **`MergnModule.<method> is unavailable`** thrown in JS — the native module did
347
+ not load at all. You are either in Expo Go (which cannot load native code) or
348
+ the build predates the install; re-run Step 9.
349
+
350
+ ---
351
+
352
+ ## Rollback
353
+
354
+ Nothing in the migration touches SDK storage. Same SDK version (Android 5.2.4,
355
+ iOS 19.0.0), same API key, same customer ids and attributes — identified users
356
+ stay identified across the upgrade, and back again.
357
+
358
+ To revert: `git revert` the migration commit, `npm uninstall
359
+ mergn-react-native`, then rebuild clean as in Step 9.
360
+
361
+ ## If you cannot migrate yet
362
+
363
+ Stay on your manual integration — it keeps working. **Do not install this
364
+ package alongside it**; the duplicate-class failure has no workaround short of
365
+ removing one side.
@@ -0,0 +1,149 @@
1
+ import Foundation
2
+ import React
3
+ import UIKit
4
+ import mergn_ios
5
+
6
+ // iOS counterpart of android/app/src/main/java/com/android/test/MergnModule.java.
7
+ // The JS side (MergnModule.js) funnels everything through one method:
8
+ // performAction(action, jsonData) -> Promise
9
+ // so the same action strings must be honoured here. Written in Swift because
10
+ // mergn_ios ships only a .swiftmodule (no Objective-C headers), so ObjC cannot
11
+ // import it.
12
+ @objc(MergnModule)
13
+ class MergnModule: NSObject {
14
+
15
+ private enum Action {
16
+ static let registerApi = "register_api"
17
+ static let performEvent = "perform_event"
18
+ static let setAttribute = "add_attribute"
19
+ static let login = "login_mergn"
20
+ static let firebaseToken = "firebase_token_mergn"
21
+ static let appContext = "mergn_app_context"
22
+ }
23
+
24
+ private let manager = EventManager.shared
25
+
26
+ // Matches the Android module: the SDK is a singleton, so re-registering the
27
+ // API key on every call would be wasted work. Tracked only to log it.
28
+ private static var didRegister = false
29
+
30
+ @objc static func requiresMainQueueSetup() -> Bool { true }
31
+
32
+ override init() {
33
+ super.init()
34
+ // Install notification tracking once the module is constructed. By this
35
+ // point expo-notifications / RNFB have claimed UNUserNotificationCenter's
36
+ // delegate, so the handler wraps theirs instead of replacing it.
37
+ // Deferred to main because it touches UIKit.
38
+ DispatchQueue.main.async {
39
+ MergnNotificationHandler.shared.install()
40
+ }
41
+ }
42
+
43
+ /// The SDK renders in-app popups, so it needs a live view controller. Android
44
+ /// gets this from getCurrentActivity(); on iOS we hand the SDK the top-most
45
+ /// view controller before any call that might present UI.
46
+ private func refreshTopViewController() {
47
+ if let top = UIApplication.topViewController() {
48
+ SDKManager.shared.setCurrentViewController(top)
49
+ }
50
+ }
51
+
52
+ @objc(performAction:jsonData:resolver:rejecter:)
53
+ func performAction(_ action: String,
54
+ jsonData: String,
55
+ resolver resolve: @escaping RCTPromiseResolveBlock,
56
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
57
+
58
+ // Everything here can touch UIKit (popups, view controllers), so stay on main.
59
+ DispatchQueue.main.async {
60
+ let options: [String: Any]
61
+ do {
62
+ guard let data = jsonData.data(using: .utf8),
63
+ let parsed = try JSONSerialization.jsonObject(with: data) as? [String: Any]
64
+ else {
65
+ throw NSError(domain: "MergnModule", code: 1,
66
+ userInfo: [NSLocalizedDescriptionKey:
67
+ "jsonData is not a JSON object: \(jsonData)"])
68
+ }
69
+ options = parsed
70
+ } catch {
71
+ NSLog("[MergnModule] performAction failed to parse payload for \(action): \(error)")
72
+ reject("MERGN_ERROR", error.localizedDescription, error)
73
+ return
74
+ }
75
+
76
+ func requireString(_ key: String) throws -> String {
77
+ guard let value = options[key] as? String else {
78
+ throw NSError(domain: "MergnModule", code: 2,
79
+ userInfo: [NSLocalizedDescriptionKey:
80
+ "Missing required string '\(key)' for action '\(action)'"])
81
+ }
82
+ return value
83
+ }
84
+
85
+ do {
86
+ switch action {
87
+
88
+ case Action.registerApi:
89
+ let apiKey = try requireString("apiKey")
90
+ self.refreshTopViewController()
91
+ self.manager.registerAPI(clientApiKey: apiKey)
92
+ MergnModule.didRegister = true
93
+ NSLog("[MergnModule] Registered API key")
94
+ resolve("API Key Registered")
95
+
96
+ case Action.performEvent:
97
+ let eventName = try requireString("eventName")
98
+ // Android stringifies every property value; the iOS SDK takes
99
+ // [String: Any], so pass the JSON values through as-is.
100
+ let properties = options["eventPropertiesMap"] as? [String: Any] ?? [:]
101
+ self.refreshTopViewController()
102
+ self.manager.sendEvent(eventName: eventName, properties: properties)
103
+ NSLog("[MergnModule] Event Sent: \(eventName)")
104
+ resolve("Event Performed")
105
+
106
+ case Action.setAttribute:
107
+ let name = try requireString("attributeName")
108
+ let value = try requireString("attributeValue")
109
+ self.manager.sendAttribute(attributeName: name, attributeValue: value)
110
+ NSLog("[MergnModule] Attribute Set: \(name)")
111
+ resolve("Attribute Set")
112
+
113
+ case Action.login:
114
+ let identifier = try requireString("uniqueIdentifier")
115
+ // Android's single login() both persists the identifier and tells the
116
+ // backend. iOS splits these, so do both:
117
+ // saveCustomerId -> writes customerId to UserDefaults, so later
118
+ // sendEvent calls are attributed to this user
119
+ // postIdentification-> POST /customer/set-identity, merging the
120
+ // anonymous profile into the identified one
121
+ // postIdentification may well save the id itself, in which case the
122
+ // first call is a harmless duplicate write. Doing only the POST would
123
+ // risk unattributed events, so both are called deliberately.
124
+ _ = self.manager.saveCustomerId(customerId: identifier)
125
+ let afterSave = self.manager.getCustomerId()
126
+ self.manager.postIdentification(identity: identifier)
127
+ NSLog("[MergnModule] User Logged In: \(identifier) (customerId now '\(afterSave)')")
128
+ resolve("User Logged In")
129
+
130
+ case Action.firebaseToken, Action.appContext:
131
+ // Android routes both actions down the same path; keep that parity.
132
+ let token = try requireString("fcmTokenMergn")
133
+ _ = self.manager.saveFirebaseToken(token: token)
134
+ self.manager.firebaseToken(token: token)
135
+ let label = action == Action.appContext
136
+ ? "Firebase Token in App Context Set" : "Firebase Token Set"
137
+ NSLog("[MergnModule] \(label)")
138
+ resolve(label)
139
+
140
+ default:
141
+ reject("UNSUPPORTED_ACTION", "Action \(action) is not supported.", nil)
142
+ }
143
+ } catch {
144
+ NSLog("[MergnModule] performAction failed for action \(action): \(error)")
145
+ reject("MERGN_ERROR", error.localizedDescription, error)
146
+ }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,14 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ // Registers the Swift MergnModule with React Native. The implementation lives in
4
+ // MergnModule.swift (mergn_ios ships a .swiftmodule only, so the SDK cannot be
5
+ // imported from Objective-C); this interface is what makes
6
+ // NativeModules.MergnModule resolve on the JS side.
7
+ @interface RCT_EXTERN_MODULE(MergnModule, NSObject)
8
+
9
+ RCT_EXTERN_METHOD(performAction:(NSString *)action
10
+ jsonData:(NSString *)jsonData
11
+ resolver:(RCTPromiseResolveBlock)resolve
12
+ rejecter:(RCTPromiseRejectBlock)reject)
13
+
14
+ @end