@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
@@ -0,0 +1,330 @@
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
+ }
@@ -0,0 +1,281 @@
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.
@@ -0,0 +1,40 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDisplayName</key>
6
+ <string>SwanNotificationContent</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>1</string>
21
+ <key>NSExtension</key>
22
+ <dict>
23
+ <key>NSExtensionAttributes</key>
24
+ <dict>
25
+ <key>UNNotificationExtensionCategory</key>
26
+ <array>
27
+ <string>swan_carousel</string>
28
+ </array>
29
+ <key>UNNotificationExtensionInitialContentSizeRatio</key>
30
+ <real>0.7</real>
31
+ <key>UNNotificationExtensionDefaultContentHidden</key>
32
+ <false/>
33
+ </dict>
34
+ <key>NSExtensionPointIdentifier</key>
35
+ <string>com.apple.usernotifications.content-extension</string>
36
+ <key>NSExtensionPrincipalClass</key>
37
+ <string>SwanNotificationContentExtension.NotificationViewController</string>
38
+ </dict>
39
+ </dict>
40
+ </plist>