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
@@ -0,0 +1,160 @@
1
+ import Foundation
2
+ import UIKit
3
+ import UserNotifications
4
+ import mergn_ios
5
+
6
+ /// Reports notification impressions and taps to the Mergn SDK, and keeps the
7
+ /// SDK's current view controller up to date so in-app popups have somewhere to
8
+ /// present.
9
+ ///
10
+ /// Mergn's reference iOS integration puts these calls directly in the host
11
+ /// app's AppDelegate:
12
+ ///
13
+ /// UNUserNotificationCenter.current().delegate = self
14
+ /// SDKManager.shared.setCurrentViewController(rootViewController)
15
+ ///
16
+ /// func userNotificationCenter(_:willPresent:withCompletionHandler:) {
17
+ /// EventManager.shared.notificationViewed(notificationData: notification.request)
18
+ /// }
19
+ /// func userNotificationCenter(_:didReceive response:withCompletionHandler:) {
20
+ /// EventManager.shared.notificationTapped(notificationData: response.notification.request)
21
+ /// }
22
+ ///
23
+ /// A React Native app cannot follow that pattern as-is: expo-notifications and
24
+ /// @react-native-firebase both claim `UNUserNotificationCenter.delegate`, and
25
+ /// whichever loads last wins. Assigning it here would silently break their
26
+ /// notification handling (and vice versa).
27
+ ///
28
+ /// So instead of owning the delegate, this observes it. `swizzle()` wraps any
29
+ /// existing delegate's two methods, forwards to Mergn, then calls the original
30
+ /// implementation — so Mergn tracking is additive and the app's own handling is
31
+ /// untouched. On Android the SDK does this inside its own
32
+ /// FireBaseMessagingService, which is why there is no JS API for it on either
33
+ /// platform: both are automatic.
34
+ @objc(MergnNotificationHandler)
35
+ public final class MergnNotificationHandler: NSObject {
36
+
37
+ @objc public static let shared = MergnNotificationHandler()
38
+
39
+ private var installed = false
40
+
41
+ /// Called from the module's `+load`-time hook, after the JS bridge is up so
42
+ /// that whichever notification library the app uses has already claimed the
43
+ /// delegate.
44
+ @objc public func install() {
45
+ guard !installed else { return }
46
+ installed = true
47
+
48
+ // The SDK needs a live view controller for in-app popups. Refresh it now and
49
+ // again whenever the app returns to the foreground, since the top-most
50
+ // controller changes as the user navigates.
51
+ refreshViewController()
52
+ NotificationCenter.default.addObserver(
53
+ self,
54
+ selector: #selector(refreshViewController),
55
+ name: UIApplication.didBecomeActiveNotification,
56
+ object: nil
57
+ )
58
+
59
+ observeNotificationCenterDelegate()
60
+ }
61
+
62
+ @objc private func refreshViewController() {
63
+ DispatchQueue.main.async {
64
+ if let top = UIApplication.topViewController() {
65
+ SDKManager.shared.setCurrentViewController(top)
66
+ }
67
+ }
68
+ }
69
+
70
+ // MARK: - Delegate observation
71
+
72
+ private func observeNotificationCenterDelegate() {
73
+ let center = UNUserNotificationCenter.current()
74
+
75
+ guard let delegate = center.delegate else {
76
+ // Nothing else claimed the delegate, so take it ourselves. This is the
77
+ // plain-RN case with no notification library installed.
78
+ center.delegate = FallbackDelegate.shared
79
+ return
80
+ }
81
+
82
+ swizzle(type(of: delegate))
83
+ }
84
+
85
+ /// Wraps the delegate's willPresent / didReceive so Mergn sees every
86
+ /// notification without displacing the existing handler.
87
+ private func swizzle(_ cls: AnyClass) {
88
+ swizzleWillPresent(cls)
89
+ swizzleDidReceive(cls)
90
+ }
91
+
92
+ private func swizzleWillPresent(_ cls: AnyClass) {
93
+ let selector = #selector(
94
+ UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)
95
+ )
96
+ guard let original = class_getInstanceMethod(cls, selector) else { return }
97
+
98
+ typealias Impl = @convention(c) (
99
+ AnyObject, Selector, UNUserNotificationCenter, UNNotification,
100
+ @escaping (UNNotificationPresentationOptions) -> Void
101
+ ) -> Void
102
+ let originalImp = unsafeBitCast(method_getImplementation(original), to: Impl.self)
103
+
104
+ let block: @convention(block) (
105
+ AnyObject, UNUserNotificationCenter, UNNotification,
106
+ @escaping (UNNotificationPresentationOptions) -> Void
107
+ ) -> Void = { receiver, center, notification, completion in
108
+ EventManager.shared.notificationViewed(notificationData: notification.request)
109
+ originalImp(receiver, selector, center, notification, completion)
110
+ }
111
+
112
+ method_setImplementation(original, imp_implementationWithBlock(block))
113
+ }
114
+
115
+ private func swizzleDidReceive(_ cls: AnyClass) {
116
+ let selector = #selector(
117
+ UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)
118
+ )
119
+ guard let original = class_getInstanceMethod(cls, selector) else { return }
120
+
121
+ typealias Impl = @convention(c) (
122
+ AnyObject, Selector, UNUserNotificationCenter, UNNotificationResponse,
123
+ @escaping () -> Void
124
+ ) -> Void
125
+ let originalImp = unsafeBitCast(method_getImplementation(original), to: Impl.self)
126
+
127
+ let block: @convention(block) (
128
+ AnyObject, UNUserNotificationCenter, UNNotificationResponse, @escaping () -> Void
129
+ ) -> Void = { receiver, center, response, completion in
130
+ EventManager.shared.notificationTapped(notificationData: response.notification.request)
131
+ originalImp(receiver, selector, center, response, completion)
132
+ }
133
+
134
+ method_setImplementation(original, imp_implementationWithBlock(block))
135
+ }
136
+
137
+ /// Used only when no other delegate exists, so notifications still surface and
138
+ /// get reported.
139
+ private final class FallbackDelegate: NSObject, UNUserNotificationCenterDelegate {
140
+ static let shared = FallbackDelegate()
141
+
142
+ func userNotificationCenter(
143
+ _ center: UNUserNotificationCenter,
144
+ willPresent notification: UNNotification,
145
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
146
+ ) {
147
+ EventManager.shared.notificationViewed(notificationData: notification.request)
148
+ completionHandler([.banner, .list, .sound, .badge])
149
+ }
150
+
151
+ func userNotificationCenter(
152
+ _ center: UNUserNotificationCenter,
153
+ didReceive response: UNNotificationResponse,
154
+ withCompletionHandler completionHandler: @escaping () -> Void
155
+ ) {
156
+ EventManager.shared.notificationTapped(notificationData: response.notification.request)
157
+ completionHandler()
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,31 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>NotificationService</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>$(EXECUTABLE_NAME)</string>
11
+ <key>CFBundleIdentifier</key>
12
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13
+ <key>CFBundleInfoDictionaryVersion</key>
14
+ <string>6.0</string>
15
+ <key>CFBundleName</key>
16
+ <string>$(PRODUCT_NAME)</string>
17
+ <key>CFBundlePackageType</key>
18
+ <string>XPC!</string>
19
+ <key>CFBundleShortVersionString</key>
20
+ <string>1.0.0</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>1</string>
23
+ <key>NSExtension</key>
24
+ <dict>
25
+ <key>NSExtensionPointIdentifier</key>
26
+ <string>com.apple.usernotifications.service</string>
27
+ <key>NSExtensionPrincipalClass</key>
28
+ <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
29
+ </dict>
30
+ </dict>
31
+ </plist>
@@ -0,0 +1,82 @@
1
+ import UserNotifications
2
+
3
+ // Rich-push image support. This is Mergn's own iOS implementation, used as
4
+ // given.
5
+ //
6
+ // iOS never downloads a notification image by itself: a push carrying
7
+ // "mutable-content": 1 wakes this extension, which has ~30s to fetch the image
8
+ // and hand it back as a UNNotificationAttachment. With no extension in the app
9
+ // the payload's "image" key is ignored and the banner renders as plain text —
10
+ // which is why real pushes were arriving without the picture.
11
+ //
12
+ // This is the iOS counterpart of what Mergn's FireBaseMessagingService does
13
+ // inside the SDK on Android. The iOS SDK has no attachment handling, and an
14
+ // extension is a separate binary that must be a target of the app, so it cannot
15
+ // be supplied by a framework.
16
+ class NotificationService: UNNotificationServiceExtension {
17
+
18
+ var contentHandler: ((UNNotificationContent) -> Void)?
19
+ var bestAttemptContent: UNMutableNotificationContent?
20
+
21
+ override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
22
+ self.contentHandler = contentHandler
23
+ bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
24
+
25
+ // Check for the media URL
26
+ if let mediaUrlString = request.content.userInfo["image"] as? String,
27
+ let mediaUrl = URL(string: mediaUrlString) {
28
+
29
+ // Download the image
30
+ downloadImage(from: mediaUrl) { imageData in
31
+ if let imageData = imageData {
32
+ // Attach the image as a big picture to the notification
33
+ self.attachBigImageToNotification(imageData: imageData)
34
+ }
35
+
36
+ // Call the content handler to deliver the notification
37
+ contentHandler(self.bestAttemptContent!)
38
+ }
39
+ } else {
40
+ // If no image, just return the original notification content
41
+ contentHandler(self.bestAttemptContent!)
42
+ }
43
+ }
44
+
45
+ override func serviceExtensionTimeWillExpire() {
46
+ // Called when the extension is about to time out
47
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
48
+ contentHandler(bestAttemptContent)
49
+ }
50
+ }
51
+
52
+ // Helper function to download the image
53
+ func downloadImage(from url: URL, completion: @escaping (Data?) -> Void) {
54
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
55
+ if let error = error {
56
+ print("Error downloading image: \(error)")
57
+ completion(nil)
58
+ } else {
59
+ completion(data)
60
+ }
61
+ }
62
+ task.resume()
63
+ }
64
+
65
+ // Helper function to save the image to a temporary directory
66
+ func saveImageToTempDirectory(data: Data) -> URL {
67
+ let tempDirectory = FileManager.default.temporaryDirectory
68
+ let fileURL = tempDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
69
+ try? data.write(to: fileURL)
70
+ return fileURL
71
+ }
72
+
73
+ // Helper function to attach the image as a big picture to the notification
74
+ func attachBigImageToNotification(imageData: Data) {
75
+ let imageURL = saveImageToTempDirectory(data: imageData)
76
+
77
+ if let attachment = try? UNNotificationAttachment(identifier: "image", url: imageURL, options: nil) {
78
+ // Attach the image to the notification content
79
+ bestAttemptContent?.attachments = [attachment]
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
3
+
4
+ # React Native autolinking finds this podspec automatically, so the client never
5
+ # edits their Podfile: `npm install` + `pod install` is enough.
6
+ Pod::Spec.new do |s|
7
+ s.name = 'mergn-react-native'
8
+ s.version = package['version']
9
+ s.summary = package['description']
10
+ s.homepage = package['homepage']
11
+ s.license = { :type => 'Commercial' }
12
+ s.author = package['author']
13
+ s.source = { :git => 'https://mergn.com', :tag => s.version.to_s }
14
+
15
+ # mergn_ios.xcframework is built for iOS 14 (see its .swiftinterface flags).
16
+ # Setting it here means the client's Podfile inherits the floor automatically.
17
+ s.platforms = { :ios => '14.0' }
18
+ s.swift_version = '5.0'
19
+
20
+ s.source_files = 'ios/*.{h,m,mm,swift}'
21
+
22
+ s.dependency 'MergnSDK', '~> 19.0'
23
+ s.dependency 'React-Core'
24
+
25
+ # The Mergn iOS SDK ships inside this package rather than being downloaded at
26
+ # install time. That keeps `pod install` offline-safe and avoids depending on
27
+ # CocoaPods' local-vs-remote pod semantics, which silently fail to link a
28
+ # vendored framework fetched by prepare_command.
29
+ #
30
+ # vendored_frameworks is resolved relative to this podspec, so the framework
31
+ # must stay in ios/ alongside it.
32
+ end
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "mergn-react-native",
3
+ "version": "1.0.0",
4
+ "description": "MERGN SDK for React Native \u2014 analytics, attributes, identity and push for Android and iOS from one JS API.",
5
+ "keywords": [
6
+ "react-native",
7
+ "expo",
8
+ "mergn",
9
+ "analytics",
10
+ "push-notifications"
11
+ ],
12
+ "license": "SEE LICENSE IN LICENSE",
13
+ "author": "Mergn",
14
+ "homepage": "https://github.com/SHamzaHMergn/mergn-react-native#readme",
15
+ "main": "src/index.js",
16
+ "types": "src/index.d.ts",
17
+ "app.plugin.js": "plugin/index.js",
18
+ "files": [
19
+ "src",
20
+ "plugin",
21
+ "ios",
22
+ "android",
23
+ "app.plugin.js",
24
+ "mergn-react-native.podspec",
25
+ "README.md",
26
+ "scripts",
27
+ "react-native.config.js",
28
+ "sdk",
29
+ "docs",
30
+ "LICENSE"
31
+ ],
32
+ "peerDependencies": {
33
+ "expo": ">=50",
34
+ "react-native": ">=0.72"
35
+ },
36
+ "scripts": {
37
+ "prepack": "node scripts/verify-package.js"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/SHamzaHMergn/mergn-react-native.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/SHamzaHMergn/mergn-react-native/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,219 @@
1
+ const {
2
+ withAndroidManifest,
3
+ withXcodeProject,
4
+ withDangerousMod,
5
+ AndroidConfig,
6
+ } = require("expo/config-plugins");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+
10
+ // Autolinking handles registering the native module on both platforms. This
11
+ // plugin covers what autolinking cannot:
12
+ // - Android: the SDK's FirebaseMessagingService + the permissions it needs
13
+ // - iOS: CocoaPods/Xcode quirks that otherwise break the build
14
+ // - iOS: the optional rich-push NotificationService extension
15
+ //
16
+ // Everything here is idempotent, so re-running prebuild is safe.
17
+
18
+ const SERVICE = "com.mergn.insights.firebaseservices.FireBaseMessagingService";
19
+
20
+ const PERMISSIONS = [
21
+ "android.permission.INTERNET",
22
+ "android.permission.POST_NOTIFICATIONS",
23
+ "android.permission.VIBRATE",
24
+ ];
25
+
26
+ function withMergnAndroid(config) {
27
+ return withAndroidManifest(config, (cfg) => {
28
+ const manifest = cfg.modResults;
29
+
30
+ for (const name of PERMISSIONS) {
31
+ AndroidConfig.Manifest.ensurePermission(manifest, name);
32
+ }
33
+
34
+ const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
35
+ application.service = application.service ?? [];
36
+
37
+ // The SDK's own messaging service must be declared for Mergn pushes to be
38
+ // delivered on Android; there is no autolinking equivalent for manifest
39
+ // entries.
40
+ if (!application.service.some((s) => s.$?.["android:name"] === SERVICE)) {
41
+ application.service.push({
42
+ $: {
43
+ "android:name": SERVICE,
44
+ "android:exported": "true",
45
+ "android:enabled": "true",
46
+ },
47
+ "intent-filter": [
48
+ { action: [{ $: { "android:name": "com.google.firebase.MESSAGING_EVENT" } }] },
49
+ ],
50
+ });
51
+ }
52
+
53
+ return cfg;
54
+ });
55
+ }
56
+
57
+ // Xcode 16/26 writes objectVersion 70+, which CocoaPods' Xcodeproj (<=1.16)
58
+ // cannot serialize — `pod install` dies with "Unable to find compatibility
59
+ // version string for object version `70`". Xcodeproj knows 63 and Xcode opens
60
+ // 63 fine, so pin it. Re-applied each prebuild because Xcode bumps it back
61
+ // whenever the project is opened in the IDE.
62
+ function withCocoaPodsCompatibleProject(config) {
63
+ return withDangerousMod(config, [
64
+ "ios",
65
+ (cfg) => {
66
+ const pbxproj = path.join(
67
+ cfg.modRequest.platformProjectRoot,
68
+ `${cfg.modRequest.projectName}.xcodeproj`,
69
+ "project.pbxproj"
70
+ );
71
+ if (fs.existsSync(pbxproj)) {
72
+ const before = fs.readFileSync(pbxproj, "utf8");
73
+ const after = before.replace(/objectVersion = 7\d;/, "objectVersion = 63;");
74
+ if (after !== before) fs.writeFileSync(pbxproj, after);
75
+ }
76
+ return cfg;
77
+ },
78
+ ]);
79
+ }
80
+
81
+ // @react-native-firebase/app adds a script phase that declares the app's BUILT
82
+ // Info.plist as an input but no outputs. Once any app extension has to be
83
+ // embedded, that produces a dependency cycle:
84
+ // embed .appex -> needs app bundle -> needs Info.plist -> RNFB script -> ...
85
+ // and the build fails with "Cycle inside <app>". Clearing the input and marking
86
+ // the phase always-out-of-date breaks the cycle; the script still runs every
87
+ // build, which is what it wants anyway.
88
+ function withoutFirebaseScriptPhaseCycle(config) {
89
+ return withDangerousMod(config, [
90
+ "ios",
91
+ (cfg) => {
92
+ const pbxproj = path.join(
93
+ cfg.modRequest.platformProjectRoot,
94
+ `${cfg.modRequest.projectName}.xcodeproj`,
95
+ "project.pbxproj"
96
+ );
97
+ if (!fs.existsSync(pbxproj)) return cfg;
98
+
99
+ let contents = fs.readFileSync(pbxproj, "utf8");
100
+ contents = contents.replace(
101
+ /\/\* \[CP-User\] \[RNFB\] Core Configuration \*\/ = \{([\s\S]*?)\};/g,
102
+ (match, body) => {
103
+ let next = body.replace(/inputPaths = \([\s\S]*?\);/, "inputPaths = (\n\t\t\t);");
104
+ if (!/alwaysOutOfDate/.test(next)) {
105
+ next = next.replace(
106
+ "isa = PBXShellScriptBuildPhase;",
107
+ "isa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;"
108
+ );
109
+ }
110
+ return `/* [CP-User] [RNFB] Core Configuration */ = {${next}};`;
111
+ }
112
+ );
113
+ fs.writeFileSync(pbxproj, contents);
114
+ return cfg;
115
+ },
116
+ ]);
117
+ }
118
+
119
+ // The Mergn SDK is a separate pod (sdk/MergnSDK.podspec) rather than part of the
120
+ // autolinked wrapper, because a vendored framework declared on an autolinked pod
121
+ // does not produce link flags - the app builds with no -framework "mergn_ios"
122
+ // and then fails with "Unable to resolve module dependency: 'mergn_ios'".
123
+ //
124
+ // Autolinking cannot add a second pod, so inject it here. :path works because
125
+ // the framework is bundled in the package; it points at the DIRECTORY, and
126
+ // CocoaPods matches the podspec filename to the pod name - which is why
127
+ // MergnSDK.podspec sits alone in sdk/ with the framework.
128
+ function withMergnSdkPod(config) {
129
+ return withDangerousMod(config, [
130
+ "ios",
131
+ (cfg) => {
132
+ const podfile = path.join(cfg.modRequest.platformProjectRoot, "Podfile");
133
+ if (!fs.existsSync(podfile)) return cfg;
134
+
135
+ let contents = fs.readFileSync(podfile, "utf8");
136
+ if (contents.includes("pod 'MergnSDK'")) return cfg;
137
+
138
+ // Resolve from this plugin's own location so npm, yarn workspaces and
139
+ // pnpm layouts all work.
140
+ const packageRoot = path.join(__dirname, "..");
141
+ const relative = path
142
+ .relative(cfg.modRequest.platformProjectRoot, path.join(packageRoot, "sdk"))
143
+ .split(path.sep)
144
+ .join("/");
145
+
146
+ const line = ` pod 'MergnSDK', :path => '${relative}'`;
147
+ const anchor = " use_expo_modules!";
148
+ if (!contents.includes(anchor)) {
149
+ throw new Error(
150
+ "mergn-react-native: could not find 'use_expo_modules!' in the Podfile. " +
151
+ "Add this line inside your app target manually:\n" + line
152
+ );
153
+ }
154
+ contents = contents.replace(anchor, `${anchor}\n${line}`);
155
+ fs.writeFileSync(podfile, contents);
156
+ return cfg;
157
+ },
158
+ ]);
159
+ }
160
+
161
+ // mergn_ios.xcframework is built for iOS 14, but Expo's Podfile template
162
+ // defaults to 13.4 — so `pod install` fails with "they required a higher minimum
163
+ // deployment target". Raise the floor here so the client does not have to know
164
+ // the SDK's minimum, and only ever raise it (never lower an app that already
165
+ // targets something newer).
166
+ const MIN_IOS = "14.0";
167
+
168
+ function withMinimumIosVersion(config) {
169
+ return withDangerousMod(config, [
170
+ "ios",
171
+ (cfg) => {
172
+ const file = path.join(
173
+ cfg.modRequest.platformProjectRoot,
174
+ "Podfile.properties.json"
175
+ );
176
+ let props = {};
177
+ if (fs.existsSync(file)) {
178
+ try {
179
+ props = JSON.parse(fs.readFileSync(file, "utf8"));
180
+ } catch {
181
+ props = {};
182
+ }
183
+ }
184
+
185
+ let changed = false;
186
+
187
+ const current = parseFloat(props["ios.deploymentTarget"] ?? "0");
188
+ if (!(current >= parseFloat(MIN_IOS))) {
189
+ props["ios.deploymentTarget"] = MIN_IOS;
190
+ changed = true;
191
+ }
192
+
193
+ // mergn_ios ships as a Swift .xcframework with only a .swiftmodule (no
194
+ // ObjC headers). Without use_frameworks! the module is not importable and
195
+ // the build fails with "Unable to resolve module dependency: 'mergn_ios'".
196
+ // "static" is chosen over "dynamic" because it is also what Firebase needs
197
+ // (its Swift pods cannot build against non-modular GoogleUtilities as
198
+ // plain static libraries), and most RN apps using Mergn also use Firebase.
199
+ if (!props["ios.useFrameworks"]) {
200
+ props["ios.useFrameworks"] = "static";
201
+ changed = true;
202
+ }
203
+
204
+ if (changed) {
205
+ fs.writeFileSync(file, JSON.stringify(props, null, 2) + "\n");
206
+ }
207
+ return cfg;
208
+ },
209
+ ]);
210
+ }
211
+
212
+ module.exports = function withMergn(config, props = {}) {
213
+ let next = withMergnAndroid(config);
214
+ next = withMinimumIosVersion(next);
215
+ next = withMergnSdkPod(next);
216
+ next = withoutFirebaseScriptPhaseCycle(next);
217
+ next = withCocoaPodsCompatibleProject(next);
218
+ return next;
219
+ };
@@ -0,0 +1,19 @@
1
+ // Tells React Native autolinking where the native code lives, so both platforms
2
+ // register themselves on install — no manual MainApplication or Podfile edits.
3
+ module.exports = {
4
+ dependency: {
5
+ platforms: {
6
+ android: {
7
+ sourceDir: 'android',
8
+ packageImportPath: 'import com.mergn.reactnative.MergnPackage;',
9
+ packageInstance: 'new MergnPackage()',
10
+ },
11
+ ios: {
12
+ // Only the wrapper podspec is listed; it declares a dependency on
13
+ // MergnSDK, which CocoaPods resolves from the podspec in this package
14
+ // (see the podspec_repo note in README) or from the local path below.
15
+ podspecPath: __dirname + '/mergn-react-native.podspec',
16
+ },
17
+ },
18
+ },
19
+ };
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # Publish the Mergn iOS xcframework to GitHub Releases and update the podspec's
3
+ # checksum, so the binary does not have to ship inside the npm package.
4
+ #
5
+ # ./scripts/release-ios.sh 19.0.0 /path/to/mergn_ios.xcframework
6
+ #
7
+ # Requires the `gh` CLI, authenticated (`gh auth login`).
8
+ # Free: public GitHub repos have unlimited release-asset storage and bandwidth.
9
+ set -euo pipefail
10
+
11
+ VERSION="${1:?usage: release-ios.sh <version> <path-to-xcframework>}"
12
+ FRAMEWORK="${2:?usage: release-ios.sh <version> <path-to-xcframework>}"
13
+ REPO="${MERGN_IOS_REPO:-SHamzaHMergn/Mergn_sdk_ios}"
14
+
15
+ [ -d "$FRAMEWORK" ] || { echo "not a directory: $FRAMEWORK" >&2; exit 1; }
16
+
17
+ # The framework's own minimum iOS must match the podspec, or clients hit link
18
+ # errors that point nowhere useful.
19
+ IFACE=$(find "$FRAMEWORK" -name "*-apple-ios.swiftinterface" | head -1)
20
+ if [ -n "$IFACE" ]; then
21
+ MIN=$(grep -o "target arm64-apple-ios[0-9.]*" "$IFACE" | head -1 | sed 's/.*ios//')
22
+ echo "framework minimum iOS: ${MIN:-unknown}"
23
+ fi
24
+
25
+ WORK=$(mktemp -d)
26
+ trap 'rm -rf "$WORK"' EXIT
27
+
28
+ # Zip from the framework's PARENT dir so the archive contains
29
+ # mergn_ios.xcframework/... and not an absolute path prefix.
30
+ cp -R "$FRAMEWORK" "$WORK/"
31
+ NAME=$(basename "$FRAMEWORK")
32
+ ( cd "$WORK" && zip -qr "$WORK/mergn_ios.xcframework.zip" "$NAME" )
33
+
34
+ ZIP="$WORK/mergn_ios.xcframework.zip"
35
+ SHA=$(shasum -a 256 "$ZIP" | awk '{print $1}')
36
+ echo "zip: $(du -h "$ZIP" | cut -f1)"
37
+ echo "sha256: $SHA"
38
+
39
+ # The upload needs either the gh CLI or a manual step; the zip and digest are
40
+ # already produced either way, so the podspec can be updated regardless.
41
+ OUT_DIR="${MERGN_RELEASE_OUT:-$PWD/dist}"
42
+ mkdir -p "$OUT_DIR"
43
+ cp "$ZIP" "$OUT_DIR/"
44
+
45
+ if command -v gh >/dev/null 2>&1; then
46
+ gh release view "$VERSION" --repo "$REPO" >/dev/null 2>&1 \
47
+ && gh release upload "$VERSION" "$ZIP" --repo "$REPO" --clobber \
48
+ || gh release create "$VERSION" "$ZIP" --repo "$REPO" \
49
+ --title "Mergn iOS SDK $VERSION" \
50
+ --notes "xcframework for CocoaPods. sha256: $SHA"
51
+ echo "uploaded to https://github.com/$REPO/releases/tag/$VERSION"
52
+ else
53
+ cat <<MSG
54
+
55
+ gh CLI not found - the zip is ready but was NOT uploaded.
56
+
57
+ zip: $OUT_DIR/mergn_ios.xcframework.zip
58
+
59
+ Either install the CLI:
60
+ brew install gh && gh auth login
61
+
62
+ or upload by hand:
63
+ 1. https://github.com/$REPO/releases/new
64
+ 2. Tag: $VERSION
65
+ 3. Attach the zip above, publish.
66
+
67
+ The podspec below has been updated with the digest either way, so once the
68
+ asset is live at the release URL nothing else needs changing.
69
+ MSG
70
+ fi
71
+
72
+ # Keep the podspec's version and digest in lockstep with what was uploaded.
73
+ SPEC="$(cd "$(dirname "$0")/.." && pwd)/sdk/MergnSDK.podspec"
74
+ /usr/bin/sed -i '' -E "s/ s\.version = '.*'/ s.version = '$VERSION'/" "$SPEC"
75
+ /usr/bin/sed -i '' -E "s/:sha256 => '[a-f0-9]*'/:sha256 => '$SHA'/" "$SPEC"
76
+
77
+ echo
78
+ echo "Updated $SPEC"
79
+ echo "Verify with: pod spec lint MergnSDK.podspec"