saasco-sdk 0.1.41 → 0.1.42

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/index.cjs.js CHANGED
@@ -8,7 +8,7 @@ var jsSha256 = require('js-sha256');
8
8
  var uuid$1 = require('@lukeed/uuid');
9
9
  var zod = require('zod');
10
10
 
11
- var version = "0.1.41";
11
+ var version = "0.1.42";
12
12
 
13
13
  const timezones = {
14
14
  'Asia/Barnaul': 'RU',
@@ -555,8 +555,8 @@ function getBrowserContext() {
555
555
  };
556
556
  }
557
557
 
558
- const isBrowser$1 = typeof window !== 'undefined';
559
- const isServer$1 = !isBrowser$1;
558
+ const isBrowser$2 = typeof window !== 'undefined';
559
+ const isServer$2 = !isBrowser$2;
560
560
  const standardFacebookEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Donate', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
561
561
  /**
562
562
  * Create a Facebook Pixel integration instance
@@ -567,7 +567,7 @@ function createFacebookPixelIntegration(config) {
567
567
  name: 'facebook-pixel',
568
568
  environments: ['client'],
569
569
  init: () => tslib.__awaiter(this, void 0, void 0, function* () {
570
- if (isServer$1) {
570
+ if (isServer$2) {
571
571
  // On server, Facebook Pixel is a no-op but doesn't throw
572
572
  // This allows the integration to be registered but remain inactive
573
573
  return;
@@ -646,6 +646,10 @@ function createFacebookPixelIntegration(config) {
646
646
  const amValues = coerceMetaConversionsAmValues(Object.assign(Object.assign({}, properties || {}), userId ? {
647
647
  external_id: userId
648
648
  } : {}));
649
+ // Advanced Matching
650
+ // Add event to identify user and hash email field
651
+ // FB pixel handles hashing automatically
652
+ // https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
649
653
  const am = {};
650
654
  if (amValues.external_id) am['external_id'] = amValues.external_id;
651
655
  if (amValues.em) am['em'] = jsSha256.sha256(amValues.em);
@@ -674,6 +678,18 @@ function getFacebookEventName(eventName, eventMapping) {
674
678
  if (!eventMapping) return eventName;
675
679
  return eventMapping[eventName] || eventName;
676
680
  }
681
+ /**
682
+ * Get Advanced Matching values from properties
683
+ *
684
+ * Documentation for Pixel
685
+ * https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
686
+ *
687
+ * Documentation for Conversions API
688
+ * https://developers.secure.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
689
+ *
690
+ * @param properties
691
+ * @returns
692
+ */
677
693
  function coerceMetaConversionsAmValues(properties) {
678
694
  const coerce = [{
679
695
  key: 'em',
@@ -781,6 +797,214 @@ function coerceMetaConversionsAmValues(properties) {
781
797
  }
782
798
  }];
783
799
  const result = {};
800
+ for (const {
801
+ key,
802
+ coerceFrom,
803
+ transform
804
+ } of coerce) {
805
+ const foundValue = findFirstProperty$1(properties, coerceFrom);
806
+ if (foundValue !== null && transform) {
807
+ const transformedValue = transform(foundValue);
808
+ if (transformedValue) {
809
+ result[key] = transformedValue;
810
+ }
811
+ } else if (foundValue !== null && typeof foundValue === 'string') {
812
+ result[key] = foundValue;
813
+ }
814
+ }
815
+ return result;
816
+ }
817
+ function findFirstProperty$1(properties, keys) {
818
+ for (const key of keys) {
819
+ if (properties[key] !== undefined) {
820
+ return properties[key];
821
+ }
822
+ }
823
+ return null;
824
+ }
825
+
826
+ /**
827
+ * TikTok Pixel Integration
828
+ *
829
+ * This integration provides TikTok Pixel tracking capabilities including:
830
+ * - Event tracking for standard and custom events
831
+ * - Advanced matching with hashed user identification data
832
+ * - Automatic script loading and initialization
833
+ *
834
+ * Key Resources:
835
+ * - Advanced Matching: https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
836
+ * - Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
837
+ */
838
+ const isBrowser$1 = typeof window !== 'undefined';
839
+ const isServer$1 = !isBrowser$1;
840
+ /**
841
+ * Standard TikTok events supported by the pixel
842
+ *
843
+ * Note: This list reflects TikTok's official standard events. Events like 'CompletePayment'
844
+ * are not standard events and should be tracked as custom events if needed.
845
+ *
846
+ * Documentation: https://business-api.tiktok.com/portal/docs?id=1771101186666498
847
+ */
848
+ const standardTikTokEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'ApplicationApproval', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Download', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
849
+ /**
850
+ * Create a TikTok Pixel integration instance
851
+ */
852
+ function createTikTokPixelIntegration(config) {
853
+ let isPixelReady = false;
854
+ return {
855
+ name: 'tiktok-pixel',
856
+ environments: ['client'],
857
+ init: () => tslib.__awaiter(this, void 0, void 0, function* () {
858
+ if (isServer$1) {
859
+ // On server, TikTok Pixel is a no-op but doesn't throw
860
+ // This allows the integration to be registered but remain inactive
861
+ return;
862
+ }
863
+ // Check if TikTok Pixel is already loaded
864
+ if (window.ttq) {
865
+ console.warn('TikTok Pixel is already initialized');
866
+ isPixelReady = true;
867
+ return;
868
+ }
869
+ // Initialize TikTok Pixel
870
+ return new Promise((resolve, reject) => {
871
+ try {
872
+ (function (w, d, t) {
873
+ w.TiktokAnalyticsObject = t;
874
+ const ttq = w[t] = w[t] || [];
875
+ ttq.methods = ['page', 'track', 'identify', 'instances', 'debug', 'on', 'off', 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie', 'holdConsent', 'revokeConsent', 'grantConsent'];
876
+ ttq.setAndDefer = function (t, e) {
877
+ t[e] = function () {
878
+ t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
879
+ };
880
+ };
881
+ for (let i = 0; i < ttq.methods.length; i++) ttq.setAndDefer(ttq, ttq.methods[i]);
882
+ ttq.instance = function (t) {
883
+ const e = ttq._i[t] || [];
884
+ for (let n = 0; n < ttq.methods.length; n++) ttq.setAndDefer(e, ttq.methods[n]);
885
+ return e;
886
+ };
887
+ ttq.load = function (e, n) {
888
+ const r = 'https://analytics.tiktok.com/i18n/pixel/events.js';
889
+ const o = n && n.partner;
890
+ ttq._i = ttq._i || {};
891
+ ttq._i[e] = [];
892
+ ttq._i[e]._u = r;
893
+ ttq._t = ttq._t || {};
894
+ ttq._t[e] = +new Date();
895
+ ttq._o = ttq._o || {};
896
+ ttq._o[e] = n || {};
897
+ const script = document.createElement('script');
898
+ script.type = 'text/javascript';
899
+ script.async = true;
900
+ script.src = r + '?sdkid=' + e + '&lib=' + t;
901
+ // Add load event listener
902
+ script.onload = () => {
903
+ isPixelReady = true;
904
+ resolve();
905
+ };
906
+ script.onerror = () => {
907
+ reject(new Error('Failed to load TikTok Pixel script'));
908
+ };
909
+ const firstScript = document.getElementsByTagName('script')[0];
910
+ firstScript.parentNode.insertBefore(script, firstScript);
911
+ };
912
+ // Load the pixel with the provided ID
913
+ ttq.load(config.pixelId, config.testMode ? {
914
+ test_mode: true
915
+ } : undefined);
916
+ ttq.page();
917
+ // If script loads synchronously, mark as ready
918
+ if (window.ttq && typeof window.ttq === 'function') {
919
+ isPixelReady = true;
920
+ resolve();
921
+ }
922
+ })(window, document, 'ttq');
923
+ } catch (error) {
924
+ reject(error);
925
+ }
926
+ });
927
+ }),
928
+ track: (eventName, properties, context) => {
929
+ if (!isPixelReady || !window.ttq) return;
930
+ const ttEventName = getTikTokEventName(eventName, config.eventMapping);
931
+ // Handle PageView separately as it uses ttq.page()
932
+ if (ttEventName === 'PageView') {
933
+ window.ttq.page();
934
+ return;
935
+ }
936
+ // Map parameters based on specific event requirements
937
+ const ttParams = mapTikTokEventParameters(ttEventName, properties);
938
+ if (ttParams && Object.keys(ttParams).length > 0) {
939
+ window.ttq.track(ttEventName, ttParams);
940
+ } else {
941
+ window.ttq.track(ttEventName);
942
+ }
943
+ },
944
+ identify: (userId, properties, context) => {
945
+ try {
946
+ if (typeof window !== 'undefined' && window.ttq) {
947
+ const identifyData = coerceTikTokIdentifyValues(Object.assign(Object.assign({}, properties || {}), userId ? {
948
+ external_id: userId
949
+ } : {}));
950
+ if (Object.keys(identifyData).length > 0) {
951
+ window.ttq.identify(identifyData);
952
+ }
953
+ }
954
+ } catch (error) {
955
+ console.error('Error identifying user in TikTok Pixel:', error);
956
+ }
957
+ }
958
+ };
959
+ }
960
+ function getTikTokEventName(eventName, eventMapping) {
961
+ // Convert default saasco Page View event by default
962
+ if (eventName === 'Page View') return 'PageView';
963
+ if (!eventMapping) return eventName;
964
+ return eventMapping[eventName] || eventName;
965
+ }
966
+ /**
967
+ * Get identification values from properties for TikTok Pixel
968
+ *
969
+ * Advanced Matching Documentation:
970
+ * https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
971
+ *
972
+ * Standard Events Documentation:
973
+ * https://business-api.tiktok.com/portal/docs?id=1771101186666498
974
+ *
975
+ * @param properties
976
+ * @returns
977
+ */
978
+ function coerceTikTokIdentifyValues(properties) {
979
+ const coerce = [{
980
+ key: 'email',
981
+ coerceFrom: ['email', 'user_email', 'userEmail', 'email_address', 'emailAddress', 'e_mail', 'E_Mail', 'mail', 'Mail', 'contact_email', 'contactEmail', 'primary_email', 'primaryEmail'],
982
+ transform: value => {
983
+ if (typeof value !== 'string') return undefined;
984
+ const normalized = value.trim().toLowerCase();
985
+ // TikTok requires hashed email
986
+ return normalized ? jsSha256.sha256(normalized) : undefined;
987
+ }
988
+ }, {
989
+ key: 'phone_number',
990
+ coerceFrom: ['phone', 'phone_number', 'phoneNumber', 'mobile', 'mobile_number', 'mobileNumber', 'contact_number', 'contactNumber', 'tel', 'telephone'],
991
+ transform: value => {
992
+ if (typeof value !== 'string' && typeof value !== 'number') return undefined;
993
+ const phoneStr = String(value).replace(/\D/g, '');
994
+ // TikTok requires hashed phone number
995
+ return phoneStr ? jsSha256.sha256(phoneStr) : undefined;
996
+ }
997
+ }, {
998
+ key: 'external_id',
999
+ coerceFrom: ['external_id', 'externalId', 'user_id', 'userId', 'id', 'distinctId', 'distinct_id', 'customer_id', 'customerId', 'member_id', 'memberId'],
1000
+ transform: value => {
1001
+ if (value == null) return undefined;
1002
+ const stringValue = String(value);
1003
+ // TikTok requires hashed external_id
1004
+ return stringValue ? jsSha256.sha256(stringValue) : undefined;
1005
+ }
1006
+ }];
1007
+ const result = {};
784
1008
  for (const {
785
1009
  key,
786
1010
  coerceFrom,
@@ -806,6 +1030,63 @@ function findFirstProperty(properties, keys) {
806
1030
  }
807
1031
  return null;
808
1032
  }
1033
+ /**
1034
+ * Maps event properties to TikTok-specific parameters based on event type
1035
+ *
1036
+ * Parameter Documentation: https://ads.tiktok.com/help/article/about-parameters
1037
+ * Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
1038
+ */
1039
+ function mapTikTokEventParameters(eventName, properties) {
1040
+ if (!properties) return null;
1041
+ const params = {};
1042
+ // Helper to safely add parameters, skipping objects/null/undefined
1043
+ const addParam = (key, value) => {
1044
+ if (value != null && typeof value !== 'object') {
1045
+ params[key] = value;
1046
+ }
1047
+ };
1048
+ switch (eventName) {
1049
+ case 'AddToCart':
1050
+ case 'Purchase':
1051
+ case 'ViewContent':
1052
+ addParam('content_type', properties['content_type']);
1053
+ addParam('content_ids', properties['content_ids']);
1054
+ addParam('quantity', properties['quantity']);
1055
+ addParam('description', properties['description']);
1056
+ addParam('currency', properties['currency']);
1057
+ addParam('value', properties['value']);
1058
+ addParam('content_name', properties['content_name']);
1059
+ addParam('content_category', properties['content_category']);
1060
+ break;
1061
+ case 'StartTrial':
1062
+ addParam('content_ids', properties['content_ids']);
1063
+ addParam('currency', properties['currency']);
1064
+ addParam('value', properties['value']);
1065
+ break;
1066
+ case 'Search':
1067
+ const searchString = properties['search_string'] || properties['query'] || properties['search_query'];
1068
+ addParam('search_string', searchString);
1069
+ break;
1070
+ case 'InitiateCheckout':
1071
+ case 'AddPaymentInfo':
1072
+ addParam('value', properties['value']);
1073
+ addParam('currency', properties['currency']);
1074
+ addParam('content_ids', properties['content_ids']);
1075
+ addParam('quantity', properties['quantity']);
1076
+ break;
1077
+ case 'CompleteRegistration':
1078
+ case 'Subscribe':
1079
+ addParam('value', properties['value']);
1080
+ addParam('currency', properties['currency']);
1081
+ break;
1082
+ default:
1083
+ // Pass through common parameters for any other events
1084
+ addParam('value', properties['value']);
1085
+ addParam('currency', properties['currency']);
1086
+ break;
1087
+ }
1088
+ return Object.keys(params).length > 0 ? params : null;
1089
+ }
809
1090
 
810
1091
  /* eslint-disable @typescript-eslint/no-explicit-any */
811
1092
  class AnalyticsLogger {
@@ -1334,6 +1615,10 @@ class Saasco {
1334
1615
  this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
1335
1616
  const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
1336
1617
  yield this.integrationManager.registerIntegration(fbIntegration);
1618
+ } else if (integrationConfig.type === 'tiktok-pixel') {
1619
+ this.logger.log('Registering TikTok Pixel integration with ID:', integrationConfig.config.pixelId);
1620
+ const tiktokIntegration = createTikTokPixelIntegration(integrationConfig.config);
1621
+ yield this.integrationManager.registerIntegration(tiktokIntegration);
1337
1622
  }
1338
1623
  } catch (error) {
1339
1624
  this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
@@ -1599,6 +1884,8 @@ exports.Logger = Logger;
1599
1884
  exports.Saasco = Saasco;
1600
1885
  exports.browserContextSchema = browserContextSchema;
1601
1886
  exports.createFacebookPixelIntegration = createFacebookPixelIntegration;
1887
+ exports.createTikTokPixelIntegration = createTikTokPixelIntegration;
1602
1888
  exports.getBrowserContext = getBrowserContext;
1603
1889
  exports.standardFacebookEvents = standardFacebookEvents;
1890
+ exports.standardTikTokEvents = standardTikTokEvents;
1604
1891
  exports.timezones = timezones;
package/index.esm.js CHANGED
@@ -4,7 +4,7 @@ import { sha256 } from 'js-sha256';
4
4
  import { v4 } from '@lukeed/uuid';
5
5
  import { z } from 'zod';
6
6
 
7
- var version = "0.1.41";
7
+ var version = "0.1.42";
8
8
 
9
9
  const timezones = {
10
10
  'Asia/Barnaul': 'RU',
@@ -551,8 +551,8 @@ function getBrowserContext() {
551
551
  };
552
552
  }
553
553
 
554
- const isBrowser$1 = typeof window !== 'undefined';
555
- const isServer$1 = !isBrowser$1;
554
+ const isBrowser$2 = typeof window !== 'undefined';
555
+ const isServer$2 = !isBrowser$2;
556
556
  const standardFacebookEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Donate', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
557
557
  /**
558
558
  * Create a Facebook Pixel integration instance
@@ -563,7 +563,7 @@ function createFacebookPixelIntegration(config) {
563
563
  name: 'facebook-pixel',
564
564
  environments: ['client'],
565
565
  init: () => __awaiter(this, void 0, void 0, function* () {
566
- if (isServer$1) {
566
+ if (isServer$2) {
567
567
  // On server, Facebook Pixel is a no-op but doesn't throw
568
568
  // This allows the integration to be registered but remain inactive
569
569
  return;
@@ -642,6 +642,10 @@ function createFacebookPixelIntegration(config) {
642
642
  const amValues = coerceMetaConversionsAmValues(Object.assign(Object.assign({}, properties || {}), userId ? {
643
643
  external_id: userId
644
644
  } : {}));
645
+ // Advanced Matching
646
+ // Add event to identify user and hash email field
647
+ // FB pixel handles hashing automatically
648
+ // https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
645
649
  const am = {};
646
650
  if (amValues.external_id) am['external_id'] = amValues.external_id;
647
651
  if (amValues.em) am['em'] = sha256(amValues.em);
@@ -670,6 +674,18 @@ function getFacebookEventName(eventName, eventMapping) {
670
674
  if (!eventMapping) return eventName;
671
675
  return eventMapping[eventName] || eventName;
672
676
  }
677
+ /**
678
+ * Get Advanced Matching values from properties
679
+ *
680
+ * Documentation for Pixel
681
+ * https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
682
+ *
683
+ * Documentation for Conversions API
684
+ * https://developers.secure.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
685
+ *
686
+ * @param properties
687
+ * @returns
688
+ */
673
689
  function coerceMetaConversionsAmValues(properties) {
674
690
  const coerce = [{
675
691
  key: 'em',
@@ -777,6 +793,214 @@ function coerceMetaConversionsAmValues(properties) {
777
793
  }
778
794
  }];
779
795
  const result = {};
796
+ for (const {
797
+ key,
798
+ coerceFrom,
799
+ transform
800
+ } of coerce) {
801
+ const foundValue = findFirstProperty$1(properties, coerceFrom);
802
+ if (foundValue !== null && transform) {
803
+ const transformedValue = transform(foundValue);
804
+ if (transformedValue) {
805
+ result[key] = transformedValue;
806
+ }
807
+ } else if (foundValue !== null && typeof foundValue === 'string') {
808
+ result[key] = foundValue;
809
+ }
810
+ }
811
+ return result;
812
+ }
813
+ function findFirstProperty$1(properties, keys) {
814
+ for (const key of keys) {
815
+ if (properties[key] !== undefined) {
816
+ return properties[key];
817
+ }
818
+ }
819
+ return null;
820
+ }
821
+
822
+ /**
823
+ * TikTok Pixel Integration
824
+ *
825
+ * This integration provides TikTok Pixel tracking capabilities including:
826
+ * - Event tracking for standard and custom events
827
+ * - Advanced matching with hashed user identification data
828
+ * - Automatic script loading and initialization
829
+ *
830
+ * Key Resources:
831
+ * - Advanced Matching: https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
832
+ * - Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
833
+ */
834
+ const isBrowser$1 = typeof window !== 'undefined';
835
+ const isServer$1 = !isBrowser$1;
836
+ /**
837
+ * Standard TikTok events supported by the pixel
838
+ *
839
+ * Note: This list reflects TikTok's official standard events. Events like 'CompletePayment'
840
+ * are not standard events and should be tracked as custom events if needed.
841
+ *
842
+ * Documentation: https://business-api.tiktok.com/portal/docs?id=1771101186666498
843
+ */
844
+ const standardTikTokEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'ApplicationApproval', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Download', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
845
+ /**
846
+ * Create a TikTok Pixel integration instance
847
+ */
848
+ function createTikTokPixelIntegration(config) {
849
+ let isPixelReady = false;
850
+ return {
851
+ name: 'tiktok-pixel',
852
+ environments: ['client'],
853
+ init: () => __awaiter(this, void 0, void 0, function* () {
854
+ if (isServer$1) {
855
+ // On server, TikTok Pixel is a no-op but doesn't throw
856
+ // This allows the integration to be registered but remain inactive
857
+ return;
858
+ }
859
+ // Check if TikTok Pixel is already loaded
860
+ if (window.ttq) {
861
+ console.warn('TikTok Pixel is already initialized');
862
+ isPixelReady = true;
863
+ return;
864
+ }
865
+ // Initialize TikTok Pixel
866
+ return new Promise((resolve, reject) => {
867
+ try {
868
+ (function (w, d, t) {
869
+ w.TiktokAnalyticsObject = t;
870
+ const ttq = w[t] = w[t] || [];
871
+ ttq.methods = ['page', 'track', 'identify', 'instances', 'debug', 'on', 'off', 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie', 'holdConsent', 'revokeConsent', 'grantConsent'];
872
+ ttq.setAndDefer = function (t, e) {
873
+ t[e] = function () {
874
+ t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
875
+ };
876
+ };
877
+ for (let i = 0; i < ttq.methods.length; i++) ttq.setAndDefer(ttq, ttq.methods[i]);
878
+ ttq.instance = function (t) {
879
+ const e = ttq._i[t] || [];
880
+ for (let n = 0; n < ttq.methods.length; n++) ttq.setAndDefer(e, ttq.methods[n]);
881
+ return e;
882
+ };
883
+ ttq.load = function (e, n) {
884
+ const r = 'https://analytics.tiktok.com/i18n/pixel/events.js';
885
+ const o = n && n.partner;
886
+ ttq._i = ttq._i || {};
887
+ ttq._i[e] = [];
888
+ ttq._i[e]._u = r;
889
+ ttq._t = ttq._t || {};
890
+ ttq._t[e] = +new Date();
891
+ ttq._o = ttq._o || {};
892
+ ttq._o[e] = n || {};
893
+ const script = document.createElement('script');
894
+ script.type = 'text/javascript';
895
+ script.async = true;
896
+ script.src = r + '?sdkid=' + e + '&lib=' + t;
897
+ // Add load event listener
898
+ script.onload = () => {
899
+ isPixelReady = true;
900
+ resolve();
901
+ };
902
+ script.onerror = () => {
903
+ reject(new Error('Failed to load TikTok Pixel script'));
904
+ };
905
+ const firstScript = document.getElementsByTagName('script')[0];
906
+ firstScript.parentNode.insertBefore(script, firstScript);
907
+ };
908
+ // Load the pixel with the provided ID
909
+ ttq.load(config.pixelId, config.testMode ? {
910
+ test_mode: true
911
+ } : undefined);
912
+ ttq.page();
913
+ // If script loads synchronously, mark as ready
914
+ if (window.ttq && typeof window.ttq === 'function') {
915
+ isPixelReady = true;
916
+ resolve();
917
+ }
918
+ })(window, document, 'ttq');
919
+ } catch (error) {
920
+ reject(error);
921
+ }
922
+ });
923
+ }),
924
+ track: (eventName, properties, context) => {
925
+ if (!isPixelReady || !window.ttq) return;
926
+ const ttEventName = getTikTokEventName(eventName, config.eventMapping);
927
+ // Handle PageView separately as it uses ttq.page()
928
+ if (ttEventName === 'PageView') {
929
+ window.ttq.page();
930
+ return;
931
+ }
932
+ // Map parameters based on specific event requirements
933
+ const ttParams = mapTikTokEventParameters(ttEventName, properties);
934
+ if (ttParams && Object.keys(ttParams).length > 0) {
935
+ window.ttq.track(ttEventName, ttParams);
936
+ } else {
937
+ window.ttq.track(ttEventName);
938
+ }
939
+ },
940
+ identify: (userId, properties, context) => {
941
+ try {
942
+ if (typeof window !== 'undefined' && window.ttq) {
943
+ const identifyData = coerceTikTokIdentifyValues(Object.assign(Object.assign({}, properties || {}), userId ? {
944
+ external_id: userId
945
+ } : {}));
946
+ if (Object.keys(identifyData).length > 0) {
947
+ window.ttq.identify(identifyData);
948
+ }
949
+ }
950
+ } catch (error) {
951
+ console.error('Error identifying user in TikTok Pixel:', error);
952
+ }
953
+ }
954
+ };
955
+ }
956
+ function getTikTokEventName(eventName, eventMapping) {
957
+ // Convert default saasco Page View event by default
958
+ if (eventName === 'Page View') return 'PageView';
959
+ if (!eventMapping) return eventName;
960
+ return eventMapping[eventName] || eventName;
961
+ }
962
+ /**
963
+ * Get identification values from properties for TikTok Pixel
964
+ *
965
+ * Advanced Matching Documentation:
966
+ * https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
967
+ *
968
+ * Standard Events Documentation:
969
+ * https://business-api.tiktok.com/portal/docs?id=1771101186666498
970
+ *
971
+ * @param properties
972
+ * @returns
973
+ */
974
+ function coerceTikTokIdentifyValues(properties) {
975
+ const coerce = [{
976
+ key: 'email',
977
+ coerceFrom: ['email', 'user_email', 'userEmail', 'email_address', 'emailAddress', 'e_mail', 'E_Mail', 'mail', 'Mail', 'contact_email', 'contactEmail', 'primary_email', 'primaryEmail'],
978
+ transform: value => {
979
+ if (typeof value !== 'string') return undefined;
980
+ const normalized = value.trim().toLowerCase();
981
+ // TikTok requires hashed email
982
+ return normalized ? sha256(normalized) : undefined;
983
+ }
984
+ }, {
985
+ key: 'phone_number',
986
+ coerceFrom: ['phone', 'phone_number', 'phoneNumber', 'mobile', 'mobile_number', 'mobileNumber', 'contact_number', 'contactNumber', 'tel', 'telephone'],
987
+ transform: value => {
988
+ if (typeof value !== 'string' && typeof value !== 'number') return undefined;
989
+ const phoneStr = String(value).replace(/\D/g, '');
990
+ // TikTok requires hashed phone number
991
+ return phoneStr ? sha256(phoneStr) : undefined;
992
+ }
993
+ }, {
994
+ key: 'external_id',
995
+ coerceFrom: ['external_id', 'externalId', 'user_id', 'userId', 'id', 'distinctId', 'distinct_id', 'customer_id', 'customerId', 'member_id', 'memberId'],
996
+ transform: value => {
997
+ if (value == null) return undefined;
998
+ const stringValue = String(value);
999
+ // TikTok requires hashed external_id
1000
+ return stringValue ? sha256(stringValue) : undefined;
1001
+ }
1002
+ }];
1003
+ const result = {};
780
1004
  for (const {
781
1005
  key,
782
1006
  coerceFrom,
@@ -802,6 +1026,63 @@ function findFirstProperty(properties, keys) {
802
1026
  }
803
1027
  return null;
804
1028
  }
1029
+ /**
1030
+ * Maps event properties to TikTok-specific parameters based on event type
1031
+ *
1032
+ * Parameter Documentation: https://ads.tiktok.com/help/article/about-parameters
1033
+ * Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
1034
+ */
1035
+ function mapTikTokEventParameters(eventName, properties) {
1036
+ if (!properties) return null;
1037
+ const params = {};
1038
+ // Helper to safely add parameters, skipping objects/null/undefined
1039
+ const addParam = (key, value) => {
1040
+ if (value != null && typeof value !== 'object') {
1041
+ params[key] = value;
1042
+ }
1043
+ };
1044
+ switch (eventName) {
1045
+ case 'AddToCart':
1046
+ case 'Purchase':
1047
+ case 'ViewContent':
1048
+ addParam('content_type', properties['content_type']);
1049
+ addParam('content_ids', properties['content_ids']);
1050
+ addParam('quantity', properties['quantity']);
1051
+ addParam('description', properties['description']);
1052
+ addParam('currency', properties['currency']);
1053
+ addParam('value', properties['value']);
1054
+ addParam('content_name', properties['content_name']);
1055
+ addParam('content_category', properties['content_category']);
1056
+ break;
1057
+ case 'StartTrial':
1058
+ addParam('content_ids', properties['content_ids']);
1059
+ addParam('currency', properties['currency']);
1060
+ addParam('value', properties['value']);
1061
+ break;
1062
+ case 'Search':
1063
+ const searchString = properties['search_string'] || properties['query'] || properties['search_query'];
1064
+ addParam('search_string', searchString);
1065
+ break;
1066
+ case 'InitiateCheckout':
1067
+ case 'AddPaymentInfo':
1068
+ addParam('value', properties['value']);
1069
+ addParam('currency', properties['currency']);
1070
+ addParam('content_ids', properties['content_ids']);
1071
+ addParam('quantity', properties['quantity']);
1072
+ break;
1073
+ case 'CompleteRegistration':
1074
+ case 'Subscribe':
1075
+ addParam('value', properties['value']);
1076
+ addParam('currency', properties['currency']);
1077
+ break;
1078
+ default:
1079
+ // Pass through common parameters for any other events
1080
+ addParam('value', properties['value']);
1081
+ addParam('currency', properties['currency']);
1082
+ break;
1083
+ }
1084
+ return Object.keys(params).length > 0 ? params : null;
1085
+ }
805
1086
 
806
1087
  /* eslint-disable @typescript-eslint/no-explicit-any */
807
1088
  class AnalyticsLogger {
@@ -1330,6 +1611,10 @@ class Saasco {
1330
1611
  this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
1331
1612
  const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
1332
1613
  yield this.integrationManager.registerIntegration(fbIntegration);
1614
+ } else if (integrationConfig.type === 'tiktok-pixel') {
1615
+ this.logger.log('Registering TikTok Pixel integration with ID:', integrationConfig.config.pixelId);
1616
+ const tiktokIntegration = createTikTokPixelIntegration(integrationConfig.config);
1617
+ yield this.integrationManager.registerIntegration(tiktokIntegration);
1333
1618
  }
1334
1619
  } catch (error) {
1335
1620
  this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
@@ -1590,4 +1875,4 @@ const browserContextSchema = z.object({
1590
1875
  $utmAdId: z.string().nullable()
1591
1876
  });
1592
1877
 
1593
- export { IntegrationManager, Logger, Saasco, browserContextSchema, createFacebookPixelIntegration, getBrowserContext, standardFacebookEvents, timezones };
1878
+ export { IntegrationManager, Logger, Saasco, browserContextSchema, createFacebookPixelIntegration, createTikTokPixelIntegration, getBrowserContext, standardFacebookEvents, standardTikTokEvents, timezones };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasco-sdk",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "dependencies": {
5
5
  "tslib": "^2.3.0",
6
6
  "@lukeed/uuid": "^2.0.1",
@@ -1,4 +1,5 @@
1
1
  import { FacebookPixelIntegrationConfig } from './integrations/facebook-pixel';
2
+ import { TikTokPixelIntegrationConfig } from './integrations/tiktok-pixel';
2
3
  declare global {
3
4
  interface Window {
4
5
  saascoAutoPageTrackingActive?: boolean;
@@ -29,7 +30,7 @@ export type IntegrationConfigBase = {
29
30
  type: string;
30
31
  config: Record<string, any>;
31
32
  };
32
- export type IntegrationsConfig = FacebookPixelIntegrationConfig[];
33
+ export type IntegrationsConfig = (FacebookPixelIntegrationConfig | TikTokPixelIntegrationConfig)[];
33
34
  export declare class Saasco {
34
35
  private config;
35
36
  private lastPageViewPath;
@@ -1,2 +1,3 @@
1
1
  export * from './integration-manager';
2
2
  export * from './facebook-pixel';
3
+ export * from './tiktok-pixel';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * TikTok Pixel Integration
3
+ *
4
+ * This integration provides TikTok Pixel tracking capabilities including:
5
+ * - Event tracking for standard and custom events
6
+ * - Advanced matching with hashed user identification data
7
+ * - Automatic script loading and initialization
8
+ *
9
+ * Key Resources:
10
+ * - Advanced Matching: https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
11
+ * - Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
12
+ */
13
+ import { IntegrationConfigBase } from '../analytics';
14
+ import { Integration } from './integration-manager';
15
+ declare global {
16
+ interface Window {
17
+ ttq?: Ttq;
18
+ TiktokAnalyticsObject?: string;
19
+ }
20
+ interface Ttq {
21
+ (...args: any[]): void;
22
+ methods?: string[];
23
+ _i?: Record<string, any>;
24
+ _t?: Record<string, number>;
25
+ _o?: Record<string, any>;
26
+ load?: (pixelId: string, options?: any) => void;
27
+ page?: () => void;
28
+ track?: (eventName: string, properties?: Record<string, unknown>) => void;
29
+ identify?: (properties?: Record<string, unknown>) => void;
30
+ setAndDefer?: (obj: any, method: string) => void;
31
+ instance?: (pixelId: string) => any;
32
+ }
33
+ }
34
+ export type TikTokEventMapping = {
35
+ [key: string]: StandardTikTokEvent;
36
+ };
37
+ export type TikTokPixelIntegrationConfig = IntegrationConfigBase & {
38
+ type: 'tiktok-pixel';
39
+ config: TikTokPixelConfig;
40
+ };
41
+ export type TikTokPixelConfig = {
42
+ pixelId: string;
43
+ eventMapping?: TikTokEventMapping;
44
+ testMode?: boolean;
45
+ };
46
+ /**
47
+ * Standard TikTok events supported by the pixel
48
+ *
49
+ * Note: This list reflects TikTok's official standard events. Events like 'CompletePayment'
50
+ * are not standard events and should be tracked as custom events if needed.
51
+ *
52
+ * Documentation: https://business-api.tiktok.com/portal/docs?id=1771101186666498
53
+ */
54
+ export declare const standardTikTokEvents: readonly ["AddPaymentInfo", "AddToCart", "AddToWishlist", "ApplicationApproval", "CompleteRegistration", "Contact", "CustomizeProduct", "Download", "FindLocation", "InitiateCheckout", "Lead", "Purchase", "Schedule", "Search", "StartTrial", "SubmitApplication", "Subscribe", "ViewContent", "PageView"];
55
+ export type StandardTikTokEvent = (typeof standardTikTokEvents)[number];
56
+ /**
57
+ * Create a TikTok Pixel integration instance
58
+ */
59
+ export declare function createTikTokPixelIntegration(config: TikTokPixelConfig): Integration;