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,342 @@
1
+ import type { LinkFortyConfig } from './models/config';
2
+ import type { InstallAttributionResponse } from './models/install-response';
3
+ import type { DeepLinkData } from './models/deep-link-data';
4
+ import type { CreateLinkOptions } from './models/create-link-options';
5
+ import type { CreateLinkResult } from './models/create-link-result';
6
+ import type { DeferredDeepLinkCallback, DeepLinkCallback } from './deeplink/deep-link-handler';
7
+ import { validateConfig } from './models/config';
8
+ import { LinkFortyError } from './errors/linkforty-error';
9
+ import { NetworkManager } from './network/network-manager';
10
+ import { StorageManager } from './storage/storage-manager';
11
+ import { FingerprintCollector } from './fingerprint/fingerprint-collector';
12
+ import { AttributionManager } from './attribution/attribution-manager';
13
+ import { AttributionContext } from './attribution/attribution-context';
14
+ import { DeepLinkHandler } from './deeplink/deep-link-handler';
15
+ import { EventTracker } from './events/event-tracker';
16
+ import { EventQueue } from './events/event-queue';
17
+ import { NavigationTracker } from './navigation/navigation-tracker';
18
+ import { logger } from './logger';
19
+
20
+ export class LinkFortySDK {
21
+ private config: LinkFortyConfig | null = null;
22
+ private networkManager: NetworkManager | null = null;
23
+ private attributionManager: AttributionManager | null = null;
24
+ private attributionContext: AttributionContext | null = null;
25
+ private deepLinkHandler: DeepLinkHandler | null = null;
26
+ private eventTracker: EventTracker | null = null;
27
+ private navigationTracker: NavigationTracker | null = null;
28
+ private externalUserId: string | null = null;
29
+ private _isInitialized = false;
30
+
31
+ get isInitialized(): boolean {
32
+ return this._isInitialized;
33
+ }
34
+
35
+ // -- Initialization --
36
+
37
+ async initialize(config: LinkFortyConfig): Promise<InstallAttributionResponse> {
38
+ if (this._isInitialized) {
39
+ throw LinkFortyError.alreadyInitialized();
40
+ }
41
+
42
+ validateConfig(config);
43
+ this.config = config;
44
+ logger.setDebug(config.debug ?? false);
45
+
46
+ logger.log('Initializing SDK with config:', {
47
+ baseUrl: config.baseUrl,
48
+ debug: config.debug,
49
+ attributionWindowHours: config.attributionWindowHours,
50
+ linkPathPrefix: config.linkPathPrefix,
51
+ apiKey: config.apiKey ? '***' : undefined,
52
+ });
53
+
54
+ // Create managers
55
+ const networkManager = new NetworkManager(config.baseUrl, config.apiKey);
56
+ const storageManager = new StorageManager();
57
+ const fingerprintCollector = new FingerprintCollector();
58
+ const eventQueue = new EventQueue();
59
+ await eventQueue.load();
60
+
61
+ this.networkManager = networkManager;
62
+
63
+ this.attributionManager = new AttributionManager(
64
+ networkManager,
65
+ storageManager,
66
+ fingerprintCollector,
67
+ );
68
+
69
+ // Last-click attribution context: restore any persisted active link before
70
+ // we attribute the install or handle deep links.
71
+ const attributionContext = new AttributionContext();
72
+ await attributionContext.load();
73
+ this.attributionContext = attributionContext;
74
+
75
+ this.eventTracker = new EventTracker(
76
+ networkManager,
77
+ storageManager,
78
+ eventQueue,
79
+ attributionContext,
80
+ );
81
+
82
+ const deepLinkHandler = new DeepLinkHandler(
83
+ config.baseUrl,
84
+ networkManager,
85
+ fingerprintCollector,
86
+ config.linkPathPrefix,
87
+ );
88
+ this.deepLinkHandler = deepLinkHandler;
89
+
90
+ // Record every direct (re-engagement) deep-link open into the attribution
91
+ // context so subsequent events credit the link the user just tapped — not
92
+ // their original install link. Registered before any developer callbacks.
93
+ deepLinkHandler.onDeepLink((_url, data) => {
94
+ if (data?.linkId) {
95
+ void attributionContext.recordDeepLinkOpen(data.linkId);
96
+ }
97
+ });
98
+
99
+ this._isInitialized = true;
100
+
101
+ // Report install
102
+ const attributionWindowHours = config.attributionWindowHours ?? 168;
103
+ const response = await this.attributionManager.reportInstall(
104
+ attributionWindowHours,
105
+ undefined, // deviceId — IDFA/GAID not collected by Expo SDK
106
+ config.appToken,
107
+ );
108
+
109
+ // If attributed, deliver deferred deep link + seed the attribution context so
110
+ // the new user's first-session events credit the install link.
111
+ if (response.attributed && response.deepLinkData) {
112
+ if (response.deepLinkData.linkId) {
113
+ await attributionContext.recordDeepLinkOpen(response.deepLinkData.linkId);
114
+ }
115
+ deepLinkHandler.deliverDeferredDeepLink(response.deepLinkData);
116
+ } else {
117
+ deepLinkHandler.deliverDeferredDeepLink(null);
118
+ }
119
+
120
+ // Start listening for direct deep links
121
+ deepLinkHandler.startListening();
122
+
123
+ // Optional auto screen-view tracking. Screen views flow through trackEvent,
124
+ // so they inherit the attribution stamp. Guarded: a missing/invalid
125
+ // navigationRef (or no react-navigation) is a no-op.
126
+ if (config.autoTrackNavigation) {
127
+ if (config.navigationRef) {
128
+ const navOptions =
129
+ typeof config.autoTrackNavigation === 'object' ? config.autoTrackNavigation : {};
130
+ this.navigationTracker = new NavigationTracker(
131
+ config.navigationRef,
132
+ (name, properties) => {
133
+ void this.trackEvent(name, properties);
134
+ },
135
+ { captureParams: navOptions.captureParams, debounceMs: navOptions.debounceMs },
136
+ );
137
+ this.navigationTracker.start();
138
+ } else {
139
+ logger.warn(
140
+ 'autoTrackNavigation is enabled but no navigationRef was provided — screen tracking is disabled.',
141
+ );
142
+ }
143
+ }
144
+
145
+ logger.log('SDK initialized successfully (attributed:', response.attributed, ')');
146
+
147
+ return response;
148
+ }
149
+
150
+ // -- Deep Linking --
151
+
152
+ onDeferredDeepLink(callback: DeferredDeepLinkCallback): void {
153
+ this.requireDeepLinkHandler().onDeferredDeepLink(callback);
154
+ }
155
+
156
+ onDeepLink(callback: DeepLinkCallback): void {
157
+ this.requireDeepLinkHandler().onDeepLink(callback);
158
+ }
159
+
160
+ handleDeepLink(url: string): void {
161
+ this.requireDeepLinkHandler().handleDeepLink(url);
162
+ }
163
+
164
+ // -- User Identity --
165
+
166
+ /**
167
+ * Sets the external user ID for attribution. This ID will be attached to all
168
+ * links created via createLink() unless overridden per-call. Pass null to clear.
169
+ */
170
+ setExternalUserId(id: string | null): void {
171
+ this.externalUserId = id;
172
+ logger.log('External user ID set:', id);
173
+ }
174
+
175
+ /**
176
+ * Returns the current external user ID, if set.
177
+ */
178
+ getExternalUserId(): string | null {
179
+ return this.externalUserId;
180
+ }
181
+
182
+ // -- Event Tracking --
183
+
184
+ async trackEvent(name: string, properties?: Record<string, unknown>): Promise<void> {
185
+ return this.requireEventTracker().trackEvent(name, properties);
186
+ }
187
+
188
+ async trackRevenue(
189
+ amount: number,
190
+ currency: string,
191
+ properties?: Record<string, unknown>,
192
+ ): Promise<void> {
193
+ return this.requireEventTracker().trackRevenue(amount, currency, properties);
194
+ }
195
+
196
+ async flushEvents(): Promise<void> {
197
+ return this.requireEventTracker().flushQueue();
198
+ }
199
+
200
+ async clearEventQueue(): Promise<void> {
201
+ return this.requireEventTracker().clearQueue();
202
+ }
203
+
204
+ get queuedEventCount(): number {
205
+ return this.eventTracker?.queuedEventCount ?? 0;
206
+ }
207
+
208
+ // -- Link Creation --
209
+
210
+ async createLink(options: CreateLinkOptions): Promise<CreateLinkResult> {
211
+ this.requireInitialized();
212
+
213
+ if (!this.config!.apiKey) {
214
+ throw LinkFortyError.missingApiKey();
215
+ }
216
+
217
+ const body: Record<string, unknown> = {};
218
+ if (options.templateId) body.templateId = options.templateId;
219
+ if (options.deepLinkParameters) body.deepLinkParameters = options.deepLinkParameters;
220
+ if (options.title) body.title = options.title;
221
+ if (options.description) body.description = options.description;
222
+ if (options.customCode) body.customCode = options.customCode;
223
+ if (options.utmParameters) body.utmParameters = options.utmParameters;
224
+ if (options.webFallbackUrl) body.webFallbackUrl = options.webFallbackUrl;
225
+ // Per-call externalUserId takes precedence, then fall back to SDK-level value
226
+ const resolvedUserId = options.externalUserId ?? this.externalUserId;
227
+ if (resolvedUserId) body.externalUserId = resolvedUserId;
228
+
229
+ const useSimplifiedEndpoint = !options.templateId;
230
+ const endpoint = useSimplifiedEndpoint ? '/api/sdk/v1/links' : '/api/links';
231
+
232
+ const response = await this.networkManager!.request<{
233
+ id: string;
234
+ short_code: string;
235
+ url?: string;
236
+ shortCode?: string;
237
+ linkId?: string;
238
+ deduplicated?: boolean;
239
+ }>(endpoint, {
240
+ method: 'POST',
241
+ body: JSON.stringify(body),
242
+ });
243
+
244
+ // SDK endpoint returns { url, shortCode, linkId, deduplicated? } directly
245
+ if (useSimplifiedEndpoint && response.url) {
246
+ return {
247
+ url: response.url,
248
+ shortCode: response.shortCode || response.short_code,
249
+ linkId: response.linkId || response.id,
250
+ deduplicated: response.deduplicated,
251
+ };
252
+ }
253
+
254
+ // Dashboard endpoint returns { id, short_code } — build URL from parts
255
+ const shortCode = response.short_code;
256
+ const baseUrl = this.config!.baseUrl.replace(/\/+$/, '');
257
+ const prefix = (options.templateSlug || this.config!.linkPathPrefix || '').replace(/^\/+|\/+$/g, '');
258
+ const url = prefix
259
+ ? `${baseUrl}/${prefix}/${shortCode}`
260
+ : `${baseUrl}/${shortCode}`;
261
+
262
+ return {
263
+ url,
264
+ shortCode,
265
+ linkId: response.id,
266
+ };
267
+ }
268
+
269
+ // -- Attribution Data --
270
+
271
+ async getInstallId(): Promise<string | null> {
272
+ return this.attributionManager?.getInstallId() ?? null;
273
+ }
274
+
275
+ async getInstallData(): Promise<DeepLinkData | null> {
276
+ return this.attributionManager?.getInstallData() ?? null;
277
+ }
278
+
279
+ async isFirstLaunch(): Promise<boolean> {
280
+ return this.attributionManager?.isFirstLaunch() ?? true;
281
+ }
282
+
283
+ // -- Data Management --
284
+
285
+ async clearData(): Promise<void> {
286
+ await this.attributionManager?.clearData();
287
+ await this.attributionContext?.clear();
288
+ await this.eventTracker?.clearQueue();
289
+ this.deepLinkHandler?.clearCallbacks();
290
+ this.externalUserId = null;
291
+ logger.log('All SDK data cleared');
292
+ }
293
+
294
+ reset(): void {
295
+ this.deepLinkHandler?.cleanup();
296
+ this.navigationTracker?.stop();
297
+ this.config = null;
298
+ this.networkManager = null;
299
+ this.attributionManager = null;
300
+ this.attributionContext = null;
301
+ this.deepLinkHandler = null;
302
+ this.eventTracker = null;
303
+ this.navigationTracker = null;
304
+ this.externalUserId = null;
305
+ this._isInitialized = false;
306
+ logger.log('SDK reset to uninitialized state');
307
+ }
308
+
309
+ // -- Attribution Session --
310
+
311
+ /**
312
+ * Current session id — identifies one app-open journey. Rotates on cold start
313
+ * and on each new deep-link open. Used to group a visit's screen-flow.
314
+ */
315
+ getSessionId(): string | null {
316
+ return this.attributionContext?.getSessionId() ?? null;
317
+ }
318
+
319
+ // -- Guards --
320
+
321
+ private requireInitialized(): void {
322
+ if (!this._isInitialized) {
323
+ throw LinkFortyError.notInitialized();
324
+ }
325
+ }
326
+
327
+ private requireDeepLinkHandler(): DeepLinkHandler {
328
+ if (!this._isInitialized || !this.deepLinkHandler) {
329
+ throw LinkFortyError.notInitialized();
330
+ }
331
+ return this.deepLinkHandler;
332
+ }
333
+
334
+ private requireEventTracker(): EventTracker {
335
+ if (!this._isInitialized || !this.eventTracker) {
336
+ throw LinkFortyError.notInitialized();
337
+ }
338
+ return this.eventTracker;
339
+ }
340
+ }
341
+
342
+ export default new LinkFortySDK();
package/src/logger.ts ADDED
@@ -0,0 +1,23 @@
1
+ const TAG = '[LinkForty]';
2
+
3
+ let debugEnabled = false;
4
+
5
+ export const logger = {
6
+ setDebug(enabled: boolean): void {
7
+ debugEnabled = enabled;
8
+ },
9
+
10
+ log(...args: unknown[]): void {
11
+ if (debugEnabled) {
12
+ console.log(TAG, ...args);
13
+ }
14
+ },
15
+
16
+ warn(...args: unknown[]): void {
17
+ console.warn(TAG, ...args);
18
+ },
19
+
20
+ error(...args: unknown[]): void {
21
+ console.error(TAG, ...args);
22
+ },
23
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Last-click attribution models (SIT-237).
3
+ *
4
+ * In-app activity (screen views + custom events) is attributed to the deep link
5
+ * that drove it. Every deep-link open pins an active context; the newest open
6
+ * supersedes the previous one; events are stamped with the active context so the
7
+ * backend can credit the link under a last-click + window model.
8
+ */
9
+
10
+ /** The deep link currently credited for in-app activity (last-click). */
11
+ export interface ActiveAttribution {
12
+ /** The link the deep link resolved to (`DeepLinkData.linkId`) */
13
+ linkId: string;
14
+ /** Optional originating click id (link-level attribution works without it) */
15
+ clickId?: string;
16
+ /** ISO timestamp of when this deep link opened the app */
17
+ openedAt: string;
18
+ }
19
+
20
+ /**
21
+ * Attribution fields merged into every event payload. `sessionId` is always
22
+ * present; the link fields are absent for organic activity (no deep link yet).
23
+ */
24
+ export interface AttributionStamp {
25
+ attributedLinkId?: string;
26
+ attributedClickId?: string;
27
+ linkOpenedAt?: string;
28
+ sessionId: string;
29
+ }
@@ -0,0 +1,109 @@
1
+ import { LinkFortyError } from '../errors/linkforty-error';
2
+
3
+ export interface LinkFortyConfig {
4
+ /** Base URL of your LinkForty instance (e.g., 'https://go.yourdomain.com') */
5
+ baseUrl: string;
6
+ /**
7
+ * Per-app short-link path prefix (e.g. '/mQuDYblrf6'). Required when multiple
8
+ * apps share one domain so short codes resolve under this app's path.
9
+ */
10
+ linkPathPrefix?: string;
11
+ /** Optional API key for Cloud authentication */
12
+ apiKey?: string;
13
+ /**
14
+ * Public workspace token (LinkForty Cloud only). Recommended — required
15
+ * for organic installs (App Store discovery, social mentions, etc.) to
16
+ * be attributed to your workspace. Find it in the dashboard under
17
+ * Workspace Settings → App Token. Safe to ship in your app bundle.
18
+ * Format: `at_<32 hex chars>`.
19
+ */
20
+ appToken?: string;
21
+ /** Enable debug logging (default: false) */
22
+ debug?: boolean;
23
+ /** Attribution window in hours (default: 168 = 7 days) */
24
+ attributionWindowHours?: number;
25
+ /**
26
+ * Auto-emit `screen_view` events from React Navigation state (no manual
27
+ * per-screen calls). Requires `navigationRef`. Off by default.
28
+ *
29
+ * Pass `true` for the privacy-safe default (screen name only, no params). To
30
+ * capture specific non-PII params, pass an options object with an explicit
31
+ * allow-list: `{ captureParams: ['productId', 'category'] }`. Screen views flow
32
+ * through the normal event pipeline and carry the active deep-link attribution
33
+ * context. Apps without react-navigation are unaffected.
34
+ */
35
+ autoTrackNavigation?: boolean | AutoTrackNavigationOptions;
36
+ /**
37
+ * The app's React Navigation container ref (`createNavigationContainerRef()`),
38
+ * required when `autoTrackNavigation` is enabled. Typed structurally so this
39
+ * SDK never has a compile-time dependency on `@react-navigation/native`.
40
+ */
41
+ navigationRef?: NavigationContainerRefLike;
42
+ }
43
+
44
+ /**
45
+ * Options for `autoTrackNavigation`. No route params are captured by default;
46
+ * opt in per key via `captureParams`.
47
+ */
48
+ export interface AutoTrackNavigationOptions {
49
+ /**
50
+ * Explicit allow-list of route param keys whose primitive values may be
51
+ * captured on `screen_view`. Omitted/empty = capture no params. Never list
52
+ * keys that can hold personal data.
53
+ */
54
+ captureParams?: string[];
55
+ /** Debounce window for rapid transitions, in ms. Default 350. */
56
+ debounceMs?: number;
57
+ }
58
+
59
+ /**
60
+ * The slice of a React Navigation route the SDK reads. Structural — matches
61
+ * `getCurrentRoute()` without importing `@react-navigation/native`.
62
+ */
63
+ export interface NavigationRouteLike {
64
+ name: string;
65
+ params?: Record<string, unknown>;
66
+ }
67
+
68
+ /** The slice of a React Navigation container ref the SDK uses. Structural. */
69
+ export interface NavigationContainerRefLike {
70
+ addListener: (
71
+ type: 'state',
72
+ callback: (event?: unknown) => void,
73
+ ) => (() => void) | { remove?: () => void } | void;
74
+ getCurrentRoute: () => NavigationRouteLike | undefined;
75
+ isReady?: () => boolean;
76
+ }
77
+
78
+ const LOCALHOST_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', '10.0.2.2'];
79
+
80
+ export function validateConfig(config: LinkFortyConfig): void {
81
+ if (!config.baseUrl) {
82
+ throw LinkFortyError.invalidConfiguration('baseUrl is required');
83
+ }
84
+
85
+ // Parse URL to validate scheme
86
+ let parsed: URL;
87
+ try {
88
+ parsed = new URL(config.baseUrl);
89
+ } catch {
90
+ throw LinkFortyError.invalidConfiguration(`Invalid base URL: ${config.baseUrl}`);
91
+ }
92
+
93
+ // HTTPS required except for localhost
94
+ if (parsed.protocol !== 'https:' && !LOCALHOST_HOSTS.includes(parsed.hostname)) {
95
+ throw LinkFortyError.invalidConfiguration(
96
+ 'Base URL must use HTTPS (HTTP only allowed for localhost)',
97
+ );
98
+ }
99
+
100
+ // Validate attribution window bounds
101
+ const windowHours = config.attributionWindowHours;
102
+ if (windowHours !== undefined) {
103
+ if (!Number.isFinite(windowHours) || windowHours < 1 || windowHours > 2160) {
104
+ throw LinkFortyError.invalidConfiguration(
105
+ 'Attribution window must be between 1 and 2160 hours',
106
+ );
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,15 @@
1
+ import type { UTMParameters } from './utm-parameters';
2
+
3
+ export interface CreateLinkOptions {
4
+ templateId?: string;
5
+ templateSlug?: string;
6
+ deepLinkParameters?: Record<string, string>;
7
+ title?: string;
8
+ description?: string;
9
+ customCode?: string;
10
+ utmParameters?: UTMParameters;
11
+ /** Identifier for the app user creating the link (enables per-user deduplication and share attribution) */
12
+ externalUserId?: string;
13
+ /** Web fallback URL — used when the app is not installed or on desktop browsers */
14
+ webFallbackUrl?: string;
15
+ }
@@ -0,0 +1,7 @@
1
+ export interface CreateLinkResult {
2
+ url: string;
3
+ shortCode: string;
4
+ linkId: string;
5
+ /** True if an existing link was returned instead of creating a new one (per-user deduplication) */
6
+ deduplicated?: boolean;
7
+ }
@@ -0,0 +1,14 @@
1
+ import type { UTMParameters } from './utm-parameters';
2
+
3
+ export interface DeepLinkData {
4
+ shortCode: string;
5
+ iosUrl?: string;
6
+ androidUrl?: string;
7
+ webUrl?: string;
8
+ utmParameters?: UTMParameters;
9
+ customParameters?: Record<string, string>;
10
+ deepLinkPath?: string;
11
+ appScheme?: string;
12
+ clickedAt?: string;
13
+ linkId?: string;
14
+ }
@@ -0,0 +1,12 @@
1
+ export interface DeviceFingerprint {
2
+ userAgent: string;
3
+ timezone: string;
4
+ language: string;
5
+ screenWidth: number;
6
+ screenHeight: number;
7
+ platform: string;
8
+ platformVersion: string;
9
+ appVersion: string;
10
+ deviceId?: string;
11
+ attributionWindowHours: number;
12
+ }
@@ -0,0 +1,15 @@
1
+ export interface EventRequest {
2
+ installId: string;
3
+ eventName: string;
4
+ eventData: Record<string, unknown>;
5
+ timestamp: string;
6
+ // Last-click attribution stamp (SIT-237) — optional / backward compatible.
7
+ // Captured at event time so queued events keep their point-in-time attribution.
8
+ attributedLinkId?: string;
9
+ attributedClickId?: string;
10
+ linkOpenedAt?: string;
11
+ sessionId?: string;
12
+ // SDK identity for health/version diagnostics (SIT-235)
13
+ sdkName?: string;
14
+ sdkVersion?: string;
15
+ }
@@ -0,0 +1,9 @@
1
+ import type { DeepLinkData } from './deep-link-data';
2
+
3
+ export interface InstallAttributionResponse {
4
+ installId: string;
5
+ attributed: boolean;
6
+ confidenceScore: number;
7
+ matchedFactors: string[];
8
+ deepLinkData: DeepLinkData | null;
9
+ }
@@ -0,0 +1,7 @@
1
+ export interface UTMParameters {
2
+ source?: string;
3
+ medium?: string;
4
+ campaign?: string;
5
+ term?: string;
6
+ content?: string;
7
+ }