@posthog/react-native-plugin 2.9.1 → 2.9.3
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/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginModule.kt
CHANGED
|
@@ -221,7 +221,7 @@ class PosthogReactNativePluginModule(
|
|
|
221
221
|
|
|
222
222
|
setIdentify(config.cachePreferences, distinctId, anonymousId)
|
|
223
223
|
|
|
224
|
-
captureColdStartPushOpenIfNeeded(config)
|
|
224
|
+
captureColdStartPushOpenIfNeeded(config, jsOptedOut = theOptOut)
|
|
225
225
|
} catch (e: Throwable) {
|
|
226
226
|
logError(method, e)
|
|
227
227
|
} finally {
|
|
@@ -488,7 +488,15 @@ class PosthogReactNativePluginModule(
|
|
|
488
488
|
// cold-start tray tap it exists for is the one creation it can never observe here. Read
|
|
489
489
|
// the launch intent directly, then strip the marker so the integration (or a re-run)
|
|
490
490
|
// can't capture the same tap again from this intent object.
|
|
491
|
-
|
|
491
|
+
//
|
|
492
|
+
// jsOptedOut is the consent JS passed into this setup(). The native SDK lets the opt-out it
|
|
493
|
+
// persisted itself win over that value, so after an earlier launch opted in, config.optOut no
|
|
494
|
+
// longer says what JS said and native would capture. A tap JS considers denied is consumed
|
|
495
|
+
// here without being captured, so a later opt-in cannot resurrect it either.
|
|
496
|
+
private fun captureColdStartPushOpenIfNeeded(
|
|
497
|
+
config: PostHogAndroidConfig,
|
|
498
|
+
jsOptedOut: Boolean,
|
|
499
|
+
) {
|
|
492
500
|
if (!config.capturePushNotificationOpened) {
|
|
493
501
|
return
|
|
494
502
|
}
|
|
@@ -500,6 +508,10 @@ class PosthogReactNativePluginModule(
|
|
|
500
508
|
}
|
|
501
509
|
try {
|
|
502
510
|
intent.getStringExtra(GOOGLE_MESSAGE_ID) ?: return
|
|
511
|
+
if (jsOptedOut) {
|
|
512
|
+
intent.removeExtra(GOOGLE_MESSAGE_ID)
|
|
513
|
+
return
|
|
514
|
+
}
|
|
503
515
|
// Unmarshalling extras throws BadParcelableException on a Parcelable class this
|
|
504
516
|
// classloader lacks; read before stripping the marker so a failed read leaves the
|
|
505
517
|
// intent as the native integration expects it.
|
|
@@ -75,3 +75,25 @@ RCT_EXTERN_METHOD(providePushIdentityToken:(NSString)requestId
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
@end
|
|
78
|
+
|
|
79
|
+
#if TARGET_OS_IOS
|
|
80
|
+
#import <UIKit/UIKit.h>
|
|
81
|
+
|
|
82
|
+
// Implemented in PosthogReactNativePlugin.swift.
|
|
83
|
+
@interface PosthogReactNativePlugin (PushNotificationOpenPrewarm)
|
|
84
|
+
+ (void)prewarmPushNotificationOpenCapture;
|
|
85
|
+
@end
|
|
86
|
+
|
|
87
|
+
// A notification tap that cold-launches the app is delivered right after launch, long before JS
|
|
88
|
+
// reaches setup(), and native modules are created lazily, so no module code runs in time. This
|
|
89
|
+
// runs at image load, like the RCT_EXTERN_MODULE registration above, so the host app needs no code.
|
|
90
|
+
__attribute__((constructor)) static void PosthogReactNativePluginObserveLaunch(void)
|
|
91
|
+
{
|
|
92
|
+
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
|
|
93
|
+
object:nil
|
|
94
|
+
queue:nil
|
|
95
|
+
usingBlock:^(__unused NSNotification *notification) {
|
|
96
|
+
[PosthogReactNativePlugin prewarmPushNotificationOpenCapture];
|
|
97
|
+
}];
|
|
98
|
+
}
|
|
99
|
+
#endif
|
|
@@ -47,6 +47,50 @@ private func isReactNativeFatalJsError(_ event: PostHogEvent) -> Bool {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/// Carries the distinct id and consent JS holds into `beforeSend` for the length of
|
|
51
|
+
/// `PostHogSDK.setup(_:)`.
|
|
52
|
+
///
|
|
53
|
+
/// `setup()` installs the push-open integration, which replays a tap that cold-launched the app
|
|
54
|
+
/// synchronously — before `setIdentify` can mirror that id into native storage, so the replayed
|
|
55
|
+
/// event would carry whatever identity the previous launch left behind, and before the JS layer
|
|
56
|
+
/// re-asserts its consent: the native SDK prefers the opt-out it persisted itself, so after an
|
|
57
|
+
/// earlier launch opted in, `config.optOut` no longer says what JS said and the replay is captured
|
|
58
|
+
/// for a user JS considers opted out. posthog-ios offers no seam for either earlier (its storage
|
|
59
|
+
/// manager is created inside `setup()`, with an internal initializer, and disabling the integration
|
|
60
|
+
/// to delay the replay discards the held tap instead of deferring it) — so both are applied on the
|
|
61
|
+
/// way out instead: the id is stamped, and a tap JS denied is dropped, consumed so a later opt-in
|
|
62
|
+
/// cannot resurrect it. Cleared once `setup()` returns, after which every event resolves its
|
|
63
|
+
/// identity and consent from native state as before.
|
|
64
|
+
private final class SetupWindow {
|
|
65
|
+
private let lock = NSLock()
|
|
66
|
+
private var distinctId: String?
|
|
67
|
+
private var jsOptedOut: Bool
|
|
68
|
+
|
|
69
|
+
init(distinctId: String, jsOptedOut: Bool) {
|
|
70
|
+
self.distinctId = distinctId.isEmpty ? nil : distinctId
|
|
71
|
+
self.jsOptedOut = jsOptedOut
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var pendingDistinctId: String? {
|
|
75
|
+
lock.lock()
|
|
76
|
+
defer { lock.unlock() }
|
|
77
|
+
return distinctId
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var dropsPushOpens: Bool {
|
|
81
|
+
lock.lock()
|
|
82
|
+
defer { lock.unlock() }
|
|
83
|
+
return jsOptedOut
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func settled() {
|
|
87
|
+
lock.lock()
|
|
88
|
+
defer { lock.unlock() }
|
|
89
|
+
distinctId = nil
|
|
90
|
+
jsOptedOut = false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
50
94
|
// A nil identity token sends the request unauthenticated, which a project requiring
|
|
51
95
|
// identity verification rejects server-side. Log the reason so that failure is greppable
|
|
52
96
|
// and distinct from a host that deliberately returned nil.
|
|
@@ -69,6 +113,23 @@ public class PosthogReactNativePlugin: RCTEventEmitter {
|
|
|
69
113
|
private var hasPushListeners = false
|
|
70
114
|
private var pushIdentityCompletions: [String: (String?) -> Void] = [:]
|
|
71
115
|
|
|
116
|
+
#if os(iOS)
|
|
117
|
+
/// The one opt-out that reaches both halves of push-open capture, named and read exactly as in
|
|
118
|
+
/// posthog-flutter: `false` skips the launch hook below *and* forces the native config flag off
|
|
119
|
+
/// in setup(), so the JS flag can't re-enable what the plist turned off.
|
|
120
|
+
private static var plistCapturePushNotificationOpened: Bool {
|
|
121
|
+
Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.CAPTURE_PUSH_NOTIFICATION_OPENED") as? Bool ?? true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Runs at launch, before JS config exists, so the Info.plist key is the only way to skip it.
|
|
125
|
+
@objc static func prewarmPushNotificationOpenCapture() {
|
|
126
|
+
guard plistCapturePushNotificationOpened else { return }
|
|
127
|
+
if #available(iOS 14.0, *) {
|
|
128
|
+
PostHogSDK.prewarmPushNotificationOpenCapture()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
#endif
|
|
132
|
+
|
|
72
133
|
public override func supportedEvents() -> [String]! {
|
|
73
134
|
[PosthogReactNativePlugin.pushIdentityEvent]
|
|
74
135
|
}
|
|
@@ -183,10 +244,31 @@ public class PosthogReactNativePlugin: RCTEventEmitter {
|
|
|
183
244
|
}
|
|
184
245
|
#endif
|
|
185
246
|
|
|
247
|
+
let distinctId = sdkOptions["distinctId"] as? String ?? ""
|
|
248
|
+
let anonymousId = sdkOptions["anonymousId"] as? String ?? ""
|
|
249
|
+
let jsOptedOut = sdkOptions["optOut"] as? Bool ?? false
|
|
250
|
+
let setupWindow = SetupWindow(distinctId: distinctId, jsOptedOut: jsOptedOut)
|
|
251
|
+
// Every exit below is past the point where native storage carries the ids, or past a
|
|
252
|
+
// failure that left the SDK disabled, so no path can leave the window armed.
|
|
253
|
+
defer { setupWindow.settled() }
|
|
254
|
+
|
|
186
255
|
// React Native rethrows fatal JS errors natively (RCTFatalException / ExceptionsManager).
|
|
187
256
|
// The JS layer already captured them, so drop the native duplicate.
|
|
188
257
|
config.setBeforeSend { event in
|
|
189
|
-
isReactNativeFatalJsError(event)
|
|
258
|
+
if isReactNativeFatalJsError(event) {
|
|
259
|
+
return nil
|
|
260
|
+
}
|
|
261
|
+
// Only the replayed tap. `setup()` also replays the previous launch's crash report,
|
|
262
|
+
// and that `$exception` carries the distinct id recorded at crash time, which stands.
|
|
263
|
+
if event.event == "$push_notification_opened" {
|
|
264
|
+
if setupWindow.dropsPushOpens {
|
|
265
|
+
return nil
|
|
266
|
+
}
|
|
267
|
+
if let distinctId = setupWindow.pendingDistinctId {
|
|
268
|
+
event.distinctId = distinctId
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return event
|
|
190
272
|
}
|
|
191
273
|
|
|
192
274
|
// Surveys and session replay are iOS-only in posthog-ios, so the APIs below
|
|
@@ -238,15 +320,12 @@ public class PosthogReactNativePlugin: RCTEventEmitter {
|
|
|
238
320
|
}
|
|
239
321
|
#endif
|
|
240
322
|
|
|
241
|
-
let distinctId = sdkOptions["distinctId"] as? String ?? ""
|
|
242
|
-
let anonymousId = sdkOptions["anonymousId"] as? String ?? ""
|
|
243
|
-
|
|
244
323
|
let sdkVersion = sdkOptions["sdkVersion"] as? String ?? ""
|
|
245
324
|
|
|
246
325
|
let flushAt = sdkOptions["flushAt"] as? Int ?? 20
|
|
247
326
|
config.flushAt = flushAt
|
|
248
327
|
|
|
249
|
-
config.optOut =
|
|
328
|
+
config.optOut = jsOptedOut
|
|
250
329
|
// JS owns flags; it tells us when the native preload would be a duplicate. posthog-ios
|
|
251
330
|
// has no remoteConfig switch to mirror — it deprecated the option and always loads.
|
|
252
331
|
config.preloadFeatureFlags = sdkOptions["preloadFeatureFlags"] as? Bool ?? true
|
|
@@ -271,6 +350,13 @@ public class PosthogReactNativePlugin: RCTEventEmitter {
|
|
|
271
350
|
if let capturePushOpened = pushConfig["capturePushNotificationOpened"] as? Bool {
|
|
272
351
|
config.capturePushNotificationOpened = capturePushOpened
|
|
273
352
|
}
|
|
353
|
+
#if os(iOS)
|
|
354
|
+
// Only ever turns capture off: the plist is the app's build-time kill switch, so it wins
|
|
355
|
+
// over the JS flag, while a plist `true` leaves a JS opt-out alone.
|
|
356
|
+
if !PosthogReactNativePlugin.plistCapturePushNotificationOpened {
|
|
357
|
+
config.capturePushNotificationOpened = false
|
|
358
|
+
}
|
|
359
|
+
#endif
|
|
274
360
|
|
|
275
361
|
// Installed only when JS asked for it: an uninvited bridging provider would change
|
|
276
362
|
// how the native SDK handles a 401 on the subscription call.
|