@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.
- package/android/build.gradle +66 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +43 -0
- package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationPackage.kt +16 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationActionReceiver.kt +49 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationTemplate.kt +20 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanTemplateRegistry.kt +47 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +103 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +132 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +129 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +412 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationBitmapCache.kt +70 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationImageLoader.kt +97 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationStateManager.kt +85 -0
- package/android/src/main/res/anim/swan_fade_in.xml +6 -0
- package/android/src/main/res/anim/swan_fade_out.xml +6 -0
- package/android/src/main/res/anim/swan_slide_in_right.xml +8 -0
- package/android/src/main/res/anim/swan_slide_out_left.xml +8 -0
- package/android/src/main/res/drawable/swan_ic_chevron_left.xml +11 -0
- package/android/src/main/res/drawable/swan_ic_chevron_right.xml +11 -0
- package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +51 -0
- package/android/src/main/res/layout/swan_carousel_collapsed.xml +31 -0
- package/android/src/main/res/layout/swan_carousel_expanded.xml +96 -0
- package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +115 -0
- package/android/src/main/res/layout/swan_carousel_flipper_item.xml +7 -0
- package/android/src/test/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplateTest.kt +125 -0
- package/docs/SDK_INDUSTRY_REVIEW_REPORT.md +347 -0
- package/docs/Swan_Push_Notifications.postman_collection.json +330 -0
- package/docs/deep-link-attribution.md +281 -0
- package/ios/SwanNotificationContentExtension/Info.plist +40 -0
- package/ios/SwanNotificationContentExtension/MainInterface.storyboard +19 -0
- package/ios/SwanNotificationContentExtension/NotificationViewController.swift +190 -0
- package/ios/SwanNotificationContentExtension/SwanNotificationContentExtension.entitlements +10 -0
- package/ios/SwanNotificationContentExtension/common/ImageDownloader.swift +32 -0
- package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +336 -0
- package/lib/commonjs/constants/ApiUrls.js.map +1 -1
- package/lib/commonjs/index.js +117 -35
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/providers/NullPushProvider.js.map +1 -1
- package/lib/commonjs/services/DeviceRegistrationService.js.map +1 -1
- package/lib/commonjs/state/AuthStateMachine.js.map +1 -1
- package/lib/commonjs/state/DeviceStateMachine.js.map +1 -1
- package/lib/commonjs/state/PushStateMachine.js.map +1 -1
- package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -1
- package/lib/commonjs/utils/Logger.js.map +1 -1
- package/lib/commonjs/utils/SharedCredentialsManager.js +28 -0
- package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/index.js +117 -35
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/NullPushProvider.js.map +1 -1
- package/lib/module/services/DeviceRegistrationService.js.map +1 -1
- package/lib/module/state/AuthStateMachine.js.map +1 -1
- package/lib/module/state/DeviceStateMachine.js.map +1 -1
- package/lib/module/state/PushStateMachine.js.map +1 -1
- package/lib/module/utils/FirebaseNotificationManager.js.map +1 -1
- package/lib/module/utils/Logger.js.map +1 -1
- package/lib/module/utils/SharedCredentialsManager.js +28 -0
- package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/Logger.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +13 -0
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/constants/ApiUrls.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -1
- package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts.map +1 -1
- package/lib/typescript/module/src/state/AuthStateMachine.d.ts.map +1 -1
- package/lib/typescript/module/src/state/DeviceStateMachine.d.ts.map +1 -1
- package/lib/typescript/module/src/state/PushStateMachine.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/Logger.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +13 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +7 -3
- package/react-native.config.json +12 -0
- package/scripts/setup-ios-extension.js +100 -20
- package/scripts/test-carousel-push.js +266 -0
- package/swan-react-native-sdk.podspec +18 -0
package/lib/commonjs/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
2656
|
-
data:
|
|
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:
|
|
2698
|
-
data:
|
|
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
|
-
//
|
|
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
|
|
3431
|
-
data:
|
|
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]
|
|
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
|
|
3503
|
-
data:
|
|
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
|
-
//
|
|
3558
|
-
|
|
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
|
|
3572
|
-
data:
|
|
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]
|
|
3677
|
+
_Logger.default.log('[SwanSDK] Background: click ACK sent via SDK');
|
|
3596
3678
|
}
|
|
3597
3679
|
return;
|
|
3598
3680
|
}
|