@loyalytics/swan-react-native-sdk 2.1.3-beta.2 → 2.1.3-beta.3

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.
@@ -8,5 +8,5 @@ exports.SDK_VERSION = void 0;
8
8
  // This file is generated from package.json version during build.
9
9
  // See scripts/generate-version.js
10
10
 
11
- const SDK_VERSION = exports.SDK_VERSION = '2.1.3-beta.2';
11
+ const SDK_VERSION = exports.SDK_VERSION = '2.1.3-beta.3';
12
12
  //# sourceMappingURL=version.js.map
@@ -4,5 +4,5 @@
4
4
  // This file is generated from package.json version during build.
5
5
  // See scripts/generate-version.js
6
6
 
7
- export const SDK_VERSION = '2.1.3-beta.2';
7
+ export const SDK_VERSION = '2.1.3-beta.3';
8
8
  //# sourceMappingURL=version.js.map
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.1.3-beta.2";
1
+ export declare const SDK_VERSION = "2.1.3-beta.3";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.1.3-beta.2";
1
+ export declare const SDK_VERSION = "2.1.3-beta.3";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loyalytics/swan-react-native-sdk",
3
- "version": "2.1.3-beta.2",
3
+ "version": "2.1.3-beta.3",
4
4
  "description": "React Native SDK for Swan",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
@@ -23,7 +23,6 @@
23
23
  "ios",
24
24
  "cpp",
25
25
  "scripts",
26
- "docs",
27
26
  "*.podspec",
28
27
  "react-native.config.json",
29
28
  "!ios/build",
@@ -214,9 +213,6 @@
214
213
  "@react-native-async-storage/async-storage": "2.2.0",
215
214
  "@react-native-community/geolocation": "^3.4.0",
216
215
  "@react-native-community/netinfo": "^11.0.0",
217
- "expo": "~52.0.47",
218
- "react": "*",
219
- "react-native": "*",
220
216
  "react-native-base64": "^0.2.1",
221
217
  "react-native-device-info": "^14.0.1",
222
218
  "react-native-shared-group-preferences": "^1.1.24",
@@ -1,335 +0,0 @@
1
- # iOS Notification Service Extension Setup
2
-
3
- ## Overview
4
-
5
- The Swan SDK's Notification Service Extension enables:
6
- - ✅ **Delivery tracking** when app is killed or in background
7
- - ✅ **Rich media support** (images in notifications)
8
- - ✅ **Reliable ACKs** even when app is not running
9
-
10
- This extension is **required for iOS** to track push notification delivery in all app states.
11
-
12
- ---
13
-
14
- ## Why is this needed?
15
-
16
- ### The iOS Limitation
17
-
18
- On iOS, when your app is in background or killed state and receives a push notification with a visible alert, React Native's `setBackgroundMessageHandler` **is NOT called**. This is fundamental iOS behavior, not a bug.
19
-
20
- **What this means:**
21
- - ❌ No delivery tracking when app is killed
22
- - ❌ No custom notification display logic in background
23
- - ❌ No image processing without extension
24
-
25
- ### The Solution: Notification Service Extension
26
-
27
- A Notification Service Extension is a separate iOS app extension that:
28
- - ✅ Runs in a **separate process** from your main app
29
- - ✅ Intercepts **all incoming notifications** before they're displayed
30
- - ✅ Has **30 seconds** to process the notification
31
- - ✅ Can send delivery ACKs, download images, and modify content
32
-
33
- **Industry standard:** Firebase, OneSignal, CleverTap, Airship all use this approach.
34
-
35
- ---
36
-
37
- ## Installation
38
-
39
- ### Step 1: Install Dependencies
40
-
41
- ```bash
42
- npm install react-native-shared-group-preferences
43
- cd ios && pod install && cd ..
44
- ```
45
-
46
- **Why needed:** Share credentials between app and extension.
47
-
48
- ---
49
-
50
- ### Step 2: Configure in Xcode
51
-
52
- #### 2.1 Add Extension Target
53
-
54
- 1. Open `ios/*.xcworkspace` in Xcode
55
- 2. Go to **File → New → Target**
56
- 3. Choose **"Notification Service Extension"**
57
- 4. **Product Name:** `SwanNotificationServiceExtension`
58
- 5. **Language:** Swift
59
- 6. Click **Finish**
60
- 7. When prompted "Activate scheme?", click **Cancel**
61
-
62
- #### 2.2 Configure App Groups
63
-
64
- **App Groups** allow the main app and extension to share credentials.
65
-
66
- ##### Main App Target
67
-
68
- 1. Select your **main app target** (e.g., `YourApp`)
69
- 2. Go to **"Signing & Capabilities"** tab
70
- 3. Click **"+ Capability"**
71
- 4. Search for and add **"App Groups"**
72
- 5. Click **"+"** under App Groups
73
- 6. Enter: `group.swan.sdk.notifications`
74
- 7. Click **OK**
75
-
76
- ##### Extension Target
77
-
78
- 1. Select **SwanNotificationServiceExtension** target
79
- 2. Go to **"Signing & Capabilities"** tab
80
- 3. Click **"+ Capability"**
81
- 4. Search for and add **"App Groups"**
82
- 5. Click **"+"** under App Groups
83
- 6. Enter: `group.swan.sdk.notifications` (**MUST match main app!**)
84
- 7. Click **OK**
85
-
86
- **⚠️ IMPORTANT:** Both targets must have the **exact same** App Group ID.
87
-
88
- #### 2.3 Verify Bundle Identifier
89
-
90
- 1. Select **SwanNotificationServiceExtension** target
91
- 2. Go to **"General"** tab
92
- 3. Check **Bundle Identifier**: Should be `com.yourcompany.app.SwanNotificationServiceExtension`
93
- 4. Make sure it's a **child** of your main app's bundle ID
94
-
95
- ---
96
-
97
- ### Step 3: Run Setup Script
98
-
99
- Now that the extension target is created, run the setup script to copy the Swan implementation files. This will overwrite the default files created by Xcode.
100
-
101
- ```bash
102
- node node_modules/swan-react-native-sdk/scripts/setup-ios-extension.js
103
- ```
104
-
105
- **What it does:**
106
- - Copies extension files to `ios/SwanNotificationServiceExtension/`
107
- - Validates dependencies
108
- - Ensures the correct implementation is in place
109
-
110
- ---
111
-
112
- ### Step 4: Testing
113
-
114
- ### Build and Archive
115
-
116
- ```bash
117
- # Clean build
118
- cd ios
119
- rm -rf build
120
- xcodebuild clean -workspace YourApp.xcworkspace -scheme YourApp
121
-
122
- # Build
123
- xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -configuration Release
124
-
125
- # Archive (for TestFlight or Ad Hoc)
126
- xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -configuration Release archive
127
- ```
128
-
129
- ### Test on Real Device
130
-
131
- **IMPORTANT:** Push notifications don't work on iOS Simulator!
132
-
133
- 1. Archive your app (Product → Archive in Xcode)
134
- 2. Export to device or upload to TestFlight
135
- 3. Install on a real iOS device
136
- 4. Send a test notification
137
- 5. Check Xcode **Device Console** for logs
138
-
139
- ### Expected Logs
140
-
141
- When extension receives a notification:
142
-
143
- ```
144
- [SwanSDK Extension] Notification Service Extension triggered
145
- [SwanSDK Extension] Request identifier: 1234567890
146
- [SwanSDK Extension] UserInfo: {...}
147
- [SwanSDK Extension] Found messageId in gcm.message_id: 0:1234567890
148
- [SwanSDK Extension] Sending delivery ACK for messageId: 0:1234567890
149
- [SwanSDK Extension] Credentials loaded - appId: YOUR_APP_ID
150
- [SwanSDK Extension] ACK payload: {...}
151
- [SwanSDK Extension] ACK response status: 200
152
- [SwanSDK Extension] ✅ Delivery ACK sent successfully
153
- ```
154
-
155
- If image is present:
156
- ```
157
- [SwanSDK Extension] Downloading notification image: https://example.com/image.png
158
- [SwanSDK Extension] ✅ Image downloaded successfully
159
- ```
160
-
161
- ### View Logs on Real Device
162
-
163
- **Option 1: Xcode Device Console**
164
- 1. Connect device via USB
165
- 2. Xcode → Window → Devices and Simulators
166
- 3. Select your device
167
- 4. Click **"Open Console"**
168
- 5. Filter by "SwanSDK Extension"
169
-
170
- **Option 2: Console.app (macOS)**
171
- 1. Open Console app on Mac
172
- 2. Select your connected device
173
- 3. Search for "SwanSDK Extension"
174
-
175
- ---
176
-
177
- ## Troubleshooting
178
-
179
- ### Extension logs don't appear
180
-
181
- **Symptom:** No `[SwanSDK Extension]` logs when notification arrives
182
-
183
- **Possible causes:**
184
-
185
- 1. **Missing mutable-content: 1**
186
- - Check FCM payload includes `mutable-content: 1` in APNS
187
- - Without it, iOS won't trigger the extension
188
-
189
- 2. **App Groups not configured**
190
- - Verify both targets have "App Groups" capability
191
- - Verify App Group ID is `group.swan.sdk.notifications` in BOTH targets
192
- - IDs must match exactly (no typos!)
193
-
194
- 3. **Extension not included in build**
195
- - In Xcode, go to Product → Scheme → Edit Scheme
196
- - Check that SwanNotificationServiceExtension is included
197
-
198
- 4. **Testing on simulator**
199
- - Push notifications don't work on iOS Simulator
200
- - Test on a real device
201
-
202
- ### Extension runs but no ACK sent
203
-
204
- **Symptom:** See extension logs but backend doesn't receive ACK
205
-
206
- **Possible causes:**
207
-
208
- 1. **Credentials not saved to App Group**
209
- - Ensure SDK is initialized: `SwanSDK.init({ appId: '...' })`
210
- - Credentials are saved automatically on init
211
- - Check logs for: `[SharedCredentials] ✅ Credentials saved to iOS App Group`
212
-
213
- 2. **App Group ID mismatch**
214
- - Extension uses `group.swan.sdk.notifications` by default
215
- - If you changed it, update `NotificationService.swift` line 143
216
-
217
- 3. **Network issue**
218
- - Extension has only 30 seconds to complete
219
- - Check device has internet connection
220
-
221
- ### Images not appearing
222
-
223
- **Symptom:** Notification arrives but image doesn't display
224
-
225
- **Possible causes:**
226
-
227
- 1. **Image URL invalid**
228
- - Check image URL is accessible (try in browser)
229
- - Must be HTTPS (HTTP won't work)
230
-
231
- 2. **Image format unsupported**
232
- - Supported: JPEG, PNG, GIF
233
- - Max size: 10MB (iOS limit)
234
-
235
- 3. **mutable-content missing**
236
- - Extension won't run without `mutable-content: 1`
237
-
238
- ### Build errors
239
-
240
- **Error:** `No such module 'UserNotifications'`
241
- - Solution: Make sure deployment target is iOS 10.0+
242
-
243
- **Error:** `Use of undeclared type 'UNNotificationServiceExtension'`
244
- - Solution: Verify extension target is set to iOS 10.0+ deployment target
245
-
246
- **Error:** `Could not find or use auto-linked library 'swiftFoundation'`
247
- - Solution: Add `use_frameworks!` to Podfile
248
-
249
- **Error:** `CocoaPods Error: Unable to find compatibility version string for object version '70'`
250
- - Solution: In Xcode, select your Project (blue icon) → Build Settings (or Info) → Project Format. Change it to **Xcode 16.0-compatible** or later.
251
-
252
- ---
253
-
254
- ## Customization
255
-
256
- ### Custom App Group ID
257
-
258
- If you want to use a different App Group ID:
259
-
260
- 1. **In Xcode:** Update App Groups in both targets to use your custom ID (e.g., `group.com.yourcompany.app`)
261
-
262
- 2. **In SDK:** Configure before calling `SwanSDK.init()`:
263
-
264
- ```typescript
265
- import { SharedCredentialsManager } from 'swan-react-native-sdk';
266
-
267
- // Set custom App Group ID BEFORE SDK init
268
- SharedCredentialsManager.setAppGroupId('group.com.yourcompany.app');
269
-
270
- // Then initialize SDK
271
- await SwanSDK.init({ appId: 'YOUR_APP_ID' });
272
- ```
273
-
274
- 3. **In Extension:** Update `NotificationService.swift`:
275
-
276
- ```swift
277
- let appGroupId = "group.com.yourcompany.app"
278
- ```
279
-
280
-
281
-
282
- ## FAQs
283
-
284
- ### Q: Do I need to modify the extension code?
285
-
286
- **A:** No. The extension works out of the box. Only modify if you need custom App Group ID.
287
-
288
- ### Q: Will this increase my app size?
289
-
290
- **A:** Minimally. The extension adds ~50KB to your IPA.
291
-
292
- ### Q: Does this affect app performance?
293
-
294
- **A:** No. The extension runs in a separate process and only when notifications arrive.
295
-
296
- ### Q: Can I test without archiving?
297
-
298
- **A:** Yes, but you must run on a real device. Push doesn't work on simulator.
299
-
300
- ### Q: What if I already have a Notification Service Extension?
301
-
302
- **A:** You'll need to merge Swan's code into your existing extension. The key parts are:
303
- 1. Reading credentials from App Group
304
- 2. Sending delivery ACK
305
- 3. Downloading images
306
-
307
- ### Q: Do I need this for Android?
308
-
309
- **A:** No. Android data-only push notifications work without an extension. This is iOS-only.
310
-
311
- ---
312
-
313
- ## Support
314
-
315
- If you encounter issues:
316
-
317
- 1. Check logs in Xcode Device Console
318
- 2. Verify all steps in this guide
319
- 3. Test with the provided FCM payload example
320
- 4. Check that App Group IDs match in both targets
321
-
322
- For detailed debugging, enable verbose logging:
323
- - Main app: `SwanSDK.setLoggingEnabled(true)`
324
- - Extension: Logs are always enabled, check Device Console
325
-
326
- ---
327
-
328
- ## Summary
329
-
330
- ✅ **Install:** `npm install react-native-shared-group-preferences`
331
- ✅ **Xcode:** Add extension target + configure App Groups
332
- ✅ **Run:** `node node_modules/swan-react-native-sdk/scripts/setup-ios-extension.js`
333
- ✅ **Test:** Archive and test on real device
334
-
335
- That's it! Your iOS notifications now have reliable delivery tracking and rich media support. 🎉
@@ -1,347 +0,0 @@
1
- # Swan React Native SDK - Industry Standards Review Report
2
-
3
- **Prepared for:** Senior Stakeholders
4
- **Date:** February 12, 2026
5
- **SDK Version:** 2.1.1
6
- **Review Scope:** Architecture, reliability, and industry benchmarking against CleverTap, MoEngage, OneSignal, and Braze SDKs
7
-
8
- ---
9
-
10
- ## Executive Summary
11
-
12
- The Swan React Native SDK has undergone a significant architectural overhaul and **now follows industry-standard patterns used by CleverTap, MoEngage, OneSignal, and Braze**. The core architecture — offline-first event queuing with SQLite, non-blocking initialization, data-only push notifications, and state machine-driven flows — is **solid and production-grade**.
13
-
14
- Three issues identified during this review (API timeout bug, unprofessional database naming, and queue size limit) have been **fixed as part of this review** (PR #22). The only remaining area for future consideration is payload compression alignment (LZW64 vs industry-standard gzip), which is functional but non-standard.
15
-
16
- **Overall Assessment: 9/10** — Production-ready, industry-aligned architecture.
17
-
18
- ---
19
-
20
- ## 1. Architecture Comparison Matrix
21
-
22
- | Capability | Swan SDK | CleverTap | MoEngage | OneSignal | Braze |
23
- |---|---|---|---|---|---|
24
- | **Offline Event Queuing** | SQLite | SQLite | SQLite | SQLite | SQLite + SharedPrefs |
25
- | **Non-blocking Init** | ~100ms | Yes | Yes | Yes (~8ms main thread) | Yes |
26
- | **Event Batching** | 10 events / 30s | 10-50 events / 15-60s | Similar | Similar | Similar |
27
- | **Exponential Backoff** | 2s, 4s, 8s (3 retries) | Yes | Yes | Yes | Yes |
28
- | **Data-only Push** | Yes (FCM + Notifee) | Supported | Supported | Supported | Supported |
29
- | **Payload Compression** | LZW64 (custom) | gzip | gzip | gzip | gzip |
30
- | **State Machines** | 3 (Device/Auth/Push) | Internal | Internal | Internal | Internal |
31
- | **Android Notification Channels** | 5 predefined | Yes | Yes | Yes | Yes |
32
- | **iOS App Group (NES)** | Yes | Yes | Yes | No | Yes |
33
- | **Queue Size Limits** | 5,000 events | ~10,000 | ~10,000 | Similar | Similar |
34
- | **Provider Abstraction** | Yes (interface) | N/A (native) | N/A (native) | N/A (native) | N/A (native) |
35
- | **Session Management** | 20-min window | 20-min window | 30-min window | Similar | Similar |
36
- | **API Timeout** | 10s | Varies | Varies | Varies | Varies |
37
-
38
- ---
39
-
40
- ## 2. What Swan SDK Does RIGHT (Industry-Aligned)
41
-
42
- ### 2.1 SQLite for Event Persistence — Industry Standard
43
-
44
- **Verdict: Correct choice. Same as CleverTap and MoEngage.**
45
-
46
- SQLite is the de facto standard for mobile SDK event queuing. All major customer engagement platforms use it:
47
-
48
- - **CleverTap**: Uses SQLite for event storage on both Android and iOS
49
- - **MoEngage**: Uses SQLite for offline event persistence
50
- - **Braze**: Uses SQLite combined with SharedPreferences
51
- - **Segment**: Uses SQLite for their analytics queue
52
-
53
- **Why SQLite is the right choice:**
54
- - ACID-compliant transactions prevent data loss during app crashes
55
- - Handles concurrent read/write safely
56
- - Zero-configuration, embedded, no server needed
57
- - Battle-tested on billions of mobile devices
58
- - Survives app kills and restarts (unlike in-memory stores)
59
-
60
- **Swan's implementation is solid:**
61
- - Proper table schema with indexes on `status`, `priority`, and `timestamp` (`EventQueueManager.ts:54-79`)
62
- - Atomic SELECT + UPDATE in `dequeue()` to prevent duplicate processing (`EventQueueManager.ts:147-209`)
63
- - Stale event recovery for events stuck in `sending` state after app crash (`EventQueueManager.ts:416-451`)
64
- - Queue size enforcement with oldest-first eviction (`EventQueueManager.ts:373-409`)
65
-
66
- > **To answer the client's question directly:** Yes, we use SQLite — the same database that CleverTap, MoEngage, Braze, Segment, and essentially every serious mobile SDK uses for event persistence. It is the industry standard.
67
-
68
- ### 2.2 Non-blocking Initialization — Matches OneSignal Best Practice
69
-
70
- **Verdict: Excellent. Better than many competitor implementations.**
71
-
72
- Swan SDK init completes in ~100ms. Device registration, push setup, and location updates all run in the background without blocking the host app.
73
-
74
- For comparison:
75
- - **OneSignal** recently refactored their init to reduce main thread time from ~49ms to ~8ms — Swan's approach of moving everything off the critical path is the same philosophy
76
- - **CleverTap** also uses non-blocking init with background device registration
77
- - **CleverTap** has historically suffered from ANR (Application Not Responding) bugs caused by blocking initialization — Swan avoids this entirely
78
-
79
- **Swan's phased initialization:**
80
- ```
81
- Phase 1: Core infrastructure (SQLite + EventQueue) — synchronous, ~50ms
82
- Phase 2: Device registration — BACKGROUND, non-blocking
83
- Phase 3: Push notifications — BACKGROUND, non-blocking
84
- Phase 4: Location update — BACKGROUND, non-blocking
85
- Phase 5: Deep link listeners — non-blocking
86
- ```
87
-
88
- This is clean, well-ordered, and follows the principle of "fast init, eventual consistency."
89
-
90
- ### 2.3 Data-Only Push Architecture — Industry-Leading Approach
91
-
92
- **Verdict: Premium-tier architecture, same as Braze.**
93
-
94
- Swan uses data-only FCM messages exclusively, displayed via Notifee. This is the same architecture used by:
95
-
96
- - **Braze**: Data-only push as the recommended approach
97
- - **MoEngage**: Data-only messages with custom display
98
- - **CleverTap**: Data-only with custom notification rendering
99
- - **OneSignal**: Hybrid approach but recommends data-only for advanced use
100
-
101
- **Benefits achieved by Swan:**
102
- - Single code path for foreground/background/killed states
103
- - Firebase `messageId` used as Notifee notification ID for accurate click tracking
104
- - No duplicate notifications (a common bug with notification+data mixed payloads)
105
- - Full control over notification appearance in all app states
106
-
107
- The `sdkCapabilities.dataOnlyPush` flag sent during push subscription is a smart backward-compatibility mechanism that allows the backend to gracefully support both old and new SDK versions.
108
-
109
- ### 2.4 State Machine Architecture — Better Than Industry Average
110
-
111
- **Verdict: Above industry standard for React Native SDKs.**
112
-
113
- Swan uses three explicit state machines:
114
-
115
- | State Machine | States | Purpose |
116
- |---|---|---|
117
- | `DeviceStateMachine` | UNINITIALIZED → REGISTERING → REGISTERED/FAILED | Prevents duplicate registrations |
118
- | `AuthStateMachine` | LOGGED_OUT → LOGGING_IN → LOGGED_IN → LOGGING_OUT | Prevents concurrent login/logout |
119
- | `PushStateMachine` | DISABLED → INITIALIZING → READY → TOKEN_PENDING → ACTIVE | Manages push lifecycle |
120
-
121
- Most competitor SDKs use boolean flags internally. State machines are **more robust** because:
122
- - They make impossible states unrepresentable
123
- - Race conditions are eliminated by design
124
- - State transitions are logged and debuggable
125
-
126
- ### 2.5 Profile Switching with Queue Flush — Correctly Handles CDID Boundaries
127
-
128
- **Verdict: Critical feature, correctly implemented.**
129
-
130
- Before login/logout, Swan flushes the event queue to ensure all events tagged with the old CDID are sent before switching profiles. This is the same approach used by CleverTap and MoEngage.
131
-
132
- ```
133
- Login flow:
134
- 1. Flush existing queue (old CDID)
135
- 2. Send login event directly (bypass queue)
136
- 3. Update stored CDID
137
- 4. Re-sync push subscription with new CDID
138
- 5. Subsequent events use new CDID
139
- ```
140
-
141
- ### 2.6 Additional Industry-Aligned Features
142
-
143
- - **Notification Channels**: 5 predefined Android channels (transactional, alerts, promotional, general, default) — matches Android 8+ best practices
144
- - **iOS App Group + Notification Service Extension**: Enables delivery ACK when app is killed — same as CleverTap and Braze
145
- - **Provider Abstraction**: `PushNotificationProvider` interface allows swapping Firebase for other providers — good extensibility
146
- - **Deep Link Attribution**: Automatic tracking of `swan_` prefixed UTM parameters — matches industry campaign attribution patterns
147
- - **Session Management**: 20-minute inactivity window — matches CleverTap's default
148
-
149
- ---
150
-
151
- ## 3. Issues Found and Resolved
152
-
153
- ### 3.1 FIXED: Timeout Value Mismatch (Bug)
154
-
155
- **Severity: High | Type: Bug | Status: FIXED in PR #22**
156
-
157
- The code comment said "10s timeout" but the actual value was 40,000ms (40 seconds):
158
-
159
- ```typescript
160
- // BEFORE (src/index.tsx:1238)
161
- timeoutId = setTimeout(() => controller.abort(), 40000); // 10s timeout
162
-
163
- // AFTER (FIXED)
164
- timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
165
- ```
166
-
167
- This meant users were waiting 4x longer than expected when the API was unreachable. Now correctly times out after 10 seconds as documented.
168
-
169
- ### 3.2 FIXED: Database Named "test.db"
170
-
171
- **Severity: Medium | Type: Unprofessional | Status: FIXED in PR #22**
172
-
173
- ```typescript
174
- // BEFORE (src/index.tsx:925)
175
- this.db = SQLite.openDatabase('test.db', '1.0', '', 1);
176
-
177
- // AFTER (FIXED)
178
- this.db = SQLite.openDatabase('swan_sdk.db', '1.0', '', 1);
179
- ```
180
-
181
- The production database was named `test.db`. Now named `swan_sdk.db`, matching industry convention (CleverTap uses `clevertap.db`, MoEngage uses `moengage_db`).
182
-
183
- ### 3.3 FIXED: Queue Size Limit Increased
184
-
185
- **Severity: Low | Type: Configuration | Status: FIXED in PR #22**
186
-
187
- ```typescript
188
- // BEFORE (BatchConfig.ts:25)
189
- maxQueueSize: 1000,
190
-
191
- // AFTER (FIXED)
192
- maxQueueSize: 5000,
193
- ```
194
-
195
- Increased from 1,000 to 5,000 events. This provides better coverage for prolonged offline periods (flights, poor connectivity areas) while remaining memory-efficient.
196
-
197
- ---
198
-
199
- ## 4. Remaining Considerations
200
-
201
- ### 4.1 LOW: LZW64 vs gzip Compression
202
-
203
- **Severity: Low | Industry Gap: Minor | Status: Not critical**
204
-
205
- Swan uses a custom LZW64 compression implementation (`lzw64_encode` in `index.tsx:1174-1206`). The industry standard is **gzip**:
206
-
207
- - **CleverTap**: gzip
208
- - **Braze**: gzip
209
- - **MoEngage**: gzip
210
- - **Segment**: gzip
211
-
212
- LZW64 is not inherently wrong, but:
213
- - Custom compression algorithms are harder to debug
214
- - gzip has better tooling support (every HTTP server/proxy understands it)
215
- - The custom implementation lacks error handling for edge cases
216
-
217
- **Recommendation:** Consider migrating to gzip in a future release. This is functional as-is and does not affect reliability. Low priority.
218
-
219
- ### 4.2 NOTE: Encryption at Rest — Not Required for Current Data
220
-
221
- **Context: Addressed during review, deliberately deprioritized.**
222
-
223
- Competitor SDKs (CleverTap, Braze, MoEngage) use AES-256 encryption at rest. Swan uses Base64 encoding. However, **Swan does not store PII on-device**. The only data stored locally is:
224
-
225
- | Data | Type | PII? |
226
- |------|------|------|
227
- | Device ID | SDK-generated UUID | No |
228
- | CDID | Backend-assigned device identifier | No |
229
- | App ID | Configuration value | No |
230
- | FCM Push Token | Firebase token | No |
231
- | Event Queue | Event names + properties + timestamps | No* |
232
- | isProduction | Boolean flag | No |
233
-
234
- *Event properties depend on what the host app passes to `trackEvent()`. The SDK itself does not add PII.
235
-
236
- **Why encryption is not critical for Swan:**
237
- - No names, emails, passwords, or phone numbers are stored
238
- - All stored values are opaque system identifiers (UUIDs, tokens)
239
- - The worst-case scenario on a rooted device is access to identifiers that aren't useful on their own
240
- - Competitor SDKs encrypt because they store user profile data (email, phone, attributes) on-device — Swan doesn't
241
-
242
- **If asked by a client's security team:** "We don't store PII on-device. All locally stored values are opaque system identifiers (UUIDs, device tokens) with no personal data. Encryption at rest can be added in a future version if the SDK begins caching user profile attributes."
243
-
244
- ---
245
-
246
- ## 5. Reliability Assessment
247
-
248
- ### 5.1 What Happens in Each Failure Scenario
249
-
250
- | Scenario | Behavior | Rating |
251
- |---|---|---|
252
- | **App crash during event send** | Stale events recovered to `pending` on next init | Excellent |
253
- | **Network goes offline** | Events queue in SQLite, flush on network restore | Excellent |
254
- | **Device registration fails** | Retry on network restore, events queue locally | Good |
255
- | **App killed in background** | SQLite persists events, recovered on relaunch | Excellent |
256
- | **Login during offline** | Login fails (direct API call), events stay queued | Acceptable |
257
- | **Push notification when app killed** | Notifee displays via background handler, ACK sent directly | Excellent |
258
- | **Queue overflow** | Oldest events dropped, newest preserved (5,000 limit) | Good |
259
- | **Server returns partial failure** | Failed events retry individually with backoff | Excellent |
260
- | **API timeout** | Aborts after 10 seconds, retries with backoff | Good |
261
-
262
- ### 5.2 Data Loss Risk Assessment
263
-
264
- | Risk | Mitigation | Residual Risk |
265
- |---|---|---|
266
- | Events lost during crash | SQLite transactions + stale recovery | Very Low |
267
- | Events lost during offline | Persistent SQLite queue | Very Low |
268
- | Events lost on queue overflow | 5,000 event limit with oldest-first eviction | Very Low |
269
- | Events permanently failed | 3 retries + 7-day cleanup | Low |
270
- | Credentials lost | AsyncStorage persistence | Very Low |
271
-
272
- ---
273
-
274
- ## 6. Key Talking Points for Client Conversations
275
-
276
- ### "Is offline-first architecture an industry standard?"
277
-
278
- > **Yes, absolutely.** Every major customer engagement SDK — CleverTap, MoEngage, Braze, OneSignal, Segment, Amplitude, Mixpanel — uses offline-first architecture with local event queuing. It is the universally accepted best practice because:
279
- > 1. Mobile networks are unreliable by nature
280
- > 2. Users expect apps to work instantly, not wait for network calls
281
- > 3. Data loss is unacceptable for analytics and attribution
282
- > 4. Battery efficiency requires batched network calls, not individual requests
283
-
284
- ### "Which database do you use?"
285
-
286
- > **SQLite** — the same database used by CleverTap, MoEngage, Braze, Segment, and virtually every major mobile SDK. SQLite is embedded in every iOS and Android device, provides ACID transactions (meaning no data loss even during crashes), and handles millions of events reliably. Our implementation includes proper indexes, atomic dequeue operations, stale event recovery, and queue size management.
287
-
288
- ### "How do you ensure events aren't lost?"
289
-
290
- > Our event pipeline has multiple layers of protection:
291
- > 1. **Immediate SQLite persistence**: Events are written to disk the moment they're tracked
292
- > 2. **Atomic status transitions**: Events move through `pending → sending → sent/failed` atomically
293
- > 3. **Crash recovery**: Events stuck in `sending` state are automatically recovered on next app launch
294
- > 4. **Retry with exponential backoff**: Failed events retry 3 times with 2s/4s/8s delays
295
- > 5. **Network-aware flushing**: Events automatically send when network restores
296
- > 6. **Queue overflow protection**: If the queue fills up (5,000 events), oldest events are evicted to make room for newer ones
297
-
298
- ### "How does your push notification architecture work?"
299
-
300
- > We use **data-only FCM messages** — the same architecture used by Braze, MoEngage, and CleverTap. This gives us:
301
- > - **Reliable click tracking**: We set notification ID = Firebase messageId for accurate delivery/click attribution
302
- > - **Consistent behavior**: Same code path in foreground, background, and killed states
303
- > - **No duplicate notifications**: A common bug with mixed notification+data payloads that we avoid entirely
304
- > - **Full customization**: Rich notifications with images, channels, and custom actions
305
- > - **iOS delivery tracking**: Notification Service Extension with App Group sharing enables delivery ACK even when the app is killed
306
-
307
- ### "How does your SDK compare to CleverTap/MoEngage?"
308
-
309
- > Swan SDK follows the same core architectural patterns:
310
- > - **Same database** (SQLite) for event persistence
311
- > - **Same offline-first approach** with local queuing and network-aware flushing
312
- > - **Same push architecture** (data-only FCM with custom display)
313
- > - **Same session management** (20-minute inactivity window, matching CleverTap)
314
- > - **Better state management** (explicit state machines vs boolean flags used by most competitors)
315
- > - **Better initialization** (non-blocking ~100ms init, avoiding ANR issues that have historically affected CleverTap)
316
-
317
- ### "Do you store any user data on-device?"
318
-
319
- > We do not store PII (Personally Identifiable Information) on-device. The only locally stored data consists of opaque system identifiers — device IDs, session tokens, and the event queue. No names, emails, phone numbers, or personal attributes are cached locally. All user data lives server-side.
320
-
321
- ---
322
-
323
- ## 7. Summary of Actions Taken
324
-
325
- | # | Item | Status | PR |
326
- |---|---|---|---|
327
- | 1 | Fix timeout bug (40s → 10s) | **FIXED** | #22 |
328
- | 2 | Rename database (test.db → swan_sdk.db) | **FIXED** | #22 |
329
- | 3 | Increase queue limit (1,000 → 5,000) | **FIXED** | #22 |
330
- | 4 | Evaluate gzip compression | Deferred | Future consideration |
331
- | 5 | Encryption at rest | Not required | No PII stored on-device |
332
-
333
- ---
334
-
335
- ## 8. Conclusion
336
-
337
- The Swan React Native SDK v2.1 is a **well-architected, production-ready SDK** that follows the same fundamental patterns as CleverTap, MoEngage, OneSignal, and Braze. The offline-first architecture with SQLite, non-blocking initialization, state machine-driven flows, and data-only push notifications are all industry best practices.
338
-
339
- Three issues identified during this review have been fixed immediately (PR #22). The remaining consideration (LZW64 vs gzip compression) is a future optimization, not a reliability concern.
340
-
341
- The client's concern about "offline-first not being industry standard" is **factually incorrect** — it is the universal standard. Every major SDK in this space uses this exact pattern. The Swan SDK's architecture is not only industry-standard but in some areas (state machines, non-blocking init, data-only push) is **above the industry average** for React Native SDKs.
342
-
343
- **Final Assessment: 9/10** — Production-ready, industry-aligned, with targeted improvements already applied.
344
-
345
- ---
346
-
347
- *Report prepared through comprehensive code review and competitive analysis against CleverTap, MoEngage, OneSignal, and Braze SDKs.*
@@ -1,330 +0,0 @@
1
- {
2
- "info": {
3
- "_postman_id": "swan-push-notifications-v1",
4
- "name": "Swan SDK — Push Notifications",
5
- "description": "Complete collection for testing all Swan SDK push notification types across Android and iOS.\n\n## Setup\n1. Set `project_id` to your Firebase project ID\n2. Set `fcm_token` to the device's FCM token\n3. Set `access_token` by running: `gcloud auth print-access-token`\n\n## How it works\n- All requests use the FCM v1 HTTP API\n- Variables are shared across all requests\n- Just update the token and send",
6
- "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7
- },
8
- "variable": [
9
- {
10
- "key": "project_id",
11
- "value": "loyalytics-app",
12
- "type": "string"
13
- },
14
- {
15
- "key": "fcm_token",
16
- "value": "<PASTE_DEVICE_FCM_TOKEN_HERE>",
17
- "type": "string"
18
- },
19
- {
20
- "key": "access_token",
21
- "value": "<PASTE_GCLOUD_ACCESS_TOKEN_HERE>",
22
- "type": "string"
23
- },
24
- {
25
- "key": "fcm_url",
26
- "value": "https://fcm.googleapis.com/v1/projects/{{project_id}}/messages:send",
27
- "type": "string"
28
- }
29
- ],
30
- "item": [
31
- {
32
- "name": "Android",
33
- "item": [
34
- {
35
- "name": "Standard Push",
36
- "request": {
37
- "method": "POST",
38
- "header": [
39
- {
40
- "key": "Authorization",
41
- "value": "Bearer {{access_token}}"
42
- },
43
- {
44
- "key": "Content-Type",
45
- "value": "application/json"
46
- }
47
- ],
48
- "url": {
49
- "raw": "{{fcm_url}}",
50
- "host": ["{{fcm_url}}"]
51
- },
52
- "body": {
53
- "mode": "raw",
54
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"title\": \"Your order has shipped!\",\n \"body\": \"Track your package #ABC123\",\n \"channelId\": \"swan_transactional\",\n \"route\": \"/orders/123\"\n },\n \"android\": {\n \"priority\": \"high\"\n }\n }\n}"
55
- }
56
- }
57
- },
58
- {
59
- "name": "Standard Push — With Image",
60
- "request": {
61
- "method": "POST",
62
- "header": [
63
- {
64
- "key": "Authorization",
65
- "value": "Bearer {{access_token}}"
66
- },
67
- {
68
- "key": "Content-Type",
69
- "value": "application/json"
70
- }
71
- ],
72
- "url": {
73
- "raw": "{{fcm_url}}",
74
- "host": ["{{fcm_url}}"]
75
- },
76
- "body": {
77
- "mode": "raw",
78
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"title\": \"Summer Sale!\",\n \"body\": \"50% off everything\",\n \"channelId\": \"swan_promotional\",\n \"route\": \"/sales/summer\",\n \"image\": \"https://picsum.photos/id/1/1024/512\"\n },\n \"android\": {\n \"priority\": \"high\"\n }\n }\n}"
79
- }
80
- }
81
- },
82
- {
83
- "name": "Carousel — Manual / Standard",
84
- "request": {
85
- "method": "POST",
86
- "header": [
87
- {
88
- "key": "Authorization",
89
- "value": "Bearer {{access_token}}"
90
- },
91
- {
92
- "key": "Content-Type",
93
- "value": "application/json"
94
- }
95
- ],
96
- "url": {
97
- "raw": "{{fcm_url}}",
98
- "host": ["{{fcm_url}}"]
99
- },
100
- "body": {
101
- "mode": "raw",
102
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"notificationType\": \"carousel\",\n \"carouselMode\": \"manual\",\n \"carouselVariant\": \"standard\",\n \"title\": \"Weekend Flash Sale\",\n \"body\": \"Swipe to explore deals!\",\n \"defaultRoute\": \"/collections/weekend-sale\",\n \"channelId\": \"swan_notifications\",\n \"carouselItems\": \"[{\\\"imageUrl\\\":\\\"https://picsum.photos/id/1/720/360\\\",\\\"title\\\":\\\"Summer Collection\\\",\\\"body\\\":\\\"50% off\\\",\\\"route\\\":\\\"/product/summer\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/20/720/360\\\",\\\"title\\\":\\\"New Arrivals\\\",\\\"body\\\":\\\"Just dropped\\\",\\\"route\\\":\\\"/product/new\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/30/720/360\\\",\\\"title\\\":\\\"Best Sellers\\\",\\\"body\\\":\\\"Top picks\\\",\\\"route\\\":\\\"/product/best\\\"}]\"\n },\n \"android\": {\n \"priority\": \"high\"\n }\n }\n}"
103
- }
104
- }
105
- },
106
- {
107
- "name": "Carousel — Manual / Filmstrip",
108
- "request": {
109
- "method": "POST",
110
- "header": [
111
- {
112
- "key": "Authorization",
113
- "value": "Bearer {{access_token}}"
114
- },
115
- {
116
- "key": "Content-Type",
117
- "value": "application/json"
118
- }
119
- ],
120
- "url": {
121
- "raw": "{{fcm_url}}",
122
- "host": ["{{fcm_url}}"]
123
- },
124
- "body": {
125
- "mode": "raw",
126
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"notificationType\": \"carousel\",\n \"carouselMode\": \"manual\",\n \"carouselVariant\": \"filmstrip\",\n \"title\": \"Trending Now\",\n \"body\": \"See what's popular\",\n \"defaultRoute\": \"/trending\",\n \"channelId\": \"swan_notifications\",\n \"carouselItems\": \"[{\\\"imageUrl\\\":\\\"https://picsum.photos/id/40/720/360\\\",\\\"title\\\":\\\"Watches\\\",\\\"body\\\":\\\"From $99\\\",\\\"route\\\":\\\"/product/watches\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/50/720/360\\\",\\\"title\\\":\\\"Sneakers\\\",\\\"body\\\":\\\"New colors\\\",\\\"route\\\":\\\"/product/sneakers\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/60/720/360\\\",\\\"title\\\":\\\"Bags\\\",\\\"body\\\":\\\"Premium leather\\\",\\\"route\\\":\\\"/product/bags\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/70/720/360\\\",\\\"title\\\":\\\"Sunglasses\\\",\\\"body\\\":\\\"UV protection\\\",\\\"route\\\":\\\"/product/sunglasses\\\"}]\"\n },\n \"android\": {\n \"priority\": \"high\"\n }\n }\n}"
127
- }
128
- }
129
- },
130
- {
131
- "name": "Carousel — Auto Scroll",
132
- "request": {
133
- "method": "POST",
134
- "header": [
135
- {
136
- "key": "Authorization",
137
- "value": "Bearer {{access_token}}"
138
- },
139
- {
140
- "key": "Content-Type",
141
- "value": "application/json"
142
- }
143
- ],
144
- "url": {
145
- "raw": "{{fcm_url}}",
146
- "host": ["{{fcm_url}}"]
147
- },
148
- "body": {
149
- "mode": "raw",
150
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"notificationType\": \"carousel\",\n \"carouselMode\": \"auto\",\n \"carouselVariant\": \"standard\",\n \"carouselInterval\": \"4000\",\n \"title\": \"Flash Deals\",\n \"body\": \"Auto-scrolling deals!\",\n \"defaultRoute\": \"/deals\",\n \"channelId\": \"swan_notifications\",\n \"carouselItems\": \"[{\\\"imageUrl\\\":\\\"https://picsum.photos/id/10/720/360\\\",\\\"title\\\":\\\"Deal 1\\\",\\\"body\\\":\\\"60% off\\\",\\\"route\\\":\\\"/deal/1\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/11/720/360\\\",\\\"title\\\":\\\"Deal 2\\\",\\\"body\\\":\\\"Buy 1 Get 1\\\",\\\"route\\\":\\\"/deal/2\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/12/720/360\\\",\\\"title\\\":\\\"Deal 3\\\",\\\"body\\\":\\\"Free shipping\\\",\\\"route\\\":\\\"/deal/3\\\"}]\"\n },\n \"android\": {\n \"priority\": \"high\"\n }\n }\n}"
151
- }
152
- }
153
- },
154
- {
155
- "name": "Silent Push",
156
- "request": {
157
- "method": "POST",
158
- "header": [
159
- {
160
- "key": "Authorization",
161
- "value": "Bearer {{access_token}}"
162
- },
163
- {
164
- "key": "Content-Type",
165
- "value": "application/json"
166
- }
167
- ],
168
- "url": {
169
- "raw": "{{fcm_url}}",
170
- "host": ["{{fcm_url}}"]
171
- },
172
- "body": {
173
- "mode": "raw",
174
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"silent\": \"true\",\n \"action\": \"sync_config\"\n },\n \"android\": {\n \"priority\": \"high\"\n }\n }\n}"
175
- }
176
- }
177
- }
178
- ]
179
- },
180
- {
181
- "name": "iOS",
182
- "item": [
183
- {
184
- "name": "Standard Push",
185
- "request": {
186
- "method": "POST",
187
- "header": [
188
- {
189
- "key": "Authorization",
190
- "value": "Bearer {{access_token}}"
191
- },
192
- {
193
- "key": "Content-Type",
194
- "value": "application/json"
195
- }
196
- ],
197
- "url": {
198
- "raw": "{{fcm_url}}",
199
- "host": ["{{fcm_url}}"]
200
- },
201
- "body": {
202
- "mode": "raw",
203
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"title\": \"Your order has shipped!\",\n \"body\": \"Track your package #ABC123\",\n \"route\": \"/orders/123\"\n },\n \"apns\": {\n \"headers\": {\n \"apns-push-type\": \"alert\",\n \"apns-priority\": \"10\"\n },\n \"payload\": {\n \"aps\": {\n \"content-available\": 1\n }\n }\n }\n }\n}"
204
- }
205
- }
206
- },
207
- {
208
- "name": "Standard Push — With Image",
209
- "request": {
210
- "method": "POST",
211
- "header": [
212
- {
213
- "key": "Authorization",
214
- "value": "Bearer {{access_token}}"
215
- },
216
- {
217
- "key": "Content-Type",
218
- "value": "application/json"
219
- }
220
- ],
221
- "url": {
222
- "raw": "{{fcm_url}}",
223
- "host": ["{{fcm_url}}"]
224
- },
225
- "body": {
226
- "mode": "raw",
227
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"title\": \"Summer Sale!\",\n \"body\": \"50% off everything\",\n \"route\": \"/sales/summer\",\n \"image\": \"https://picsum.photos/id/1/1024/512\"\n },\n \"apns\": {\n \"headers\": {\n \"apns-push-type\": \"alert\",\n \"apns-priority\": \"10\"\n },\n \"payload\": {\n \"aps\": {\n \"content-available\": 1\n }\n }\n }\n }\n}"
228
- }
229
- }
230
- },
231
- {
232
- "name": "Carousel — Manual / Standard",
233
- "request": {
234
- "method": "POST",
235
- "header": [
236
- {
237
- "key": "Authorization",
238
- "value": "Bearer {{access_token}}"
239
- },
240
- {
241
- "key": "Content-Type",
242
- "value": "application/json"
243
- }
244
- ],
245
- "url": {
246
- "raw": "{{fcm_url}}",
247
- "host": ["{{fcm_url}}"]
248
- },
249
- "body": {
250
- "mode": "raw",
251
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"notificationType\": \"carousel\",\n \"carouselMode\": \"manual\",\n \"carouselVariant\": \"standard\",\n \"title\": \"Weekend Flash Sale\",\n \"body\": \"Swipe to explore deals!\",\n \"defaultRoute\": \"/collections/weekend-sale\",\n \"channelId\": \"swan_notifications\",\n \"carouselItems\": \"[{\\\"imageUrl\\\":\\\"https://picsum.photos/id/1/720/360\\\",\\\"title\\\":\\\"Summer Collection\\\",\\\"body\\\":\\\"50% off\\\",\\\"route\\\":\\\"/product/summer\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/20/720/360\\\",\\\"title\\\":\\\"New Arrivals\\\",\\\"body\\\":\\\"Just dropped\\\",\\\"route\\\":\\\"/product/new\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/30/720/360\\\",\\\"title\\\":\\\"Best Sellers\\\",\\\"body\\\":\\\"Top picks\\\",\\\"route\\\":\\\"/product/best\\\"}]\"\n },\n \"apns\": {\n \"headers\": {\n \"apns-push-type\": \"alert\",\n \"apns-priority\": \"10\"\n },\n \"payload\": {\n \"aps\": {\n \"mutable-content\": 1,\n \"category\": \"swan_carousel\",\n \"alert\": {\n \"title\": \"Weekend Flash Sale\",\n \"body\": \"Swipe to explore deals!\"\n }\n }\n }\n }\n }\n}"
252
- }
253
- }
254
- },
255
- {
256
- "name": "Carousel — Manual / Filmstrip",
257
- "request": {
258
- "method": "POST",
259
- "header": [
260
- {
261
- "key": "Authorization",
262
- "value": "Bearer {{access_token}}"
263
- },
264
- {
265
- "key": "Content-Type",
266
- "value": "application/json"
267
- }
268
- ],
269
- "url": {
270
- "raw": "{{fcm_url}}",
271
- "host": ["{{fcm_url}}"]
272
- },
273
- "body": {
274
- "mode": "raw",
275
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"notificationType\": \"carousel\",\n \"carouselMode\": \"manual\",\n \"carouselVariant\": \"filmstrip\",\n \"title\": \"Trending Now\",\n \"body\": \"See what's popular\",\n \"defaultRoute\": \"/trending\",\n \"channelId\": \"swan_notifications\",\n \"carouselItems\": \"[{\\\"imageUrl\\\":\\\"https://picsum.photos/id/40/720/360\\\",\\\"title\\\":\\\"Watches\\\",\\\"body\\\":\\\"From $99\\\",\\\"route\\\":\\\"/product/watches\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/50/720/360\\\",\\\"title\\\":\\\"Sneakers\\\",\\\"body\\\":\\\"New colors\\\",\\\"route\\\":\\\"/product/sneakers\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/60/720/360\\\",\\\"title\\\":\\\"Bags\\\",\\\"body\\\":\\\"Premium leather\\\",\\\"route\\\":\\\"/product/bags\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/70/720/360\\\",\\\"title\\\":\\\"Sunglasses\\\",\\\"body\\\":\\\"UV protection\\\",\\\"route\\\":\\\"/product/sunglasses\\\"}]\"\n },\n \"apns\": {\n \"headers\": {\n \"apns-push-type\": \"alert\",\n \"apns-priority\": \"10\"\n },\n \"payload\": {\n \"aps\": {\n \"mutable-content\": 1,\n \"category\": \"swan_carousel\",\n \"alert\": {\n \"title\": \"Trending Now\",\n \"body\": \"See what's popular\"\n }\n }\n }\n }\n }\n}"
276
- }
277
- }
278
- },
279
- {
280
- "name": "Carousel — Auto Scroll",
281
- "request": {
282
- "method": "POST",
283
- "header": [
284
- {
285
- "key": "Authorization",
286
- "value": "Bearer {{access_token}}"
287
- },
288
- {
289
- "key": "Content-Type",
290
- "value": "application/json"
291
- }
292
- ],
293
- "url": {
294
- "raw": "{{fcm_url}}",
295
- "host": ["{{fcm_url}}"]
296
- },
297
- "body": {
298
- "mode": "raw",
299
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"notificationType\": \"carousel\",\n \"carouselMode\": \"auto\",\n \"carouselVariant\": \"standard\",\n \"carouselInterval\": \"4000\",\n \"title\": \"Flash Deals\",\n \"body\": \"Auto-scrolling deals!\",\n \"defaultRoute\": \"/deals\",\n \"channelId\": \"swan_notifications\",\n \"carouselItems\": \"[{\\\"imageUrl\\\":\\\"https://picsum.photos/id/10/720/360\\\",\\\"title\\\":\\\"Deal 1\\\",\\\"body\\\":\\\"60% off\\\",\\\"route\\\":\\\"/deal/1\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/11/720/360\\\",\\\"title\\\":\\\"Deal 2\\\",\\\"body\\\":\\\"Buy 1 Get 1\\\",\\\"route\\\":\\\"/deal/2\\\"},{\\\"imageUrl\\\":\\\"https://picsum.photos/id/12/720/360\\\",\\\"title\\\":\\\"Deal 3\\\",\\\"body\\\":\\\"Free shipping\\\",\\\"route\\\":\\\"/deal/3\\\"}]\"\n },\n \"apns\": {\n \"headers\": {\n \"apns-push-type\": \"alert\",\n \"apns-priority\": \"10\"\n },\n \"payload\": {\n \"aps\": {\n \"mutable-content\": 1,\n \"category\": \"swan_carousel\",\n \"alert\": {\n \"title\": \"Flash Deals\",\n \"body\": \"Auto-scrolling deals!\"\n }\n }\n }\n }\n }\n}"
300
- }
301
- }
302
- },
303
- {
304
- "name": "Silent Push",
305
- "request": {
306
- "method": "POST",
307
- "header": [
308
- {
309
- "key": "Authorization",
310
- "value": "Bearer {{access_token}}"
311
- },
312
- {
313
- "key": "Content-Type",
314
- "value": "application/json"
315
- }
316
- ],
317
- "url": {
318
- "raw": "{{fcm_url}}",
319
- "host": ["{{fcm_url}}"]
320
- },
321
- "body": {
322
- "mode": "raw",
323
- "raw": "{\n \"message\": {\n \"token\": \"{{fcm_token}}\",\n \"data\": {\n \"silent\": \"true\",\n \"action\": \"sync_config\"\n },\n \"apns\": {\n \"headers\": {\n \"apns-push-type\": \"background\",\n \"apns-priority\": \"5\"\n },\n \"payload\": {\n \"aps\": {\n \"content-available\": 1\n }\n }\n }\n }\n}"
324
- }
325
- }
326
- }
327
- ]
328
- }
329
- ]
330
- }
@@ -1,281 +0,0 @@
1
- # Deep Link Attribution Tracking
2
-
3
- Track campaign clicks from email, SMS, and WhatsApp to measure which campaigns drive app engagement.
4
-
5
- ## Overview
6
-
7
- When your marketing team sends emails, SMS, or WhatsApp messages containing links to your app, the Swan SDK automatically detects these clicks and reports them to the Swan backend for campaign attribution. This enables funnel tracking: **link click → app open → add to cart → purchase**.
8
-
9
- **No code changes required.** The SDK handles everything automatically after `init()`. Your existing deep link routing continues to work as-is — the SDK only adds silent attribution tracking on top.
10
-
11
- ## How It Works
12
-
13
- ```
14
- User receives email/SMS/WhatsApp with campaign link
15
-
16
- User taps link → App opens via deep link
17
-
18
- React Native Linking API fires → Swan SDK intercepts
19
-
20
- SDK checks for swan_ parameters in the URL
21
-
22
- ┌─ swan_ params found → Send click ACK to Swan backend
23
- └─ No swan_ params → Ignore (not a Swan campaign link)
24
-
25
- App's existing deep link routing handles navigation (unaffected)
26
- ```
27
-
28
- The SDK supports both:
29
- - **Warm start** — app is already running in the background
30
- - **Cold start** — app is launched from a killed state via the deep link
31
-
32
- ## Prerequisites
33
-
34
- For the SDK to receive deep links, your app must be configured to handle them at the platform level. Most React Native apps that support any form of deep linking already have this set up.
35
-
36
- ### Android
37
-
38
- Add intent filters to your `AndroidManifest.xml` inside the `<activity>` tag for `MainActivity`:
39
-
40
- **Custom scheme** (e.g., `yourapp://products/123`):
41
-
42
- ```xml
43
- <intent-filter>
44
- <action android:name="android.intent.action.VIEW" />
45
- <category android:name="android.intent.category.DEFAULT" />
46
- <category android:name="android.intent.category.BROWSABLE" />
47
- <data android:scheme="yourapp" />
48
- </intent-filter>
49
- ```
50
-
51
- **HTTPS App Links** (e.g., `https://yourstore.com/products/123`):
52
-
53
- ```xml
54
- <intent-filter android:autoVerify="true">
55
- <action android:name="android.intent.action.VIEW" />
56
- <category android:name="android.intent.category.DEFAULT" />
57
- <category android:name="android.intent.category.BROWSABLE" />
58
- <data android:scheme="https" android:host="yourstore.com" />
59
- </intent-filter>
60
- ```
61
-
62
- > For HTTPS App Links, you also need to host a Digital Asset Links file at `https://yourstore.com/.well-known/assetlinks.json`. See the [Android App Links documentation](https://developer.android.com/training/app-links) for details.
63
-
64
- ### iOS
65
-
66
- **Custom scheme:**
67
-
68
- Add your URL scheme in Xcode: **Target → Info → URL Types → Add URL Scheme** (e.g., `yourapp`).
69
-
70
- Or in `Info.plist`:
71
-
72
- ```xml
73
- <key>CFBundleURLTypes</key>
74
- <array>
75
- <dict>
76
- <key>CFBundleURLSchemes</key>
77
- <array>
78
- <string>yourapp</string>
79
- </array>
80
- </dict>
81
- </array>
82
- ```
83
-
84
- **AppDelegate setup:**
85
-
86
- Ensure your `AppDelegate.mm` passes URLs to React Native (this is included by default in most React Native projects):
87
-
88
- ```objc
89
- - (BOOL)application:(UIApplication *)application
90
- openURL:(NSURL *)url
91
- options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
92
- {
93
- return [RCTLinkingManager application:application openURL:url options:options];
94
- }
95
-
96
- // For Universal Links
97
- - (BOOL)application:(UIApplication *)application
98
- continueUserActivity:(nonnull NSUserActivity *)userActivity
99
- restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
100
- {
101
- return [RCTLinkingManager application:application
102
- continueUserActivity:userActivity
103
- restorationHandler:restorationHandler];
104
- }
105
- ```
106
-
107
- **Universal Links** (e.g., `https://yourstore.com/products/123`):
108
-
109
- 1. Add the Associated Domains entitlement in Xcode: `applinks:yourstore.com`
110
- 2. Host an `apple-app-site-association` file at `https://yourstore.com/.well-known/apple-app-site-association`
111
-
112
- See the [Apple Universal Links documentation](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) for details.
113
-
114
- ### Expo
115
-
116
- Add the `scheme` property to your `app.json`:
117
-
118
- ```json
119
- {
120
- "expo": {
121
- "scheme": "yourapp"
122
- }
123
- }
124
- ```
125
-
126
- Then run `npx expo prebuild` to regenerate native projects.
127
-
128
- ## Campaign URL Format
129
-
130
- For attribution tracking to work, the backend must append Swan parameters to the campaign links before sending them. The SDK looks for query parameters prefixed with `swan_`.
131
-
132
- ### Required Parameters
133
-
134
- | Parameter | Description | Example |
135
- |-----------|-------------|---------|
136
- | `swan_comm_id` | Communication ID (generated by Swan before sending) | `swan_comm_id=comm_abc123` |
137
-
138
- ### Optional Parameters
139
-
140
- | Parameter | Description | Example |
141
- |-----------|-------------|---------|
142
- | `swan_link_id` | Identifies which link within the message was clicked (useful for emails with multiple CTAs) | `swan_link_id=hero_banner` |
143
-
144
- ### Example Campaign URLs
145
-
146
- **Email with multiple links:**
147
- ```
148
- https://yourstore.com/sale?swan_comm_id=email_001&swan_link_id=hero_banner
149
- https://yourstore.com/products/shoes?swan_comm_id=email_001&swan_link_id=category_cta
150
- https://yourstore.com/cart?swan_comm_id=email_001&swan_link_id=footer_link
151
- ```
152
-
153
- **SMS with single link:**
154
- ```
155
- https://yourstore.com/offer/50off?swan_comm_id=sms_002
156
- ```
157
-
158
- **WhatsApp with custom scheme:**
159
- ```
160
- yourapp://products/flash-sale?swan_comm_id=wa_003&swan_link_id=buy_now
161
- ```
162
-
163
- > URLs without `swan_comm_id` are silently ignored by the SDK — there is zero overhead for non-campaign links.
164
-
165
- ## Integration
166
-
167
- **No code changes required.** The SDK sets up deep link listeners automatically during `init()`. As long as:
168
-
169
- 1. Your app's platform configuration handles deep links (see Prerequisites above)
170
- 2. Campaign URLs include `swan_comm_id` as a query parameter
171
-
172
- ...the SDK will automatically track clicks and send them to the Swan backend.
173
-
174
- ```javascript
175
- import SwanEcomSDK from '@loyalytics/swan-react-native-sdk';
176
-
177
- // This is all you need — deep link tracking is included automatically
178
- const sdk = SwanEcomSDK.getInstance('YOUR_APP_ID', {
179
- isProduction: true,
180
- });
181
- ```
182
-
183
- Your existing deep link navigation code continues to work unchanged. The SDK does not interfere with routing — it only reads the URL parameters for attribution.
184
-
185
- ## Backend Webhook Payload
186
-
187
- When a campaign deep link is clicked, the SDK sends a click acknowledgment to the Swan webhook endpoint. The payload includes:
188
-
189
- ```json
190
- {
191
- "commId": "email_001",
192
- "appId": "your-app-id",
193
- "CDID": "customer-device-id",
194
- "event": "clicked",
195
- "deviceId": "device-id",
196
- "type": "deepLink",
197
- "linkId": "hero_banner"
198
- }
199
- ```
200
-
201
- | Field | Always present | Description |
202
- |-------|---------------|-------------|
203
- | `commId` | Yes | The `swan_comm_id` from the URL |
204
- | `type` | Yes | Always `"deepLink"` — distinguishes from push notification clicks |
205
- | `event` | Yes | Always `"clicked"` |
206
- | `linkId` | Only if `swan_link_id` was in the URL | Identifies which specific link was clicked |
207
- | `appId` | Yes | Your app ID |
208
- | `CDID` | Yes | Customer device ID (logged-in user) or generated anonymous ID |
209
- | `deviceId` | Yes | Device identifier |
210
-
211
- ## Testing
212
-
213
- ### Android
214
-
215
- ```bash
216
- # Campaign link with all parameters
217
- adb shell am start -a android.intent.action.VIEW \
218
- -d "yourapp://products/123?swan_comm_id=test_001&swan_link_id=cta1"
219
-
220
- # Campaign link without link ID (single-link SMS)
221
- adb shell am start -a android.intent.action.VIEW \
222
- -d "yourapp://offer?swan_comm_id=test_002"
223
-
224
- # Non-campaign link (should be silently ignored)
225
- adb shell am start -a android.intent.action.VIEW \
226
- -d "yourapp://products/123?utm_source=google"
227
- ```
228
-
229
- ### iOS Simulator
230
-
231
- ```bash
232
- # Campaign link with all parameters
233
- xcrun simctl openurl booted \
234
- "yourapp://products/123?swan_comm_id=test_001&swan_link_id=cta1"
235
-
236
- # Campaign link without link ID
237
- xcrun simctl openurl booted \
238
- "yourapp://offer?swan_comm_id=test_002"
239
-
240
- # Non-campaign link (should be silently ignored)
241
- xcrun simctl openurl booted \
242
- "yourapp://products/123?utm_source=google"
243
- ```
244
-
245
- ### Expected Log Output
246
-
247
- Enable SDK logging to verify attribution tracking:
248
-
249
- ```javascript
250
- SwanEcomSDK.setLoggingEnabled(true);
251
- ```
252
-
253
- **Campaign link detected:**
254
- ```
255
- [SwanSDK] Deep link received (warm start): yourapp://products/123?swan_comm_id=test_001&swan_link_id=cta1
256
- [SwanSDK] Deep link attribution: found SWAN parameters: {"swan_comm_id":"test_001","swan_link_id":"cta1"}
257
- [SwanSDK] Notification ACK queued: test_001 clicked
258
- ```
259
-
260
- **Non-campaign link (no output):**
261
- ```
262
- [SwanSDK] Deep link received (warm start): yourapp://products/123?utm_source=google
263
- ```
264
- No further logs — the SDK silently ignores it.
265
-
266
- ## FAQ
267
-
268
- **Q: Do I need to change any code in my app?**
269
- No. The SDK handles deep link attribution automatically. Your existing navigation and deep link routing remains unchanged.
270
-
271
- **Q: What if my app already uses React Navigation's deep linking?**
272
- That's fine. The SDK uses `Linking.addEventListener` to listen for URLs, which works alongside React Navigation's `linking` configuration. Both receive the URL — React Navigation handles routing, the SDK handles attribution.
273
-
274
- **Q: What happens if the user clicks a link but is offline?**
275
- The click event is queued in the SDK's local SQLite database and sent to the backend when the network is restored.
276
-
277
- **Q: Does this add any overhead for non-campaign links?**
278
- Negligible. The SDK checks for `swan_` prefixed parameters in the URL query string. If none are found, it returns immediately with no further processing.
279
-
280
- **Q: What SDK version is required?**
281
- Deep link attribution tracking is available from version **2.1.0** onwards.