@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
@@ -63,7 +63,7 @@ function copyFileOrDirectory(src, dest) {
63
63
 
64
64
  // Copy all files in directory
65
65
  const files = fs.readdirSync(src);
66
- files.forEach(file => {
66
+ files.forEach((file) => {
67
67
  const srcPath = path.join(src, file);
68
68
  const destPath = path.join(dest, file);
69
69
  copyFileOrDirectory(srcPath, destPath);
@@ -93,7 +93,9 @@ function setup() {
93
93
  // Check if ios/ directory exists
94
94
  const iosDir = path.join(projectRoot, 'ios');
95
95
  if (!fs.existsSync(iosDir)) {
96
- logError('iOS directory not found! This script must be run from a React Native project root.');
96
+ logError(
97
+ 'iOS directory not found! This script must be run from a React Native project root.'
98
+ );
97
99
  logError('Expected directory: ' + iosDir);
98
100
  process.exit(1);
99
101
  }
@@ -112,10 +114,17 @@ function setup() {
112
114
  logSuccess(`Project name: ${projectName}`);
113
115
 
114
116
  // Find SDK directory
115
- const sdkDir = path.join(projectRoot, 'node_modules', '@loyalytics', 'swan-react-native-sdk');
117
+ const sdkDir = path.join(
118
+ projectRoot,
119
+ 'node_modules',
120
+ '@loyalytics',
121
+ 'swan-react-native-sdk'
122
+ );
116
123
  if (!fs.existsSync(sdkDir)) {
117
124
  logError('@loyalytics/swan-react-native-sdk not found in node_modules!');
118
- logError('Make sure the SDK is installed: npm install @loyalytics/swan-react-native-sdk');
125
+ logError(
126
+ 'Make sure the SDK is installed: npm install @loyalytics/swan-react-native-sdk'
127
+ );
119
128
  process.exit(1);
120
129
  }
121
130
  logSuccess('Swan SDK found');
@@ -142,7 +151,7 @@ function setup() {
142
151
 
143
152
  if (missingDeps.length > 0) {
144
153
  logWarning('Missing required dependencies:');
145
- missingDeps.forEach(dep => {
154
+ missingDeps.forEach((dep) => {
146
155
  log(` - ${dep}`, colors.yellow);
147
156
  });
148
157
  logInfo('Install missing dependencies with:');
@@ -153,8 +162,15 @@ function setup() {
153
162
  // Step 3: Copy extension files
154
163
  logStep(3, 'Copying Notification Service Extension files');
155
164
 
156
- const extensionSrcDir = path.join(sdkDir, 'ios', 'SwanNotificationServiceExtension');
157
- const extensionDestDir = path.join(iosDir, 'SwanNotificationServiceExtension');
165
+ const extensionSrcDir = path.join(
166
+ sdkDir,
167
+ 'ios',
168
+ 'SwanNotificationServiceExtension'
169
+ );
170
+ const extensionDestDir = path.join(
171
+ iosDir,
172
+ 'SwanNotificationServiceExtension'
173
+ );
158
174
 
159
175
  if (fs.existsSync(extensionDestDir)) {
160
176
  logWarning('SwanNotificationServiceExtension directory already exists');
@@ -163,21 +179,62 @@ function setup() {
163
179
 
164
180
  try {
165
181
  copyFileOrDirectory(extensionSrcDir, extensionDestDir);
166
- logSuccess('Extension files copied successfully');
182
+ logSuccess('Service Extension files copied successfully');
167
183
  logInfo(`Destination: ${extensionDestDir}`);
168
184
  } catch (error) {
169
185
  logError(`Failed to copy extension files: ${error.message}`);
170
186
  process.exit(1);
171
187
  }
172
188
 
189
+ // Step 3b: Copy Content Extension files (for carousel, rich notifications)
190
+ logStep('3b', 'Copying Notification Content Extension files');
191
+
192
+ const contentExtSrcDir = path.join(
193
+ sdkDir,
194
+ 'ios',
195
+ 'SwanNotificationContentExtension'
196
+ );
197
+ const contentExtDestDir = path.join(
198
+ iosDir,
199
+ 'SwanNotificationContentExtension'
200
+ );
201
+
202
+ if (fs.existsSync(contentExtSrcDir)) {
203
+ if (fs.existsSync(contentExtDestDir)) {
204
+ logWarning('SwanNotificationContentExtension directory already exists');
205
+ logInfo('Overwriting existing files...');
206
+ }
207
+
208
+ try {
209
+ copyFileOrDirectory(contentExtSrcDir, contentExtDestDir);
210
+ logSuccess('Content Extension files copied successfully');
211
+ logInfo(`Destination: ${contentExtDestDir}`);
212
+ } catch (error) {
213
+ logWarning(`Failed to copy Content Extension files: ${error.message}`);
214
+ logInfo(
215
+ 'Content Extension is optional — carousel push will fall back to standard notification'
216
+ );
217
+ }
218
+ } else {
219
+ logInfo(
220
+ 'Content Extension source not found (optional, carousel will use standard notification)'
221
+ );
222
+ }
223
+
173
224
  // Step 4: Generate manual setup instructions
174
225
  logStep(4, 'Setup Instructions');
175
226
 
176
227
  log('\n' + '─'.repeat(60), colors.cyan);
177
- log('IMPORTANT: Manual Xcode Configuration Required', colors.bright + colors.yellow);
228
+ log(
229
+ 'IMPORTANT: Manual Xcode Configuration Required',
230
+ colors.bright + colors.yellow
231
+ );
178
232
  log('─'.repeat(60) + '\n', colors.cyan);
179
233
 
180
- log('The extension files have been copied to your ios/ directory.', colors.cyan);
234
+ log(
235
+ 'The extension files have been copied to your ios/ directory.',
236
+ colors.cyan
237
+ );
181
238
  log('You must now complete the setup in Xcode:\n');
182
239
 
183
240
  const instructions = [
@@ -192,7 +249,7 @@ function setup() {
192
249
  'Click Finish',
193
250
  'When prompted "Activate scheme?", click "Cancel"',
194
251
  'STOP HERE: If you have not done this, the next steps will fail or be overwritten.',
195
- ]
252
+ ],
196
253
  },
197
254
  {
198
255
  title: '2. Verify Files in Xcode Project',
@@ -202,7 +259,7 @@ function setup() {
202
259
  'In Xcode Project Navigator, expand `SwanNotificationServiceExtension` folder.',
203
260
  'Verify that `NotificationService.swift`, `Info.plist`, and `SwanNotificationServiceExtension.entitlements` are present and contain the SwanSDK code.',
204
261
  'If any files are missing from the Xcode Project Navigator (even if they exist on disk), right-click on the `SwanNotificationServiceExtension` folder in Xcode and choose "Add Files to YourProject" to re-add them, making sure "Add to target: SwanNotificationServiceExtension" is checked and "Copy items if needed" is UNCHECKED.',
205
- ]
262
+ ],
206
263
  },
207
264
  {
208
265
  title: '3. Configure App Groups',
@@ -216,18 +273,36 @@ function setup() {
216
273
  'Click "+ Capability" and add "App Groups"',
217
274
  'Click "+" under App Groups and add: group.swan.sdk.notifications',
218
275
  '(IMPORTANT: Both targets must have the SAME App Group ID)',
219
- ]
276
+ ],
220
277
  },
221
278
  {
222
- title: '4. Build and Test',
279
+ title: '4. Add Content Extension Target (OPTIONAL — for carousel push)',
280
+ steps: [
281
+ 'In Xcode, go to File → New → Target',
282
+ 'Choose "Notification Content Extension"',
283
+ 'Product Name: SwanNotificationContentExtension',
284
+ 'Language: Swift',
285
+ 'Click Finish, click "Cancel" when prompted to activate scheme',
286
+ 'Delete the auto-generated files and verify the Swan files are visible:',
287
+ ' - NotificationViewController.swift',
288
+ ' - templates/CarouselView.swift',
289
+ ' - common/ImageDownloader.swift',
290
+ ' - Info.plist, MainInterface.storyboard, entitlements',
291
+ 'Add App Group "group.swan.sdk.notifications" to this target too',
292
+ 'Set deployment target to match your main app',
293
+ ],
294
+ },
295
+ {
296
+ title: '5. Build and Test',
223
297
  steps: [
224
298
  'Build your app: Product → Build (Cmd+B)',
225
299
  'Archive your app: Product → Archive',
226
- 'Test on a REAL device (push doesn\'t work on simulator)',
300
+ "Test on a REAL device (push doesn't work on simulator)",
227
301
  'Send a test notification and check Xcode console for logs',
228
302
  'Look for: [SwanSDK Extension] logs',
229
- ]
230
- }
303
+ 'Test carousel: send push with notificationType: "carousel" and long-press',
304
+ ],
305
+ },
231
306
  ];
232
307
 
233
308
  instructions.forEach((section, index) => {
@@ -247,19 +322,24 @@ function setup() {
247
322
  log('Troubleshooting', colors.bright);
248
323
  log('─'.repeat(60) + '\n', colors.cyan);
249
324
 
250
- log('If extension logs don\'t appear:', colors.yellow);
325
+ log("If extension logs don't appear:", colors.yellow);
251
326
  log(' • Make sure App Groups are configured in BOTH targets');
252
327
  log(' • Verify App Group ID matches: group.swan.sdk.notifications');
253
328
  log(' • Check that mutable-content: 1 is in FCM APNS payload');
254
329
  log(' • Extension only runs for APNS notifications with mutable-content');
255
330
  log(' • Test on a real device, not simulator\n');
256
331
 
257
- log('If you see "CocoaPods Error: Unable to find compatibility version string for object version 70":', colors.yellow);
332
+ log(
333
+ 'If you see "CocoaPods Error: Unable to find compatibility version string for object version 70":',
334
+ colors.yellow
335
+ );
258
336
  log(' • In Xcode, select your Project → Build Settings → Project Format');
259
337
  log(' • Set it to "Xcode 16.0-compatible" or later\n');
260
338
 
261
339
  log('For detailed documentation, see:', colors.cyan);
262
- log(' node_modules/@loyalytics/swan-react-native-sdk/docs/IOS_NOTIFICATION_EXTENSION_SETUP.md\n');
340
+ log(
341
+ ' node_modules/@loyalytics/swan-react-native-sdk/docs/IOS_NOTIFICATION_EXTENSION_SETUP.md\n'
342
+ );
263
343
 
264
344
  logSuccess('Setup script completed!');
265
345
  log('');
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Swan SDK - Carousel Push Notification Test Script
5
+ *
6
+ * Sends test carousel push notifications via Firebase Admin SDK.
7
+ * Requires firebase-admin to be installed (already in devDependencies).
8
+ *
9
+ * Usage:
10
+ * node scripts/test-carousel-push.js --token <FCM_TOKEN> [--mode manual|auto] [--scenario <name>]
11
+ *
12
+ * Environment:
13
+ * GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
14
+ *
15
+ * Scenarios:
16
+ * manual-3 — Manual carousel with 3 products (default)
17
+ * auto-5 — Auto carousel with 5 products, 2s interval (ViewFlipper)
18
+ * filmstrip-4 — Filmstrip variant with 4 products (3-image preview strip)
19
+ * single-item — Single item carousel (should hide arrows)
20
+ * invalid-json — Invalid carouselItems JSON (tests fallback)
21
+ * no-images — Items without imageUrl (tests filtering)
22
+ */
23
+
24
+ const admin = require('firebase-admin');
25
+
26
+ // Parse CLI args
27
+ const args = process.argv.slice(2);
28
+ const getArg = (name) => {
29
+ const idx = args.indexOf(`--${name}`);
30
+ return idx !== -1 ? args[idx + 1] : undefined;
31
+ };
32
+
33
+ const fcmToken = getArg('token');
34
+ const mode = getArg('mode') || 'manual';
35
+ const scenarioName = getArg('scenario') || 'manual-3';
36
+
37
+ if (!fcmToken) {
38
+ console.error(
39
+ 'Usage: node scripts/test-carousel-push.js --token <FCM_TOKEN> [--mode manual|auto] [--scenario <name>]'
40
+ );
41
+ console.error(
42
+ '\nAvailable scenarios: manual-3, auto-5, filmstrip-4, single-item, invalid-json, no-images'
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ // Initialize Firebase Admin
48
+ if (!admin.apps.length) {
49
+ admin.initializeApp({
50
+ credential: admin.credential.applicationDefault(),
51
+ });
52
+ }
53
+
54
+ const SAMPLE_IMAGES = [
55
+ 'https://picsum.photos/seed/swan1/400/200',
56
+ 'https://picsum.photos/seed/swan2/400/200',
57
+ 'https://picsum.photos/seed/swan3/400/200',
58
+ 'https://picsum.photos/seed/swan4/400/200',
59
+ 'https://picsum.photos/seed/swan5/400/200',
60
+ ];
61
+
62
+ const scenarios = {
63
+ 'manual-3': {
64
+ name: 'Manual carousel — 3 products',
65
+ data: {
66
+ notificationType: 'carousel',
67
+ carouselMode: 'manual',
68
+ title: 'Flash Sale - Top Picks!',
69
+ body: 'Swipe to see our best deals',
70
+ channelId: 'swan_promotional',
71
+ carouselItems: JSON.stringify([
72
+ {
73
+ imageUrl: SAMPLE_IMAGES[0],
74
+ title: 'Wireless Earbuds',
75
+ body: '50% off - $29.99',
76
+ route: '/product/101',
77
+ },
78
+ {
79
+ imageUrl: SAMPLE_IMAGES[1],
80
+ title: 'Smart Watch',
81
+ body: 'New arrival',
82
+ route: '/product/102',
83
+ },
84
+ {
85
+ imageUrl: SAMPLE_IMAGES[2],
86
+ title: 'Phone Case',
87
+ body: 'Best seller',
88
+ route: '/product/103',
89
+ },
90
+ ]),
91
+ },
92
+ },
93
+ 'auto-5': {
94
+ name: 'Auto carousel — 5 products, 2s interval (ViewFlipper)',
95
+ data: {
96
+ notificationType: 'carousel',
97
+ carouselMode: 'auto',
98
+ carouselInterval: '2000',
99
+ title: 'Recommended for You',
100
+ body: 'Based on your recent browsing',
101
+ channelId: 'swan_general',
102
+ defaultRoute: '/recommended',
103
+ carouselItems: JSON.stringify([
104
+ {
105
+ imageUrl: SAMPLE_IMAGES[0],
106
+ title: 'Product A',
107
+ body: '$19.99',
108
+ route: '/product/201',
109
+ },
110
+ {
111
+ imageUrl: SAMPLE_IMAGES[1],
112
+ title: 'Product B',
113
+ body: '$24.99',
114
+ route: '/product/202',
115
+ },
116
+ {
117
+ imageUrl: SAMPLE_IMAGES[2],
118
+ title: 'Product C',
119
+ body: '$14.99',
120
+ route: '/product/203',
121
+ },
122
+ {
123
+ imageUrl: SAMPLE_IMAGES[3],
124
+ title: 'Product D',
125
+ body: '$39.99',
126
+ route: '/product/204',
127
+ },
128
+ {
129
+ imageUrl: SAMPLE_IMAGES[4],
130
+ title: 'Product E',
131
+ body: '$9.99',
132
+ route: '/product/205',
133
+ },
134
+ ]),
135
+ },
136
+ },
137
+ 'filmstrip-4': {
138
+ name: 'Filmstrip carousel — 4 products with side previews',
139
+ data: {
140
+ notificationType: 'carousel',
141
+ carouselMode: 'manual',
142
+ carouselVariant: 'filmstrip',
143
+ title: 'Summer Collection',
144
+ body: 'Tap the side images to browse',
145
+ channelId: 'swan_promotional',
146
+ carouselItems: JSON.stringify([
147
+ {
148
+ imageUrl: SAMPLE_IMAGES[0],
149
+ title: 'Summer Dress',
150
+ body: '$49.99',
151
+ route: '/product/401',
152
+ },
153
+ {
154
+ imageUrl: SAMPLE_IMAGES[1],
155
+ title: 'Beach Sandals',
156
+ body: '$29.99',
157
+ route: '/product/402',
158
+ },
159
+ {
160
+ imageUrl: SAMPLE_IMAGES[2],
161
+ title: 'Sun Hat',
162
+ body: '$19.99',
163
+ route: '/product/403',
164
+ },
165
+ {
166
+ imageUrl: SAMPLE_IMAGES[3],
167
+ title: 'Sunglasses',
168
+ body: '$34.99',
169
+ route: '/product/404',
170
+ },
171
+ ]),
172
+ },
173
+ },
174
+ 'single-item': {
175
+ name: 'Single item carousel (should look like normal image notification)',
176
+ data: {
177
+ notificationType: 'carousel',
178
+ carouselMode: 'manual',
179
+ title: 'Just for You',
180
+ body: 'Check this out',
181
+ channelId: 'swan_promotional',
182
+ carouselItems: JSON.stringify([
183
+ {
184
+ imageUrl: SAMPLE_IMAGES[0],
185
+ title: 'Featured Product',
186
+ body: 'Limited time offer',
187
+ route: '/product/301',
188
+ },
189
+ ]),
190
+ },
191
+ },
192
+ 'invalid-json': {
193
+ name: 'Invalid carouselItems JSON (should fall back to standard notification)',
194
+ data: {
195
+ notificationType: 'carousel',
196
+ carouselMode: 'manual',
197
+ title: 'Bad Payload Test',
198
+ body: 'Should show as standard notification',
199
+ channelId: 'swan_general',
200
+ carouselItems: 'this is not valid json {{{',
201
+ },
202
+ },
203
+ 'no-images': {
204
+ name: 'Items without imageUrl (should filter and fall back)',
205
+ data: {
206
+ notificationType: 'carousel',
207
+ carouselMode: 'manual',
208
+ title: 'No Images Test',
209
+ body: 'Items lack imageUrl',
210
+ channelId: 'swan_general',
211
+ carouselItems: JSON.stringify([
212
+ { title: 'No image 1', body: 'Missing imageUrl' },
213
+ { title: 'No image 2', body: 'Also missing' },
214
+ ]),
215
+ },
216
+ },
217
+ };
218
+
219
+ async function sendPush(scenario) {
220
+ console.log(`\nSending: ${scenario.name}`);
221
+ console.log(`Mode: ${scenario.data.carouselMode || 'manual'}`);
222
+ console.log(`Token: ${fcmToken.substring(0, 20)}...`);
223
+
224
+ const message = {
225
+ token: fcmToken,
226
+ data: scenario.data,
227
+ android: {
228
+ priority: 'high',
229
+ },
230
+ apns: {
231
+ payload: {
232
+ aps: {
233
+ 'mutable-content': 1,
234
+ 'category': 'swan_carousel',
235
+ },
236
+ },
237
+ },
238
+ };
239
+
240
+ try {
241
+ const response = await admin.messaging().send(message);
242
+ console.log(`Sent successfully! messageId: ${response}`);
243
+ return response;
244
+ } catch (error) {
245
+ console.error(`Failed to send: ${error.message}`);
246
+ return null;
247
+ }
248
+ }
249
+
250
+ async function main() {
251
+ const scenario = scenarios[scenarioName];
252
+ if (!scenario) {
253
+ console.error(`Unknown scenario: ${scenarioName}`);
254
+ console.error(`Available: ${Object.keys(scenarios).join(', ')}`);
255
+ process.exit(1);
256
+ }
257
+
258
+ // Override mode if --mode flag is provided and scenario supports it
259
+ if (mode && scenario.data.carouselMode !== undefined) {
260
+ scenario.data.carouselMode = mode;
261
+ }
262
+
263
+ await sendPush(scenario);
264
+ }
265
+
266
+ main().catch(console.error);
@@ -0,0 +1,18 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "swan-react-native-sdk"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+ s.source = { :git => package["repository"]["url"], :tag => "v#{s.version}" }
13
+
14
+ s.platforms = { :ios => min_ios_version_supported }
15
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
16
+
17
+ install_modules_dependencies(s)
18
+ end