@linkforty/mobile-sdk-react-native 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.
- package/CHANGELOG.md +136 -0
- package/CONTRIBUTING.md +340 -0
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/dist/DeepLinkHandler.d.ts +25 -0
- package/dist/DeepLinkHandler.d.ts.map +1 -0
- package/dist/DeepLinkHandler.js +81 -0
- package/dist/FingerprintCollector.d.ts +23 -0
- package/dist/FingerprintCollector.d.ts.map +1 -0
- package/dist/FingerprintCollector.js +79 -0
- package/dist/LinkFortySDK.d.ts +58 -0
- package/dist/LinkFortySDK.d.ts.map +1 -0
- package/dist/LinkFortySDK.js +258 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/examples/AdvancedHooks.tsx +230 -0
- package/examples/BasicSetup.tsx +100 -0
- package/examples/EcommerceTracking.tsx +142 -0
- package/examples/README.md +318 -0
- package/examples/SelfHostedSetup.tsx +126 -0
- package/package.json +68 -0
- package/src/DeepLinkHandler.ts +96 -0
- package/src/FingerprintCollector.ts +90 -0
- package/src/LinkFortySDK.ts +320 -0
- package/src/index.ts +26 -0
- package/src/types.ts +112 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linkforty/mobile-sdk-react-native",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native SDK for LinkForty - Open-source deep linking and mobile attribution platform",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepare": "npm run build",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"linkforty",
|
|
15
|
+
"deep-linking",
|
|
16
|
+
"deferred-deep-linking",
|
|
17
|
+
"mobile-attribution",
|
|
18
|
+
"react-native",
|
|
19
|
+
"universal-links",
|
|
20
|
+
"app-links",
|
|
21
|
+
"app-attribution",
|
|
22
|
+
"branch-alternative",
|
|
23
|
+
"appsflyer-alternative",
|
|
24
|
+
"adjust-alternative",
|
|
25
|
+
"self-hosted",
|
|
26
|
+
"open-source",
|
|
27
|
+
"privacy-focused"
|
|
28
|
+
],
|
|
29
|
+
"author": "LinkForty",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/linkforty/react-native-sdk.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/linkforty/react-native-sdk/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/linkforty/react-native-sdk#readme",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20.0.0",
|
|
41
|
+
"npm": ">=10.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=18.3.0",
|
|
45
|
+
"react-native": ">=0.76.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
49
|
+
"react-native-device-info": "^15.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^19.1.0",
|
|
53
|
+
"typescript": "^5.9.0"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist/**/*",
|
|
57
|
+
"src/**/*",
|
|
58
|
+
"examples/**/*",
|
|
59
|
+
"README.md",
|
|
60
|
+
"CHANGELOG.md",
|
|
61
|
+
"CONTRIBUTING.md",
|
|
62
|
+
"LICENSE"
|
|
63
|
+
],
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public",
|
|
66
|
+
"registry": "https://registry.npmjs.org/"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepLinkHandler - Handles Universal Links (iOS) and App Links (Android)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Linking } from 'react-native';
|
|
6
|
+
import type { DeepLinkData, DeepLinkCallback } from './types';
|
|
7
|
+
|
|
8
|
+
export class DeepLinkHandler {
|
|
9
|
+
private callback: DeepLinkCallback | null = null;
|
|
10
|
+
private baseUrl: string | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize deep link listener
|
|
14
|
+
*/
|
|
15
|
+
initialize(baseUrl: string, callback: DeepLinkCallback): void {
|
|
16
|
+
this.baseUrl = baseUrl;
|
|
17
|
+
this.callback = callback;
|
|
18
|
+
|
|
19
|
+
// Listen for deep links when app is already open
|
|
20
|
+
Linking.addEventListener('url', this.handleDeepLink);
|
|
21
|
+
|
|
22
|
+
// Check if app was opened via deep link
|
|
23
|
+
Linking.getInitialURL().then((url) => {
|
|
24
|
+
if (url) {
|
|
25
|
+
this.handleDeepLink({ url });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Clean up listeners
|
|
32
|
+
*/
|
|
33
|
+
cleanup(): void {
|
|
34
|
+
Linking.removeAllListeners('url');
|
|
35
|
+
this.callback = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handle incoming deep link
|
|
40
|
+
*/
|
|
41
|
+
private handleDeepLink = ({ url }: { url: string }): void => {
|
|
42
|
+
if (!this.callback || !url) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const deepLinkData = this.parseURL(url);
|
|
47
|
+
this.callback(url, deepLinkData);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse deep link URL and extract data
|
|
52
|
+
*/
|
|
53
|
+
parseURL(url: string): DeepLinkData | null {
|
|
54
|
+
try {
|
|
55
|
+
const parsedUrl = new URL(url);
|
|
56
|
+
|
|
57
|
+
// Check if this is a LinkForty URL
|
|
58
|
+
if (this.baseUrl && !url.startsWith(this.baseUrl)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extract short code from path
|
|
63
|
+
const shortCode = parsedUrl.pathname.replace('/', '');
|
|
64
|
+
|
|
65
|
+
if (!shortCode) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract UTM parameters
|
|
70
|
+
const utmParameters = {
|
|
71
|
+
source: parsedUrl.searchParams.get('utm_source') || undefined,
|
|
72
|
+
medium: parsedUrl.searchParams.get('utm_medium') || undefined,
|
|
73
|
+
campaign: parsedUrl.searchParams.get('utm_campaign') || undefined,
|
|
74
|
+
term: parsedUrl.searchParams.get('utm_term') || undefined,
|
|
75
|
+
content: parsedUrl.searchParams.get('utm_content') || undefined,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Extract all other query parameters as custom parameters
|
|
79
|
+
const customParameters: Record<string, string> = {};
|
|
80
|
+
parsedUrl.searchParams.forEach((value, key) => {
|
|
81
|
+
if (!key.startsWith('utm_')) {
|
|
82
|
+
customParameters[key] = value;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
shortCode,
|
|
88
|
+
utmParameters,
|
|
89
|
+
customParameters: Object.keys(customParameters).length > 0 ? customParameters : undefined,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Failed to parse deep link URL:', error);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FingerprintCollector - Collects device fingerprint data for attribution matching
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Platform, Dimensions, NativeModules } from 'react-native';
|
|
6
|
+
import DeviceInfo from 'react-native-device-info';
|
|
7
|
+
import type { DeviceFingerprint } from './types';
|
|
8
|
+
|
|
9
|
+
export class FingerprintCollector {
|
|
10
|
+
/**
|
|
11
|
+
* Collect device fingerprint data
|
|
12
|
+
*/
|
|
13
|
+
static async collect(): Promise<DeviceFingerprint> {
|
|
14
|
+
const { width, height } = Dimensions.get('window');
|
|
15
|
+
const timezone = this.getTimezone();
|
|
16
|
+
const language = this.getLanguage();
|
|
17
|
+
|
|
18
|
+
const fingerprint: DeviceFingerprint = {
|
|
19
|
+
userAgent: await DeviceInfo.getUserAgent(),
|
|
20
|
+
timezone,
|
|
21
|
+
language,
|
|
22
|
+
screenResolution: `${width}x${height}`,
|
|
23
|
+
platform: Platform.OS,
|
|
24
|
+
deviceModel: await DeviceInfo.getModel(),
|
|
25
|
+
osVersion: await DeviceInfo.getSystemVersion(),
|
|
26
|
+
appVersion: await DeviceInfo.getVersion(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return fingerprint;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate query parameters from fingerprint for URL
|
|
34
|
+
*/
|
|
35
|
+
static async generateQueryParams(): Promise<string> {
|
|
36
|
+
const fingerprint = await this.collect();
|
|
37
|
+
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
ua: fingerprint.userAgent,
|
|
40
|
+
tz: fingerprint.timezone,
|
|
41
|
+
lang: fingerprint.language,
|
|
42
|
+
screen: fingerprint.screenResolution,
|
|
43
|
+
platform: fingerprint.platform,
|
|
44
|
+
...(fingerprint.deviceModel && { model: fingerprint.deviceModel }),
|
|
45
|
+
...(fingerprint.osVersion && { os: fingerprint.osVersion }),
|
|
46
|
+
...(fingerprint.appVersion && { app_version: fingerprint.appVersion }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return params.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get device timezone
|
|
54
|
+
*/
|
|
55
|
+
private static getTimezone(): string {
|
|
56
|
+
try {
|
|
57
|
+
// Get timezone using Intl API
|
|
58
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return 'UTC';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get device language
|
|
66
|
+
*/
|
|
67
|
+
private static getLanguage(): string {
|
|
68
|
+
try {
|
|
69
|
+
// Try to get locale from NativeModules
|
|
70
|
+
const locale = Platform.select({
|
|
71
|
+
ios: NativeModules.SettingsManager?.settings?.AppleLocale ||
|
|
72
|
+
NativeModules.SettingsManager?.settings?.AppleLanguages?.[0],
|
|
73
|
+
android: NativeModules.I18nManager?.localeIdentifier,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (locale) {
|
|
77
|
+
return locale;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback to navigator if available
|
|
81
|
+
if (typeof navigator !== 'undefined' && navigator.language) {
|
|
82
|
+
return navigator.language;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return 'en-US';
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return 'en-US';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkFortySDK - Main SDK class for LinkForty deep linking and attribution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
6
|
+
import { FingerprintCollector } from './FingerprintCollector';
|
|
7
|
+
import { DeepLinkHandler } from './DeepLinkHandler';
|
|
8
|
+
import type {
|
|
9
|
+
LinkFortyConfig,
|
|
10
|
+
InstallAttributionResponse,
|
|
11
|
+
DeepLinkData,
|
|
12
|
+
DeferredDeepLinkCallback,
|
|
13
|
+
DeepLinkCallback,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
const STORAGE_KEYS = {
|
|
17
|
+
INSTALL_ID: '@linkforty:install_id',
|
|
18
|
+
INSTALL_DATA: '@linkforty:install_data',
|
|
19
|
+
FIRST_LAUNCH: '@linkforty:first_launch',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class LinkFortySDK {
|
|
23
|
+
private config: LinkFortyConfig | null = null;
|
|
24
|
+
private deepLinkHandler: DeepLinkHandler | null = null;
|
|
25
|
+
private deferredDeepLinkCallback: DeferredDeepLinkCallback | null = null;
|
|
26
|
+
private installId: string | null = null;
|
|
27
|
+
private initialized: boolean = false;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the SDK
|
|
31
|
+
*/
|
|
32
|
+
async init(config: LinkFortyConfig): Promise<void> {
|
|
33
|
+
if (this.initialized) {
|
|
34
|
+
console.warn('LinkForty SDK already initialized');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.config = config;
|
|
39
|
+
|
|
40
|
+
if (config.debug) {
|
|
41
|
+
console.log('[LinkForty] Initializing SDK with config:', config);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if this is first launch
|
|
45
|
+
const isFirstLaunch = await this.isFirstLaunch();
|
|
46
|
+
|
|
47
|
+
if (isFirstLaunch) {
|
|
48
|
+
// Report install and get deferred deep link
|
|
49
|
+
await this.reportInstall();
|
|
50
|
+
} else {
|
|
51
|
+
// Load cached install data
|
|
52
|
+
await this.loadInstallData();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Initialize deep link handler for direct deep links
|
|
56
|
+
this.deepLinkHandler = new DeepLinkHandler();
|
|
57
|
+
|
|
58
|
+
this.initialized = true;
|
|
59
|
+
|
|
60
|
+
if (config.debug) {
|
|
61
|
+
console.log('[LinkForty] SDK initialized successfully');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get install attribution data (deferred deep link)
|
|
67
|
+
*/
|
|
68
|
+
async getInstallData(): Promise<DeepLinkData | null> {
|
|
69
|
+
const cached = await AsyncStorage.getItem(STORAGE_KEYS.INSTALL_DATA);
|
|
70
|
+
if (cached) {
|
|
71
|
+
return JSON.parse(cached);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Register callback for deferred deep links (new installs)
|
|
78
|
+
*/
|
|
79
|
+
onDeferredDeepLink(callback: DeferredDeepLinkCallback): void {
|
|
80
|
+
this.deferredDeepLinkCallback = callback;
|
|
81
|
+
|
|
82
|
+
// If we already have install data, call the callback immediately
|
|
83
|
+
this.getInstallData().then((data) => {
|
|
84
|
+
if (data && this.deferredDeepLinkCallback) {
|
|
85
|
+
this.deferredDeepLinkCallback(data);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Register callback for direct deep links (existing users)
|
|
92
|
+
*/
|
|
93
|
+
onDeepLink(callback: DeepLinkCallback): void {
|
|
94
|
+
if (!this.config) {
|
|
95
|
+
throw new Error('SDK not initialized. Call init() first.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!this.deepLinkHandler) {
|
|
99
|
+
this.deepLinkHandler = new DeepLinkHandler();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.deepLinkHandler.initialize(this.config.baseUrl, callback);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Track in-app event
|
|
107
|
+
*/
|
|
108
|
+
async trackEvent(name: string, properties?: Record<string, any>): Promise<void> {
|
|
109
|
+
if (!this.config) {
|
|
110
|
+
throw new Error('SDK not initialized. Call init() first.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!this.installId) {
|
|
114
|
+
console.warn('[LinkForty] Cannot track event: No install ID available');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Backend expects: { installId, eventName, eventData, timestamp }
|
|
119
|
+
const requestBody = {
|
|
120
|
+
installId: this.installId,
|
|
121
|
+
eventName: name,
|
|
122
|
+
eventData: properties || {},
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const response = await this.apiRequest('/api/sdk/v1/event', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
body: JSON.stringify(requestBody),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (this.config.debug) {
|
|
133
|
+
console.log('[LinkForty] Event tracked:', name, response);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('[LinkForty] Failed to track event:', error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get install ID
|
|
142
|
+
*/
|
|
143
|
+
async getInstallId(): Promise<string | null> {
|
|
144
|
+
if (this.installId) {
|
|
145
|
+
return this.installId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cached = await AsyncStorage.getItem(STORAGE_KEYS.INSTALL_ID);
|
|
149
|
+
if (cached) {
|
|
150
|
+
this.installId = cached;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this.installId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Clear all cached data (for testing)
|
|
158
|
+
*/
|
|
159
|
+
async clearData(): Promise<void> {
|
|
160
|
+
await AsyncStorage.multiRemove([
|
|
161
|
+
STORAGE_KEYS.INSTALL_ID,
|
|
162
|
+
STORAGE_KEYS.INSTALL_DATA,
|
|
163
|
+
STORAGE_KEYS.FIRST_LAUNCH,
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
this.installId = null;
|
|
167
|
+
|
|
168
|
+
if (this.config?.debug) {
|
|
169
|
+
console.log('[LinkForty] All data cleared');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if this is the first app launch
|
|
175
|
+
*/
|
|
176
|
+
private async isFirstLaunch(): Promise<boolean> {
|
|
177
|
+
const firstLaunchFlag = await AsyncStorage.getItem(STORAGE_KEYS.FIRST_LAUNCH);
|
|
178
|
+
|
|
179
|
+
if (!firstLaunchFlag) {
|
|
180
|
+
await AsyncStorage.setItem(STORAGE_KEYS.FIRST_LAUNCH, 'true');
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Report app installation to LinkForty backend
|
|
189
|
+
*/
|
|
190
|
+
private async reportInstall(): Promise<void> {
|
|
191
|
+
if (!this.config) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
// Collect device fingerprint
|
|
197
|
+
const fingerprint = await FingerprintCollector.collect();
|
|
198
|
+
|
|
199
|
+
if (this.config.debug) {
|
|
200
|
+
console.log('[LinkForty] Reporting install with fingerprint:', fingerprint);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Parse screen resolution (e.g., "1080x1920" -> [1080, 1920])
|
|
204
|
+
const [screenWidth, screenHeight] = fingerprint.screenResolution
|
|
205
|
+
.split('x')
|
|
206
|
+
.map(Number);
|
|
207
|
+
|
|
208
|
+
// Convert attribution window from days to hours
|
|
209
|
+
const attributionWindowHours = (this.config.attributionWindow || 7) * 24;
|
|
210
|
+
|
|
211
|
+
// Call install endpoint with flattened structure matching backend contract
|
|
212
|
+
const response = await this.apiRequest<InstallAttributionResponse>(
|
|
213
|
+
'/api/sdk/v1/install',
|
|
214
|
+
{
|
|
215
|
+
method: 'POST',
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
userAgent: fingerprint.userAgent,
|
|
218
|
+
timezone: fingerprint.timezone,
|
|
219
|
+
language: fingerprint.language,
|
|
220
|
+
screenWidth,
|
|
221
|
+
screenHeight,
|
|
222
|
+
platform: fingerprint.platform,
|
|
223
|
+
platformVersion: fingerprint.osVersion,
|
|
224
|
+
deviceId: undefined, // Optional: Can add IDFA/GAID if available
|
|
225
|
+
attributionWindowHours,
|
|
226
|
+
}),
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (this.config.debug) {
|
|
231
|
+
console.log('[LinkForty] Install response:', response);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Store install ID
|
|
235
|
+
if (response.installId) {
|
|
236
|
+
this.installId = response.installId;
|
|
237
|
+
await AsyncStorage.setItem(STORAGE_KEYS.INSTALL_ID, response.installId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If attributed, store deep link data
|
|
241
|
+
if (response.attributed && response.deepLinkData) {
|
|
242
|
+
await AsyncStorage.setItem(
|
|
243
|
+
STORAGE_KEYS.INSTALL_DATA,
|
|
244
|
+
JSON.stringify(response.deepLinkData)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Call deferred deep link callback if registered
|
|
248
|
+
if (this.deferredDeepLinkCallback) {
|
|
249
|
+
this.deferredDeepLinkCallback(response.deepLinkData as DeepLinkData);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this.config.debug) {
|
|
253
|
+
console.log('[LinkForty] Install attributed with confidence:', response.confidenceScore);
|
|
254
|
+
console.log('[LinkForty] Matched factors:', response.matchedFactors);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
if (this.config.debug) {
|
|
258
|
+
console.log('[LinkForty] Install not attributed (organic install)');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Call callback with null to indicate organic install
|
|
262
|
+
if (this.deferredDeepLinkCallback) {
|
|
263
|
+
this.deferredDeepLinkCallback(null);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('[LinkForty] Failed to report install:', error);
|
|
268
|
+
|
|
269
|
+
// Call callback with null on error
|
|
270
|
+
if (this.deferredDeepLinkCallback) {
|
|
271
|
+
this.deferredDeepLinkCallback(null);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Load cached install data
|
|
278
|
+
*/
|
|
279
|
+
private async loadInstallData(): Promise<void> {
|
|
280
|
+
this.installId = await AsyncStorage.getItem(STORAGE_KEYS.INSTALL_ID);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Make API request to LinkForty backend
|
|
285
|
+
*/
|
|
286
|
+
private async apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
287
|
+
if (!this.config) {
|
|
288
|
+
throw new Error('SDK not initialized');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const url = `${this.config.baseUrl}${endpoint}`;
|
|
292
|
+
|
|
293
|
+
const headers: Record<string, string> = {
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Add API key if provided
|
|
298
|
+
if (this.config.apiKey) {
|
|
299
|
+
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const response = await fetch(url, {
|
|
303
|
+
...options,
|
|
304
|
+
headers: {
|
|
305
|
+
...headers,
|
|
306
|
+
...options.headers,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const error = await response.json().catch(() => ({ message: 'Network error' }));
|
|
312
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return response.json();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Export singleton instance
|
|
320
|
+
export default new LinkFortySDK();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkForty React Native SDK
|
|
3
|
+
*
|
|
4
|
+
* Deep linking and mobile attribution for React Native apps
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Export singleton SDK instance as default
|
|
10
|
+
export { default } from './LinkFortySDK';
|
|
11
|
+
|
|
12
|
+
// Export classes
|
|
13
|
+
export { LinkFortySDK } from './LinkFortySDK';
|
|
14
|
+
export { FingerprintCollector } from './FingerprintCollector';
|
|
15
|
+
export { DeepLinkHandler } from './DeepLinkHandler';
|
|
16
|
+
|
|
17
|
+
// Export types
|
|
18
|
+
export type {
|
|
19
|
+
LinkFortyConfig,
|
|
20
|
+
DeviceFingerprint,
|
|
21
|
+
DeepLinkData,
|
|
22
|
+
InstallAttributionResponse,
|
|
23
|
+
EventData,
|
|
24
|
+
DeferredDeepLinkCallback,
|
|
25
|
+
DeepLinkCallback,
|
|
26
|
+
} from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkForty React Native SDK Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for initializing the LinkForty SDK
|
|
7
|
+
*/
|
|
8
|
+
export interface LinkFortyConfig {
|
|
9
|
+
/** Base URL of your LinkForty instance (e.g., 'https://go.yourdomain.com') */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/** Optional API key for Cloud authentication */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
/** Enable debug logging */
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
/** Custom attribution window in days (default: 7) */
|
|
16
|
+
attributionWindow?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Device fingerprint data collected for attribution matching
|
|
21
|
+
*/
|
|
22
|
+
export interface DeviceFingerprint {
|
|
23
|
+
/** IP address (server-side) */
|
|
24
|
+
ip?: string;
|
|
25
|
+
/** User agent string */
|
|
26
|
+
userAgent: string;
|
|
27
|
+
/** Device timezone */
|
|
28
|
+
timezone: string;
|
|
29
|
+
/** Device language */
|
|
30
|
+
language: string;
|
|
31
|
+
/** Screen resolution (width x height) */
|
|
32
|
+
screenResolution: string;
|
|
33
|
+
/** Device platform (ios/android) */
|
|
34
|
+
platform: string;
|
|
35
|
+
/** Device model */
|
|
36
|
+
deviceModel?: string;
|
|
37
|
+
/** OS version */
|
|
38
|
+
osVersion?: string;
|
|
39
|
+
/** App version */
|
|
40
|
+
appVersion?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Deep link data containing URLs and parameters
|
|
45
|
+
*/
|
|
46
|
+
export interface DeepLinkData {
|
|
47
|
+
/** Short code that was clicked */
|
|
48
|
+
shortCode: string;
|
|
49
|
+
/** iOS app URL */
|
|
50
|
+
iosUrl?: string;
|
|
51
|
+
/** Android app URL */
|
|
52
|
+
androidUrl?: string;
|
|
53
|
+
/** Web fallback URL */
|
|
54
|
+
webUrl?: string;
|
|
55
|
+
/** UTM parameters */
|
|
56
|
+
utmParameters?: {
|
|
57
|
+
source?: string;
|
|
58
|
+
medium?: string;
|
|
59
|
+
campaign?: string;
|
|
60
|
+
term?: string;
|
|
61
|
+
content?: string;
|
|
62
|
+
};
|
|
63
|
+
/** Custom query parameters */
|
|
64
|
+
customParameters?: Record<string, string>;
|
|
65
|
+
/** Click timestamp */
|
|
66
|
+
clickedAt?: string;
|
|
67
|
+
/** Link ID */
|
|
68
|
+
linkId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Response from install attribution endpoint
|
|
73
|
+
* Matches backend contract from /api/sdk/v1/install
|
|
74
|
+
*/
|
|
75
|
+
export interface InstallAttributionResponse {
|
|
76
|
+
/** Install event UUID */
|
|
77
|
+
installId: string;
|
|
78
|
+
/** Whether attribution was successful */
|
|
79
|
+
attributed: boolean;
|
|
80
|
+
/** Attribution confidence score (0-100) */
|
|
81
|
+
confidenceScore: number;
|
|
82
|
+
/** Array of matched fingerprint factors (e.g., ['ip', 'user_agent', 'timezone', 'language', 'screen']) */
|
|
83
|
+
matchedFactors: string[];
|
|
84
|
+
/** Deep link data if attributed (contains shortCode, URLs, UTM params, confidence, etc.) */
|
|
85
|
+
deepLinkData: DeepLinkData | Record<string, never>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Event data for tracking in-app events
|
|
90
|
+
*/
|
|
91
|
+
export interface EventData {
|
|
92
|
+
/** Event name (e.g., 'purchase', 'signup', 'add_to_cart') */
|
|
93
|
+
name: string;
|
|
94
|
+
/** Event properties */
|
|
95
|
+
properties?: Record<string, any>;
|
|
96
|
+
/** Revenue amount (for conversion events) */
|
|
97
|
+
revenue?: number;
|
|
98
|
+
/** Currency code (e.g., 'USD') */
|
|
99
|
+
currency?: string;
|
|
100
|
+
/** Install ID for attribution */
|
|
101
|
+
installId?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Callback function for deferred deep links
|
|
106
|
+
*/
|
|
107
|
+
export type DeferredDeepLinkCallback = (deepLinkData: DeepLinkData | null) => void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Callback function for direct deep links
|
|
111
|
+
*/
|
|
112
|
+
export type DeepLinkCallback = (url: string, deepLinkData: DeepLinkData | null) => void;
|