appambit-push-notifications 0.3.1 → 1.0.1

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 (28) hide show
  1. package/AppAmbitSdkPushNotifications.podspec +14 -4
  2. package/README.md +184 -105
  3. package/android/build.gradle +21 -3
  4. package/android/src/main/AndroidManifest.xml +107 -1
  5. package/android/src/main/java/com/appambitpushnotifications/AppAmbitContextHolder.kt +22 -0
  6. package/android/src/main/java/com/appambitpushnotifications/AppAmbitHeadlessService.kt +177 -0
  7. package/android/src/main/java/com/appambitpushnotifications/AppAmbitInitProvider.kt +73 -0
  8. package/android/src/main/java/com/appambitpushnotifications/AppAmbitMessagingService.kt +12 -0
  9. package/android/src/main/java/com/appambitpushnotifications/AppAmbitNotificationSerializer.kt +88 -0
  10. package/android/src/main/java/com/appambitpushnotifications/AppAmbitPayloadUtils.kt +59 -0
  11. package/android/src/main/java/com/appambitpushnotifications/AppAmbitPushEventEmitter.kt +100 -0
  12. package/android/src/main/java/com/appambitpushnotifications/AppAmbitRNServiceExtension.kt +75 -0
  13. package/android/src/main/java/com/appambitpushnotifications/AppAmbitRemoteMessageStore.kt +26 -0
  14. package/android/src/main/java/com/appambitpushnotifications/AppambitPushNotificationsModule.kt +377 -76
  15. package/ios/AppAmbitNotificationSwizzler.m +290 -0
  16. package/ios/AppAmbitPushWrapper.swift +165 -25
  17. package/ios/AppAmbitRNNotificationService.swift +46 -0
  18. package/ios/AppambitPushNotifications.mm +264 -10
  19. package/lib/module/NativeAppambitPushNotifications.js.map +1 -1
  20. package/lib/module/index.js +46 -10
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/typescript/src/NativeAppambitPushNotifications.d.ts +2 -1
  23. package/lib/typescript/src/NativeAppambitPushNotifications.d.ts.map +1 -1
  24. package/lib/typescript/src/index.d.ts +32 -6
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/NativeAppambitPushNotifications.ts +7 -1
  28. package/src/index.tsx +93 -20
@@ -1,12 +1,67 @@
1
1
  #import "AppambitPushNotifications.h"
2
2
  #import <AppAmbitSdkPushNotifications-Swift.h>
3
3
 
4
+ // Defined in AppAmbitNotificationSwizzler.m — call when JS background handler resolves
5
+ extern "C" void AppAmbitNotifyJSBackgroundHandlerCompleted(void);
6
+
7
+ static NSString * const kPushEnabledStateKey = @"appambit_push_enabled_state";
8
+ static NSString * const kPushHasStateKey = @"appambit_push_has_enabled_state";
9
+ // Pending consumer-sync intent: the last value the user toggled that has not yet
10
+ // been confirmed as delivered to the backend. Replayed when connectivity returns.
11
+ static NSString * const kPushPendingKey = @"appambit_push_pending_sync";
12
+ static NSString * const kPushPendingValueKey = @"appambit_push_pending_value";
13
+
4
14
  @implementation AppambitPushNotifications {
5
15
  BOOL _hasListeners;
16
+ BOOL _sdkListenerInstalled;
17
+ NSMutableArray<NSDictionary *> *_pendingBackgroundEvents;
18
+ NSMutableArray<NSDictionary *> *_pendingOpenedEvents;
19
+ NSMutableArray<NSDictionary *> *_pendingForegroundEvents;
6
20
  }
7
21
 
8
22
  RCT_EXPORT_MODULE(AppambitPushNotifications)
9
23
 
24
+ - (instancetype)init {
25
+ self = [super init];
26
+ if (self) {
27
+ _pendingBackgroundEvents = [NSMutableArray new];
28
+ _pendingOpenedEvents = [NSMutableArray new];
29
+ _pendingForegroundEvents = [NSMutableArray new];
30
+ NSArray<NSDictionary *> *earlyBackground = [AppAmbitPushWrapper getAndClearPendingBackgroundPayloads];
31
+ if (earlyBackground.count > 0) {
32
+ [_pendingBackgroundEvents addObjectsFromArray:earlyBackground];
33
+ }
34
+
35
+ NSArray<NSDictionary *> *earlyOpened = [AppAmbitPushWrapper getAndClearPendingOpenedPayloads];
36
+ if (earlyOpened.count > 0) {
37
+ [_pendingOpenedEvents addObjectsFromArray:earlyOpened];
38
+ }
39
+
40
+ [[NSNotificationCenter defaultCenter] addObserver:self
41
+ selector:@selector(handleBackgroundNotification:)
42
+ name:@"AppAmbit_onBackgroundNotification"
43
+ object:nil];
44
+
45
+ [[NSNotificationCenter defaultCenter] addObserver:self
46
+ selector:@selector(handleOpenedNotification:)
47
+ name:@"AppAmbit_onOpenedNotification"
48
+ object:nil];
49
+
50
+ [[NSNotificationCenter defaultCenter] addObserver:self
51
+ selector:@selector(handleAppBecameActive:)
52
+ name:UIApplicationDidBecomeActiveNotification
53
+ object:nil];
54
+
55
+ // Replay any deferred consumer sync as soon as the network comes back.
56
+ [[NSNotificationCenter defaultCenter] addObserver:self
57
+ selector:@selector(handleNetworkAvailable:)
58
+ name:@"AppAmbit_networkAvailable"
59
+ object:nil];
60
+ [AppAmbitPushWrapper startNetworkMonitor];
61
+ }
62
+ return self;
63
+ }
64
+
10
65
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
11
66
  (const facebook::react::ObjCTurboModule::InitParams &)params
12
67
  {
@@ -16,20 +71,131 @@ RCT_EXPORT_MODULE(AppambitPushNotifications)
16
71
  // MARK: - Event Emitter
17
72
 
18
73
  - (NSArray<NSString *> *)supportedEvents {
19
- return @[@"onNotificationReceived"];
74
+ return @[
75
+ @"AppAmbit_onForegroundNotification",
76
+ @"AppAmbit_onBackgroundNotification",
77
+ @"AppAmbit_onOpenedNotification"
78
+ ];
20
79
  }
21
80
 
22
81
  - (void)startObserving {
23
82
  _hasListeners = YES;
24
83
  }
25
84
 
85
+ // Called lazily the first time JS subscribes to any event — mirrors Flutter's
86
+ // installNotificationListenerIfNeeded. At this point _hasListeners is already
87
+ // YES (set by startObserving via [super addListener:]) so SDK cold-start replay
88
+ // can be sent immediately, exactly like Flutter's DispatchQueue.main.async trick.
89
+ - (void)installSDKListenerIfNeeded {
90
+ if (_sdkListenerInstalled) return;
91
+ _sdkListenerInstalled = YES;
92
+
93
+ __weak AppambitPushNotifications *weakSelf = self;
94
+ [AppAmbitPushWrapper setNotificationListener:^(NSDictionary * _Nonnull payload, NSInteger state) {
95
+ AppambitPushNotifications *strongSelf = weakSelf;
96
+ if (!strongSelf) return;
97
+
98
+ if (state == 0) {
99
+ // Foreground
100
+ if (strongSelf->_hasListeners) {
101
+ [strongSelf sendEventWithName:@"AppAmbit_onForegroundNotification" body:payload];
102
+ } else {
103
+ [strongSelf->_pendingForegroundEvents addObject:payload];
104
+ }
105
+ } else {
106
+ // Opened/tapped — queue if app is still transitioning; applicationDidBecomeActive: flushes.
107
+ // Do NOT call isDuplicateOpenedPayload: here — it marks the payload as seen even when
108
+ // _hasListeners is NO, which would cause the subsequent flush in addListener: to skip it.
109
+ BOOL appIsActive = [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
110
+ if (strongSelf->_hasListeners && appIsActive) {
111
+ if (![strongSelf isDuplicateOpenedPayload:payload]) {
112
+ [strongSelf sendEventWithName:@"AppAmbit_onOpenedNotification" body:payload];
113
+ }
114
+ } else {
115
+ [strongSelf->_pendingOpenedEvents addObject:payload];
116
+ }
117
+ }
118
+ }];
119
+ }
120
+
121
+ - (void)handleBackgroundNotification:(NSNotification *)notification {
122
+ if (!notification.userInfo) return;
123
+ if (_hasListeners) {
124
+ [self sendEventWithName:@"AppAmbit_onBackgroundNotification" body:notification.userInfo];
125
+ } else {
126
+ [_pendingBackgroundEvents addObject:notification.userInfo];
127
+ }
128
+ }
129
+
130
+ - (void)handleAppBecameActive:(NSNotification *)notification {
131
+ // The SDK has had time to finish its async init (and register the device token)
132
+ // by the time the app becomes active, so this is a safe place to replay a
133
+ // deferred consumer sync (covers "toggle offline → reopen online").
134
+ [self flushPendingConsumerSync];
135
+
136
+ if (!_hasListeners || _pendingOpenedEvents.count == 0) return;
137
+ NSArray<NSDictionary *> *toSend = [_pendingOpenedEvents copy];
138
+ [_pendingOpenedEvents removeAllObjects];
139
+ for (NSDictionary *payload in toSend) {
140
+ if (![self isDuplicateOpenedPayload:payload]) {
141
+ [self sendEventWithName:@"AppAmbit_onOpenedNotification" body:payload];
142
+ }
143
+ }
144
+ }
145
+
146
+ - (void)handleNetworkAvailable:(NSNotification *)notification {
147
+ [self flushPendingConsumerSync];
148
+ }
149
+
150
+ - (void)handleOpenedNotification:(NSNotification *)notification {
151
+ if (!notification.userInfo) return;
152
+ // Only call isDuplicateOpenedPayload: when actually sending to JS.
153
+ // Calling it when _hasListeners is NO would mark the payload as seen before it's
154
+ // delivered, causing the addListener: flush to skip it (the payload never reaches JS).
155
+ if (_hasListeners) {
156
+ if (![self isDuplicateOpenedPayload:notification.userInfo]) {
157
+ [self sendEventWithName:@"AppAmbit_onOpenedNotification" body:notification.userInfo];
158
+ }
159
+ } else {
160
+ [_pendingOpenedEvents addObject:notification.userInfo];
161
+ }
162
+ }
163
+
164
+ - (BOOL)isDuplicateOpenedPayload:(NSDictionary *)payload {
165
+ static NSMutableArray<NSDictionary *> *sentPayloads = nil;
166
+ static dispatch_once_t onceToken;
167
+ dispatch_once(&onceToken, ^{
168
+ sentPayloads = [NSMutableArray new];
169
+ });
170
+
171
+ for (NSDictionary *sent in sentPayloads) {
172
+ if ([sent isEqualToDictionary:payload]) {
173
+ return YES;
174
+ }
175
+ }
176
+
177
+ [sentPayloads addObject:payload];
178
+ if (sentPayloads.count > 10) {
179
+ [sentPayloads removeObjectAtIndex:0];
180
+ }
181
+ return NO;
182
+ }
183
+
26
184
  - (void)stopObserving {
27
185
  _hasListeners = NO;
28
186
  }
29
187
 
188
+ - (void)dealloc {
189
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
190
+ }
191
+
30
192
  // MARK: - TurboModule Methods
31
193
 
32
194
  - (void)start {
195
+ // Initialize the SDK. Do NOT call flushPendingSyncIfNeeded here — PushNotifications.start()
196
+ // registers the device token asynchronously, so getCurrentToken() returns nil immediately
197
+ // after start(), causing updateConsumer to fail. handleAppBecameActive: fires after the SDK
198
+ // has had time to complete its async init and is the correct place to flush pending sync.
33
199
  [AppAmbitPushWrapper start];
34
200
  }
35
201
 
@@ -40,31 +206,119 @@ RCT_EXPORT_MODULE(AppambitPushNotifications)
40
206
  - (void)requestNotificationPermissionWithResult:(RCTPromiseResolveBlock)resolve
41
207
  reject:(RCTPromiseRejectBlock)reject {
42
208
  [AppAmbitPushWrapper requestNotificationPermissionWithListener:^(BOOL granted) {
209
+ if (granted) {
210
+ // Cache immediately so hasNotificationPermission() returns true on next
211
+ // restart even if the system permission resets (e.g. simulator reinstall).
212
+ [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"appambit_push_has_permission"];
213
+ [[NSUserDefaults standardUserDefaults] synchronize];
214
+ }
43
215
  resolve(@(granted));
44
216
  }];
45
217
  }
46
218
 
47
219
  - (void)setNotificationsEnabled:(BOOL)enabled {
48
- [AppAmbitPushWrapper setNotificationsEnabled:enabled];
220
+ NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
221
+ // 1. Persist the user's intended state for UI consistency across restarts.
222
+ [ud setBool:enabled forKey:kPushEnabledStateKey];
223
+ [ud setBool:YES forKey:kPushHasStateKey];
224
+ // 2. Record a pending consumer-sync intent. It is cleared only once the
225
+ // backend update actually succeeds (see flushPendingConsumerSync).
226
+ [ud setBool:enabled forKey:kPushPendingValueKey];
227
+ [ud setBool:YES forKey:kPushPendingKey];
228
+ [ud synchronize];
229
+ // 3. Update the SDK's local enabled flag (no network) so cold-start token
230
+ // sync knows the user's intent.
231
+ [AppAmbitPushWrapper setNotificationsEnabledLocal:enabled];
232
+ // 4. Try to push it to the backend now. When offline this is a no-op: calling
233
+ // updateConsumer offline poisons the SDK's dedup cache (it writes the new
234
+ // state to its DB before the failed network send, so identical retries are
235
+ // skipped as "already synced"). The pending intent is replayed when the
236
+ // network returns or the app becomes active.
237
+ [self flushPendingConsumerSync];
49
238
  }
50
239
 
51
240
  - (void)isNotificationsEnabled:(RCTPromiseResolveBlock)resolve
52
241
  reject:(RCTPromiseRejectBlock)reject {
53
- resolve(@([AppAmbitPushWrapper isNotificationsEnabled]));
242
+ NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
243
+ if ([ud boolForKey:kPushHasStateKey]) {
244
+ // Return our own persisted state — this is always the last value the user
245
+ // explicitly set, survives cold restarts and SDK state inconsistencies.
246
+ resolve(@([ud boolForKey:kPushEnabledStateKey]));
247
+ } else {
248
+ // First-ever launch: no stored state yet, ask the SDK.
249
+ resolve(@([AppAmbitPushWrapper isNotificationsEnabled]));
250
+ }
54
251
  }
55
252
 
56
- - (void)setNotificationCustomizer {
57
- __weak AppambitPushNotifications *weakSelf = self;
58
- [AppAmbitPushWrapper setNotificationCustomizer:^(NSDictionary * _Nonnull payload) {
59
- AppambitPushNotifications *strongSelf = weakSelf;
60
- if (strongSelf && strongSelf->_hasListeners) {
61
- [strongSelf sendEventWithName:@"onNotificationReceived" body:payload];
253
+ - (void)hasNotificationPermission:(RCTPromiseResolveBlock)resolve
254
+ reject:(RCTPromiseRejectBlock)reject {
255
+ [AppAmbitPushWrapper hasNotificationPermissionWithCompletion:^(BOOL granted) {
256
+ resolve(@(granted));
257
+ }];
258
+ }
259
+
260
+ // Called by JS when the background notification async handler Promise resolves.
261
+ // Signals the iOS system that background processing is complete so iOS can reclaim
262
+ // time and allow future background wake-ups.
263
+ - (void)backgroundHandlerCompleted {
264
+ AppAmbitNotifyJSBackgroundHandlerCompleted();
265
+ }
266
+
267
+ // MARK: - Offline-resilient consumer sync
268
+
269
+ // Replays the pending consumer-sync intent to the backend when online. The SDK
270
+ // has no offline retry queue and dedups consumer updates against its local DB,
271
+ // so we must only call updateConsumer with real connectivity. The pending flag
272
+ // is cleared only on a confirmed successful backend update.
273
+ - (void)flushPendingConsumerSync {
274
+ NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
275
+ if (![ud boolForKey:kPushPendingKey]) return;
276
+ if (![AppAmbitPushWrapper isNetworkAvailable]) return;
277
+
278
+ BOOL desired = [ud boolForKey:kPushPendingValueKey];
279
+ [AppAmbitPushWrapper setNotificationsEnabled:desired completion:^(BOOL success) {
280
+ if (!success) return;
281
+ NSUserDefaults *u = [NSUserDefaults standardUserDefaults];
282
+ // Only clear if the pending value still matches what we just synced — a
283
+ // newer toggle during the network call must keep its own pending intent.
284
+ if ([u boolForKey:kPushPendingKey] && [u boolForKey:kPushPendingValueKey] == desired) {
285
+ [u removeObjectForKey:kPushPendingKey];
286
+ [u removeObjectForKey:kPushPendingValueKey];
287
+ [u synchronize];
62
288
  }
63
289
  }];
64
290
  }
65
291
 
66
292
  - (void)addListener:(NSString *)eventName {
67
- [super addListener:eventName];
293
+ [super addListener:eventName]; // sets _hasListeners = YES via startObserving
294
+
295
+ // Install SDK listener once JS is ready — same as Flutter's installNotificationListenerIfNeeded.
296
+ // _hasListeners is now YES so SDK cold-start replay fires sendEventWithName: directly.
297
+ [self installSDKListenerIfNeeded];
298
+
299
+ if ([eventName isEqualToString:@"AppAmbit_onBackgroundNotification"]) {
300
+ for (NSDictionary *payload in _pendingBackgroundEvents) {
301
+ [self sendEventWithName:@"AppAmbit_onBackgroundNotification" body:payload];
302
+ }
303
+ [_pendingBackgroundEvents removeAllObjects];
304
+ } else if ([eventName isEqualToString:@"AppAmbit_onForegroundNotification"]) {
305
+ for (NSDictionary *payload in _pendingForegroundEvents) {
306
+ [self sendEventWithName:@"AppAmbit_onForegroundNotification" body:payload];
307
+ }
308
+ [_pendingForegroundEvents removeAllObjects];
309
+ } else if ([eventName isEqualToString:@"AppAmbit_onOpenedNotification"]) {
310
+ NSArray<NSDictionary *> *earlyOpened = [AppAmbitPushWrapper getAndClearPendingOpenedPayloads];
311
+ if (earlyOpened.count > 0) {
312
+ [_pendingOpenedEvents addObjectsFromArray:earlyOpened];
313
+ }
314
+
315
+ for (NSDictionary *payload in _pendingOpenedEvents) {
316
+ if (![self isDuplicateOpenedPayload:payload]) {
317
+ [self sendEventWithName:@"AppAmbit_onOpenedNotification" body:payload];
318
+ }
319
+ }
320
+ [_pendingOpenedEvents removeAllObjects];
321
+ }
68
322
  }
69
323
 
70
324
  - (void)removeListeners:(double)count {
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeAppambitPushNotifications.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAcpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,2BAA2B,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeAppambitPushNotifications.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAoBpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,2BAA2B,CAAC","ignoreList":[]}
@@ -1,8 +1,15 @@
1
1
  "use strict";
2
2
 
3
- import { NativeEventEmitter } from 'react-native';
3
+ import { NativeEventEmitter, Platform } from 'react-native';
4
4
  import AppambitPushNotifications from "./NativeAppambitPushNotifications.js";
5
+ export const BACKGROUND_NOTIFICATION_TASK = 'AppAmbitBackgroundNotification';
6
+ const EVENT_FOREGROUND = 'AppAmbit_onForegroundNotification';
7
+ const EVENT_BACKGROUND = 'AppAmbit_onBackgroundNotification';
8
+ const EVENT_OPENED = 'AppAmbit_onOpenedNotification';
5
9
  const eventEmitter = new NativeEventEmitter(AppambitPushNotifications);
10
+ let foregroundSub = null;
11
+ let backgroundSub = null;
12
+ let openedSub = null;
6
13
  export const start = () => {
7
14
  AppambitPushNotifications.start();
8
15
  };
@@ -10,19 +17,48 @@ export const requestNotificationPermission = () => {
10
17
  AppambitPushNotifications.requestNotificationPermission();
11
18
  };
12
19
  export const requestNotificationPermissionWithResult = async () => {
13
- return await AppambitPushNotifications.requestNotificationPermissionWithResult();
20
+ return AppambitPushNotifications.requestNotificationPermissionWithResult();
14
21
  };
15
22
  export const setNotificationsEnabled = enabled => {
16
23
  AppambitPushNotifications.setNotificationsEnabled(enabled);
17
24
  };
18
25
  export const isNotificationsEnabled = async () => {
19
- return await AppambitPushNotifications.isNotificationsEnabled();
20
- };
21
- export const setNotificationCustomizer = callback => {
22
- AppambitPushNotifications.setNotificationCustomizer();
23
- eventEmitter.removeAllListeners('onNotificationReceived');
24
- eventEmitter.addListener('onNotificationReceived', payload => {
25
- callback(payload);
26
- });
26
+ return AppambitPushNotifications.isNotificationsEnabled();
27
+ };
28
+ export const hasNotificationPermission = async () => {
29
+ return AppambitPushNotifications.hasNotificationPermission();
30
+ };
31
+ export const setForegroundListener = callback => {
32
+ foregroundSub?.remove();
33
+ foregroundSub = eventEmitter.addListener(EVENT_FOREGROUND, callback);
34
+ return () => {
35
+ foregroundSub?.remove();
36
+ foregroundSub = null;
37
+ };
38
+ };
39
+ export const setOpenedListener = callback => {
40
+ openedSub?.remove();
41
+ openedSub = eventEmitter.addListener(EVENT_OPENED, callback);
42
+ return () => {
43
+ openedSub?.remove();
44
+ openedSub = null;
45
+ };
46
+ };
47
+ export const Android = {
48
+ setBackgroundListener: callback => {
49
+ if (Platform.OS !== 'android') {
50
+ return () => {};
51
+ }
52
+ backgroundSub?.remove();
53
+ backgroundSub = eventEmitter.addListener(EVENT_BACKGROUND, payload => {
54
+ void callback(payload).finally(() => {
55
+ AppambitPushNotifications.backgroundHandlerCompleted();
56
+ });
57
+ });
58
+ return () => {
59
+ backgroundSub?.remove();
60
+ backgroundSub = null;
61
+ };
62
+ }
27
63
  };
28
64
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["NativeEventEmitter","AppambitPushNotifications","eventEmitter","start","requestNotificationPermission","requestNotificationPermissionWithResult","setNotificationsEnabled","enabled","isNotificationsEnabled","setNotificationCustomizer","callback","removeAllListeners","addListener","payload"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,cAAc;AACjD,OAAOC,yBAAyB,MAAM,sCAAmC;AAEzE,MAAMC,YAAY,GAAG,IAAIF,kBAAkB,CAACC,yBAAyB,CAAC;AAUtE,OAAO,MAAME,KAAK,GAAGA,CAAA,KAAY;EAC/BF,yBAAyB,CAACE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,OAAO,MAAMC,6BAA6B,GAAGA,CAAA,KAAY;EACvDH,yBAAyB,CAACG,6BAA6B,CAAC,CAAC;AAC3D,CAAC;AAED,OAAO,MAAMC,uCAAuC,GAAG,MAAAA,CAAA,KAA8B;EACnF,OAAO,MAAMJ,yBAAyB,CAACI,uCAAuC,CAAC,CAAC;AAClF,CAAC;AAED,OAAO,MAAMC,uBAAuB,GAAIC,OAAgB,IAAW;EACjEN,yBAAyB,CAACK,uBAAuB,CAACC,OAAO,CAAC;AAC5D,CAAC;AAED,OAAO,MAAMC,sBAAsB,GAAG,MAAAA,CAAA,KAA8B;EAClE,OAAO,MAAMP,yBAAyB,CAACO,sBAAsB,CAAC,CAAC;AACjE,CAAC;AAED,OAAO,MAAMC,yBAAyB,GACpCC,QAAgD,IACvC;EACTT,yBAAyB,CAACQ,yBAAyB,CAAC,CAAC;EACrDP,YAAY,CAACS,kBAAkB,CAAC,wBAAwB,CAAC;EACzDT,YAAY,CAACU,WAAW,CAAC,wBAAwB,EAAGC,OAAO,IAAK;IAC9DH,QAAQ,CAACG,OAAc,CAAC;EAC1B,CAAC,CAAC;AACJ,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["NativeEventEmitter","Platform","AppambitPushNotifications","BACKGROUND_NOTIFICATION_TASK","EVENT_FOREGROUND","EVENT_BACKGROUND","EVENT_OPENED","eventEmitter","foregroundSub","backgroundSub","openedSub","start","requestNotificationPermission","requestNotificationPermissionWithResult","setNotificationsEnabled","enabled","isNotificationsEnabled","hasNotificationPermission","setForegroundListener","callback","remove","addListener","setOpenedListener","Android","setBackgroundListener","OS","payload","finally","backgroundHandlerCompleted"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,EAAEC,QAAQ,QAAQ,cAAc;AAC3D,OAAOC,yBAAyB,MAAM,sCAAmC;AAiCzE,OAAO,MAAMC,4BAA4B,GAAG,gCAAgC;AAE5E,MAAMC,gBAAgB,GAAG,mCAAmC;AAC5D,MAAMC,gBAAgB,GAAG,mCAAmC;AAC5D,MAAMC,YAAY,GAAO,+BAA+B;AAExD,MAAMC,YAAY,GAAG,IAAIP,kBAAkB,CAACE,yBAAyB,CAAC;AAEtE,IAAIM,aAAiE,GAAG,IAAI;AAC5E,IAAIC,aAAiE,GAAG,IAAI;AAC5E,IAAIC,SAAiE,GAAG,IAAI;AAE5E,OAAO,MAAMC,KAAK,GAAGA,CAAA,KAAY;EAC/BT,yBAAyB,CAACS,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,OAAO,MAAMC,6BAA6B,GAAGA,CAAA,KAAY;EACvDV,yBAAyB,CAACU,6BAA6B,CAAC,CAAC;AAC3D,CAAC;AAED,OAAO,MAAMC,uCAAuC,GAClD,MAAAA,CAAA,KAA8B;EAC5B,OAAOX,yBAAyB,CAACW,uCAAuC,CAAC,CAAC;AAC5E,CAAC;AAEH,OAAO,MAAMC,uBAAuB,GAAIC,OAAgB,IAAW;EACjEb,yBAAyB,CAACY,uBAAuB,CAACC,OAAO,CAAC;AAC5D,CAAC;AAED,OAAO,MAAMC,sBAAsB,GAAG,MAAAA,CAAA,KAA8B;EAClE,OAAOd,yBAAyB,CAACc,sBAAsB,CAAC,CAAC;AAC3D,CAAC;AAED,OAAO,MAAMC,yBAAyB,GAAG,MAAAA,CAAA,KAA8B;EACrE,OAAOf,yBAAyB,CAACe,yBAAyB,CAAC,CAAC;AAC9D,CAAC;AAED,OAAO,MAAMC,qBAAqB,GAChCC,QAA8B,IACb;EACjBX,aAAa,EAAEY,MAAM,CAAC,CAAC;EACvBZ,aAAa,GAAGD,YAAY,CAACc,WAAW,CAACjB,gBAAgB,EAAEe,QAAe,CAAC;EAC3E,OAAO,MAAM;IACXX,aAAa,EAAEY,MAAM,CAAC,CAAC;IACvBZ,aAAa,GAAG,IAAI;EACtB,CAAC;AACH,CAAC;AAED,OAAO,MAAMc,iBAAiB,GAC5BH,QAA8B,IACb;EACjBT,SAAS,EAAEU,MAAM,CAAC,CAAC;EACnBV,SAAS,GAAGH,YAAY,CAACc,WAAW,CAACf,YAAY,EAAEa,QAAe,CAAC;EACnE,OAAO,MAAM;IACXT,SAAS,EAAEU,MAAM,CAAC,CAAC;IACnBV,SAAS,GAAG,IAAI;EAClB,CAAC;AACH,CAAC;AAED,OAAO,MAAMa,OAAO,GAAG;EACrBC,qBAAqB,EACnBL,QAAwC,IACvB;IACjB,IAAIlB,QAAQ,CAACwB,EAAE,KAAK,SAAS,EAAE;MAC7B,OAAO,MAAM,CAAC,CAAC;IACjB;IACAhB,aAAa,EAAEW,MAAM,CAAC,CAAC;IACvBX,aAAa,GAAGF,YAAY,CAACc,WAAW,CACtChB,gBAAgB,EACdqB,OAA4B,IAAK;MACjC,KAAKP,QAAQ,CAACO,OAAO,CAAC,CAACC,OAAO,CAAC,MAAM;QACnCzB,yBAAyB,CAAC0B,0BAA0B,CAAC,CAAC;MACxD,CAAC,CAAC;IACJ,CACF,CAAC;IACD,OAAO,MAAM;MACXnB,aAAa,EAAEW,MAAM,CAAC,CAAC;MACvBX,aAAa,GAAG,IAAI;IACtB,CAAC;EACH;AACF,CAAC","ignoreList":[]}
@@ -5,7 +5,8 @@ export interface Spec extends TurboModule {
5
5
  requestNotificationPermissionWithResult(): Promise<boolean>;
6
6
  setNotificationsEnabled(enabled: boolean): void;
7
7
  isNotificationsEnabled(): Promise<boolean>;
8
- setNotificationCustomizer(): void;
8
+ hasNotificationPermission(): Promise<boolean>;
9
+ backgroundHandlerCompleted(): void;
9
10
  addListener(eventName: string): void;
10
11
  removeListeners(count: number): void;
11
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NativeAppambitPushNotifications.d.ts","sourceRoot":"","sources":["../../../src/NativeAppambitPushNotifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,KAAK,IAAI,IAAI,CAAC;IACd,6BAA6B,IAAI,IAAI,CAAC;IACtC,uCAAuC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,sBAAsB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAE3C,yBAAyB,IAAI,IAAI,CAAC;IAClC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAmF"}
1
+ {"version":3,"file":"NativeAppambitPushNotifications.d.ts","sourceRoot":"","sources":["../../../src/NativeAppambitPushNotifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,KAAK,IAAI,IAAI,CAAC;IAEd,6BAA6B,IAAI,IAAI,CAAC;IACtC,uCAAuC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,sBAAsB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAK9C,0BAA0B,IAAI,IAAI,CAAC;IAEnC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAmF"}
@@ -1,14 +1,40 @@
1
+ export interface AndroidNotificationData {
2
+ color: string | null;
3
+ smallIconName: string | null;
4
+ ticker: string | null;
5
+ sticky: boolean | null;
6
+ visibility: string | null;
7
+ channelId: string | null;
8
+ tag: string | null;
9
+ sound: string | null;
10
+ clickAction: string | null;
11
+ }
12
+ export interface IosNotificationData {
13
+ badge: number | null;
14
+ sound: string | null;
15
+ category: string | null;
16
+ threadId: string | null;
17
+ }
1
18
  export interface NotificationPayload {
2
- notification: {
3
- title: string;
4
- body: string;
5
- };
6
- data?: Record<string, string>;
19
+ title: string | null;
20
+ body: string | null;
21
+ imageUrl: string | null;
22
+ data: Record<string, any>;
23
+ android: AndroidNotificationData | null;
24
+ ios: IosNotificationData | null;
7
25
  }
26
+ export type NotificationListener = (notification: NotificationPayload) => void;
27
+ export type BackgroundNotificationListener = (notification: NotificationPayload) => Promise<void>;
28
+ export declare const BACKGROUND_NOTIFICATION_TASK = "AppAmbitBackgroundNotification";
8
29
  export declare const start: () => void;
9
30
  export declare const requestNotificationPermission: () => void;
10
31
  export declare const requestNotificationPermissionWithResult: () => Promise<boolean>;
11
32
  export declare const setNotificationsEnabled: (enabled: boolean) => void;
12
33
  export declare const isNotificationsEnabled: () => Promise<boolean>;
13
- export declare const setNotificationCustomizer: (callback: (payload: NotificationPayload) => void) => void;
34
+ export declare const hasNotificationPermission: () => Promise<boolean>;
35
+ export declare const setForegroundListener: (callback: NotificationListener) => (() => void);
36
+ export declare const setOpenedListener: (callback: NotificationListener) => (() => void);
37
+ export declare const Android: {
38
+ setBackgroundListener: (callback: BackgroundNotificationListener) => (() => void);
39
+ };
14
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAA;IACD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,eAAO,MAAM,KAAK,QAAO,IAExB,CAAC;AAEF,eAAO,MAAM,6BAA6B,QAAO,IAEhD,CAAC;AAEF,eAAO,MAAM,uCAAuC,QAAa,OAAO,CAAC,OAAO,CAE/E,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,SAAS,OAAO,KAAG,IAE1D,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAAa,OAAO,CAAC,OAAO,CAE9D,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,UAAU,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,KAC/C,IAMF,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACxC,GAAG,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,YAAY,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAC/E,MAAM,MAAM,8BAA8B,GAAG,CAAC,YAAY,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAElG,eAAO,MAAM,4BAA4B,mCAAmC,CAAC;AAY7E,eAAO,MAAM,KAAK,QAAO,IAExB,CAAC;AAEF,eAAO,MAAM,6BAA6B,QAAO,IAEhD,CAAC;AAEF,eAAO,MAAM,uCAAuC,QACxC,OAAO,CAAC,OAAO,CAExB,CAAC;AAEJ,eAAO,MAAM,uBAAuB,GAAI,SAAS,OAAO,KAAG,IAE1D,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAAa,OAAO,CAAC,OAAO,CAE9D,CAAC;AAEF,eAAO,MAAM,yBAAyB,QAAa,OAAO,CAAC,OAAO,CAEjE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,UAAU,oBAAoB,KAC7B,CAAC,MAAM,IAAI,CAOb,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,UAAU,oBAAoB,KAC7B,CAAC,MAAM,IAAI,CAOb,CAAC;AAEF,eAAO,MAAM,OAAO;sCAEN,8BAA8B,KACvC,CAAC,MAAM,IAAI,CAAC;CAkBhB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appambit-push-notifications",
3
- "version": "0.3.1",
3
+ "version": "1.0.1",
4
4
  "description": "Push Notifications SDK for Android to send push notifications via AppAmbit platform.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -2,12 +2,18 @@ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
2
 
3
3
  export interface Spec extends TurboModule {
4
4
  start(): void;
5
+
5
6
  requestNotificationPermission(): void;
6
7
  requestNotificationPermissionWithResult(): Promise<boolean>;
7
8
  setNotificationsEnabled(enabled: boolean): void;
8
9
  isNotificationsEnabled(): Promise<boolean>;
10
+ hasNotificationPermission(): Promise<boolean>;
11
+
12
+ // iOS: call when the background notification async handler Promise resolves.
13
+ // Signals iOS that background processing is complete so the system can reclaim
14
+ // time and continue scheduling background wake-ups.
15
+ backgroundHandlerCompleted(): void;
9
16
 
10
- setNotificationCustomizer(): void;
11
17
  addListener(eventName: string): void;
12
18
  removeListeners(count: number): void;
13
19
  }
package/src/index.tsx CHANGED
@@ -1,16 +1,49 @@
1
- import { NativeEventEmitter } from 'react-native';
1
+ import { NativeEventEmitter, Platform } from 'react-native';
2
2
  import AppambitPushNotifications from './NativeAppambitPushNotifications';
3
3
 
4
- const eventEmitter = new NativeEventEmitter(AppambitPushNotifications);
4
+ export interface AndroidNotificationData {
5
+ color: string | null;
6
+ smallIconName: string | null;
7
+ ticker: string | null;
8
+ sticky: boolean | null;
9
+ visibility: string | null;
10
+ channelId: string | null;
11
+ tag: string | null;
12
+ sound: string | null;
13
+ clickAction: string | null;
14
+ }
15
+
16
+ export interface IosNotificationData {
17
+ badge: number | null;
18
+ sound: string | null;
19
+ category: string | null;
20
+ threadId: string | null;
21
+ }
5
22
 
6
23
  export interface NotificationPayload {
7
- notification: {
8
- title: string;
9
- body: string;
10
- }
11
- data?: Record<string, string>;
24
+ title: string | null;
25
+ body: string | null;
26
+ imageUrl: string | null;
27
+ data: Record<string, any>;
28
+ android: AndroidNotificationData | null;
29
+ ios: IosNotificationData | null;
12
30
  }
13
31
 
32
+ export type NotificationListener = (notification: NotificationPayload) => void;
33
+ export type BackgroundNotificationListener = (notification: NotificationPayload) => Promise<void>;
34
+
35
+ export const BACKGROUND_NOTIFICATION_TASK = 'AppAmbitBackgroundNotification';
36
+
37
+ const EVENT_FOREGROUND = 'AppAmbit_onForegroundNotification';
38
+ const EVENT_BACKGROUND = 'AppAmbit_onBackgroundNotification';
39
+ const EVENT_OPENED = 'AppAmbit_onOpenedNotification';
40
+
41
+ const eventEmitter = new NativeEventEmitter(AppambitPushNotifications);
42
+
43
+ let foregroundSub: ReturnType<typeof eventEmitter.addListener> | null = null;
44
+ let backgroundSub: ReturnType<typeof eventEmitter.addListener> | null = null;
45
+ let openedSub: ReturnType<typeof eventEmitter.addListener> | null = null;
46
+
14
47
  export const start = (): void => {
15
48
  AppambitPushNotifications.start();
16
49
  };
@@ -19,24 +52,64 @@ export const requestNotificationPermission = (): void => {
19
52
  AppambitPushNotifications.requestNotificationPermission();
20
53
  };
21
54
 
22
- export const requestNotificationPermissionWithResult = async (): Promise<boolean> => {
23
- return await AppambitPushNotifications.requestNotificationPermissionWithResult();
24
- };
55
+ export const requestNotificationPermissionWithResult =
56
+ async (): Promise<boolean> => {
57
+ return AppambitPushNotifications.requestNotificationPermissionWithResult();
58
+ };
25
59
 
26
60
  export const setNotificationsEnabled = (enabled: boolean): void => {
27
61
  AppambitPushNotifications.setNotificationsEnabled(enabled);
28
62
  };
29
63
 
30
64
  export const isNotificationsEnabled = async (): Promise<boolean> => {
31
- return await AppambitPushNotifications.isNotificationsEnabled();
65
+ return AppambitPushNotifications.isNotificationsEnabled();
32
66
  };
33
67
 
34
- export const setNotificationCustomizer = (
35
- callback: (payload: NotificationPayload) => void
36
- ): void => {
37
- AppambitPushNotifications.setNotificationCustomizer();
38
- eventEmitter.removeAllListeners('onNotificationReceived');
39
- eventEmitter.addListener('onNotificationReceived', (payload) => {
40
- callback(payload as any);
41
- });
42
- };
68
+ export const hasNotificationPermission = async (): Promise<boolean> => {
69
+ return AppambitPushNotifications.hasNotificationPermission();
70
+ };
71
+
72
+ export const setForegroundListener = (
73
+ callback: NotificationListener
74
+ ): (() => void) => {
75
+ foregroundSub?.remove();
76
+ foregroundSub = eventEmitter.addListener(EVENT_FOREGROUND, callback as any);
77
+ return () => {
78
+ foregroundSub?.remove();
79
+ foregroundSub = null;
80
+ };
81
+ };
82
+
83
+ export const setOpenedListener = (
84
+ callback: NotificationListener
85
+ ): (() => void) => {
86
+ openedSub?.remove();
87
+ openedSub = eventEmitter.addListener(EVENT_OPENED, callback as any);
88
+ return () => {
89
+ openedSub?.remove();
90
+ openedSub = null;
91
+ };
92
+ };
93
+
94
+ export const Android = {
95
+ setBackgroundListener: (
96
+ callback: BackgroundNotificationListener
97
+ ): (() => void) => {
98
+ if (Platform.OS !== 'android') {
99
+ return () => {};
100
+ }
101
+ backgroundSub?.remove();
102
+ backgroundSub = eventEmitter.addListener(
103
+ EVENT_BACKGROUND,
104
+ ((payload: NotificationPayload) => {
105
+ void callback(payload).finally(() => {
106
+ AppambitPushNotifications.backgroundHandlerCompleted();
107
+ });
108
+ }) as any
109
+ );
110
+ return () => {
111
+ backgroundSub?.remove();
112
+ backgroundSub = null;
113
+ };
114
+ }
115
+ };