@posthog/react-native-plugin 2.9.0 → 2.9.2

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.
@@ -91,7 +91,7 @@ dependencies {
91
91
  //noinspection GradleDynamicVersion
92
92
  implementation "com.facebook.react:react-native:+"
93
93
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
94
- implementation "com.posthog:posthog-android:3.64.0"
94
+ implementation "com.posthog:posthog-android:3.65.2"
95
95
 
96
96
  testImplementation "junit:junit:4.13.2"
97
97
  }
package/ios/Package.swift CHANGED
@@ -31,7 +31,7 @@ let package = Package(
31
31
  dependencies: reactNativeDependencies + [
32
32
  .package(
33
33
  url: "https://github.com/PostHog/posthog-ios.git",
34
- .upToNextMinor(from: "3.74.0")
34
+ .upToNextMinor(from: "3.75.2")
35
35
  ),
36
36
  ],
37
37
  targets: [
@@ -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) ? nil : 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 = sdkOptions["optOut"] as? Bool ?? false
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/react-native-plugin",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "PostHog React Native plugin for iOS and Android integrations",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
@@ -6,7 +6,7 @@ folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1
6
6
  # Single source of truth for the posthog-ios native dependency version.
7
7
  # Used by both the SPM and CocoaPods resolution paths below; bump this
8
8
  # line when picking up a new posthog-ios release.
9
- posthog_ios_version = '3.74.0'
9
+ posthog_ios_version = '3.75.2'
10
10
 
11
11
  Pod::Spec.new do |s|
12
12
  s.name = "posthog-react-native-plugin"