react-native-insider 8.1.0 → 9.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.
Files changed (48) hide show
  1. package/README.md +89 -660
  2. package/RNInsider.podspec +33 -12
  3. package/android/build.gradle +16 -2
  4. package/android/src/main/java/com/useinsider/react/RNInsiderAppFrameViewManager.java +253 -0
  5. package/android/src/main/java/com/useinsider/react/RNInsiderModule.java +238 -174
  6. package/android/src/main/java/com/useinsider/react/RNInsiderPackage.java +5 -1
  7. package/android/src/main/java/com/useinsider/react/RNUtils.java +111 -0
  8. package/babel.config.js +4 -1
  9. package/index.d.ts +196 -161
  10. package/index.js +60 -11
  11. package/ios/RNInsider/RNInsider.m +245 -341
  12. package/ios/RNInsider/RNInsiderAppFrameView.h +33 -0
  13. package/ios/RNInsider/RNInsiderAppFrameView.mm +315 -0
  14. package/ios/RNInsider/RNNotificationHandler.h +4 -1
  15. package/ios/RNInsider/RNNotificationHandler.m +16 -18
  16. package/ios/RNInsider/RNUtils.h +2 -0
  17. package/ios/RNInsider/RNUtils.m +79 -0
  18. package/package.json +43 -12
  19. package/src/EventHandler.js +17 -0
  20. package/src/InsiderAppFrame.d.ts +45 -0
  21. package/src/InsiderAppFrame.js +265 -0
  22. package/src/InsiderAppFrameLogic.d.ts +35 -0
  23. package/src/InsiderAppFrameLogic.js +192 -0
  24. package/src/InsiderAppFrameStatus.d.ts +22 -0
  25. package/src/InsiderAppFrameStatus.js +35 -0
  26. package/src/InsiderAppFramesError.d.ts +25 -0
  27. package/src/InsiderAppFramesError.js +77 -0
  28. package/src/InsiderEvent.js +1 -0
  29. package/src/InsiderEvents.d.ts +93 -0
  30. package/src/InsiderEvents.js +124 -0
  31. package/src/InsiderIdentifier.js +1 -1
  32. package/src/InsiderUser.d.ts +3 -2
  33. package/src/InsiderUser.js +27 -0
  34. package/src/Util.js +2 -1
  35. package/src/specs/InsiderAppFrameNativeComponent.ts +54 -0
  36. package/.orbit-output/config/MASTER.md +0 -129
  37. package/.orbit-output/config/architecture/API_DOCS.md +0 -342
  38. package/.orbit-output/config/architecture/ARCHITECTURE.md +0 -209
  39. package/.orbit-output/config/architecture/CODE-CATALOG.md +0 -172
  40. package/.orbit-output/config/architecture/project.yaml +0 -138
  41. package/.orbit-output/config/automation/AUTOMATION-SETUP.md +0 -50
  42. package/.orbit-output/config/enforcement/ENFORCEMENT-RULES.md +0 -41
  43. package/.orbit-output/config/enforcement/validate.sh +0 -49
  44. package/.orbit-output/skills/custom/academy-doc-generator.md +0 -598
  45. package/android/.settings/org.eclipse.buildship.core.prefs +0 -13
  46. package/android/src/test/java/com/useinsider/react/RNInsiderModuleTest.java +0 -169
  47. package/docs/app-cards/app-cards_new.md +0 -569
  48. package/ios/RNInsiderTests/RNInsiderTriggerPushProcessTests.m +0 -91
@@ -0,0 +1,33 @@
1
+ // Fabric-only. Guarded here as well as in the .mm, because the .mm imports this header before
2
+ // its own guard and a legacy-architecture build would otherwise pull in RCTViewComponentView.h.
3
+ #ifdef RCT_NEW_ARCH_ENABLED
4
+
5
+ #import <React/RCTViewComponentView.h>
6
+ #import <UIKit/UIKit.h>
7
+
8
+ NS_ASSUME_NONNULL_BEGIN
9
+
10
+ @interface RNInsiderAppFrameView : RCTViewComponentView
11
+ @end
12
+
13
+ /**
14
+ * The error-code name that crosses the bridge for a delegate error.
15
+ *
16
+ * Answers `@"UNKNOWN"` for an error outside App Frames' own domain, and for every error when the
17
+ * linked InsiderMobile has no App Frames. The signature uses no InsiderMobile type so it compiles
18
+ * either way.
19
+ */
20
+ FOUNDATION_EXPORT NSString *RNInsiderAppFramesErrorCodeNameForError(NSError *error);
21
+
22
+ /**
23
+ * The status name that crosses the bridge for a native status value.
24
+ *
25
+ * Answers `@"UNRECOGNIZED"` for a value outside the SDK's enum, and for every value when the
26
+ * linked InsiderMobile has no App Frames. The signature uses no InsiderMobile type so it compiles
27
+ * either way.
28
+ */
29
+ FOUNDATION_EXPORT NSString *RNInsiderAppFramesStatusNameForRawStatus(NSInteger status);
30
+
31
+ NS_ASSUME_NONNULL_END
32
+
33
+ #endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,315 @@
1
+ #import "RNInsiderAppFrameView.h"
2
+
3
+ // The codegen headers below exist only under the New Architecture. The legacy architecture is
4
+ // the default on React Native 0.74-0.75 and still selectable on 0.76-0.81 (removed in 0.82), all
5
+ // in the supported range, and this pod must still compile there.
6
+ #ifdef RCT_NEW_ARCH_ENABLED
7
+
8
+
9
+ #import <react/renderer/components/RNInsiderSpec/ComponentDescriptors.h>
10
+ #import <react/renderer/components/RNInsiderSpec/EventEmitters.h>
11
+ #import <react/renderer/components/RNInsiderSpec/Props.h>
12
+ #import <react/renderer/components/RNInsiderSpec/RCTComponentViewHelpers.h>
13
+
14
+ #import <React/RCTConversions.h>
15
+
16
+ // Diagnostics go through +[Insider sendError:desc:], like the rest of the bridge, rather than
17
+ // NSLog into a host app console the integrator cannot switch off.
18
+ #import <InsiderMobile/Insider.h>
19
+
20
+ // InsiderMobile releases before 16.0.0 have no App Frames headers. Codegen registers
21
+ // "InsiderAppFrameView" in every app regardless of the linked SDK, so InsiderAppFrameViewCls()
22
+ // must still resolve without them, or the app fails to link.
23
+ #if __has_include(<InsiderMobile/InsiderAppFramesView.h>)
24
+ #define INSIDER_APP_FRAMES_AVAILABLE 1
25
+ #import <InsiderMobile/InsiderAppFramesView.h>
26
+ #import <InsiderMobile/InsiderAppFramesError.h>
27
+ #else
28
+ #define INSIDER_APP_FRAMES_AVAILABLE 0
29
+ #endif
30
+
31
+ using namespace facebook::react;
32
+
33
+ #if INSIDER_APP_FRAMES_AVAILABLE
34
+
35
+ // Matches NO_DISMISS_CODE in src/InsiderAppFramesError.js.
36
+ static const NSInteger kInsiderAppFramesNoDismissCode = -1;
37
+
38
+ static NSString *RNInsiderAppFramesErrorCodeName(InsiderAppFramesErrorCode code) {
39
+ switch (code) {
40
+ case InsiderAppFramesErrorCodeResolutionFailed:
41
+ return @"RESOLUTION_FAILED";
42
+ case InsiderAppFramesErrorCodeResponseMalformed:
43
+ return @"RESPONSE_MALFORMED";
44
+ case InsiderAppFramesErrorCodeDownloadingFailed:
45
+ return @"DOWNLOADING_FAILED";
46
+ case InsiderAppFramesErrorCodePlacementUntrusted:
47
+ return @"PLACEMENT_UNTRUSTED";
48
+ case InsiderAppFramesErrorCodeContentDisplayFailed:
49
+ return @"CONTENT_DISPLAY_FAILED";
50
+ case InsiderAppFramesErrorCodeRenderingFailed:
51
+ return @"RENDERING_FAILED";
52
+ case InsiderAppFramesErrorCodeUnknown:
53
+ return @"UNKNOWN";
54
+ }
55
+
56
+ // No `default:`, and the parameter is the enum type rather than NSInteger, so a code added
57
+ // to the SDK trips -Wswitch instead of silently mapping to UNKNOWN.
58
+ return @"UNKNOWN";
59
+ }
60
+
61
+ // JS compares against its own UPPER_SNAKE enum (src/InsiderAppFrameStatus.js), so the SDK's
62
+ // lowerCamelCase `stringValue` would match nothing, silently.
63
+ static NSString *RNInsiderAppFramesStatusName(InsiderAppFramesViewStatus status) {
64
+ switch (status) {
65
+ case InsiderAppFramesViewStatusDetached:
66
+ return @"DETACHED";
67
+ case InsiderAppFramesViewStatusNoPlacement:
68
+ return @"NO_PLACEMENT";
69
+ case InsiderAppFramesViewStatusResolving:
70
+ return @"RESOLVING";
71
+ case InsiderAppFramesViewStatusDownloading:
72
+ return @"DOWNLOADING";
73
+ case InsiderAppFramesViewStatusRendering:
74
+ return @"RENDERING";
75
+ case InsiderAppFramesViewStatusUnavailable:
76
+ return @"UNAVAILABLE";
77
+ case InsiderAppFramesViewStatusReady:
78
+ return @"READY";
79
+ case InsiderAppFramesViewStatusDismissed:
80
+ return @"DISMISSED";
81
+ case InsiderAppFramesViewStatusErrorResolving:
82
+ return @"ERROR_RESOLVING";
83
+ case InsiderAppFramesViewStatusErrorDownloading:
84
+ return @"ERROR_DOWNLOADING";
85
+ case InsiderAppFramesViewStatusErrorRendering:
86
+ return @"ERROR_RENDERING";
87
+ case InsiderAppFramesViewStatusDisabled:
88
+ return @"DISABLED";
89
+ }
90
+
91
+ // Reachable only for an out-of-enum runtime value. UNRECOGNIZED is a declared member of the
92
+ // JS status union; a real status such as DETACHED would tell JS something specific and wrong.
93
+ [Insider sendError:[NSException exceptionWithName:@"InsiderAppFrameUnrecognisedStatus"
94
+ reason:[NSString stringWithFormat:
95
+ @"Unrecognised InsiderAppFramesViewStatus: %ld",
96
+ (long)status]
97
+ userInfo:nil]
98
+ desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
99
+ return @"UNRECOGNIZED";
100
+ }
101
+
102
+ // Test seam; the delegate callbacks call the static mapper directly.
103
+ NSString *RNInsiderAppFramesStatusNameForRawStatus(NSInteger status) {
104
+ return RNInsiderAppFramesStatusName((InsiderAppFramesViewStatus)status);
105
+ }
106
+
107
+ @interface RNInsiderAppFrameView () <InsiderAppFramesViewDelegate>
108
+ @end
109
+
110
+ static SharedViewProps RNInsiderAppFrameViewDefaultProps() {
111
+ // SharedViewProps, not Props::Shared: `_props` is shared_ptr<const ViewProps>, and a
112
+ // shared_ptr<const Props> will not implicitly convert down to it.
113
+ static const auto defaultProps = std::make_shared<const InsiderAppFrameViewProps>();
114
+ return defaultProps;
115
+ }
116
+
117
+ @implementation RNInsiderAppFrameView {
118
+ InsiderAppFramesView *_frameView;
119
+ }
120
+
121
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
122
+ return concreteComponentDescriptorProvider<InsiderAppFrameViewComponentDescriptor>();
123
+ }
124
+
125
+ - (instancetype)initWithFrame:(CGRect)frame {
126
+ if (self = [super initWithFrame:frame]) {
127
+ _props = RNInsiderAppFrameViewDefaultProps();
128
+
129
+ _frameView = [[InsiderAppFramesView alloc] initWithFrame:self.bounds];
130
+ _frameView.delegate = self;
131
+ self.contentView = _frameView;
132
+ }
133
+ return self;
134
+ }
135
+
136
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
137
+ const auto &newProps = *std::static_pointer_cast<InsiderAppFrameViewProps const>(props);
138
+ const auto &previousProps = *std::static_pointer_cast<InsiderAppFrameViewProps const>(_props);
139
+
140
+ if (newProps.placementId != previousProps.placementId) {
141
+ NSString *placementId = RCTNSStringFromStringNilIfEmpty(newProps.placementId);
142
+ _frameView.placementId = placementId;
143
+ }
144
+
145
+ [super updateProps:props oldProps:oldProps];
146
+ }
147
+
148
+ // A recycled view's next updateProps: diffs against `_props`, which RCTViewComponentView does
149
+ // not reset; without resetting it here, a view reused for the same placementId never applies it.
150
+ - (void)prepareForRecycle {
151
+ [super prepareForRecycle];
152
+
153
+ // Replace the SDK view rather than reset it. A recycled view is already off-window, so it sits
154
+ // in DETACHED, which keeps its reported height; native suppresses a repeat of that height, so
155
+ // the next occupant would never receive one and render into a zero-height box.
156
+ _frameView.delegate = nil;
157
+ [_frameView removeFromSuperview];
158
+
159
+ _frameView = [[InsiderAppFramesView alloc] initWithFrame:self.bounds];
160
+ _frameView.delegate = self;
161
+ self.contentView = _frameView;
162
+
163
+ _props = RNInsiderAppFrameViewDefaultProps();
164
+ }
165
+
166
+ - (std::shared_ptr<const InsiderAppFrameViewEventEmitter>)frameEventEmitter {
167
+ if (!_eventEmitter) {
168
+ // Not expected: the emitter is set at mount, before the view reaches a window. Reported
169
+ // because JS mirrors status and error from these events, so a dropped one leaves it stale.
170
+ [Insider sendError:[NSException exceptionWithName:@"InsiderAppFrameDroppedEvent"
171
+ reason:@"Dropped an event: no event emitter attached yet."
172
+ userInfo:nil]
173
+ desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
174
+ return nullptr;
175
+ }
176
+ return std::static_pointer_cast<const InsiderAppFrameViewEventEmitter>(_eventEmitter);
177
+ }
178
+
179
+ - (std::string)currentPlacementId {
180
+ return RCTStringFromNSString(_frameView.placementId ?: @"");
181
+ }
182
+
183
+ #pragma mark - InsiderAppFramesViewDelegate
184
+
185
+ - (void)appFramesView:(InsiderAppFramesView *)view
186
+ didChangeStatusTo:(InsiderAppFramesViewStatus)status
187
+ from:(InsiderAppFramesViewStatus)previousStatus {
188
+ auto emitter = [self frameEventEmitter];
189
+ if (emitter) {
190
+ emitter->onStatusChange({
191
+ .placementId = [self currentPlacementId],
192
+ .status = RCTStringFromNSString(RNInsiderAppFramesStatusName(status)),
193
+ .previousStatus = RCTStringFromNSString(RNInsiderAppFramesStatusName(previousStatus)),
194
+ });
195
+ }
196
+ }
197
+
198
+ // The cast below would reinterpret a foreign-domain code as a real App Frames code.
199
+ NSString *RNInsiderAppFramesErrorCodeNameForError(NSError *error) {
200
+ if (![error.domain isEqualToString:InsiderAppFramesErrorDomain]) {
201
+ return @"UNKNOWN";
202
+ }
203
+ return RNInsiderAppFramesErrorCodeName((InsiderAppFramesErrorCode)error.code);
204
+ }
205
+
206
+ - (void)appFramesView:(InsiderAppFramesView *)view didFailLoadingWithError:(NSError *)error {
207
+ auto emitter = [self frameEventEmitter];
208
+ if (!emitter) {
209
+ return;
210
+ }
211
+
212
+ // `message` is the canonical per-code text, as on Android; the underlying error goes in `cause`.
213
+ NSError *underlying = error.userInfo[NSUnderlyingErrorKey];
214
+ NSString *message = error.localizedDescription ?: @"";
215
+ NSString *cause = underlying.localizedDescription ?: @"";
216
+
217
+ // Set when a template dismisses with a non-default code. The SDK exports no constant for this
218
+ // userInfo key.
219
+ NSNumber *dismissCode = error.userInfo[@"dismiss_code"];
220
+
221
+ NSString *codeName = RNInsiderAppFramesErrorCodeNameForError(error);
222
+
223
+ emitter->onLoadError({
224
+ .placementId = [self currentPlacementId],
225
+ .code = RCTStringFromNSString(codeName),
226
+ .message = RCTStringFromNSString(message),
227
+ .cause = RCTStringFromNSString(cause),
228
+ // The ternary's common type is NSInteger, which the Int32 braced initialiser rejects as
229
+ // narrowing (-Wc++11-narrowing).
230
+ .dismissCode = static_cast<int32_t>(
231
+ dismissCode != nil ? dismissCode.integerValue : kInsiderAppFramesNoDismissCode),
232
+ });
233
+ }
234
+
235
+ // The height is applied in JS: Fabric assigns frames without Auto Layout, so the native view's
236
+ // intrinsicContentSize is never consulted.
237
+ - (void)appFramesView:(InsiderAppFramesView *)view didRequestHeightChangeTo:(CGFloat)optimalHeight {
238
+ auto emitter = [self frameEventEmitter];
239
+ if (emitter) {
240
+ emitter->onHeightChange({
241
+ .placementId = [self currentPlacementId],
242
+ .height = static_cast<double>(optimalHeight),
243
+ });
244
+ }
245
+ }
246
+
247
+ - (void)appFramesViewDidRequestDismiss:(InsiderAppFramesView *)view {
248
+ auto emitter = [self frameEventEmitter];
249
+ if (emitter) {
250
+ emitter->onDismissRequest({.placementId = [self currentPlacementId]});
251
+ }
252
+ }
253
+
254
+ - (void)appFramesView:(InsiderAppFramesView *)view
255
+ didTriggerActionWithData:(NSDictionary<NSString *, id> *)actionData {
256
+ auto emitter = [self frameEventEmitter];
257
+ if (!emitter) {
258
+ return;
259
+ }
260
+
261
+ NSString *json = nil;
262
+ if ([NSJSONSerialization isValidJSONObject:actionData]) {
263
+ NSData *data = [NSJSONSerialization dataWithJSONObject:actionData options:0 error:nil];
264
+ if (data) {
265
+ json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
266
+ }
267
+ }
268
+ if (!json) {
269
+ // The payload is lost either way, since the codegen field is non-nullable; the report is what
270
+ // makes the loss visible.
271
+ [Insider sendError:[NSException exceptionWithName:@"InsiderAppFrameUnserializableAction"
272
+ reason:@"Action data could not be serialized; forwarding {}."
273
+ userInfo:nil]
274
+ desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
275
+ json = @"{}";
276
+ }
277
+
278
+ emitter->onAction({
279
+ .placementId = [self currentPlacementId],
280
+ .data = RCTStringFromNSString(json),
281
+ });
282
+ }
283
+
284
+ @end
285
+
286
+ #else // !INSIDER_APP_FRAMES_AVAILABLE
287
+
288
+ // The only signal for this configuration: the inert stub registers cleanly and nothing at runtime
289
+ // reports it. `#pragma message` rather than `#warning`, so a -Werror build does not break over it.
290
+ #pragma message("[Insider] App Frames is unavailable in the linked InsiderMobile, so InsiderAppFrameView compiles as an inert stub: it registers, renders nothing and emits no events. Update InsiderMobile to 16.0.0 or later.")
291
+
292
+ @implementation RNInsiderAppFrameView
293
+
294
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
295
+ return concreteComponentDescriptorProvider<InsiderAppFrameViewComponentDescriptor>();
296
+ }
297
+
298
+ @end
299
+
300
+ // The header declares these unconditionally, so they are defined here to avoid a link error.
301
+ NSString *RNInsiderAppFramesErrorCodeNameForError(__unused NSError *error) {
302
+ return @"UNKNOWN";
303
+ }
304
+
305
+ NSString *RNInsiderAppFramesStatusNameForRawStatus(__unused NSInteger status) {
306
+ return @"UNRECOGNIZED";
307
+ }
308
+
309
+ #endif // INSIDER_APP_FRAMES_AVAILABLE
310
+
311
+ Class<RCTComponentViewProtocol> InsiderAppFrameViewCls(void) {
312
+ return RNInsiderAppFrameView.class;
313
+ }
314
+
315
+ #endif // RCT_NEW_ARCH_ENABLED
@@ -15,6 +15,7 @@
15
15
  #endif
16
16
 
17
17
  @interface RNNotificationHandler : RCTEventEmitter<RCTBridgeModule>
18
+
18
19
  extern NSString * const NOTIFICATION_OPEN;
19
20
  extern NSString * const INAPP_BUTTON_CLICK;
20
21
  extern NSString * const TEMP_STORE_PURCHASE;
@@ -24,6 +25,8 @@ extern NSString * const INAPP_SEEN;
24
25
  extern NSString * const FOREGROUND_PUSH;
25
26
  extern NSString * const INSIDER_ID_LISTENER;
26
27
  extern NSString * const SESSION_STARTED;
28
+ extern NSString * const INSIDER_EVENT_RECORDED;
29
+
30
+ - (void)sendDataToJS:(NSString *)eventName data:(NSDictionary *)data;
27
31
 
28
- -(void)sendDataToJS:(NSString *)eventName data:(NSDictionary *)data;
29
32
  @end
@@ -24,36 +24,34 @@ NSString * const INAPP_SEEN = @"INAPP_SEEN";
24
24
  NSString * const FOREGROUND_PUSH = @"FOREGROUND_PUSH";
25
25
  NSString * const INSIDER_ID_LISTENER = @"INSIDER_ID_LISTENER";
26
26
  NSString * const SESSION_STARTED = @"SESSION_STARTED";
27
+ NSString * const INSIDER_EVENT_RECORDED = @"INSIDER_EVENT_RECORDED";
27
28
 
28
29
  static RNNotificationHandler *notificationHandler;
29
30
 
30
- +(id)allocWithZone:(struct _NSZone *)zone{
31
- @try {
32
- static dispatch_once_t onceToken;
33
- dispatch_once(&onceToken, ^{
34
- notificationHandler = [super allocWithZone:zone];
35
- });
36
- return notificationHandler;
37
- } @catch (NSException *e) {
38
- [Insider sendError:e desc:@"RNNotificationHandler.m - allocWithZone"];
39
- }
31
+ + (id)allocWithZone:(struct _NSZone *)zone {
32
+ static dispatch_once_t onceToken;
33
+ dispatch_once(&onceToken, ^{
34
+ notificationHandler = [super allocWithZone:zone];
35
+ });
36
+
37
+ return notificationHandler;
38
+ }
39
+
40
+ + (BOOL)requiresMainQueueSetup {
41
+ return NO;
40
42
  }
41
43
 
42
44
  RCT_EXPORT_MODULE();
43
45
 
44
- -(NSArray<NSString *> *)supportedEvents{
45
- return @[NOTIFICATION_OPEN, INAPP_BUTTON_CLICK, TEMP_STORE_PURCHASE, TEMP_STORE_ADDED_TO_CART, TEMP_STORE_CUSTOM_ACTION, INAPP_SEEN, FOREGROUND_PUSH, INSIDER_ID_LISTENER, SESSION_STARTED];
46
+ - (NSArray<NSString *> *)supportedEvents {
47
+ return @[NOTIFICATION_OPEN, INAPP_BUTTON_CLICK, TEMP_STORE_PURCHASE, TEMP_STORE_ADDED_TO_CART, TEMP_STORE_CUSTOM_ACTION, INAPP_SEEN, FOREGROUND_PUSH, INSIDER_ID_LISTENER, SESSION_STARTED, INSIDER_EVENT_RECORDED];
46
48
  }
47
49
 
48
- -(void)sendDataToJS:(NSString *)eventName data:(NSDictionary *)data {
50
+ - (void)sendDataToJS:(NSString *)eventName data:(NSDictionary *)data {
49
51
  @try {
50
- if (!notificationHandler){
51
- [Insider sendError:nil desc:@"RNNotificationHandler.m - sendDataToJS. NotificationHandler is not initialized."];
52
- return;
53
- }
54
52
  [self sendEventWithName:eventName body:data];
55
53
  } @catch (NSException *e) {
56
- [Insider sendError:e desc:@"RNNotificationHandler.m - sendDataToJS"];
54
+ [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
57
55
  }
58
56
  }
59
57
  @end
@@ -25,6 +25,8 @@
25
25
 
26
26
  + (nonnull NSDictionary<NSString *, id> *)parseCustomParameters:(nonnull NSArray *)customParameters;
27
27
 
28
+ + (nonnull NSDictionary<NSString *, id> *)convertParametersForBridge:(nullable NSDictionary *)parameters;
29
+
28
30
  + (nonnull NSString *)mapAppCardsErrorCode:(nonnull NSError *)error;
29
31
  + (nonnull NSDictionary *)appCardsErrorToDictionary:(nonnull NSError *)error;
30
32
 
@@ -283,6 +283,85 @@
283
283
  return mappedCustomParameters;
284
284
  }
285
285
 
286
+ /**
287
+ * Normalizes a recorded-event `parameters` dictionary into values React Native's bridge
288
+ * can actually serialize, mirroring Android's `RNUtils.convertObjectMapToWritableMap`.
289
+ *
290
+ * WHY THIS IS NOT OPTIONAL: RN converts an Obj-C payload with
291
+ * `convertIdToFollyDynamic` (ReactCommon/react/utils/platform/ios/react/utils/FollyConvert.mm).
292
+ * That function dispatches ONLY on NSNumber / NSString / NSArray / NSDictionary / nil /
293
+ * NSNull. Anything else — NSDate above all, but also NSURL, NSData, NSUUID — falls off
294
+ * the end of the chain and hits `return nil`, producing a NULL folly::dynamic. The key
295
+ * survives but its value silently arrives in JS as `null`, so a `Date` parameter that
296
+ * Android delivers as epoch-millis would reach an iOS integrator as nothing at all.
297
+ *
298
+ * Conversions applied, recursively through nested dictionaries and arrays:
299
+ * - NSDate -> epoch MILLISECONDS as an NSNumber, matching Android's
300
+ * `map.putDouble(key, ((Date) value).getTime())` and the repo's
301
+ * epoch-millis bridge convention for dates.
302
+ * - NSNull -> preserved (RN maps it to JS `null`).
303
+ * - Anything else non-serializable -> its `description`, so ONE odd value degrades to a
304
+ * readable string instead of vanishing.
305
+ *
306
+ * NOTE: this is only about `Date`s nested INSIDE `parameters`. The top-level `timestamp`
307
+ * of a recorded event stays in WHOLE SECONDS by explicit product decision — see
308
+ * `RNInsiderEventListener.onEventRecorded` in RNInsider.m.
309
+ */
310
+ + (NSDictionary<NSString *, id> *)convertParametersForBridge:(NSDictionary *)parameters {
311
+ if (![parameters isKindOfClass:[NSDictionary class]]) {
312
+ return @{};
313
+ }
314
+
315
+ NSMutableDictionary<NSString *, id> *converted = [NSMutableDictionary dictionaryWithCapacity:parameters.count];
316
+
317
+ for (id key in parameters) {
318
+ // The bridge can only key an object by a string.
319
+ NSString *stringKey = [key isKindOfClass:[NSString class]] ? (NSString *)key : [key description];
320
+ if (stringKey == nil) continue;
321
+
322
+ converted[stringKey] = [self convertValueForBridge:parameters[key]];
323
+ }
324
+
325
+ return converted;
326
+ }
327
+
328
+ /// Recursive leaf/branch conversion backing `convertParametersForBridge:`.
329
+ + (id)convertValueForBridge:(id)value {
330
+ if (value == nil || value == [NSNull null]) {
331
+ return [NSNull null];
332
+ }
333
+
334
+ if ([value isKindOfClass:[NSDictionary class]]) {
335
+ return [self convertParametersForBridge:(NSDictionary *)value];
336
+ }
337
+
338
+ if ([value isKindOfClass:[NSArray class]]) {
339
+ NSArray *array = (NSArray *)value;
340
+ NSMutableArray *convertedArray = [NSMutableArray arrayWithCapacity:array.count];
341
+
342
+ for (id element in array) {
343
+ [convertedArray addObject:[self convertValueForBridge:element]];
344
+ }
345
+
346
+ return convertedArray;
347
+ }
348
+
349
+ // NSDate -> epoch millis. Checked before the NSNumber/NSString passthrough because a
350
+ // date is the value type that actually motivated this converter.
351
+ if ([value isKindOfClass:[NSDate class]]) {
352
+ NSTimeInterval seconds = [(NSDate *)value timeIntervalSince1970];
353
+ return @((long long)llround(seconds * 1000.0));
354
+ }
355
+
356
+ // NSNumber (incl. booleans) and NSString cross the bridge untouched.
357
+ if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) {
358
+ return value;
359
+ }
360
+
361
+ // Unknown leaf: degrade to a readable string rather than letting RN null it out.
362
+ return [value description] ?: [NSNull null];
363
+ }
364
+
286
365
  + (NSString *)mapAppCardsErrorCode:(NSError *)error {
287
366
  if ([error.domain isEqualToString:InsiderAppCardsErrorDomain]) {
288
367
  switch (error.code) {
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "react-native-insider",
3
- "version": "8.1.0",
3
+ "version": "9.0.0",
4
4
  "description": "React Native Insider SDK",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
8
  "test": "jest",
9
- "test:coverage": "jest --coverage --coverageReporters=text --coverageReporters=lcov --coverageReporters=json-summary"
9
+ "test:unit": "jest --selectProjects unit",
10
+ "test:integration": "jest --selectProjects integration",
11
+ "test:coverage": "jest --coverage --coverageReporters=text --coverageReporters=lcov --coverageReporters=json-summary",
12
+ "test:mutation": "stryker run",
13
+ "typecheck": "tsc --noEmit"
10
14
  },
11
15
  "keywords": [
12
16
  "react-native",
@@ -20,23 +24,50 @@
20
24
  ],
21
25
  "author": "insidermobile",
22
26
  "homepage": "https://github.com/useinsider/react-native-insider",
27
+ "peerDependencies": {
28
+ "react": ">=18.0.0",
29
+ "react-native": ">=0.74.0"
30
+ },
23
31
  "devDependencies": {
24
- "@babel/core": "^7.20.0",
32
+ "@babel/core": "^7.29.6",
25
33
  "@babel/preset-env": "^7.20.0",
26
34
  "@react-native/babel-preset": "0.83.0",
27
- "babel-jest": "^30.2.0",
35
+ "@stryker-mutator/core": "^9.6.1",
36
+ "@stryker-mutator/jest-runner": "^9.6.1",
37
+ "@testing-library/react-native": "^14.0.1",
38
+ "babel-jest": "^29.7.0",
28
39
  "jest": "^29.7.0",
40
+ "jest-config": "^29.7.0",
41
+ "jest-util": "^29.7.0",
29
42
  "react": "19.2.0",
30
- "react-native": "0.83.0"
43
+ "react-native": "0.83.0",
44
+ "react-test-renderer": "19.2.0",
45
+ "typescript": "^5.9.3"
31
46
  },
32
47
  "overrides": {
33
- "shell-quote": "^1.8.4"
48
+ "shell-quote": "^1.9.0",
49
+ "js-yaml": "^3.15.2",
50
+ "brace-expansion": "^1.1.18",
51
+ "@stryker-mutator/core": {
52
+ "minimatch": {
53
+ "brace-expansion": "^5.0.8"
54
+ }
55
+ },
56
+ "browserslist": "^4.28.7",
57
+ "baseline-browser-mapping": "^2.11.0",
58
+ "ws": "^7.5.11"
34
59
  },
35
- "jest": {
36
- "preset": "react-native",
37
- "modulePathIgnorePatterns": [
38
- "<rootDir>/example/node_modules",
39
- "<rootDir>/lib/"
40
- ]
60
+ "codegenConfig": {
61
+ "name": "RNInsiderSpec",
62
+ "type": "components",
63
+ "jsSrcsDir": "src/specs",
64
+ "android": {
65
+ "javaPackageName": "com.useinsider.react"
66
+ },
67
+ "ios": {
68
+ "componentProvider": {
69
+ "InsiderAppFrameView": "RNInsiderAppFrameView"
70
+ }
71
+ }
41
72
  }
42
73
  }
@@ -0,0 +1,17 @@
1
+ import { NativeModules, NativeEventEmitter } from 'react-native';
2
+
3
+ const NotificationHandler = NativeModules.RNNotificationHandler;
4
+
5
+ /**
6
+ * The single NativeEventEmitter for `RNNotificationHandler`, shared by every module that
7
+ * listens to native → JS events (`index.js` for the init/push/ID callbacks,
8
+ * `src/InsiderEvents.js` for the recorded-event stream).
9
+ *
10
+ * It MUST stay a single instance. Constructing a second emitter over the same native
11
+ * module would make the native side see two independent listener sets, so listener
12
+ * bookkeeping (`addListener` counts, `removeAllListeners`) would be split across emitters
13
+ * and each side would be blind to the other's subscriptions.
14
+ */
15
+ const eventHandler = new NativeEventEmitter(NotificationHandler);
16
+
17
+ export default eventHandler;
@@ -0,0 +1,45 @@
1
+ import type {ForwardRefExoticComponent, RefAttributes} from 'react';
2
+ import type {ViewProps} from 'react-native';
3
+ import type {InsiderAppFramesError} from './InsiderAppFramesError';
4
+ import type {InsiderAppFrameStatusType} from './InsiderAppFrameStatus';
5
+
6
+ export interface InsiderAppFrameProps extends ViewProps {
7
+ /** The placement whose content this frame renders. */
8
+ placementId: string;
9
+ /** Called on every status transition with the new status and the one it replaced. */
10
+ onStatusChange?: (
11
+ status: InsiderAppFrameStatusType,
12
+ previousStatus: InsiderAppFrameStatusType,
13
+ ) => void;
14
+ /** Called when resolving, downloading or rendering the content fails. */
15
+ onLoadError?: (error: InsiderAppFramesError) => void;
16
+ /**
17
+ * Called when the content reports a new height, in dp. The frame applies this height itself
18
+ * unless `style.height` is set, which always wins.
19
+ */
20
+ onHeightChange?: (height: number) => void;
21
+ /** Called when the content asks to be dismissed. A request, not a command. */
22
+ onDismissRequest?: () => void;
23
+ /** Called with the payload of a custom action defined in the content. */
24
+ onAction?: (data: Record<string, unknown>) => void;
25
+ }
26
+
27
+ /** Imperative access to a frame's current state, through a ref. */
28
+ export interface InsiderAppFrameHandle {
29
+ /** The frame's current status; `DETACHED` until the first transition. */
30
+ getStatus(): InsiderAppFrameStatusType;
31
+ /** The error behind the current status; non-null only while the status is an `ERROR_*` value. */
32
+ getError(): InsiderAppFramesError | null;
33
+ }
34
+
35
+ /**
36
+ * Renders the content of an App Frames placement.
37
+ *
38
+ * On iOS, requires the New Architecture and React Native 0.77 or later. Without the New
39
+ * Architecture the frame renders nothing; below 0.77 it may not register. Either case logs a
40
+ * one-time warning in development.
41
+ */
42
+ declare const InsiderAppFrame: ForwardRefExoticComponent<
43
+ InsiderAppFrameProps & RefAttributes<InsiderAppFrameHandle>
44
+ >;
45
+ export default InsiderAppFrame;