@loyalytics/swan-react-native-sdk 2.1.3-beta.0 → 2.1.3-beta.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 (89) hide show
  1. package/android/build.gradle +66 -0
  2. package/android/src/main/AndroidManifest.xml +10 -0
  3. package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +43 -0
  4. package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationPackage.kt +16 -0
  5. package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationActionReceiver.kt +49 -0
  6. package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationTemplate.kt +20 -0
  7. package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanTemplateRegistry.kt +47 -0
  8. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +103 -0
  9. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +132 -0
  10. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +129 -0
  11. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +412 -0
  12. package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationBitmapCache.kt +70 -0
  13. package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationImageLoader.kt +97 -0
  14. package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationStateManager.kt +85 -0
  15. package/android/src/main/res/anim/swan_fade_in.xml +6 -0
  16. package/android/src/main/res/anim/swan_fade_out.xml +6 -0
  17. package/android/src/main/res/anim/swan_slide_in_right.xml +8 -0
  18. package/android/src/main/res/anim/swan_slide_out_left.xml +8 -0
  19. package/android/src/main/res/drawable/swan_ic_chevron_left.xml +11 -0
  20. package/android/src/main/res/drawable/swan_ic_chevron_right.xml +11 -0
  21. package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +51 -0
  22. package/android/src/main/res/layout/swan_carousel_collapsed.xml +31 -0
  23. package/android/src/main/res/layout/swan_carousel_expanded.xml +96 -0
  24. package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +115 -0
  25. package/android/src/main/res/layout/swan_carousel_flipper_item.xml +7 -0
  26. package/android/src/test/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplateTest.kt +125 -0
  27. package/docs/SDK_INDUSTRY_REVIEW_REPORT.md +347 -0
  28. package/docs/Swan_Push_Notifications.postman_collection.json +330 -0
  29. package/docs/deep-link-attribution.md +281 -0
  30. package/ios/SwanNotificationContentExtension/Info.plist +40 -0
  31. package/ios/SwanNotificationContentExtension/MainInterface.storyboard +19 -0
  32. package/ios/SwanNotificationContentExtension/NotificationViewController.swift +190 -0
  33. package/ios/SwanNotificationContentExtension/SwanNotificationContentExtension.entitlements +10 -0
  34. package/ios/SwanNotificationContentExtension/common/ImageDownloader.swift +32 -0
  35. package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +336 -0
  36. package/lib/commonjs/constants/ApiUrls.js.map +1 -1
  37. package/lib/commonjs/index.js +117 -35
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/providers/NullPushProvider.js.map +1 -1
  40. package/lib/commonjs/services/DeviceRegistrationService.js.map +1 -1
  41. package/lib/commonjs/state/AuthStateMachine.js.map +1 -1
  42. package/lib/commonjs/state/DeviceStateMachine.js.map +1 -1
  43. package/lib/commonjs/state/PushStateMachine.js.map +1 -1
  44. package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -1
  45. package/lib/commonjs/utils/Logger.js.map +1 -1
  46. package/lib/commonjs/utils/SharedCredentialsManager.js +28 -0
  47. package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
  48. package/lib/commonjs/version.js +1 -1
  49. package/lib/module/index.js +117 -35
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/providers/NullPushProvider.js.map +1 -1
  52. package/lib/module/services/DeviceRegistrationService.js.map +1 -1
  53. package/lib/module/state/AuthStateMachine.js.map +1 -1
  54. package/lib/module/state/DeviceStateMachine.js.map +1 -1
  55. package/lib/module/state/PushStateMachine.js.map +1 -1
  56. package/lib/module/utils/FirebaseNotificationManager.js.map +1 -1
  57. package/lib/module/utils/Logger.js.map +1 -1
  58. package/lib/module/utils/SharedCredentialsManager.js +28 -0
  59. package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
  60. package/lib/module/version.js +1 -1
  61. package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/utils/Logger.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +13 -0
  71. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  73. package/lib/typescript/module/src/constants/ApiUrls.d.ts.map +1 -1
  74. package/lib/typescript/module/src/index.d.ts.map +1 -1
  75. package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -1
  76. package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts.map +1 -1
  77. package/lib/typescript/module/src/state/AuthStateMachine.d.ts.map +1 -1
  78. package/lib/typescript/module/src/state/DeviceStateMachine.d.ts.map +1 -1
  79. package/lib/typescript/module/src/state/PushStateMachine.d.ts.map +1 -1
  80. package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
  81. package/lib/typescript/module/src/utils/Logger.d.ts.map +1 -1
  82. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +13 -0
  83. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  84. package/lib/typescript/module/src/version.d.ts +1 -1
  85. package/package.json +7 -3
  86. package/react-native.config.json +12 -0
  87. package/scripts/setup-ios-extension.js +100 -20
  88. package/scripts/test-carousel-push.js +266 -0
  89. package/swan-react-native-sdk.podspec +18 -0
@@ -720,12 +720,12 @@ class SwanSDK {
720
720
  _Logger.default.log('[SwanSDK] Deep link attribution: found SWAN parameters:', JSON.stringify(swanParams));
721
721
 
722
722
  // Track click via webhook API (same endpoint as push notification clicks)
723
- const commId = swanParams['swan_comm_id'];
723
+ const commId = swanParams.swan_comm_id;
724
724
  if (!commId) {
725
725
  _Logger.default.warn('[SwanSDK] Deep link missing swan_comm_id, skipping webhook tracking');
726
726
  return;
727
727
  }
728
- const linkId = swanParams['swan_link_id'] || null;
728
+ const linkId = swanParams.swan_link_id || null;
729
729
 
730
730
  // Queue as SWAN_NOTIFICATION_ACK with type: 'deepLink' so backend
731
731
  // can distinguish from push notification ACKs (where commId is Firebase's messageId)
@@ -2112,7 +2112,11 @@ class SwanSDK {
2112
2112
  alert: true,
2113
2113
  badge: true,
2114
2114
  sound: true
2115
- }
2115
+ },
2116
+ // Pass through category for Content Extension (e.g., carousel)
2117
+ ...(remoteMessage?.category && {
2118
+ categoryId: remoteMessage.category
2119
+ })
2116
2120
  }
2117
2121
  };
2118
2122
 
@@ -2651,9 +2655,14 @@ class SwanSDK {
2651
2655
  await this.sendNotificationAck(messageId, 'clicked');
2652
2656
  }
2653
2657
  // Extract deep link information
2658
+ // For carousel notifications, the default route is in 'defaultRoute' field
2659
+ const resolvedRoute = notificationData.route || notificationData.defaultRoute;
2654
2660
  const deepLinkPayload = {
2655
- route: notificationData.route,
2656
- data: notificationData,
2661
+ route: resolvedRoute,
2662
+ data: {
2663
+ ...notificationData,
2664
+ route: resolvedRoute
2665
+ },
2657
2666
  title: initialNotification.notification?.title || notificationData.title,
2658
2667
  body: initialNotification.notification?.body || notificationData.body
2659
2668
  };
@@ -2693,9 +2702,14 @@ class SwanSDK {
2693
2702
  const notificationData = initialNotification.data || {};
2694
2703
 
2695
2704
  // Extract deep link information
2705
+ // For carousel notifications, the default route is in 'defaultRoute' field
2706
+ const resolvedRoute = notificationData.route || notificationData.defaultRoute;
2696
2707
  const deepLinkPayload = {
2697
- route: notificationData.route,
2698
- data: notificationData,
2708
+ route: resolvedRoute,
2709
+ data: {
2710
+ ...notificationData,
2711
+ route: resolvedRoute
2712
+ },
2699
2713
  title: initialNotification.notification?.title || notificationData.title,
2700
2714
  body: initialNotification.notification?.body || notificationData.body
2701
2715
  };
@@ -3355,6 +3369,14 @@ function createBackgroundMessageHandler() {
3355
3369
  }
3356
3370
  };
3357
3371
 
3372
+ // Add iOS category for Content Extension (e.g., carousel)
3373
+ if (remoteMessage?.category) {
3374
+ notificationConfig.ios = {
3375
+ ...(notificationConfig.ios || {}),
3376
+ categoryId: remoteMessage.category
3377
+ };
3378
+ }
3379
+
3358
3380
  // Add image if present
3359
3381
  if (imageUrl) {
3360
3382
  const {
@@ -3365,8 +3387,8 @@ function createBackgroundMessageHandler() {
3365
3387
  picture: imageUrl
3366
3388
  };
3367
3389
  // iOS: Add image as attachment
3368
- // Note: For best results on iOS, implement a Notification Service Extension
3369
3390
  notificationConfig.ios = {
3391
+ ...(notificationConfig.ios || {}),
3370
3392
  attachments: [{
3371
3393
  url: imageUrl
3372
3394
  }]
@@ -3412,29 +3434,51 @@ function createNotificationOpenedHandler() {
3412
3434
  return;
3413
3435
  }
3414
3436
 
3415
- // Check if iOS Notification Service Extension is active then skip delivery handling
3416
- const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3417
- if (!isNESActive && _reactNative.Platform.OS === 'ios') {
3418
- _Logger.default.log('[SwanSDK] ✅ iOS Notification Service Extension is inactive, will be handled by notifee handlers for click tracking');
3419
- return;
3420
- }
3421
- _Logger.default.log('[SwanSDK] ✅ iOS Notification Service Extension is active, click tracking will be handled now');
3422
-
3423
3437
  // Get messageId and notification data
3424
3438
  const messageId = event?.messageId;
3425
3439
  const notificationData = event?.data || {};
3426
3440
 
3441
+ // For iOS carousel notifications, iOS displays the notification (not Notifee),
3442
+ // so this handler MUST process the click regardless of NES status.
3443
+ // For non-carousel, defer to Notifee click handlers unless NES is active.
3444
+ const isIOSCarousel = _reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel';
3445
+ if (!isIOSCarousel) {
3446
+ const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3447
+ if (!isNESActive && _reactNative.Platform.OS === 'ios') {
3448
+ _Logger.default.log('[SwanSDK] Non-carousel iOS without NES, deferring to Notifee click handler');
3449
+ return;
3450
+ }
3451
+ }
3452
+
3427
3453
  // Extract deep link information
3428
- // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3454
+ // For carousel notifications, the default route is in 'defaultRoute' field
3455
+ let route = notificationData.route || notificationData.defaultRoute;
3456
+
3457
+ // For carousel on iOS, check for per-item route from Content Extension
3458
+ if (isIOSCarousel) {
3459
+ try {
3460
+ const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
3461
+ if (clickData?.route) {
3462
+ _Logger.default.log('[SwanSDK] Carousel item route from Content Extension:', clickData.route);
3463
+ route = clickData.route;
3464
+ }
3465
+ } catch (err) {
3466
+ _Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
3467
+ }
3468
+ }
3429
3469
  const deepLinkPayload = {
3430
- route: notificationData.route,
3431
- data: notificationData,
3470
+ route,
3471
+ data: {
3472
+ ...notificationData,
3473
+ route
3474
+ },
3432
3475
  title: notificationData.title,
3433
3476
  body: notificationData.body
3434
3477
  };
3435
- _Logger.default.log('[SwanSDK] Foreground notification clicked:', {
3478
+ _Logger.default.log('[SwanSDK] Notification opened:', {
3436
3479
  messageId,
3437
- route: deepLinkPayload.route
3480
+ route: deepLinkPayload.route,
3481
+ isIOSCarousel
3438
3482
  });
3439
3483
 
3440
3484
  // Emit notificationOpened event for host app to handle
@@ -3498,19 +3542,40 @@ function createNotifeeForegroundHandler() {
3498
3542
 
3499
3543
  // Extract deep link information
3500
3544
  // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3545
+ // For carousel notifications, the default route is in 'defaultRoute' field
3546
+ let route = notificationData.route || notificationData.defaultRoute;
3547
+
3548
+ // For carousel notifications on iOS, the Content Extension saves per-item
3549
+ // click data (including item-specific route) to the App Group.
3550
+ if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
3551
+ try {
3552
+ const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
3553
+ if (clickData?.route) {
3554
+ _Logger.default.log('[SwanSDK] Foreground: carousel item route from Content Extension:', clickData.route);
3555
+ route = clickData.route;
3556
+ }
3557
+ } catch (err) {
3558
+ _Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
3559
+ }
3560
+ }
3501
3561
  const deepLinkPayload = {
3502
- route: notificationData.route,
3503
- data: notificationData,
3562
+ route,
3563
+ data: {
3564
+ ...notificationData,
3565
+ route
3566
+ },
3504
3567
  title: event?.detail?.notification?.title,
3505
3568
  body: event?.detail?.notification?.body
3506
3569
  };
3507
3570
  _Logger.default.log('[SwanSDK] Foreground notification clicked (Notifee):', {
3508
3571
  messageId,
3509
- route: deepLinkPayload.route
3572
+ route: deepLinkPayload.route,
3573
+ type: notificationData.notificationType
3510
3574
  });
3511
3575
 
3512
3576
  // Emit notificationOpened event for host app to handle
3513
3577
  sdkInstance.emitNotificationOpened(deepLinkPayload);
3578
+ _Logger.default.log('[SwanSDK] Foreground: emitted notificationOpened with route:', deepLinkPayload.route);
3514
3579
 
3515
3580
  // Send click ACK
3516
3581
  if (messageId) {
@@ -3554,12 +3619,8 @@ function createNotifeeBackgroundHandler() {
3554
3619
  return;
3555
3620
  }
3556
3621
 
3557
- // Check if iOS Notification Service Extension is active then skip delivery handling
3558
- const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3559
- if (isNESActive) {
3560
- _Logger.default.log('[SwanSDK] ✅ iOS Notification Service Extension is active, skipping delivery ACK (NES handles it)');
3561
- return;
3562
- }
3622
+ // NOTE: NES handles delivery ACK, but click ACK is ALWAYS our responsibility.
3623
+ // Never skip click handling based on NES status.
3563
3624
 
3564
3625
  // Get messageId and notification data
3565
3626
  const messageId = detail?.notification?.data?.messageId;
@@ -3567,15 +3628,35 @@ function createNotifeeBackgroundHandler() {
3567
3628
 
3568
3629
  // Extract deep link information
3569
3630
  // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3631
+ // For carousel notifications, the default route is in 'defaultRoute' field
3632
+ let route = notificationData.route || notificationData.defaultRoute;
3633
+
3634
+ // For carousel notifications on iOS, the Content Extension saves per-item
3635
+ // click data (including item-specific route) to the App Group.
3636
+ if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
3637
+ try {
3638
+ const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
3639
+ if (clickData?.route) {
3640
+ _Logger.default.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
3641
+ route = clickData.route;
3642
+ }
3643
+ } catch (err) {
3644
+ _Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
3645
+ }
3646
+ }
3570
3647
  const deepLinkPayload = {
3571
- route: notificationData.route,
3572
- data: notificationData,
3648
+ route,
3649
+ data: {
3650
+ ...notificationData,
3651
+ route
3652
+ },
3573
3653
  title: detail?.notification?.title,
3574
3654
  body: detail?.notification?.body
3575
3655
  };
3576
3656
  _Logger.default.log('[SwanSDK] Background notification clicked:', {
3577
3657
  messageId,
3578
- route: deepLinkPayload.route
3658
+ route: deepLinkPayload.route,
3659
+ type: notificationData.notificationType
3579
3660
  });
3580
3661
 
3581
3662
  // Try to use SDK instance if available and ready (has deviceId)
@@ -3588,11 +3669,12 @@ function createNotifeeBackgroundHandler() {
3588
3669
 
3589
3670
  // Emit notificationOpened event for host app to handle
3590
3671
  sdkInstance.emitNotificationOpened(deepLinkPayload);
3672
+ _Logger.default.log('[SwanSDK] Background: emitted notificationOpened with route:', deepLinkPayload.route);
3591
3673
 
3592
- // Send click ACK
3674
+ // Send click ACK (NES handles delivery ACK, but click ACK is always ours)
3593
3675
  if (messageId) {
3594
3676
  await sdkInstance.sendNotificationAck(messageId, 'clicked');
3595
- _Logger.default.log('[SwanSDK] Click ACK sent via SDK');
3677
+ _Logger.default.log('[SwanSDK] Background: click ACK sent via SDK');
3596
3678
  }
3597
3679
  return;
3598
3680
  }