@loyalytics/swan-react-native-sdk 2.0.2 → 2.0.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/index.js +187 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/index.js +188 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +30 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/commonjs/src/version.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +30 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/lib/typescript/module/src/version.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/commonjs/index.js
CHANGED
|
@@ -144,6 +144,7 @@ class SwanSDK {
|
|
|
144
144
|
pushService = null;
|
|
145
145
|
pendingPushEventListeners = [];
|
|
146
146
|
appStateSubscription = null;
|
|
147
|
+
linkingSubscription = null;
|
|
147
148
|
|
|
148
149
|
// Track if initial notification was already handled to prevent duplicate ACKs
|
|
149
150
|
initialNotificationHandled = false;
|
|
@@ -322,6 +323,10 @@ class SwanSDK {
|
|
|
322
323
|
this.updateLocation().catch(error => {
|
|
323
324
|
_Logger.default.warn('[SwanSDK] Location update failed:', error);
|
|
324
325
|
});
|
|
326
|
+
|
|
327
|
+
// Phase 5: Deep link listeners (non-blocking)
|
|
328
|
+
_Logger.default.log('[SwanSDK] Phase 5: Setting up deep link listeners...');
|
|
329
|
+
this.setupDeepLinkListeners();
|
|
325
330
|
} catch (error) {
|
|
326
331
|
_Logger.default.error('[SwanSDK] SDK Initialization failed:', error);
|
|
327
332
|
throw error;
|
|
@@ -602,6 +607,180 @@ class SwanSDK {
|
|
|
602
607
|
emitNotificationOpened(payload) {
|
|
603
608
|
this.emit(SwanSDK.EVENTS.NOTIFICATION_OPENED, payload);
|
|
604
609
|
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Parse a URL string and extract path and query parameters.
|
|
613
|
+
* Uses manual parsing to avoid dependency on URL API which has inconsistent
|
|
614
|
+
* support across React Native/Hermes versions for custom schemes.
|
|
615
|
+
* @internal
|
|
616
|
+
*/
|
|
617
|
+
parseDeepLinkUrl(url) {
|
|
618
|
+
try {
|
|
619
|
+
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const trimmedUrl = url.trim();
|
|
623
|
+
|
|
624
|
+
// Split URL into base and query string
|
|
625
|
+
const questionMarkIndex = trimmedUrl.indexOf('?');
|
|
626
|
+
let basePart = '';
|
|
627
|
+
let queryString = '';
|
|
628
|
+
if (questionMarkIndex === -1) {
|
|
629
|
+
basePart = trimmedUrl;
|
|
630
|
+
} else {
|
|
631
|
+
basePart = trimmedUrl.substring(0, questionMarkIndex);
|
|
632
|
+
queryString = trimmedUrl.substring(questionMarkIndex + 1);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Extract path component
|
|
636
|
+
// For http/https URLs: strip scheme + host (https://example.com/path → /path)
|
|
637
|
+
// For custom scheme URLs: keep everything after :// (myapp://products/123 → /products/123)
|
|
638
|
+
let path = basePart;
|
|
639
|
+
const schemeEnd = basePart.indexOf('://');
|
|
640
|
+
if (schemeEnd !== -1) {
|
|
641
|
+
const scheme = basePart.substring(0, schemeEnd).toLowerCase();
|
|
642
|
+
const afterScheme = basePart.substring(schemeEnd + 3);
|
|
643
|
+
if (scheme === 'http' || scheme === 'https') {
|
|
644
|
+
// Strip host for web URLs
|
|
645
|
+
const firstSlash = afterScheme.indexOf('/');
|
|
646
|
+
path = firstSlash !== -1 ? afterScheme.substring(firstSlash) : '/';
|
|
647
|
+
} else {
|
|
648
|
+
// Custom scheme — entire afterScheme is the route
|
|
649
|
+
path = '/' + afterScheme;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Parse query string into key-value pairs
|
|
654
|
+
const params = {};
|
|
655
|
+
if (queryString.length > 0) {
|
|
656
|
+
// Strip fragment identifier if present
|
|
657
|
+
const hashIndex = queryString.indexOf('#');
|
|
658
|
+
const cleanQuery = hashIndex !== -1 ? queryString.substring(0, hashIndex) : queryString;
|
|
659
|
+
const pairs = cleanQuery.split('&');
|
|
660
|
+
for (const pair of pairs) {
|
|
661
|
+
const equalsIndex = pair.indexOf('=');
|
|
662
|
+
if (equalsIndex !== -1) {
|
|
663
|
+
const key = decodeURIComponent(pair.substring(0, equalsIndex));
|
|
664
|
+
const value = decodeURIComponent(pair.substring(equalsIndex + 1));
|
|
665
|
+
if (key.length > 0) {
|
|
666
|
+
params[key] = value;
|
|
667
|
+
}
|
|
668
|
+
} else if (pair.length > 0) {
|
|
669
|
+
params[decodeURIComponent(pair)] = '';
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
path,
|
|
675
|
+
params
|
|
676
|
+
};
|
|
677
|
+
} catch (error) {
|
|
678
|
+
_Logger.default.error('[SwanSDK] Error parsing deep link URL:', error);
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Handle a deep link URL from email, SMS, WhatsApp, or other channels.
|
|
685
|
+
*
|
|
686
|
+
* Silently parses the URL, extracts any `swan_` prefixed query parameters,
|
|
687
|
+
* and tracks a 'deepLinkClicked' event to the backend for campaign attribution.
|
|
688
|
+
* The host app's existing deep link routing handles navigation — the SDK
|
|
689
|
+
* only handles attribution tracking.
|
|
690
|
+
*
|
|
691
|
+
* If no `swan_` parameters are found, the URL is not a SWAN campaign link
|
|
692
|
+
* and no tracking occurs.
|
|
693
|
+
*
|
|
694
|
+
* @param url - The full deep link URL string
|
|
695
|
+
* @internal - Called automatically by setupDeepLinkListeners
|
|
696
|
+
*/
|
|
697
|
+
handleDeepLink(url) {
|
|
698
|
+
try {
|
|
699
|
+
_Logger.default.log('[SwanSDK] handleDeepLink called with URL:', url);
|
|
700
|
+
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const parsed = this.parseDeepLinkUrl(url);
|
|
704
|
+
if (!parsed) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Extract swan_ prefixed parameters
|
|
709
|
+
const swanParams = {};
|
|
710
|
+
for (const [key, value] of Object.entries(parsed.params)) {
|
|
711
|
+
if (key.startsWith('swan_')) {
|
|
712
|
+
swanParams[key] = value;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// If no SWAN parameters found, this is not a SWAN campaign link
|
|
717
|
+
if (Object.keys(swanParams).length === 0) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
_Logger.default.log('[SwanSDK] Deep link attribution: found SWAN parameters:', JSON.stringify(swanParams));
|
|
721
|
+
|
|
722
|
+
// Track click via webhook API (same endpoint as push notification clicks)
|
|
723
|
+
const commId = swanParams['swan_comm_id'];
|
|
724
|
+
if (!commId) {
|
|
725
|
+
_Logger.default.warn('[SwanSDK] Deep link missing swan_comm_id, skipping webhook tracking');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const linkId = swanParams['swan_link_id'] || null;
|
|
729
|
+
|
|
730
|
+
// Queue as SWAN_NOTIFICATION_ACK with type: 'deepLink' so backend
|
|
731
|
+
// can distinguish from push notification ACKs (where commId is Firebase's messageId)
|
|
732
|
+
const ackData = {
|
|
733
|
+
commId,
|
|
734
|
+
event: 'clicked',
|
|
735
|
+
type: 'deepLink',
|
|
736
|
+
...(linkId && {
|
|
737
|
+
linkId
|
|
738
|
+
}),
|
|
739
|
+
timestamp: Date.now()
|
|
740
|
+
};
|
|
741
|
+
this.trackEvent('SWAN_NOTIFICATION_ACK', ackData).catch(error => {
|
|
742
|
+
_Logger.default.error('[SwanSDK] Failed to send deep link click ACK:', error);
|
|
743
|
+
});
|
|
744
|
+
} catch (error) {
|
|
745
|
+
_Logger.default.error('[SwanSDK] handleDeepLink: unexpected error:', error);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Set up deep link listeners using React Native Linking API.
|
|
751
|
+
* Handles both warm start (app already running) and cold start (app launched via deep link).
|
|
752
|
+
* Called automatically during SDK initialization.
|
|
753
|
+
* @internal
|
|
754
|
+
*/
|
|
755
|
+
setupDeepLinkListeners() {
|
|
756
|
+
try {
|
|
757
|
+
// Clean up existing listener if any
|
|
758
|
+
if (this.linkingSubscription) {
|
|
759
|
+
this.linkingSubscription.remove();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Listen for deep links when app is already running (warm start)
|
|
763
|
+
this.linkingSubscription = _reactNative.Linking.addEventListener('url', ({
|
|
764
|
+
url
|
|
765
|
+
}) => {
|
|
766
|
+
_Logger.default.log('[SwanSDK] Deep link received (warm start):', url);
|
|
767
|
+
this.handleDeepLink(url);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Check for initial URL that launched the app (cold start)
|
|
771
|
+
_reactNative.Linking.getInitialURL().then(initialUrl => {
|
|
772
|
+
if (initialUrl) {
|
|
773
|
+
_Logger.default.log('[SwanSDK] Deep link received (cold start):', initialUrl);
|
|
774
|
+
this.handleDeepLink(initialUrl);
|
|
775
|
+
}
|
|
776
|
+
}).catch(error => {
|
|
777
|
+
_Logger.default.warn('[SwanSDK] Failed to get initial URL:', error);
|
|
778
|
+
});
|
|
779
|
+
_Logger.default.log('[SwanSDK] Deep link listeners set up successfully');
|
|
780
|
+
} catch (error) {
|
|
781
|
+
_Logger.default.error('[SwanSDK] Failed to set up deep link listeners:', error);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
605
784
|
static setLoggingEnabled(enabled) {
|
|
606
785
|
_Logger.default.enableLogs(enabled);
|
|
607
786
|
}
|
|
@@ -1108,12 +1287,14 @@ class SwanSDK {
|
|
|
1108
1287
|
results.push(...enrichResults);
|
|
1109
1288
|
}
|
|
1110
1289
|
|
|
1111
|
-
// 5. Process Ack Events (Individual to /
|
|
1290
|
+
// 5. Process Ack Events (Individual to webhook/comms/mobile-push-tracking)
|
|
1112
1291
|
if (ackEvents.length > 0) {
|
|
1113
1292
|
const ackPromises = ackEvents.map(async event => {
|
|
1114
1293
|
const {
|
|
1115
1294
|
commId,
|
|
1116
|
-
event: ackType
|
|
1295
|
+
event: ackType,
|
|
1296
|
+
type,
|
|
1297
|
+
linkId
|
|
1117
1298
|
} = event.eventData.data;
|
|
1118
1299
|
const payload = {
|
|
1119
1300
|
commId,
|
|
@@ -1122,6 +1303,10 @@ class SwanSDK {
|
|
|
1122
1303
|
event: ackType,
|
|
1123
1304
|
deviceId
|
|
1124
1305
|
};
|
|
1306
|
+
|
|
1307
|
+
// Deep link ACKs include type and linkId for backend routing
|
|
1308
|
+
if (type) payload.type = type;
|
|
1309
|
+
if (linkId) payload.linkId = linkId;
|
|
1125
1310
|
const response = await this.sendToSwan(_ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[this.isProduction], payload);
|
|
1126
1311
|
return {
|
|
1127
1312
|
id: event.id,
|