@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
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
157
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|