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.
- package/LICENSE +44 -0
- package/README.md +131 -0
- package/android/build.gradle +66 -0
- package/android/src/main/java/com/mergn/reactnative/MergnModule.java +152 -0
- package/android/src/main/java/com/mergn/reactnative/MergnPackage.java +31 -0
- package/app.plugin.js +2 -0
- package/docs/ANDROID.md +147 -0
- package/docs/IOS.md +156 -0
- package/docs/MIGRATION.md +365 -0
- package/ios/MergnModule.swift +149 -0
- package/ios/MergnModuleBridge.m +14 -0
- package/ios/MergnNotificationHandler.swift +160 -0
- package/ios/NotificationService/Info.plist +31 -0
- package/ios/NotificationService/NotificationService.swift +82 -0
- package/mergn-react-native.podspec +32 -0
- package/package.json +49 -0
- package/plugin/index.js +219 -0
- package/react-native.config.js +19 -0
- package/scripts/release-ios.sh +79 -0
- package/scripts/verify-package.js +58 -0
- package/sdk/MergnSDK.podspec +28 -0
- package/sdk/mergn_ios.xcframework/Info.plist +44 -0
- package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Info.plist +0 -0
- package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.abi.json +2517 -0
- package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.private.swiftinterface +82 -0
- package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios.swiftinterface +82 -0
- package/sdk/mergn_ios.xcframework/ios-arm64/mergn_ios.framework/mergn_ios +0 -0
- package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Info.plist +0 -0
- 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
- 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
- package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/Modules/mergn_ios.swiftmodule/arm64-apple-ios-simulator.swiftinterface +82 -0
- 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
- 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
- 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
- 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
- package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/_CodeSignature/CodeResources +101 -0
- package/sdk/mergn_ios.xcframework/ios-arm64_x86_64-simulator/mergn_ios.framework/mergn_ios +0 -0
- package/src/index.d.ts +37 -0
- 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
|
+
}
|
package/plugin/index.js
ADDED
|
@@ -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"
|