deep-link-expo 1.0.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +22 -0
  3. package/README.md +138 -0
  4. package/dist/attribution/attribution-context.d.ts +34 -0
  5. package/dist/attribution/attribution-context.d.ts.map +1 -0
  6. package/dist/attribution/attribution-context.js +98 -0
  7. package/dist/attribution/attribution-manager.d.ts +18 -0
  8. package/dist/attribution/attribution-manager.d.ts.map +1 -0
  9. package/dist/attribution/attribution-manager.js +94 -0
  10. package/dist/deeplink/deep-link-handler.d.ts +43 -0
  11. package/dist/deeplink/deep-link-handler.d.ts.map +1 -0
  12. package/dist/deeplink/deep-link-handler.js +150 -0
  13. package/dist/deeplink/url-parser.d.ts +13 -0
  14. package/dist/deeplink/url-parser.d.ts.map +1 -0
  15. package/dist/deeplink/url-parser.js +92 -0
  16. package/dist/errors/linkforty-error.d.ts +26 -0
  17. package/dist/errors/linkforty-error.d.ts.map +1 -0
  18. package/dist/errors/linkforty-error.js +50 -0
  19. package/dist/events/event-queue.d.ts +14 -0
  20. package/dist/events/event-queue.d.ts.map +1 -0
  21. package/dist/events/event-queue.js +68 -0
  22. package/dist/events/event-tracker.d.ts +18 -0
  23. package/dist/events/event-tracker.d.ts.map +1 -0
  24. package/dist/events/event-tracker.js +92 -0
  25. package/dist/fingerprint/fingerprint-collector.d.ts +8 -0
  26. package/dist/fingerprint/fingerprint-collector.d.ts.map +1 -0
  27. package/dist/fingerprint/fingerprint-collector.js +27 -0
  28. package/dist/index.d.ts +18 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +6 -0
  31. package/dist/linkforty-sdk.d.ts +53 -0
  32. package/dist/linkforty-sdk.d.ts.map +1 -0
  33. package/dist/linkforty-sdk.js +255 -0
  34. package/dist/logger.d.ts +7 -0
  35. package/dist/logger.d.ts.map +1 -0
  36. package/dist/logger.js +18 -0
  37. package/dist/models/attribution.d.ts +28 -0
  38. package/dist/models/attribution.d.ts.map +1 -0
  39. package/dist/models/attribution.js +9 -0
  40. package/dist/models/config.d.ts +72 -0
  41. package/dist/models/config.d.ts.map +1 -0
  42. package/dist/models/config.js +26 -0
  43. package/dist/models/create-link-options.d.ts +15 -0
  44. package/dist/models/create-link-options.d.ts.map +1 -0
  45. package/dist/models/create-link-options.js +1 -0
  46. package/dist/models/create-link-result.d.ts +8 -0
  47. package/dist/models/create-link-result.d.ts.map +1 -0
  48. package/dist/models/create-link-result.js +1 -0
  49. package/dist/models/deep-link-data.d.ts +14 -0
  50. package/dist/models/deep-link-data.d.ts.map +1 -0
  51. package/dist/models/deep-link-data.js +1 -0
  52. package/dist/models/device-fingerprint.d.ts +13 -0
  53. package/dist/models/device-fingerprint.d.ts.map +1 -0
  54. package/dist/models/device-fingerprint.js +1 -0
  55. package/dist/models/event-request.d.ts +13 -0
  56. package/dist/models/event-request.d.ts.map +1 -0
  57. package/dist/models/event-request.js +1 -0
  58. package/dist/models/install-response.d.ts +9 -0
  59. package/dist/models/install-response.d.ts.map +1 -0
  60. package/dist/models/install-response.js +1 -0
  61. package/dist/models/utm-parameters.d.ts +8 -0
  62. package/dist/models/utm-parameters.d.ts.map +1 -0
  63. package/dist/models/utm-parameters.js +1 -0
  64. package/dist/navigation/navigation-tracker.d.ts +52 -0
  65. package/dist/navigation/navigation-tracker.d.ts.map +1 -0
  66. package/dist/navigation/navigation-tracker.js +153 -0
  67. package/dist/network/network-manager.d.ts +13 -0
  68. package/dist/network/network-manager.d.ts.map +1 -0
  69. package/dist/network/network-manager.js +90 -0
  70. package/dist/storage/storage-keys.d.ts +8 -0
  71. package/dist/storage/storage-keys.d.ts.map +1 -0
  72. package/dist/storage/storage-keys.js +7 -0
  73. package/dist/storage/storage-manager.d.ts +20 -0
  74. package/dist/storage/storage-manager.d.ts.map +1 -0
  75. package/dist/storage/storage-manager.js +46 -0
  76. package/dist/version.d.ts +3 -0
  77. package/dist/version.d.ts.map +1 -0
  78. package/dist/version.js +3 -0
  79. package/llms.txt +392 -0
  80. package/package.json +83 -0
  81. package/src/attribution/attribution-context.ts +106 -0
  82. package/src/attribution/attribution-manager.ts +126 -0
  83. package/src/deeplink/deep-link-handler.ts +171 -0
  84. package/src/deeplink/url-parser.ts +96 -0
  85. package/src/errors/linkforty-error.ts +89 -0
  86. package/src/events/event-queue.ts +75 -0
  87. package/src/events/event-tracker.ts +118 -0
  88. package/src/fingerprint/fingerprint-collector.ts +35 -0
  89. package/src/index.ts +29 -0
  90. package/src/linkforty-sdk.ts +342 -0
  91. package/src/logger.ts +23 -0
  92. package/src/models/attribution.ts +29 -0
  93. package/src/models/config.ts +109 -0
  94. package/src/models/create-link-options.ts +15 -0
  95. package/src/models/create-link-result.ts +7 -0
  96. package/src/models/deep-link-data.ts +14 -0
  97. package/src/models/device-fingerprint.ts +12 -0
  98. package/src/models/event-request.ts +15 -0
  99. package/src/models/install-response.ts +9 -0
  100. package/src/models/utm-parameters.ts +7 -0
  101. package/src/navigation/navigation-tracker.ts +195 -0
  102. package/src/network/network-manager.ts +111 -0
  103. package/src/storage/storage-keys.ts +7 -0
  104. package/src/storage/storage-manager.ts +61 -0
  105. package/src/version.ts +3 -0
@@ -0,0 +1,150 @@
1
+ import * as Linking from 'expo-linking';
2
+ import { parseDeepLinkUrl, parseUrlString, buildQueryString } from './url-parser';
3
+ import { logger } from '../logger';
4
+ export class DeepLinkHandler {
5
+ baseUrl;
6
+ network;
7
+ fingerprint;
8
+ linkPathPrefix;
9
+ deferredCallbacks = [];
10
+ deepLinkCallbacks = [];
11
+ deferredDelivered = false;
12
+ cachedDeferredData = null;
13
+ subscription = null;
14
+ constructor(baseUrl, network, fingerprint, linkPathPrefix) {
15
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
16
+ this.network = network;
17
+ this.fingerprint = fingerprint;
18
+ this.linkPathPrefix = linkPathPrefix?.replace(/^\/+|\/+$/g, '') || undefined;
19
+ }
20
+ /**
21
+ * Start listening for deep links via expo-linking.
22
+ */
23
+ startListening() {
24
+ this.subscription = Linking.addEventListener('url', (event) => {
25
+ this.handleUrl(event.url);
26
+ });
27
+ // Check if app was opened via deep link
28
+ Linking.getInitialURL()
29
+ .then((url) => {
30
+ if (url) {
31
+ this.handleUrl(url);
32
+ }
33
+ })
34
+ .catch((e) => {
35
+ logger.warn('Failed to get initial URL:', e);
36
+ });
37
+ }
38
+ /**
39
+ * Manually pass a deep link URL.
40
+ */
41
+ handleDeepLink(url) {
42
+ this.handleUrl(url);
43
+ }
44
+ /**
45
+ * Register a deferred deep link callback.
46
+ * If data was already delivered, calls back immediately.
47
+ */
48
+ onDeferredDeepLink(callback) {
49
+ this.deferredCallbacks.push(callback);
50
+ if (this.deferredDelivered) {
51
+ callback(this.cachedDeferredData);
52
+ }
53
+ }
54
+ /**
55
+ * Register a direct deep link callback. Multiple callbacks supported.
56
+ */
57
+ onDeepLink(callback) {
58
+ this.deepLinkCallbacks.push(callback);
59
+ }
60
+ /**
61
+ * Deliver deferred deep link data (called by SDK after install attribution).
62
+ */
63
+ deliverDeferredDeepLink(data) {
64
+ this.cachedDeferredData = data;
65
+ this.deferredDelivered = true;
66
+ logger.log('Delivering deferred deep link:', data?.shortCode ?? 'organic');
67
+ for (const cb of this.deferredCallbacks) {
68
+ cb(data);
69
+ }
70
+ }
71
+ clearCallbacks() {
72
+ this.deferredCallbacks = [];
73
+ this.deepLinkCallbacks = [];
74
+ this.deferredDelivered = false;
75
+ this.cachedDeferredData = null;
76
+ }
77
+ cleanup() {
78
+ this.subscription?.remove();
79
+ this.subscription = null;
80
+ this.clearCallbacks();
81
+ }
82
+ handleUrl(url) {
83
+ if (!url || this.deepLinkCallbacks.length === 0)
84
+ return;
85
+ logger.log('Handling deep link:', url);
86
+ // Parse locally first as fallback
87
+ const localData = parseDeepLinkUrl(url, this.baseUrl);
88
+ // If this is a LinkForty URL, try server-side resolution
89
+ if (localData && url.startsWith(this.baseUrl)) {
90
+ this.resolveUrl(url)
91
+ .then((resolvedData) => {
92
+ const data = resolvedData ?? localData;
93
+ for (const cb of this.deepLinkCallbacks)
94
+ cb(url, data);
95
+ })
96
+ .catch(() => {
97
+ for (const cb of this.deepLinkCallbacks)
98
+ cb(url, localData);
99
+ });
100
+ }
101
+ else {
102
+ for (const cb of this.deepLinkCallbacks)
103
+ cb(url, localData);
104
+ }
105
+ }
106
+ async resolveUrl(url) {
107
+ const parsed = parseUrlString(url);
108
+ if (!parsed)
109
+ return null;
110
+ const pathSegments = parsed.pathname.split('/').filter(Boolean);
111
+ if (pathSegments.length === 0)
112
+ return null;
113
+ let resolvePath;
114
+ if (this.linkPathPrefix && pathSegments[0] === this.linkPathPrefix && pathSegments[1]) {
115
+ resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}/${pathSegments[1]}`;
116
+ }
117
+ else if (pathSegments.length >= 2) {
118
+ resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}/${pathSegments[1]}`;
119
+ }
120
+ else if (this.linkPathPrefix) {
121
+ resolvePath = `/api/sdk/v1/resolve/${this.linkPathPrefix}/${pathSegments[0]}`;
122
+ }
123
+ else {
124
+ resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}`;
125
+ }
126
+ // Collect fingerprint for click attribution
127
+ try {
128
+ const fp = this.fingerprint.collect(168);
129
+ const fpParams = {
130
+ fp_tz: fp.timezone,
131
+ fp_lang: fp.language,
132
+ fp_sw: String(fp.screenWidth),
133
+ fp_sh: String(fp.screenHeight),
134
+ fp_platform: fp.platform,
135
+ fp_pv: fp.platformVersion,
136
+ };
137
+ resolvePath += `?${buildQueryString(fpParams)}`;
138
+ }
139
+ catch (e) {
140
+ logger.warn('Fingerprint collection failed, resolving without fingerprint:', e);
141
+ }
142
+ try {
143
+ return await this.network.request(resolvePath);
144
+ }
145
+ catch (e) {
146
+ logger.warn('Server-side resolution failed:', e);
147
+ return null;
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,13 @@
1
+ import type { DeepLinkData } from '../models/deep-link-data';
2
+ interface ParsedUrl {
3
+ pathname: string;
4
+ searchParams: Map<string, string>;
5
+ }
6
+ /**
7
+ * Hermes-safe URL parsing (URL.pathname may throw "not implemented" in Hermes).
8
+ */
9
+ export declare function parseUrlString(url: string): ParsedUrl | null;
10
+ export declare function buildQueryString(params: Record<string, string>): string;
11
+ export declare function parseDeepLinkUrl(url: string, baseUrl?: string): DeepLinkData | null;
12
+ export {};
13
+ //# sourceMappingURL=url-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-parser.d.ts","sourceRoot":"","sources":["../../src/deeplink/url-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAmC5D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAIvE;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuCnF"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Hermes-safe URL parsing (URL.pathname may throw "not implemented" in Hermes).
3
+ */
4
+ export function parseUrlString(url) {
5
+ try {
6
+ const protocolEnd = url.indexOf('://');
7
+ if (protocolEnd === -1)
8
+ return null;
9
+ const afterProtocol = url.substring(protocolEnd + 3);
10
+ const pathStart = afterProtocol.indexOf('/');
11
+ const pathAndQuery = pathStart === -1 ? '/' : afterProtocol.substring(pathStart);
12
+ const hashIndex = pathAndQuery.indexOf('#');
13
+ const withoutHash = hashIndex === -1 ? pathAndQuery : pathAndQuery.substring(0, hashIndex);
14
+ const queryStart = withoutHash.indexOf('?');
15
+ const pathname = queryStart === -1 ? withoutHash : withoutHash.substring(0, queryStart);
16
+ const queryString = queryStart === -1 ? '' : withoutHash.substring(queryStart + 1);
17
+ const searchParams = new Map();
18
+ if (queryString) {
19
+ for (const pair of queryString.split('&')) {
20
+ const eqIndex = pair.indexOf('=');
21
+ if (eqIndex === -1) {
22
+ searchParams.set(decodeURIComponent(pair), '');
23
+ }
24
+ else {
25
+ searchParams.set(decodeURIComponent(pair.substring(0, eqIndex)), decodeURIComponent(pair.substring(eqIndex + 1)));
26
+ }
27
+ }
28
+ }
29
+ return { pathname, searchParams };
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function buildQueryString(params) {
36
+ return Object.entries(params)
37
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
38
+ .join('&');
39
+ }
40
+ const UTM_KEYS = new Set(['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']);
41
+ export function parseDeepLinkUrl(url, baseUrl) {
42
+ // If baseUrl is set, only parse URLs matching it
43
+ if (baseUrl && !url.startsWith(baseUrl)) {
44
+ return null;
45
+ }
46
+ const parsed = parseUrlString(url);
47
+ if (!parsed)
48
+ return null;
49
+ const pathSegments = parsed.pathname.split('/').filter(Boolean);
50
+ const shortCode = pathSegments[pathSegments.length - 1];
51
+ if (!shortCode)
52
+ return null;
53
+ // Extract UTM parameters
54
+ const utmParameters = {};
55
+ let hasUtm = false;
56
+ for (const [key, value] of parsed.searchParams) {
57
+ if (key === 'utm_source') {
58
+ utmParameters.source = value;
59
+ hasUtm = true;
60
+ }
61
+ else if (key === 'utm_medium') {
62
+ utmParameters.medium = value;
63
+ hasUtm = true;
64
+ }
65
+ else if (key === 'utm_campaign') {
66
+ utmParameters.campaign = value;
67
+ hasUtm = true;
68
+ }
69
+ else if (key === 'utm_term') {
70
+ utmParameters.term = value;
71
+ hasUtm = true;
72
+ }
73
+ else if (key === 'utm_content') {
74
+ utmParameters.content = value;
75
+ hasUtm = true;
76
+ }
77
+ }
78
+ // Extract custom (non-UTM) parameters
79
+ const customParameters = {};
80
+ let hasCustom = false;
81
+ for (const [key, value] of parsed.searchParams) {
82
+ if (!UTM_KEYS.has(key)) {
83
+ customParameters[key] = value;
84
+ hasCustom = true;
85
+ }
86
+ }
87
+ return {
88
+ shortCode,
89
+ utmParameters: hasUtm ? utmParameters : undefined,
90
+ customParameters: hasCustom ? customParameters : undefined,
91
+ };
92
+ }
@@ -0,0 +1,26 @@
1
+ export declare enum LinkFortyErrorCode {
2
+ NOT_INITIALIZED = "NOT_INITIALIZED",
3
+ ALREADY_INITIALIZED = "ALREADY_INITIALIZED",
4
+ INVALID_CONFIGURATION = "INVALID_CONFIGURATION",
5
+ NETWORK_ERROR = "NETWORK_ERROR",
6
+ INVALID_RESPONSE = "INVALID_RESPONSE",
7
+ DECODING_ERROR = "DECODING_ERROR",
8
+ INVALID_EVENT_DATA = "INVALID_EVENT_DATA",
9
+ INVALID_DEEP_LINK_URL = "INVALID_DEEP_LINK_URL",
10
+ MISSING_API_KEY = "MISSING_API_KEY"
11
+ }
12
+ export declare class LinkFortyError extends Error {
13
+ readonly code: LinkFortyErrorCode;
14
+ readonly cause?: Error;
15
+ constructor(code: LinkFortyErrorCode, message: string, cause?: Error);
16
+ static notInitialized(): LinkFortyError;
17
+ static alreadyInitialized(): LinkFortyError;
18
+ static invalidConfiguration(detail: string): LinkFortyError;
19
+ static networkError(cause: Error): LinkFortyError;
20
+ static invalidResponse(statusCode: number, message?: string): LinkFortyError;
21
+ static decodingError(cause: Error): LinkFortyError;
22
+ static invalidEventData(detail: string): LinkFortyError;
23
+ static invalidDeepLinkUrl(detail: string): LinkFortyError;
24
+ static missingApiKey(): LinkFortyError;
25
+ }
26
+ //# sourceMappingURL=linkforty-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkforty-error.d.ts","sourceRoot":"","sources":["../../src/errors/linkforty-error.ts"],"names":[],"mappings":"AAAA,oBAAY,kBAAkB;IAC5B,eAAe,oBAAoB;IACnC,mBAAmB,wBAAwB;IAC3C,qBAAqB,0BAA0B;IAC/C,aAAa,kBAAkB;IAC/B,gBAAgB,qBAAqB;IACrC,cAAc,mBAAmB;IACjC,kBAAkB,uBAAuB;IACzC,qBAAqB,0BAA0B;IAC/C,eAAe,oBAAoB;CACpC;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;gBAEX,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK;IAOpE,MAAM,CAAC,cAAc,IAAI,cAAc;IAOvC,MAAM,CAAC,kBAAkB,IAAI,cAAc;IAO3C,MAAM,CAAC,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAO3D,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,cAAc;IAQjD,MAAM,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc;IAQ5E,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,cAAc;IAQlD,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAOvD,MAAM,CAAC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAOzD,MAAM,CAAC,aAAa,IAAI,cAAc;CAMvC"}
@@ -0,0 +1,50 @@
1
+ export var LinkFortyErrorCode;
2
+ (function (LinkFortyErrorCode) {
3
+ LinkFortyErrorCode["NOT_INITIALIZED"] = "NOT_INITIALIZED";
4
+ LinkFortyErrorCode["ALREADY_INITIALIZED"] = "ALREADY_INITIALIZED";
5
+ LinkFortyErrorCode["INVALID_CONFIGURATION"] = "INVALID_CONFIGURATION";
6
+ LinkFortyErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
7
+ LinkFortyErrorCode["INVALID_RESPONSE"] = "INVALID_RESPONSE";
8
+ LinkFortyErrorCode["DECODING_ERROR"] = "DECODING_ERROR";
9
+ LinkFortyErrorCode["INVALID_EVENT_DATA"] = "INVALID_EVENT_DATA";
10
+ LinkFortyErrorCode["INVALID_DEEP_LINK_URL"] = "INVALID_DEEP_LINK_URL";
11
+ LinkFortyErrorCode["MISSING_API_KEY"] = "MISSING_API_KEY";
12
+ })(LinkFortyErrorCode || (LinkFortyErrorCode = {}));
13
+ export class LinkFortyError extends Error {
14
+ code;
15
+ cause;
16
+ constructor(code, message, cause) {
17
+ super(message);
18
+ this.name = 'LinkFortyError';
19
+ this.code = code;
20
+ this.cause = cause;
21
+ }
22
+ static notInitialized() {
23
+ return new LinkFortyError(LinkFortyErrorCode.NOT_INITIALIZED, 'LinkForty SDK is not initialized. Call initialize() first.');
24
+ }
25
+ static alreadyInitialized() {
26
+ return new LinkFortyError(LinkFortyErrorCode.ALREADY_INITIALIZED, 'LinkForty SDK has already been initialized.');
27
+ }
28
+ static invalidConfiguration(detail) {
29
+ return new LinkFortyError(LinkFortyErrorCode.INVALID_CONFIGURATION, `Invalid configuration: ${detail}`);
30
+ }
31
+ static networkError(cause) {
32
+ return new LinkFortyError(LinkFortyErrorCode.NETWORK_ERROR, `Network error: ${cause.message}`, cause);
33
+ }
34
+ static invalidResponse(statusCode, message) {
35
+ const detail = message ? `: ${message}` : '';
36
+ return new LinkFortyError(LinkFortyErrorCode.INVALID_RESPONSE, `Invalid server response (status: ${statusCode})${detail}`);
37
+ }
38
+ static decodingError(cause) {
39
+ return new LinkFortyError(LinkFortyErrorCode.DECODING_ERROR, `Failed to decode response: ${cause.message}`, cause);
40
+ }
41
+ static invalidEventData(detail) {
42
+ return new LinkFortyError(LinkFortyErrorCode.INVALID_EVENT_DATA, `Invalid event data: ${detail}`);
43
+ }
44
+ static invalidDeepLinkUrl(detail) {
45
+ return new LinkFortyError(LinkFortyErrorCode.INVALID_DEEP_LINK_URL, `Invalid deep link URL: ${detail}`);
46
+ }
47
+ static missingApiKey() {
48
+ return new LinkFortyError(LinkFortyErrorCode.MISSING_API_KEY, 'API key is required for this operation. Provide an apiKey in the config.');
49
+ }
50
+ }
@@ -0,0 +1,14 @@
1
+ import type { EventRequest } from '../models/event-request';
2
+ export declare class EventQueue {
3
+ private queue;
4
+ private loaded;
5
+ load(): Promise<void>;
6
+ enqueue(event: EventRequest): Promise<boolean>;
7
+ dequeue(): Promise<EventRequest | null>;
8
+ peek(): Promise<EventRequest[]>;
9
+ clear(): Promise<void>;
10
+ get count(): number;
11
+ get isEmpty(): boolean;
12
+ private persist;
13
+ }
14
+ //# sourceMappingURL=event-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-queue.d.ts","sourceRoot":"","sources":["../../src/events/event-queue.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAM5D,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAS;IAEjB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAcrB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;IAY9C,OAAO,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAQvC,IAAI,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAK/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;YAEa,OAAO;CAOtB"}
@@ -0,0 +1,68 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import { STORAGE_KEYS } from '../storage/storage-keys';
3
+ import { logger } from '../logger';
4
+ const MAX_QUEUE_SIZE = 100;
5
+ export class EventQueue {
6
+ queue = [];
7
+ loaded = false;
8
+ async load() {
9
+ if (this.loaded)
10
+ return;
11
+ try {
12
+ const json = await AsyncStorage.getItem(STORAGE_KEYS.EVENT_QUEUE);
13
+ if (json) {
14
+ this.queue = JSON.parse(json);
15
+ }
16
+ }
17
+ catch (e) {
18
+ logger.error('Failed to load event queue:', e);
19
+ this.queue = [];
20
+ }
21
+ this.loaded = true;
22
+ }
23
+ async enqueue(event) {
24
+ await this.load();
25
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
26
+ logger.log('Event queue full, dropping event:', event.eventName);
27
+ return false;
28
+ }
29
+ this.queue.push(event);
30
+ await this.persist();
31
+ logger.log(`Event queued: ${event.eventName} (queue size: ${this.queue.length})`);
32
+ return true;
33
+ }
34
+ async dequeue() {
35
+ await this.load();
36
+ if (this.queue.length === 0)
37
+ return null;
38
+ const event = this.queue.shift();
39
+ await this.persist();
40
+ return event;
41
+ }
42
+ async peek() {
43
+ await this.load();
44
+ return [...this.queue];
45
+ }
46
+ async clear() {
47
+ const size = this.queue.length;
48
+ this.queue = [];
49
+ await this.persist();
50
+ if (size > 0) {
51
+ logger.log(`Event queue cleared (${size} events removed)`);
52
+ }
53
+ }
54
+ get count() {
55
+ return this.queue.length;
56
+ }
57
+ get isEmpty() {
58
+ return this.queue.length === 0;
59
+ }
60
+ async persist() {
61
+ try {
62
+ await AsyncStorage.setItem(STORAGE_KEYS.EVENT_QUEUE, JSON.stringify(this.queue));
63
+ }
64
+ catch (e) {
65
+ logger.error('Failed to persist event queue:', e);
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,18 @@
1
+ import type { NetworkManagerProtocol } from '../network/network-manager';
2
+ import type { StorageManagerProtocol } from '../storage/storage-manager';
3
+ import type { AttributionContext } from '../attribution/attribution-context';
4
+ import { EventQueue } from './event-queue';
5
+ export declare class EventTracker {
6
+ private readonly network;
7
+ private readonly storage;
8
+ private readonly queue;
9
+ private readonly attribution?;
10
+ constructor(network: NetworkManagerProtocol, storage: StorageManagerProtocol, queue?: EventQueue, attribution?: AttributionContext);
11
+ trackEvent(name: string, properties?: Record<string, unknown>): Promise<void>;
12
+ trackRevenue(amount: number, currency: string, properties?: Record<string, unknown>): Promise<void>;
13
+ flushQueue(): Promise<void>;
14
+ clearQueue(): Promise<void>;
15
+ get queuedEventCount(): number;
16
+ private sendEvent;
17
+ }
18
+ //# sourceMappingURL=event-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-tracker.d.ts","sourceRoot":"","sources":["../../src/events/event-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAK3C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAqB;gBAGhD,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,sBAAsB,EAC/B,KAAK,CAAC,EAAE,UAAU,EAClB,WAAW,CAAC,EAAE,kBAAkB;IAQ5B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAsC7E,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,IAAI,CAAC;IAcV,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC,IAAI,gBAAgB,IAAI,MAAM,CAE7B;YAEa,SAAS;CAMxB"}
@@ -0,0 +1,92 @@
1
+ import { EventQueue } from './event-queue';
2
+ import { LinkFortyError } from '../errors/linkforty-error';
3
+ import { logger } from '../logger';
4
+ import { SDK_NAME, SDK_VERSION } from '../version';
5
+ export class EventTracker {
6
+ network;
7
+ storage;
8
+ queue;
9
+ attribution;
10
+ constructor(network, storage, queue, attribution) {
11
+ this.network = network;
12
+ this.storage = storage;
13
+ this.queue = queue ?? new EventQueue();
14
+ this.attribution = attribution;
15
+ }
16
+ async trackEvent(name, properties) {
17
+ if (!name || !name.trim()) {
18
+ throw LinkFortyError.invalidEventData('Event name cannot be empty');
19
+ }
20
+ const installId = await this.storage.getInstallId();
21
+ if (!installId) {
22
+ throw LinkFortyError.notInitialized();
23
+ }
24
+ // Stamp the event with the active last-click attribution + session so it can
25
+ // be credited to the originating deep link. Stamped at event time, so a
26
+ // queued event keeps its point-in-time attribution.
27
+ const event = {
28
+ installId,
29
+ eventName: name,
30
+ eventData: properties ?? {},
31
+ timestamp: new Date().toISOString(),
32
+ ...this.attribution?.getStamp(),
33
+ // SDK identity for health/version diagnostics (SIT-235)
34
+ sdkName: SDK_NAME,
35
+ sdkVersion: SDK_VERSION,
36
+ };
37
+ try {
38
+ await this.sendEvent(event);
39
+ logger.log('Event tracked:', name);
40
+ // If send succeeds, try to flush queued events
41
+ await this.flushQueue();
42
+ }
43
+ catch (e) {
44
+ // Queue the event on failure
45
+ await this.queue.enqueue(event);
46
+ logger.log('Event queued due to error:', e);
47
+ throw e;
48
+ }
49
+ }
50
+ async trackRevenue(amount, currency, properties) {
51
+ if (amount < 0) {
52
+ throw LinkFortyError.invalidEventData('Revenue amount must be non-negative');
53
+ }
54
+ const eventProperties = {
55
+ ...properties,
56
+ revenue: amount,
57
+ currency,
58
+ };
59
+ await this.trackEvent('revenue', eventProperties);
60
+ }
61
+ async flushQueue() {
62
+ await this.queue.load();
63
+ logger.log(`Flushing event queue (${this.queue.count} events)`);
64
+ while (!this.queue.isEmpty) {
65
+ const event = await this.queue.dequeue();
66
+ if (!event)
67
+ break;
68
+ try {
69
+ await this.sendEvent(event);
70
+ logger.log('Queued event sent:', event.eventName);
71
+ }
72
+ catch (e) {
73
+ // Re-queue the event and stop flushing
74
+ await this.queue.enqueue(event);
75
+ logger.log('Failed to send queued event:', e);
76
+ return;
77
+ }
78
+ }
79
+ }
80
+ async clearQueue() {
81
+ await this.queue.clear();
82
+ }
83
+ get queuedEventCount() {
84
+ return this.queue.count;
85
+ }
86
+ async sendEvent(event) {
87
+ await this.network.request('/api/sdk/v1/event', {
88
+ method: 'POST',
89
+ body: JSON.stringify(event),
90
+ });
91
+ }
92
+ }
@@ -0,0 +1,8 @@
1
+ import type { DeviceFingerprint } from '../models/device-fingerprint';
2
+ export interface FingerprintCollectorProtocol {
3
+ collect(attributionWindowHours: number, deviceId?: string): DeviceFingerprint;
4
+ }
5
+ export declare class FingerprintCollector implements FingerprintCollectorProtocol {
6
+ collect(attributionWindowHours: number, deviceId?: string): DeviceFingerprint;
7
+ }
8
+ //# sourceMappingURL=fingerprint-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint-collector.d.ts","sourceRoot":"","sources":["../../src/fingerprint/fingerprint-collector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAEtE,MAAM,WAAW,4BAA4B;IAC3C,OAAO,CAAC,sBAAsB,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAAC;CAC/E;AAED,qBAAa,oBAAqB,YAAW,4BAA4B;IACvE,OAAO,CAAC,sBAAsB,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,iBAAiB;CAuB9E"}
@@ -0,0 +1,27 @@
1
+ import * as Device from 'expo-device';
2
+ import * as Application from 'expo-application';
3
+ import { getLocales, getCalendars } from 'expo-localization';
4
+ import { Platform, Dimensions } from 'react-native';
5
+ export class FingerprintCollector {
6
+ collect(attributionWindowHours, deviceId) {
7
+ const { width, height } = Dimensions.get('window');
8
+ const locales = getLocales();
9
+ const calendars = getCalendars();
10
+ const appName = Application.applicationName ?? 'App';
11
+ const appVersion = Application.nativeApplicationVersion ?? '1.0.0';
12
+ const platform = Platform.OS;
13
+ const platformVersion = Device.osVersion ?? '0';
14
+ return {
15
+ userAgent: `${appName}/${appVersion} ${platform}/${platformVersion}`,
16
+ timezone: calendars[0]?.timeZone ?? 'UTC',
17
+ language: locales[0]?.languageTag ?? 'en-US',
18
+ screenWidth: Math.round(width),
19
+ screenHeight: Math.round(height),
20
+ platform,
21
+ platformVersion,
22
+ appVersion,
23
+ deviceId,
24
+ attributionWindowHours,
25
+ };
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ export { default } from './linkforty-sdk';
2
+ export { default as DeepLink } from './linkforty-sdk';
3
+ export { LinkFortySDK } from './linkforty-sdk';
4
+ export { LinkFortyError, LinkFortyErrorCode } from './errors/linkforty-error';
5
+ export type { LinkFortyConfig, AutoTrackNavigationOptions, NavigationRouteLike, NavigationContainerRefLike, } from './models/config';
6
+ export type { DeepLinkData } from './models/deep-link-data';
7
+ export type { InstallAttributionResponse } from './models/install-response';
8
+ export type { UTMParameters } from './models/utm-parameters';
9
+ export type { DeviceFingerprint } from './models/device-fingerprint';
10
+ export type { CreateLinkOptions } from './models/create-link-options';
11
+ export type { CreateLinkResult } from './models/create-link-result';
12
+ export type { EventRequest } from './models/event-request';
13
+ export type { ActiveAttribution, AttributionStamp } from './models/attribution';
14
+ export { AttributionContext } from './attribution/attribution-context';
15
+ export { NavigationTracker, sanitizeScreenParams } from './navigation/navigation-tracker';
16
+ export type { ScreenEventEmitter, NavigationTrackerOptions, } from './navigation/navigation-tracker';
17
+ export type { DeferredDeepLinkCallback, DeepLinkCallback } from './deeplink/deep-link-handler';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9E,YAAY,EACV,eAAe,EACf,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,YAAY,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAC5E,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC1F,YAAY,EACV,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AAEzC,YAAY,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { default } from './linkforty-sdk';
2
+ export { default as DeepLink } from './linkforty-sdk';
3
+ export { LinkFortySDK } from './linkforty-sdk';
4
+ export { LinkFortyError, LinkFortyErrorCode } from './errors/linkforty-error';
5
+ export { AttributionContext } from './attribution/attribution-context';
6
+ export { NavigationTracker, sanitizeScreenParams } from './navigation/navigation-tracker';