@linkforty/mobile-sdk-react-native 1.1.2 → 1.1.3
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 +5 -0
- package/dist/DeepLinkHandler.d.ts +10 -0
- package/dist/DeepLinkHandler.d.ts.map +1 -1
- package/dist/DeepLinkHandler.js +86 -19
- package/dist/FingerprintCollector.d.ts.map +1 -1
- package/dist/FingerprintCollector.js +14 -7
- package/dist/LinkFortySDK.js +1 -1
- package/package.json +2 -1
- package/src/DeepLinkHandler.ts +95 -20
- package/src/FingerprintCollector.ts +11 -7
- package/src/LinkFortySDK.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.3] - 2026-02-12
|
|
11
|
+
### Fixed
|
|
12
|
+
- Replaced `new URL()` and `URLSearchParams` usage with manual string parsing — `URL.pathname` is not implemented in React Native's Hermes engine, which caused `parseURL()` to crash silently and skip server-side deep link resolution entirely
|
|
13
|
+
- Rounded screen dimensions to integers in `FingerprintCollector` — Android's `Dimensions.get('window')` returns floats (e.g. `434.717`) which caused PostgreSQL INSERT errors on INTEGER columns in the install endpoint
|
|
14
|
+
|
|
10
15
|
## [1.1.2] - 2026-02-12
|
|
11
16
|
### Added
|
|
12
17
|
- `createLink()` method for creating short links programmatically from mobile apps via the LinkForty API
|
|
@@ -12,6 +12,16 @@ export declare class DeepLinkHandler {
|
|
|
12
12
|
private callback;
|
|
13
13
|
private baseUrl;
|
|
14
14
|
private resolveFn;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a URL string manually.
|
|
17
|
+
* The URL/URLSearchParams APIs are not fully implemented in React Native's Hermes engine
|
|
18
|
+
* (URL.pathname throws "not implemented"), so we parse with basic string operations.
|
|
19
|
+
*/
|
|
20
|
+
private static parseUrlString;
|
|
21
|
+
/**
|
|
22
|
+
* Build a query string from key-value pairs without URLSearchParams.
|
|
23
|
+
*/
|
|
24
|
+
private static buildQueryString;
|
|
15
25
|
/**
|
|
16
26
|
* Initialize deep link listener
|
|
17
27
|
* @param baseUrl - LinkForty instance base URL for detecting LinkForty URLs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeepLinkHandler.d.ts","sourceRoot":"","sources":["../src/DeepLinkHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/E,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,SAAS,CAAgC;IAEjD;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,SAAS,CAAC,EAAE,eAAe,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"DeepLinkHandler.d.ts","sourceRoot":"","sources":["../src/DeepLinkHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/E,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,SAAS,CAAgC;IAEjD;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAsC7B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAM/B;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,SAAS,CAAC,EAAE,eAAe,GAAG,IAAI;IAqB1F;;OAEG;IACH,OAAO,IAAI,IAAI;IAKf;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAiCpB;IAEF;;;;OAIG;YACW,UAAU;IAgDxB;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;CA+C3C"}
|
package/dist/DeepLinkHandler.js
CHANGED
|
@@ -13,6 +13,51 @@ export class DeepLinkHandler {
|
|
|
13
13
|
callback = null;
|
|
14
14
|
baseUrl = null;
|
|
15
15
|
resolveFn = null;
|
|
16
|
+
/**
|
|
17
|
+
* Parse a URL string manually.
|
|
18
|
+
* The URL/URLSearchParams APIs are not fully implemented in React Native's Hermes engine
|
|
19
|
+
* (URL.pathname throws "not implemented"), so we parse with basic string operations.
|
|
20
|
+
*/
|
|
21
|
+
static parseUrlString(url) {
|
|
22
|
+
try {
|
|
23
|
+
const protocolEnd = url.indexOf('://');
|
|
24
|
+
if (protocolEnd === -1)
|
|
25
|
+
return null;
|
|
26
|
+
const afterProtocol = url.substring(protocolEnd + 3);
|
|
27
|
+
const pathStart = afterProtocol.indexOf('/');
|
|
28
|
+
const pathAndQuery = pathStart === -1 ? '/' : afterProtocol.substring(pathStart);
|
|
29
|
+
const hashIndex = pathAndQuery.indexOf('#');
|
|
30
|
+
const withoutHash = hashIndex === -1 ? pathAndQuery : pathAndQuery.substring(0, hashIndex);
|
|
31
|
+
const queryStart = withoutHash.indexOf('?');
|
|
32
|
+
const pathname = queryStart === -1 ? withoutHash : withoutHash.substring(0, queryStart);
|
|
33
|
+
const queryString = queryStart === -1 ? '' : withoutHash.substring(queryStart + 1);
|
|
34
|
+
const searchParams = new Map();
|
|
35
|
+
if (queryString) {
|
|
36
|
+
for (const pair of queryString.split('&')) {
|
|
37
|
+
const eqIndex = pair.indexOf('=');
|
|
38
|
+
if (eqIndex === -1) {
|
|
39
|
+
searchParams.set(decodeURIComponent(pair), '');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
searchParams.set(decodeURIComponent(pair.substring(0, eqIndex)), decodeURIComponent(pair.substring(eqIndex + 1)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { pathname, searchParams };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('[LinkForty] Failed to parse URL string:', error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build a query string from key-value pairs without URLSearchParams.
|
|
55
|
+
*/
|
|
56
|
+
static buildQueryString(params) {
|
|
57
|
+
return Object.entries(params)
|
|
58
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
59
|
+
.join('&');
|
|
60
|
+
}
|
|
16
61
|
/**
|
|
17
62
|
* Initialize deep link listener
|
|
18
63
|
* @param baseUrl - LinkForty instance base URL for detecting LinkForty URLs
|
|
@@ -23,13 +68,18 @@ export class DeepLinkHandler {
|
|
|
23
68
|
this.baseUrl = baseUrl;
|
|
24
69
|
this.callback = callback;
|
|
25
70
|
this.resolveFn = resolveFn || null;
|
|
71
|
+
console.log('[LinkForty] DeepLinkHandler.initialize: baseUrl=', baseUrl, 'hasResolveFn=', !!resolveFn);
|
|
26
72
|
// Listen for deep links when app is already open
|
|
27
73
|
Linking.addEventListener('url', this.handleDeepLink);
|
|
28
74
|
// Check if app was opened via deep link
|
|
29
75
|
Linking.getInitialURL().then((url) => {
|
|
76
|
+
console.log('[LinkForty] getInitialURL result:', url);
|
|
30
77
|
if (url) {
|
|
31
78
|
this.handleDeepLink({ url });
|
|
32
79
|
}
|
|
80
|
+
else {
|
|
81
|
+
console.warn('[LinkForty] getInitialURL returned null — app may not have been opened via deep link, or Android consumed the Intent');
|
|
82
|
+
}
|
|
33
83
|
});
|
|
34
84
|
}
|
|
35
85
|
/**
|
|
@@ -45,25 +95,35 @@ export class DeepLinkHandler {
|
|
|
45
95
|
* Falls back to local URL parsing on failure.
|
|
46
96
|
*/
|
|
47
97
|
handleDeepLink = async ({ url }) => {
|
|
98
|
+
console.log('[LinkForty] handleDeepLink called with url:', url);
|
|
48
99
|
if (!this.callback || !url) {
|
|
100
|
+
console.warn('[LinkForty] handleDeepLink early return: callback=', !!this.callback, 'url=', url);
|
|
49
101
|
return;
|
|
50
102
|
}
|
|
51
103
|
// Parse locally first (for fallback and to detect LinkForty URLs)
|
|
52
104
|
const localData = this.parseURL(url);
|
|
105
|
+
console.log('[LinkForty] localData parsed:', JSON.stringify(localData));
|
|
53
106
|
// If this is a LinkForty URL and we have a resolver, try the server
|
|
54
107
|
if (localData && this.resolveFn && this.baseUrl && url.startsWith(this.baseUrl)) {
|
|
55
108
|
try {
|
|
109
|
+
console.log('[LinkForty] Resolving URL via server...');
|
|
56
110
|
const resolvedData = await this.resolveURL(url);
|
|
111
|
+
console.log('[LinkForty] Resolve result:', JSON.stringify(resolvedData));
|
|
57
112
|
if (resolvedData) {
|
|
58
113
|
this.callback(url, resolvedData);
|
|
59
114
|
return;
|
|
60
115
|
}
|
|
116
|
+
console.warn('[LinkForty] Resolve returned null, falling back to local parse');
|
|
61
117
|
}
|
|
62
118
|
catch (error) {
|
|
63
119
|
console.warn('[LinkForty] Failed to resolve URL from server, falling back to local parse:', error);
|
|
64
120
|
}
|
|
65
121
|
}
|
|
66
|
-
|
|
122
|
+
else {
|
|
123
|
+
console.warn('[LinkForty] Skipping server resolve: localData=', !!localData, 'resolveFn=', !!this.resolveFn, 'baseUrl=', this.baseUrl, 'urlMatch=', url.startsWith(this.baseUrl || ''));
|
|
124
|
+
}
|
|
125
|
+
// Fallback to locally-parsed data (no customParameters from server)
|
|
126
|
+
console.warn('[LinkForty] Using local fallback — customParameters will be empty for App Link URLs');
|
|
67
127
|
this.callback(url, localData);
|
|
68
128
|
};
|
|
69
129
|
/**
|
|
@@ -75,8 +135,11 @@ export class DeepLinkHandler {
|
|
|
75
135
|
if (!this.resolveFn) {
|
|
76
136
|
return null;
|
|
77
137
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
138
|
+
const parsed = DeepLinkHandler.parseUrlString(url);
|
|
139
|
+
if (!parsed) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const pathSegments = parsed.pathname.split('/').filter(Boolean);
|
|
80
143
|
if (pathSegments.length === 0) {
|
|
81
144
|
return null;
|
|
82
145
|
}
|
|
@@ -92,16 +155,17 @@ export class DeepLinkHandler {
|
|
|
92
155
|
try {
|
|
93
156
|
const fingerprint = await FingerprintCollector.collect();
|
|
94
157
|
const [sw, sh] = fingerprint.screenResolution.split('x');
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
158
|
+
const fpParams = {
|
|
159
|
+
fp_tz: fingerprint.timezone,
|
|
160
|
+
fp_lang: fingerprint.language,
|
|
161
|
+
fp_sw: sw,
|
|
162
|
+
fp_sh: sh,
|
|
163
|
+
fp_platform: fingerprint.platform,
|
|
164
|
+
};
|
|
101
165
|
if (fingerprint.osVersion) {
|
|
102
|
-
|
|
166
|
+
fpParams.fp_pv = fingerprint.osVersion;
|
|
103
167
|
}
|
|
104
|
-
resolvePath += `?${
|
|
168
|
+
resolvePath += `?${DeepLinkHandler.buildQueryString(fpParams)}`;
|
|
105
169
|
}
|
|
106
170
|
catch (error) {
|
|
107
171
|
// If fingerprint collection fails, still resolve without it
|
|
@@ -115,28 +179,31 @@ export class DeepLinkHandler {
|
|
|
115
179
|
*/
|
|
116
180
|
parseURL(url) {
|
|
117
181
|
try {
|
|
118
|
-
const parsedUrl = new URL(url);
|
|
119
182
|
// Check if this is a LinkForty URL
|
|
120
183
|
if (this.baseUrl && !url.startsWith(this.baseUrl)) {
|
|
121
184
|
return null;
|
|
122
185
|
}
|
|
186
|
+
const parsed = DeepLinkHandler.parseUrlString(url);
|
|
187
|
+
if (!parsed) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
123
190
|
// Extract short code from path (last segment handles both /shortCode and /templateSlug/shortCode)
|
|
124
|
-
const pathSegments =
|
|
191
|
+
const pathSegments = parsed.pathname.split('/').filter(Boolean);
|
|
125
192
|
const shortCode = pathSegments[pathSegments.length - 1];
|
|
126
193
|
if (!shortCode) {
|
|
127
194
|
return null;
|
|
128
195
|
}
|
|
129
196
|
// Extract UTM parameters
|
|
130
197
|
const utmParameters = {
|
|
131
|
-
source:
|
|
132
|
-
medium:
|
|
133
|
-
campaign:
|
|
134
|
-
term:
|
|
135
|
-
content:
|
|
198
|
+
source: parsed.searchParams.get('utm_source') || undefined,
|
|
199
|
+
medium: parsed.searchParams.get('utm_medium') || undefined,
|
|
200
|
+
campaign: parsed.searchParams.get('utm_campaign') || undefined,
|
|
201
|
+
term: parsed.searchParams.get('utm_term') || undefined,
|
|
202
|
+
content: parsed.searchParams.get('utm_content') || undefined,
|
|
136
203
|
};
|
|
137
204
|
// Extract all other query parameters as custom parameters
|
|
138
205
|
const customParameters = {};
|
|
139
|
-
|
|
206
|
+
parsed.searchParams.forEach((value, key) => {
|
|
140
207
|
if (!key.startsWith('utm_')) {
|
|
141
208
|
customParameters[key] = value;
|
|
142
209
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FingerprintCollector.d.ts","sourceRoot":"","sources":["../src/FingerprintCollector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,qBAAa,oBAAoB;IAC/B;;OAEG;WACU,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"FingerprintCollector.d.ts","sourceRoot":"","sources":["../src/FingerprintCollector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,qBAAa,oBAAoB;IAC/B;;OAEG;WACU,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAoBlD;;OAEG;WACU,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC;IAoBnD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAS1B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;CAuB3B"}
|
|
@@ -11,11 +11,12 @@ export class FingerprintCollector {
|
|
|
11
11
|
const { width, height } = Dimensions.get('window');
|
|
12
12
|
const timezone = this.getTimezone();
|
|
13
13
|
const language = this.getLanguage();
|
|
14
|
+
// Round dimensions — Android returns floats (e.g. 434.717) but the server expects integers
|
|
14
15
|
const fingerprint = {
|
|
15
16
|
userAgent: await DeviceInfo.getUserAgent(),
|
|
16
17
|
timezone,
|
|
17
18
|
language,
|
|
18
|
-
screenResolution: `${width}x${height}`,
|
|
19
|
+
screenResolution: `${Math.round(width)}x${Math.round(height)}`,
|
|
19
20
|
platform: Platform.OS,
|
|
20
21
|
deviceModel: await DeviceInfo.getModel(),
|
|
21
22
|
osVersion: await DeviceInfo.getSystemVersion(),
|
|
@@ -28,17 +29,23 @@ export class FingerprintCollector {
|
|
|
28
29
|
*/
|
|
29
30
|
static async generateQueryParams() {
|
|
30
31
|
const fingerprint = await this.collect();
|
|
31
|
-
|
|
32
|
+
// Build query string manually — URLSearchParams is not reliable in Hermes
|
|
33
|
+
const params = {
|
|
32
34
|
ua: fingerprint.userAgent,
|
|
33
35
|
tz: fingerprint.timezone,
|
|
34
36
|
lang: fingerprint.language,
|
|
35
37
|
screen: fingerprint.screenResolution,
|
|
36
38
|
platform: fingerprint.platform,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
};
|
|
40
|
+
if (fingerprint.deviceModel)
|
|
41
|
+
params.model = fingerprint.deviceModel;
|
|
42
|
+
if (fingerprint.osVersion)
|
|
43
|
+
params.os = fingerprint.osVersion;
|
|
44
|
+
if (fingerprint.appVersion)
|
|
45
|
+
params.app_version = fingerprint.appVersion;
|
|
46
|
+
return Object.entries(params)
|
|
47
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
48
|
+
.join('&');
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* Get device timezone
|
package/dist/LinkFortySDK.js
CHANGED
|
@@ -255,7 +255,7 @@ export class LinkFortySDK {
|
|
|
255
255
|
// Parse screen resolution (e.g., "1080x1920" -> [1080, 1920])
|
|
256
256
|
const [screenWidth, screenHeight] = fingerprint.screenResolution
|
|
257
257
|
.split('x')
|
|
258
|
-
.map(Number);
|
|
258
|
+
.map((v) => Math.round(Number(v)));
|
|
259
259
|
// Convert attribution window from days to hours
|
|
260
260
|
const attributionWindowHours = (this.config.attributionWindow || 7) * 24;
|
|
261
261
|
// Call install endpoint with flattened structure matching backend contract
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkforty/mobile-sdk-react-native",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "React Native SDK for LinkForty - Open-source deep linking and mobile attribution platform",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"react-native": ">=0.64.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@linkforty/mobile-sdk-react-native": "file:linkforty-mobile-sdk-react-native-1.1.2.tgz",
|
|
48
49
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
49
50
|
"react-native-device-info": "^15.0.0"
|
|
50
51
|
},
|
package/src/DeepLinkHandler.ts
CHANGED
|
@@ -17,6 +17,58 @@ export class DeepLinkHandler {
|
|
|
17
17
|
private baseUrl: string | null = null;
|
|
18
18
|
private resolveFn: ResolveFunction | null = null;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse a URL string manually.
|
|
22
|
+
* The URL/URLSearchParams APIs are not fully implemented in React Native's Hermes engine
|
|
23
|
+
* (URL.pathname throws "not implemented"), so we parse with basic string operations.
|
|
24
|
+
*/
|
|
25
|
+
private static parseUrlString(url: string): { pathname: string; searchParams: Map<string, string> } | null {
|
|
26
|
+
try {
|
|
27
|
+
const protocolEnd = url.indexOf('://');
|
|
28
|
+
if (protocolEnd === -1) return null;
|
|
29
|
+
|
|
30
|
+
const afterProtocol = url.substring(protocolEnd + 3);
|
|
31
|
+
const pathStart = afterProtocol.indexOf('/');
|
|
32
|
+
const pathAndQuery = pathStart === -1 ? '/' : afterProtocol.substring(pathStart);
|
|
33
|
+
|
|
34
|
+
const hashIndex = pathAndQuery.indexOf('#');
|
|
35
|
+
const withoutHash = hashIndex === -1 ? pathAndQuery : pathAndQuery.substring(0, hashIndex);
|
|
36
|
+
|
|
37
|
+
const queryStart = withoutHash.indexOf('?');
|
|
38
|
+
const pathname = queryStart === -1 ? withoutHash : withoutHash.substring(0, queryStart);
|
|
39
|
+
const queryString = queryStart === -1 ? '' : withoutHash.substring(queryStart + 1);
|
|
40
|
+
|
|
41
|
+
const searchParams = new Map<string, string>();
|
|
42
|
+
if (queryString) {
|
|
43
|
+
for (const pair of queryString.split('&')) {
|
|
44
|
+
const eqIndex = pair.indexOf('=');
|
|
45
|
+
if (eqIndex === -1) {
|
|
46
|
+
searchParams.set(decodeURIComponent(pair), '');
|
|
47
|
+
} else {
|
|
48
|
+
searchParams.set(
|
|
49
|
+
decodeURIComponent(pair.substring(0, eqIndex)),
|
|
50
|
+
decodeURIComponent(pair.substring(eqIndex + 1)),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { pathname, searchParams };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('[LinkForty] Failed to parse URL string:', error);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build a query string from key-value pairs without URLSearchParams.
|
|
65
|
+
*/
|
|
66
|
+
private static buildQueryString(params: Record<string, string>): string {
|
|
67
|
+
return Object.entries(params)
|
|
68
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
69
|
+
.join('&');
|
|
70
|
+
}
|
|
71
|
+
|
|
20
72
|
/**
|
|
21
73
|
* Initialize deep link listener
|
|
22
74
|
* @param baseUrl - LinkForty instance base URL for detecting LinkForty URLs
|
|
@@ -28,13 +80,18 @@ export class DeepLinkHandler {
|
|
|
28
80
|
this.callback = callback;
|
|
29
81
|
this.resolveFn = resolveFn || null;
|
|
30
82
|
|
|
83
|
+
console.log('[LinkForty] DeepLinkHandler.initialize: baseUrl=', baseUrl, 'hasResolveFn=', !!resolveFn);
|
|
84
|
+
|
|
31
85
|
// Listen for deep links when app is already open
|
|
32
86
|
Linking.addEventListener('url', this.handleDeepLink);
|
|
33
87
|
|
|
34
88
|
// Check if app was opened via deep link
|
|
35
89
|
Linking.getInitialURL().then((url) => {
|
|
90
|
+
console.log('[LinkForty] getInitialURL result:', url);
|
|
36
91
|
if (url) {
|
|
37
92
|
this.handleDeepLink({ url });
|
|
93
|
+
} else {
|
|
94
|
+
console.warn('[LinkForty] getInitialURL returned null — app may not have been opened via deep link, or Android consumed the Intent');
|
|
38
95
|
}
|
|
39
96
|
});
|
|
40
97
|
}
|
|
@@ -53,27 +110,37 @@ export class DeepLinkHandler {
|
|
|
53
110
|
* Falls back to local URL parsing on failure.
|
|
54
111
|
*/
|
|
55
112
|
private handleDeepLink = async ({ url }: { url: string }): Promise<void> => {
|
|
113
|
+
console.log('[LinkForty] handleDeepLink called with url:', url);
|
|
114
|
+
|
|
56
115
|
if (!this.callback || !url) {
|
|
116
|
+
console.warn('[LinkForty] handleDeepLink early return: callback=', !!this.callback, 'url=', url);
|
|
57
117
|
return;
|
|
58
118
|
}
|
|
59
119
|
|
|
60
120
|
// Parse locally first (for fallback and to detect LinkForty URLs)
|
|
61
121
|
const localData = this.parseURL(url);
|
|
122
|
+
console.log('[LinkForty] localData parsed:', JSON.stringify(localData));
|
|
62
123
|
|
|
63
124
|
// If this is a LinkForty URL and we have a resolver, try the server
|
|
64
125
|
if (localData && this.resolveFn && this.baseUrl && url.startsWith(this.baseUrl)) {
|
|
65
126
|
try {
|
|
127
|
+
console.log('[LinkForty] Resolving URL via server...');
|
|
66
128
|
const resolvedData = await this.resolveURL(url);
|
|
129
|
+
console.log('[LinkForty] Resolve result:', JSON.stringify(resolvedData));
|
|
67
130
|
if (resolvedData) {
|
|
68
131
|
this.callback(url, resolvedData);
|
|
69
132
|
return;
|
|
70
133
|
}
|
|
134
|
+
console.warn('[LinkForty] Resolve returned null, falling back to local parse');
|
|
71
135
|
} catch (error) {
|
|
72
136
|
console.warn('[LinkForty] Failed to resolve URL from server, falling back to local parse:', error);
|
|
73
137
|
}
|
|
138
|
+
} else {
|
|
139
|
+
console.warn('[LinkForty] Skipping server resolve: localData=', !!localData, 'resolveFn=', !!this.resolveFn, 'baseUrl=', this.baseUrl, 'urlMatch=', url.startsWith(this.baseUrl || ''));
|
|
74
140
|
}
|
|
75
141
|
|
|
76
|
-
// Fallback to locally-parsed data
|
|
142
|
+
// Fallback to locally-parsed data (no customParameters from server)
|
|
143
|
+
console.warn('[LinkForty] Using local fallback — customParameters will be empty for App Link URLs');
|
|
77
144
|
this.callback(url, localData);
|
|
78
145
|
};
|
|
79
146
|
|
|
@@ -87,8 +154,12 @@ export class DeepLinkHandler {
|
|
|
87
154
|
return null;
|
|
88
155
|
}
|
|
89
156
|
|
|
90
|
-
const
|
|
91
|
-
|
|
157
|
+
const parsed = DeepLinkHandler.parseUrlString(url);
|
|
158
|
+
if (!parsed) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const pathSegments = parsed.pathname.split('/').filter(Boolean);
|
|
92
163
|
|
|
93
164
|
if (pathSegments.length === 0) {
|
|
94
165
|
return null;
|
|
@@ -106,17 +177,18 @@ export class DeepLinkHandler {
|
|
|
106
177
|
try {
|
|
107
178
|
const fingerprint = await FingerprintCollector.collect();
|
|
108
179
|
const [sw, sh] = fingerprint.screenResolution.split('x');
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
180
|
+
const fpParams: Record<string, string> = {
|
|
181
|
+
fp_tz: fingerprint.timezone,
|
|
182
|
+
fp_lang: fingerprint.language,
|
|
183
|
+
fp_sw: sw,
|
|
184
|
+
fp_sh: sh,
|
|
185
|
+
fp_platform: fingerprint.platform,
|
|
186
|
+
};
|
|
115
187
|
if (fingerprint.osVersion) {
|
|
116
|
-
|
|
188
|
+
fpParams.fp_pv = fingerprint.osVersion;
|
|
117
189
|
}
|
|
118
190
|
|
|
119
|
-
resolvePath += `?${
|
|
191
|
+
resolvePath += `?${DeepLinkHandler.buildQueryString(fpParams)}`;
|
|
120
192
|
} catch (error) {
|
|
121
193
|
// If fingerprint collection fails, still resolve without it
|
|
122
194
|
console.warn('[LinkForty] Fingerprint collection failed, resolving without fingerprint:', error);
|
|
@@ -131,15 +203,18 @@ export class DeepLinkHandler {
|
|
|
131
203
|
*/
|
|
132
204
|
parseURL(url: string): DeepLinkData | null {
|
|
133
205
|
try {
|
|
134
|
-
const parsedUrl = new URL(url);
|
|
135
|
-
|
|
136
206
|
// Check if this is a LinkForty URL
|
|
137
207
|
if (this.baseUrl && !url.startsWith(this.baseUrl)) {
|
|
138
208
|
return null;
|
|
139
209
|
}
|
|
140
210
|
|
|
211
|
+
const parsed = DeepLinkHandler.parseUrlString(url);
|
|
212
|
+
if (!parsed) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
141
216
|
// Extract short code from path (last segment handles both /shortCode and /templateSlug/shortCode)
|
|
142
|
-
const pathSegments =
|
|
217
|
+
const pathSegments = parsed.pathname.split('/').filter(Boolean);
|
|
143
218
|
const shortCode = pathSegments[pathSegments.length - 1];
|
|
144
219
|
|
|
145
220
|
if (!shortCode) {
|
|
@@ -148,16 +223,16 @@ export class DeepLinkHandler {
|
|
|
148
223
|
|
|
149
224
|
// Extract UTM parameters
|
|
150
225
|
const utmParameters = {
|
|
151
|
-
source:
|
|
152
|
-
medium:
|
|
153
|
-
campaign:
|
|
154
|
-
term:
|
|
155
|
-
content:
|
|
226
|
+
source: parsed.searchParams.get('utm_source') || undefined,
|
|
227
|
+
medium: parsed.searchParams.get('utm_medium') || undefined,
|
|
228
|
+
campaign: parsed.searchParams.get('utm_campaign') || undefined,
|
|
229
|
+
term: parsed.searchParams.get('utm_term') || undefined,
|
|
230
|
+
content: parsed.searchParams.get('utm_content') || undefined,
|
|
156
231
|
};
|
|
157
232
|
|
|
158
233
|
// Extract all other query parameters as custom parameters
|
|
159
234
|
const customParameters: Record<string, string> = {};
|
|
160
|
-
|
|
235
|
+
parsed.searchParams.forEach((value, key) => {
|
|
161
236
|
if (!key.startsWith('utm_')) {
|
|
162
237
|
customParameters[key] = value;
|
|
163
238
|
}
|
|
@@ -15,11 +15,12 @@ export class FingerprintCollector {
|
|
|
15
15
|
const timezone = this.getTimezone();
|
|
16
16
|
const language = this.getLanguage();
|
|
17
17
|
|
|
18
|
+
// Round dimensions — Android returns floats (e.g. 434.717) but the server expects integers
|
|
18
19
|
const fingerprint: DeviceFingerprint = {
|
|
19
20
|
userAgent: await DeviceInfo.getUserAgent(),
|
|
20
21
|
timezone,
|
|
21
22
|
language,
|
|
22
|
-
screenResolution: `${width}x${height}`,
|
|
23
|
+
screenResolution: `${Math.round(width)}x${Math.round(height)}`,
|
|
23
24
|
platform: Platform.OS,
|
|
24
25
|
deviceModel: await DeviceInfo.getModel(),
|
|
25
26
|
osVersion: await DeviceInfo.getSystemVersion(),
|
|
@@ -35,18 +36,21 @@ export class FingerprintCollector {
|
|
|
35
36
|
static async generateQueryParams(): Promise<string> {
|
|
36
37
|
const fingerprint = await this.collect();
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
// Build query string manually — URLSearchParams is not reliable in Hermes
|
|
40
|
+
const params: Record<string, string> = {
|
|
39
41
|
ua: fingerprint.userAgent,
|
|
40
42
|
tz: fingerprint.timezone,
|
|
41
43
|
lang: fingerprint.language,
|
|
42
44
|
screen: fingerprint.screenResolution,
|
|
43
45
|
platform: fingerprint.platform,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
};
|
|
47
|
+
if (fingerprint.deviceModel) params.model = fingerprint.deviceModel;
|
|
48
|
+
if (fingerprint.osVersion) params.os = fingerprint.osVersion;
|
|
49
|
+
if (fingerprint.appVersion) params.app_version = fingerprint.appVersion;
|
|
48
50
|
|
|
49
|
-
return
|
|
51
|
+
return Object.entries(params)
|
|
52
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
53
|
+
.join('&');
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
/**
|
package/src/LinkFortySDK.ts
CHANGED
|
@@ -310,7 +310,7 @@ export class LinkFortySDK {
|
|
|
310
310
|
// Parse screen resolution (e.g., "1080x1920" -> [1080, 1920])
|
|
311
311
|
const [screenWidth, screenHeight] = fingerprint.screenResolution
|
|
312
312
|
.split('x')
|
|
313
|
-
.map(Number);
|
|
313
|
+
.map((v) => Math.round(Number(v)));
|
|
314
314
|
|
|
315
315
|
// Convert attribution window from days to hours
|
|
316
316
|
const attributionWindowHours = (this.config.attributionWindow || 7) * 24;
|