@usergist/feedback-react-native 0.1.1 → 0.2.0-beta.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 (82) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/EXPO-ACCEPTANCE.md +57 -0
  3. package/EXPO.md +103 -0
  4. package/README.md +10 -2
  5. package/UserGistFeedback.podspec +4 -1
  6. package/UserGistFeedbackExtension.podspec +1 -1
  7. package/android/build.gradle +3 -1
  8. package/android/src/main/AndroidManifest.xml +1 -0
  9. package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +78 -12
  10. package/android/src/main/java/studio/usergist/feedback/UserGistNotificationDismissReceiver.kt +17 -0
  11. package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +46 -5
  12. package/android/src/main/java/studio/usergist/feedback/UserGistPushState.kt +69 -0
  13. package/app.plugin.js +2 -0
  14. package/dist/NativeUserGistPush.d.ts +10 -0
  15. package/dist/NativeUserGistPush.d.ts.map +1 -1
  16. package/dist/NativeUserGistPush.js.map +1 -1
  17. package/dist/Push.d.ts.map +1 -1
  18. package/dist/Push.js +9 -0
  19. package/dist/Push.js.map +1 -1
  20. package/dist/UserGist.d.ts +1 -0
  21. package/dist/UserGist.d.ts.map +1 -1
  22. package/dist/UserGist.js +218 -79
  23. package/dist/UserGist.js.map +1 -1
  24. package/dist/expo/index.d.ts +68 -0
  25. package/dist/expo/index.d.ts.map +1 -0
  26. package/dist/expo/index.js +120 -0
  27. package/dist/expo/index.js.map +1 -0
  28. package/dist/internal/consent.d.ts.map +1 -1
  29. package/dist/internal/consent.js +4 -1
  30. package/dist/internal/consent.js.map +1 -1
  31. package/dist/internal/context.d.ts +1 -1
  32. package/dist/internal/context.d.ts.map +1 -1
  33. package/dist/internal/context.js +1 -1
  34. package/dist/internal/context.js.map +1 -1
  35. package/dist/internal/engine.d.ts.map +1 -1
  36. package/dist/internal/engine.js +6 -1
  37. package/dist/internal/engine.js.map +1 -1
  38. package/dist/internal/events.d.ts +1 -0
  39. package/dist/internal/events.d.ts.map +1 -1
  40. package/dist/internal/events.js +6 -0
  41. package/dist/internal/events.js.map +1 -1
  42. package/dist/internal/push-dedupe.d.ts +3 -0
  43. package/dist/internal/push-dedupe.d.ts.map +1 -0
  44. package/dist/internal/push-dedupe.js +33 -0
  45. package/dist/internal/push-dedupe.js.map +1 -0
  46. package/dist/internal/storage.d.ts +1 -0
  47. package/dist/internal/storage.d.ts.map +1 -1
  48. package/dist/internal/storage.js +1 -0
  49. package/dist/internal/storage.js.map +1 -1
  50. package/dist/internal/transport.d.ts +5 -1
  51. package/dist/internal/transport.d.ts.map +1 -1
  52. package/dist/internal/transport.js +2 -2
  53. package/dist/internal/transport.js.map +1 -1
  54. package/dist/native/events.d.ts +2 -0
  55. package/dist/native/events.d.ts.map +1 -1
  56. package/dist/native/events.js +2 -0
  57. package/dist/native/events.js.map +1 -1
  58. package/dist/native/push-adapter.d.ts +11 -0
  59. package/dist/native/push-adapter.d.ts.map +1 -0
  60. package/dist/native/push-adapter.js +10 -0
  61. package/dist/native/push-adapter.js.map +1 -0
  62. package/dist/native/push-bridge.d.ts +9 -0
  63. package/dist/native/push-bridge.d.ts.map +1 -1
  64. package/dist/native/push-bridge.js +39 -4
  65. package/dist/native/push-bridge.js.map +1 -1
  66. package/dist/ui/requests/RequestsHost.d.ts.map +1 -1
  67. package/dist/ui/requests/RequestsHost.js +8 -0
  68. package/dist/ui/requests/RequestsHost.js.map +1 -1
  69. package/expo-module.config.json +8 -0
  70. package/expo-support/UserGistExpoAppDelegateSubscriber.swift +34 -0
  71. package/expo-support/UserGistFeedbackExpo.podspec +17 -0
  72. package/ios/Extension/UserGistNotificationService.swift +52 -5
  73. package/ios/Shared/UserGistNotificationActions.swift +27 -0
  74. package/ios/Shared/UserGistPushState.swift +110 -0
  75. package/ios/UserGistPush.h +1 -6
  76. package/ios/UserGistPush.mm +18 -0
  77. package/ios/UserGistPushImpl.swift +46 -4
  78. package/ios/UserGistPushInterop.h +5 -0
  79. package/package.json +33 -7
  80. package/plugin/index.cjs +269 -0
  81. package/plugin/index.d.cts +14 -0
  82. package/plugin/templates/UserGistExpoMessagingService.kt +20 -0
@@ -1,3 +1,4 @@
1
+ import { getPushAdapter } from './push-adapter.js';
1
2
  // Runtime bridge to the UserGist native push module.
2
3
  //
3
4
  // Strategy:
@@ -19,14 +20,26 @@ function warnMissing() {
19
20
  return;
20
21
  warned = true;
21
22
  // eslint-disable-next-line no-console
22
- console.warn('[usergist] Native push module not found. Make sure @usergist/feedback-react-native is installed and rebuild the app (`pod install` for iOS, rebuild for Android). Push acquisition is disabled until then.');
23
+ console.warn('[usergist] Native push module not found. Make sure @usergist/feedback-react-native is installed and rebuild the app (`pod install` for iOS, rebuild for Android). Expo Go cannot load this module. Use an Expo development build (npx expo run:ios / run:android) or EAS Build; an OTA update cannot add native code.');
23
24
  }
24
25
  const STUB_RESULT = {
25
26
  granted: false,
26
- status: 'denied',
27
+ status: 'not_determined',
28
+ error: 'native_module_unavailable',
27
29
  platform: Platform.OS === 'ios' ? 'ios' : 'android',
28
30
  };
29
31
  export const UserGistPushNative = {
32
+ async defaultEnvironment() {
33
+ return (await Module?.getPushConfiguration?.())?.environment;
34
+ },
35
+ async syncState(state) {
36
+ if (!Module) {
37
+ warnMissing();
38
+ return;
39
+ }
40
+ const configuration = await Module?.getPushConfiguration?.();
41
+ await Module?.configurePushState?.({ ...state, push: state.push && configuration?.mode !== 'disabled' });
42
+ },
30
43
  isAvailable() {
31
44
  return Module != null;
32
45
  },
@@ -35,6 +48,14 @@ export const UserGistPushNative = {
35
48
  warnMissing();
36
49
  return STUB_RESULT;
37
50
  }
51
+ const configuration = await Module.getPushConfiguration?.();
52
+ if (configuration?.mode === 'disabled')
53
+ return { ...STUB_RESULT, error: 'push_not_configured' };
54
+ const adapter = getPushAdapter();
55
+ if (configuration?.mode === 'expo-notifications' && !adapter)
56
+ return { ...STUB_RESULT, error: 'expo_adapter_required' };
57
+ if (adapter)
58
+ return adapter.enablePush(options);
38
59
  return Module.enablePush(options);
39
60
  },
40
61
  async disablePush() {
@@ -42,16 +63,28 @@ export const UserGistPushNative = {
42
63
  warnMissing();
43
64
  return;
44
65
  }
66
+ const adapter = getPushAdapter();
67
+ if (adapter)
68
+ await adapter.disablePush();
69
+ // Native Expo mode disables only UserGist, never the host APNs/FCM token.
45
70
  await Module.disablePush();
46
71
  },
47
72
  async getPermissionStatus() {
48
- if (!Module)
49
- return 'denied';
73
+ if (!Module) {
74
+ warnMissing();
75
+ return 'not_determined';
76
+ }
77
+ const adapter = getPushAdapter();
78
+ if (adapter)
79
+ return adapter.getPermissionStatus();
50
80
  return Module.getPushPermissionStatus();
51
81
  },
52
82
  async setBadgeCount(count) {
53
83
  if (!Module)
54
84
  return;
85
+ const adapter = getPushAdapter();
86
+ if (adapter)
87
+ await adapter.setBadgeCount(count);
55
88
  await Module.setBadgeCount(count);
56
89
  },
57
90
  async getInitialNotification() {
@@ -89,4 +122,6 @@ export const onTokenReceived = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.TOKEN_RE
89
122
  export const onTokenError = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.TOKEN_ERROR, cb);
90
123
  export const onNotificationReceived = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_RECEIVED, cb);
91
124
  export const onNotificationOpened = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_OPENED, cb);
125
+ export const onNotificationDisplayed = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_DISPLAYED, cb);
126
+ export const onNotificationDismissed = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_DISMISSED, cb);
92
127
  //# sourceMappingURL=push-bridge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"push-bridge.js","sourceRoot":"","sources":["../../src/native/push-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"push-bridge.js","sourceRoot":"","sources":["../../src/native/push-bridge.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RequestsHost.d.ts","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAelD,wBAAgB,YAAY,IAAI,KAAK,CAAC,YAAY,GAAG,IAAI,CAmExD"}
1
+ {"version":3,"file":"RequestsHost.d.ts","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAelD,wBAAgB,YAAY,IAAI,KAAK,CAAC,YAAY,GAAG,IAAI,CAyExD"}
@@ -22,6 +22,8 @@ export function RequestsHost() {
22
22
  useEffect(() => {
23
23
  let unsubBoard = null;
24
24
  let unsubDetail = null;
25
+ let unsubReset = null;
26
+ let unsubDismiss = null;
25
27
  let retryTimer = null;
26
28
  function attach() {
27
29
  try {
@@ -32,6 +34,8 @@ export function RequestsHost() {
32
34
  unsubDetail = bus.on('showRequestDetail', ({ requestId }) => {
33
35
  setView({ kind: 'detail', requestId });
34
36
  });
37
+ unsubReset = bus.on('resetSurfaces', () => setView({ kind: 'closed' }));
38
+ unsubDismiss = bus.on('dismissRequests', () => setView({ kind: 'closed' }));
35
39
  }
36
40
  catch {
37
41
  retryTimer = setTimeout(attach, 250);
@@ -45,6 +49,10 @@ export function RequestsHost() {
45
49
  unsubBoard();
46
50
  if (unsubDetail)
47
51
  unsubDetail();
52
+ if (unsubReset)
53
+ unsubReset();
54
+ if (unsubDismiss)
55
+ unsubDismiss();
48
56
  };
49
57
  }, []);
50
58
  const visible = view.kind !== 'closed';
@@ -1 +1 @@
1
- {"version":3,"file":"RequestsHost.js","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RequestsHost.js","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ {
2
+ "platforms": ["apple"],
3
+ "apple": {
4
+ "podspecPath": "expo-support/UserGistFeedbackExpo.podspec",
5
+ "swiftModuleName": "UserGistFeedbackExpo",
6
+ "appDelegateSubscribers": ["UserGistExpoAppDelegateSubscriber"]
7
+ }
8
+ }
@@ -0,0 +1,34 @@
1
+ import ExpoModulesCore
2
+ import UserGistFeedback
3
+ import UIKit
4
+
5
+ public class UserGistExpoAppDelegateSubscriber: ExpoAppDelegateSubscriber {
6
+ public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
7
+ UserGistPushImpl.shared.prepareExpoLaunch()
8
+ return true
9
+ }
10
+ public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
11
+ UserGistPushImpl.shared.recordToken(deviceToken: token)
12
+ }
13
+ public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
14
+ UserGistPushImpl.shared.recordTokenError(error: error)
15
+ }
16
+ public func applicationDidBecomeActive(_ application: UIApplication) {
17
+ _ = UserGistPushState.updateBadge(count: application.applicationIconBadgeNumber)
18
+ UserGistPushState.retry()
19
+ }
20
+ public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
21
+ if userInfo["usergist_silent"] as? String == "1", let pingId = userInfo["usergist_ping_id"] as? String {
22
+ if let recipient = userInfo["usergist_anonymous_id"] as? String,
23
+ recipient != UserGistPushState.snapshot["anonymousId"] as? String {
24
+ completionHandler(.noData)
25
+ return
26
+ }
27
+ UserGistPushState.receipt("silent-ack", id: pingId) { success in
28
+ completionHandler(success ? .newData : .failed)
29
+ }
30
+ return
31
+ }
32
+ completionHandler(.noData)
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+ package = JSON.parse(File.read(File.join(__dir__, '../package.json')))
3
+ Pod::Spec.new do |s|
4
+ s.name = 'UserGistFeedbackExpo'
5
+ s.version = package['version']
6
+ s.summary = 'Expo lifecycle integration for UserGist'
7
+ s.homepage = package['homepage']
8
+ s.license = { :type => 'MIT', :file => '../LICENSE' }
9
+ s.authors = { 'UserGist' => 'engineering@usergist.com' }
10
+ s.source = { :git => 'https://github.com/Future-Picnic/usergist-js.git' }
11
+ s.platforms = { :ios => '15.1' }
12
+ s.swift_version = '5.0'
13
+ s.static_framework = true
14
+ s.source_files = '*.swift'
15
+ s.dependency 'ExpoModulesCore'
16
+ s.dependency 'UserGistFeedback', package['version']
17
+ end
@@ -57,6 +57,21 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
57
57
  self.bestAttempt = request.content.mutableCopy() as? UNMutableNotificationContent
58
58
 
59
59
  let userInfo = request.content.userInfo
60
+ let pushState = UserGistPushState.snapshot
61
+ if userInfo["usergist"] == nil && userInfo["usergist_silent"] as? String != "1" {
62
+ finish(request.content)
63
+ return
64
+ }
65
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled {
66
+ finish(userInfo["usergist"] == nil ? request.content : UNNotificationContent())
67
+ return
68
+ }
69
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true,
70
+ let recipient = userInfo["usergist_anonymous_id"] as? String,
71
+ recipient != pushState["anonymousId"] as? String {
72
+ finish(UNNotificationContent())
73
+ return
74
+ }
60
75
 
61
76
  // ---------- Silent reachability ping ----------
62
77
  if (userInfo["usergist_silent"] as? String) == "1" {
@@ -72,11 +87,25 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
72
87
  finish(request.content)
73
88
  return
74
89
  }
90
+ if bestAttempt.userInfo["usergist_anonymous_id"] == nil, let anonymousId = pushState["anonymousId"] as? String {
91
+ bestAttempt.userInfo["usergist_anonymous_id"] = anonymousId
92
+ }
93
+ if userInfo["usergist_badge_increment"] as? Bool == true {
94
+ bestAttempt.badge = UserGistPushState.updateBadge(increment: true)
95
+ } else if let badge = bestAttempt.badge {
96
+ _ = UserGistPushState.updateBadge(count: badge.intValue)
97
+ }
98
+ let actionsReady = DispatchGroup()
99
+ actionsReady.enter()
100
+ UserGistNotificationActions.configure(bestAttempt) { actionsReady.leave() }
75
101
 
76
102
  // ---------- Delivered beacon (direct, with App Group fallback) ----------
77
103
  if let deliveryId = extractDeliveryId(from: userInfo) {
78
- UserGistBeaconClient.delivered(deliveryId: deliveryId)
79
- UserGistDeliveryLedger.recordDelivered(deliveryId: deliveryId)
104
+ actionsReady.enter()
105
+ UserGistBeaconClient.delivered(deliveryId: deliveryId) { actionsReady.leave() }
106
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool != true {
107
+ UserGistDeliveryLedger.recordDelivered(deliveryId: deliveryId)
108
+ }
80
109
  }
81
110
 
82
111
  if let subtitle = userInfo["subtitle"] as? String {
@@ -87,7 +116,7 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
87
116
  let imageUrlString = extractImageUrl(from: userInfo)
88
117
 
89
118
  guard let raw = imageUrlString, let imageUrl = URL(string: raw) else {
90
- finish(bestAttempt)
119
+ actionsReady.notify(queue: .main) { self.finish(bestAttempt) }
91
120
  return
92
121
  }
93
122
 
@@ -95,7 +124,7 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
95
124
  if let attachment = attachment {
96
125
  bestAttempt.attachments = [attachment]
97
126
  }
98
- self.finish(bestAttempt)
127
+ actionsReady.notify(queue: .main) { self.finish(bestAttempt) }
99
128
  }
100
129
  }
101
130
 
@@ -116,6 +145,15 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
116
145
  let handler = contentHandler
117
146
  contentHandler = nil
118
147
  completionLock.unlock()
148
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true,
149
+ content.userInfo["usergist"] != nil {
150
+ let state = UserGistPushState.snapshot
151
+ if state["push"] as? Bool != true ||
152
+ (content.userInfo["usergist_anonymous_id"] as? String).map({ $0 != state["anonymousId"] as? String }) == true {
153
+ handler?(UNNotificationContent())
154
+ return
155
+ }
156
+ }
119
157
  handler?(content)
120
158
  }
121
159
 
@@ -173,7 +211,12 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
173
211
  /// + URLSession's background-friendly default config.
174
212
  enum UserGistBeaconClient {
175
213
 
176
- static func delivered(deliveryId: String) {
214
+ static func delivered(deliveryId: String, completion: @escaping () -> Void = {}) {
215
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true {
216
+ UserGistPushState.receipt("delivered", id: deliveryId) { _ in completion() }
217
+ return
218
+ }
219
+ defer { completion() }
177
220
  guard !deliveryId.isEmpty else { return }
178
221
  guard let writeKey = UserGistNSEConfig.writeKey,
179
222
  let apiUrl = UserGistNSEConfig.apiUrl else { return }
@@ -192,6 +235,10 @@ enum UserGistBeaconClient {
192
235
  }
193
236
 
194
237
  static func silentAck(pingId: String, completion: @escaping () -> Void) {
238
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true {
239
+ UserGistPushState.receipt("silent-ack", id: pingId) { _ in completion() }
240
+ return
241
+ }
195
242
  guard !pingId.isEmpty else { return completion() }
196
243
  guard let writeKey = UserGistNSEConfig.writeKey,
197
244
  let apiUrl = UserGistNSEConfig.apiUrl,
@@ -0,0 +1,27 @@
1
+ import Foundation
2
+ import CryptoKit
3
+ import UserNotifications
4
+
5
+ enum UserGistNotificationActions {
6
+ static func configure(_ content: UNMutableNotificationContent, completion: @escaping () -> Void) {
7
+ guard let payload = content.userInfo["usergist"] as? [String: Any] else { completion(); return }
8
+ let buttons = payload["actionButtons"] as? [[String: Any]] ?? []
9
+ let labels = buttons.prefix(4).enumerated().compactMap { index, button -> (Int, String)? in
10
+ guard let label = button["label"] as? String, !label.isEmpty else { return nil }
11
+ return (index, label)
12
+ }
13
+ let encoded = (try? JSONSerialization.data(withJSONObject: labels.map { [$0.0, $0.1] as [Any] })) ?? Data()
14
+ let suffix = SHA256.hash(data: encoded).map { String(format: "%02x", $0) }.joined()
15
+ let identifier = "USERGIST_" + suffix
16
+ let actions = labels.map { UNNotificationAction(identifier: "usergist_action_\($0.0)", title: $0.1, options: [.foreground]) }
17
+ let category = UNNotificationCategory(identifier: identifier, actions: actions, intentIdentifiers: [], options: [.customDismissAction])
18
+ let center = UNUserNotificationCenter.current()
19
+ center.getNotificationCategories { categories in
20
+ var merged = categories
21
+ merged.insert(category)
22
+ center.setNotificationCategories(merged)
23
+ content.categoryIdentifier = identifier
24
+ completion()
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,110 @@
1
+ import Foundation
2
+ import CryptoKit
3
+ import Darwin
4
+
5
+ /// Shared by the app and NSE. Each receipt has its own atomic file, avoiding
6
+ /// read/modify/write races between the two processes. No subject credentials.
7
+ public enum UserGistPushState {
8
+ private static var group: String? { Bundle.main.object(forInfoDictionaryKey: "UserGistAppGroup") as? String }
9
+ private static var root: URL? {
10
+ guard let group = group else { return nil }
11
+ return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group)?.appendingPathComponent("usergist", isDirectory: true)
12
+ }
13
+ private static var stateURL: URL? { root?.appendingPathComponent("state.json") }
14
+ public static var snapshot: [String: Any] {
15
+ guard let url = stateURL, let data = try? Data(contentsOf: url),
16
+ let state = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
17
+ return state
18
+ }
19
+ public static var enabled: Bool { snapshot["push"] as? Bool == true }
20
+ public static func updateBadge(count: Int? = nil, increment: Bool = false) -> NSNumber? {
21
+ guard let root = root else { return count.map { NSNumber(value: max(0, $0)) } }
22
+ do { try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) } catch { return nil }
23
+ let descriptor = Darwin.open(root.appendingPathComponent("badge.lock").path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR)
24
+ guard descriptor >= 0 else { return nil }
25
+ defer { Darwin.close(descriptor) }
26
+ guard flock(descriptor, LOCK_EX) == 0 else { return nil }
27
+ defer { flock(descriptor, LOCK_UN) }
28
+ let file = root.appendingPathComponent("badge.json")
29
+ let previous = (try? String(contentsOf: file, encoding: .utf8)).flatMap(Int.init) ?? 0
30
+ let next = max(0, count ?? (increment ? min(previous, Int.max - 1) + 1 : previous))
31
+ do { try Data(String(next).utf8).write(to: file, options: .atomic); return NSNumber(value: next) }
32
+ catch { return nil }
33
+ }
34
+ private static func receiptsFolder(_ state: [String: Any]) -> URL? {
35
+ let scope = "\(state["writeKey"] as? String ?? ""):\(state["anonymousId"] as? String ?? "")"
36
+ let hash = SHA256.hash(data: Data(scope.utf8)).map { String(format: "%02x", $0) }.joined()
37
+ return root?.appendingPathComponent("receipts", isDirectory: true).appendingPathComponent(hash, isDirectory: true)
38
+ }
39
+
40
+ public static func configure(_ state: [String: Any]) throws {
41
+ guard let root = root, let url = stateURL else { return }
42
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
43
+ try JSONSerialization.data(withJSONObject: state).write(to: url, options: .atomic)
44
+ if state["push"] as? Bool == true { retry() }
45
+ }
46
+ public static func disable() {
47
+ var state = snapshot
48
+ state["push"] = false
49
+ try? configure(state)
50
+ }
51
+
52
+ public static func receipt(_ kind: String, id: String, completion: @escaping (Bool) -> Void = { _ in }) {
53
+ let state = snapshot
54
+ guard state["push"] as? Bool == true, !id.isEmpty, let folder = receiptsFolder(state),
55
+ let key = state["writeKey"] as? String,
56
+ let api = state["apiUrl"] as? String,
57
+ let anonymousId = state["anonymousId"] as? String else { completion(false); return }
58
+ do {
59
+ try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
60
+ let now = ISO8601DateFormatter().string(from: Date())
61
+ let body: [String: Any] = kind == "silent-ack"
62
+ ? ["pingId": id, "anonymousId": anonymousId, "receivedAt": now]
63
+ : ["deliveryId": id, "occurredAt": now]
64
+ let record: [String: Any] = ["kind": kind, "body": body, "writeKey": key, "apiUrl": api, "anonymousId": anonymousId]
65
+ let file = folder.appendingPathComponent(UUID().uuidString + ".json")
66
+ try JSONSerialization.data(withJSONObject: record).write(to: file, options: .atomic)
67
+ send(file, completion: completion)
68
+ } catch { completion(false) }
69
+ }
70
+ public static func retry() {
71
+ guard let folder = receiptsFolder(snapshot),
72
+ let files = try? FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) else { return }
73
+ files.prefix(500).forEach { send($0) }
74
+ }
75
+ private static func send(_ file: URL, completion: @escaping (Bool) -> Void = { _ in }) {
76
+ guard let data = try? Data(contentsOf: file),
77
+ let record = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
78
+ let key = record["writeKey"] as? String, let api = record["apiUrl"] as? String,
79
+ let kind = record["kind"] as? String, let body = record["body"] as? [String: Any],
80
+ let base = URL(string: api), ["https", "http"].contains(base.scheme ?? "") else { completion(false); return }
81
+ let state = snapshot
82
+ // Never replay old identity receipts with a new session's configuration.
83
+ guard state["push"] as? Bool == true, state["writeKey"] as? String == key,
84
+ state["anonymousId"] as? String == record["anonymousId"] as? String else { completion(false); return }
85
+ var request = URLRequest(url: base.appendingPathComponent("v1/sdk/push/" + kind), timeoutInterval: 5)
86
+ request.httpMethod = "POST"
87
+ request.setValue("Bearer " + key, forHTTPHeaderField: "Authorization")
88
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
89
+ request.httpBody = try? JSONSerialization.data(withJSONObject: body)
90
+ let lock = NSLock()
91
+ var completed = false
92
+ let finish: (Bool) -> Void = { success in
93
+ lock.lock()
94
+ guard !completed else { lock.unlock(); return }
95
+ completed = true
96
+ lock.unlock()
97
+ completion(success)
98
+ }
99
+ URLSession.shared.dataTask(with: request) { _, response, error in
100
+ let success = error == nil && (200..<300).contains((response as? HTTPURLResponse)?.statusCode ?? 0)
101
+ if success {
102
+ try? FileManager.default.removeItem(at: file)
103
+ }
104
+ finish(success)
105
+ }.resume()
106
+ // Keep the extension/background task alive for the acknowledgement attempt,
107
+ // while retaining a bounded completion even if the connection stalls.
108
+ DispatchQueue.global().asyncAfter(deadline: .now() + 5.5) { finish(false) }
109
+ }
110
+ }
@@ -1,6 +1,7 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTEventEmitter.h>
3
3
  #import <Foundation/Foundation.h>
4
+ #import "UserGistPushInterop.h"
4
5
 
5
6
  // Public Objective-C bridge. The actual implementation lives in
6
7
  // UserGistPushImpl.swift; this class is a thin shim that React Native's
@@ -8,9 +9,3 @@
8
9
  // or a TurboModule registration).
9
10
  @interface UserGistPush : RCTEventEmitter <RCTBridgeModule>
10
11
  @end
11
-
12
- // Exposed by UserGistPushSwizzle.m. Returns whoever last tried to install
13
- // themselves as the UNUserNotificationCenter delegate (FirebaseAnalytics,
14
- // OneSignal, etc.). The Swift delegate uses this to chain didReceive /
15
- // willPresent so other SDKs' analytics still fire while we keep ownership.
16
- NSObject * _Nullable _UserGistGetLastForeignDelegate(void);
@@ -44,6 +44,8 @@ RCT_EXPORT_MODULE(UserGistPush)
44
44
  @"UserGistPush:tokenError",
45
45
  @"UserGistPush:notificationReceived",
46
46
  @"UserGistPush:notificationOpened",
47
+ @"UserGistPush:notificationDisplayed",
48
+ @"UserGistPush:notificationDismissed",
47
49
  ];
48
50
  }
49
51
 
@@ -56,6 +58,22 @@ RCT_EXPORT_MODULE(UserGistPush)
56
58
  }
57
59
 
58
60
  // MARK: - Exported methods
61
+ RCT_EXPORT_METHOD(getPushConfiguration:(RCTPromiseResolveBlock)resolve
62
+ rejecter:(RCTPromiseRejectBlock)reject) {
63
+ NSString *mode = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UserGistPushMode"];
64
+ NSString *environment = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UserGistApsEnvironment"];
65
+ NSMutableDictionary *result = [NSMutableDictionary new];
66
+ if (mode) result[@"mode"] = mode;
67
+ if (environment) result[@"environment"] = [environment isEqualToString:@"production"] ? @"production" : @"sandbox";
68
+ resolve(result);
69
+ }
70
+
71
+ RCT_EXPORT_METHOD(configurePushState:(NSDictionary *)state
72
+ resolver:(RCTPromiseResolveBlock)resolve
73
+ rejecter:(RCTPromiseRejectBlock)reject) {
74
+ [UserGistPushImpl.shared configurePushState:state resolver:resolve rejecter:reject];
75
+ }
76
+
59
77
 
60
78
  RCT_EXPORT_METHOD(enablePush:(NSDictionary *)options
61
79
  resolver:(RCTPromiseResolveBlock)resolve
@@ -69,16 +69,37 @@ public final class UserGistPushImpl: NSObject {
69
69
  }
70
70
  }
71
71
 
72
+ @objc public func configurePushState(_ state: [String: Any], resolver: @escaping (Any?) -> Void, rejecter: @escaping (String, String, Error?) -> Void) {
73
+ do { try UserGistPushState.configure(state); resolver(nil) }
74
+ catch { rejecter("push_state_failed", error.localizedDescription, error) }
75
+ }
76
+
77
+ /// Restore notification callbacks before iOS delivers a cold-start response.
78
+ /// This only restores an existing opt-in; it does not acquire permission or
79
+ /// register a token. Expo Notifications retains ownership in coexistence mode.
80
+ public func prepareExpoLaunch() {
81
+ guard Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true,
82
+ Bundle.main.object(forInfoDictionaryKey: "UserGistPushMode") as? String == "automatic",
83
+ UserGistPushState.enabled else { return }
84
+ Self.installUNDelegateSwizzleIfNeeded()
85
+ }
86
+
72
87
  // MARK: - Exported (called from UserGistPush.mm)
73
88
 
74
89
  @objc public func enablePush(options: [String: Any],
75
90
  resolver: @escaping (Any?) -> Void,
76
91
  rejecter: @escaping (String, String, Error?) -> Void) {
77
92
  NSLog("[UserGistPush] enablePush called")
78
- let installDelegateProxy = options["installDelegateProxy"] as? Bool ?? true
93
+ let expo = Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true
94
+ let mode = Bundle.main.object(forInfoDictionaryKey: "UserGistPushMode") as? String
95
+ if expo && mode == "disabled" {
96
+ resolver(["granted": false, "status": "not_determined", "platform": "ios", "error": "push_not_configured"])
97
+ return
98
+ }
99
+ let installDelegateProxy = mode == "expo-notifications" ? false : (options["installDelegateProxy"] as? Bool ?? true)
79
100
  if installDelegateProxy {
80
101
  Self.installUNDelegateSwizzleIfNeeded()
81
- if let swizzler: AnyClass = NSClassFromString("UserGistPushSwizzle") {
102
+ if !expo, let swizzler: AnyClass = NSClassFromString("UserGistPushSwizzle") {
82
103
  let selector = NSSelectorFromString("installIfNeeded")
83
104
  let target = swizzler as AnyObject
84
105
  if target.responds(to: selector) {
@@ -118,7 +139,10 @@ public final class UserGistPushImpl: NSObject {
118
139
  @objc public func disablePush(resolver: @escaping (Any?) -> Void,
119
140
  rejecter: @escaping (String, String, Error?) -> Void) {
120
141
  DispatchQueue.main.async {
121
- UIApplication.shared.unregisterForRemoteNotifications()
142
+ UserGistPushState.disable()
143
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool != true {
144
+ UIApplication.shared.unregisterForRemoteNotifications()
145
+ }
122
146
  self.stateLock.lock()
123
147
  self.lastApnsTokenHex = nil
124
148
  self.stateLock.unlock()
@@ -148,6 +172,7 @@ public final class UserGistPushImpl: NSObject {
148
172
  DispatchQueue.main.async {
149
173
  let center = UNUserNotificationCenter.current()
150
174
  let n = Int(count)
175
+ _ = UserGistPushState.updateBadge(count: n)
151
176
  if #available(iOS 16.0, *) {
152
177
  center.setBadgeCount(n) { error in
153
178
  if let error = error {
@@ -190,11 +215,23 @@ public final class UserGistPushImpl: NSObject {
190
215
  }
191
216
 
192
217
  func recordNotificationReceived(userInfo: [AnyHashable: Any]) {
218
+ guard userInfo["usergist"] != nil else { return }
219
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled { return }
193
220
  let normalized = normalizeUserInfo(userInfo)
194
221
  emit(name: "UserGistPush:notificationReceived", body: normalized)
195
222
  }
196
223
 
197
224
  func recordNotificationOpened(userInfo: [AnyHashable: Any], actionIdentifier: String?) {
225
+ guard userInfo["usergist"] != nil else { return }
226
+ if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled { return }
227
+ if let scope = userInfo["usergist_anonymous_id"] as? String, scope != UserGistPushState.snapshot["anonymousId"] as? String { return }
228
+ if actionIdentifier == UNNotificationDismissActionIdentifier {
229
+ if let payload = userInfo["usergist"] as? [String: Any], let id = payload["deliveryId"] as? String {
230
+ UserGistPushState.receipt("dismissed", id: id)
231
+ }
232
+ emit(name: "UserGistPush:notificationDismissed", body: normalizeUserInfo(userInfo))
233
+ return
234
+ }
198
235
  var body = normalizeUserInfo(userInfo)
199
236
  if let actionIdentifier = actionIdentifier, actionIdentifier != "com.apple.UNNotificationDefaultActionIdentifier" {
200
237
  body["actionIdentifier"] = actionIdentifier
@@ -307,7 +344,7 @@ public final class UserGistPushImpl: NSObject {
307
344
  NotificationCenter.default.addObserver(
308
345
  forName: UIApplication.didBecomeActiveNotification,
309
346
  object: nil,
310
- queue: .main,
347
+ queue: .main
311
348
  ) { _ in
312
349
  UserGistUNDelegateSwizzler.reinstallIfHijacked()
313
350
  }
@@ -368,12 +405,17 @@ public final class UserGistPushImpl: NSObject {
368
405
  func userNotificationCenter(_ center: UNUserNotificationCenter,
369
406
  willPresent notification: UNNotification,
370
407
  withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
408
+ if notification.request.content.userInfo["usergist"] != nil && Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled {
409
+ completionHandler([])
410
+ return
411
+ }
371
412
  UserGistPushImpl.shared.recordNotificationReceived(userInfo: notification.request.content.userInfo)
372
413
  if let existing = chainTarget(),
373
414
  existing.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:))) {
374
415
  existing.userNotificationCenter?(center, willPresent: notification, withCompletionHandler: completionHandler)
375
416
  return
376
417
  }
418
+ if notification.request.content.userInfo["usergist"] == nil { completionHandler([]); return }
377
419
  if #available(iOS 14.0, *) {
378
420
  completionHandler([.banner, .list, .sound, .badge])
379
421
  } else {
@@ -0,0 +1,5 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ // Swift needs this C entry point, but must not re-export React's bridge
4
+ // declarations through UserGistFeedback's public umbrella header.
5
+ FOUNDATION_EXPORT NSObject * _Nullable _UserGistGetLastForeignDelegate(void);