@linkforty/mobile-sdk-react-native 1.1.1 → 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 +11 -2
- 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.d.ts +9 -2
- package/dist/LinkFortySDK.d.ts.map +1 -1
- package/dist/LinkFortySDK.js +30 -8
- package/dist/types.d.ts +9 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/DeepLinkHandler.ts +95 -20
- package/src/FingerprintCollector.ts +11 -7
- package/src/LinkFortySDK.ts +33 -9
- package/src/types.ts +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,21 @@ 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
|
+
|
|
15
|
+
## [1.1.2] - 2026-02-12
|
|
16
|
+
### Added
|
|
17
|
+
- `createLink()` method for creating short links programmatically from mobile apps via the LinkForty API
|
|
18
|
+
- `CreateLinkOptions` type — accepts optional `templateId`, `templateSlug`, `deepLinkParameters`, `title`, `description`, `customCode`, and `utmParameters`
|
|
19
|
+
|
|
10
20
|
## [1.1.1] - 2026-02-12
|
|
11
21
|
### Added
|
|
12
|
-
- `createLink()` method for creating short links programmatically from mobile apps via the LinkForty API (`POST /api/links`)
|
|
13
|
-
- `CreateLinkOptions` type — accepts `templateId`, `templateSlug`, optional `deepLinkParameters`, `title`, `description`, `customCode`, and `utmParameters`
|
|
14
22
|
- `CreateLinkResult` type — returns `url` (full shareable URL), `shortCode`, and `linkId`
|
|
15
23
|
- Exported `CreateLinkOptions` and `CreateLinkResult` types from package entry point
|
|
24
|
+
- Simplified link creation: when `templateId` is omitted, the SDK calls `POST /api/sdk/v1/links` which auto-selects the organization's default template — mobile apps no longer need to know template IDs or slugs
|
|
16
25
|
|
|
17
26
|
## [1.1.0] - 2026-02-11
|
|
18
27
|
|
|
@@ -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.d.ts
CHANGED
|
@@ -34,16 +34,23 @@ export declare class LinkFortySDK {
|
|
|
34
34
|
* Create a new short link via the LinkForty API.
|
|
35
35
|
*
|
|
36
36
|
* Requires an API key to be configured in the SDK init options.
|
|
37
|
+
* When `templateId` is omitted, uses the SDK endpoint which auto-selects
|
|
38
|
+
* the organization's default template and returns the full URL.
|
|
37
39
|
*
|
|
38
40
|
* @example
|
|
39
41
|
* ```ts
|
|
42
|
+
* // Simple — server auto-selects template
|
|
43
|
+
* const result = await LinkFortySDK.createLink({
|
|
44
|
+
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
45
|
+
* title: 'My Video',
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Explicit — specify template
|
|
40
49
|
* const result = await LinkFortySDK.createLink({
|
|
41
50
|
* templateId: 'uuid-of-template',
|
|
42
51
|
* templateSlug: 'ToQs',
|
|
43
52
|
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
44
|
-
* title: 'My Video',
|
|
45
53
|
* });
|
|
46
|
-
* // result.url → 'https://go.example.com/ToQs/abc123'
|
|
47
54
|
* ```
|
|
48
55
|
*/
|
|
49
56
|
createLink(options: CreateLinkOptions): Promise<CreateLinkResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LinkFortySDK.d.ts","sourceRoot":"","sources":["../src/LinkFortySDK.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EACZ,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAQjB,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,wBAAwB,CAAyC;IACzE,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAkB;IAErC;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClD;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAQpD;;OAEG;IACH,kBAAkB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,IAAI;IAW5D;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAwB5C;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC/E
|
|
1
|
+
{"version":3,"file":"LinkFortySDK.d.ts","sourceRoot":"","sources":["../src/LinkFortySDK.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EACZ,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAQjB,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,wBAAwB,CAAyC;IACzE,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAkB;IAErC;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClD;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAQpD;;OAEG;IACH,kBAAkB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,IAAI;IAW5D;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAwB5C;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC/E;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoEvE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAa5C;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAchC;;OAEG;YACW,aAAa;IAW3B;;OAEG;YACW,aAAa;IA+F3B;;OAEG;YACW,eAAe;IAI7B;;OAEG;YACW,UAAU;CA+BzB;;AAGD,wBAAkC"}
|
package/dist/LinkFortySDK.js
CHANGED
|
@@ -127,16 +127,23 @@ export class LinkFortySDK {
|
|
|
127
127
|
* Create a new short link via the LinkForty API.
|
|
128
128
|
*
|
|
129
129
|
* Requires an API key to be configured in the SDK init options.
|
|
130
|
+
* When `templateId` is omitted, uses the SDK endpoint which auto-selects
|
|
131
|
+
* the organization's default template and returns the full URL.
|
|
130
132
|
*
|
|
131
133
|
* @example
|
|
132
134
|
* ```ts
|
|
135
|
+
* // Simple — server auto-selects template
|
|
136
|
+
* const result = await LinkFortySDK.createLink({
|
|
137
|
+
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
138
|
+
* title: 'My Video',
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // Explicit — specify template
|
|
133
142
|
* const result = await LinkFortySDK.createLink({
|
|
134
143
|
* templateId: 'uuid-of-template',
|
|
135
144
|
* templateSlug: 'ToQs',
|
|
136
145
|
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
137
|
-
* title: 'My Video',
|
|
138
146
|
* });
|
|
139
|
-
* // result.url → 'https://go.example.com/ToQs/abc123'
|
|
140
147
|
* ```
|
|
141
148
|
*/
|
|
142
149
|
async createLink(options) {
|
|
@@ -146,9 +153,10 @@ export class LinkFortySDK {
|
|
|
146
153
|
if (!this.config.apiKey) {
|
|
147
154
|
throw new Error('API key required to create links. Pass apiKey in init().');
|
|
148
155
|
}
|
|
149
|
-
const body = {
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
const body = {};
|
|
157
|
+
if (options.templateId) {
|
|
158
|
+
body.templateId = options.templateId;
|
|
159
|
+
}
|
|
152
160
|
if (options.deepLinkParameters) {
|
|
153
161
|
body.deepLinkParameters = options.deepLinkParameters;
|
|
154
162
|
}
|
|
@@ -164,12 +172,26 @@ export class LinkFortySDK {
|
|
|
164
172
|
if (options.utmParameters) {
|
|
165
173
|
body.utmParameters = options.utmParameters;
|
|
166
174
|
}
|
|
167
|
-
|
|
175
|
+
// Use the simplified SDK endpoint when no templateId is provided
|
|
176
|
+
const useSimplifiedEndpoint = !options.templateId;
|
|
177
|
+
const endpoint = useSimplifiedEndpoint ? '/api/sdk/v1/links' : '/api/links';
|
|
178
|
+
const response = await this.apiRequest(endpoint, {
|
|
168
179
|
method: 'POST',
|
|
169
180
|
body: JSON.stringify(body),
|
|
170
181
|
});
|
|
182
|
+
// The SDK endpoint returns { url, shortCode, linkId } directly
|
|
183
|
+
if (useSimplifiedEndpoint && response.url) {
|
|
184
|
+
return {
|
|
185
|
+
url: response.url,
|
|
186
|
+
shortCode: response.shortCode || response.short_code,
|
|
187
|
+
linkId: response.linkId || response.id,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// The dashboard endpoint returns { id, short_code, ... } — build URL from parts
|
|
171
191
|
const shortCode = response.short_code;
|
|
172
|
-
const url =
|
|
192
|
+
const url = options.templateSlug
|
|
193
|
+
? `${this.config.baseUrl}/${options.templateSlug}/${shortCode}`
|
|
194
|
+
: `${this.config.baseUrl}/${shortCode}`;
|
|
173
195
|
if (this.config.debug) {
|
|
174
196
|
console.log('[LinkForty] Created link:', url);
|
|
175
197
|
}
|
|
@@ -233,7 +255,7 @@ export class LinkFortySDK {
|
|
|
233
255
|
// Parse screen resolution (e.g., "1080x1920" -> [1080, 1920])
|
|
234
256
|
const [screenWidth, screenHeight] = fingerprint.screenResolution
|
|
235
257
|
.split('x')
|
|
236
|
-
.map(Number);
|
|
258
|
+
.map((v) => Math.round(Number(v)));
|
|
237
259
|
// Convert attribution window from days to hours
|
|
238
260
|
const attributionWindowHours = (this.config.attributionWindow || 7) * 24;
|
|
239
261
|
// Call install endpoint with flattened structure matching backend contract
|
package/dist/types.d.ts
CHANGED
|
@@ -114,13 +114,17 @@ export type DeepLinkCallback = (url: string, deepLinkData: DeepLinkData | null)
|
|
|
114
114
|
*/
|
|
115
115
|
export type ResolveFunction = (path: string) => Promise<DeepLinkData | null>;
|
|
116
116
|
/**
|
|
117
|
-
* Options for creating a new short link via the LinkForty API
|
|
117
|
+
* Options for creating a new short link via the LinkForty API.
|
|
118
|
+
*
|
|
119
|
+
* When `templateId` and `templateSlug` are omitted the SDK uses
|
|
120
|
+
* `POST /api/sdk/v1/links` which auto-selects the organization's
|
|
121
|
+
* default template and returns the full URL server-side.
|
|
118
122
|
*/
|
|
119
123
|
export interface CreateLinkOptions {
|
|
120
|
-
/** Template ID (UUID) —
|
|
121
|
-
templateId
|
|
122
|
-
/** Template slug —
|
|
123
|
-
templateSlug
|
|
124
|
+
/** Template ID (UUID) — optional; auto-selected when omitted */
|
|
125
|
+
templateId?: string;
|
|
126
|
+
/** Template slug — only needed when templateId is provided (for URL construction) */
|
|
127
|
+
templateSlug?: string;
|
|
124
128
|
/** Custom parameters embedded in the link (e.g., { route: 'VIDEO_VIEWER', id: '...' }) */
|
|
125
129
|
deepLinkParameters?: Record<string, string>;
|
|
126
130
|
/** Link title (for internal reference) */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
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
|
@@ -157,16 +157,23 @@ export class LinkFortySDK {
|
|
|
157
157
|
* Create a new short link via the LinkForty API.
|
|
158
158
|
*
|
|
159
159
|
* Requires an API key to be configured in the SDK init options.
|
|
160
|
+
* When `templateId` is omitted, uses the SDK endpoint which auto-selects
|
|
161
|
+
* the organization's default template and returns the full URL.
|
|
160
162
|
*
|
|
161
163
|
* @example
|
|
162
164
|
* ```ts
|
|
165
|
+
* // Simple — server auto-selects template
|
|
166
|
+
* const result = await LinkFortySDK.createLink({
|
|
167
|
+
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
168
|
+
* title: 'My Video',
|
|
169
|
+
* });
|
|
170
|
+
*
|
|
171
|
+
* // Explicit — specify template
|
|
163
172
|
* const result = await LinkFortySDK.createLink({
|
|
164
173
|
* templateId: 'uuid-of-template',
|
|
165
174
|
* templateSlug: 'ToQs',
|
|
166
175
|
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
167
|
-
* title: 'My Video',
|
|
168
176
|
* });
|
|
169
|
-
* // result.url → 'https://go.example.com/ToQs/abc123'
|
|
170
177
|
* ```
|
|
171
178
|
*/
|
|
172
179
|
async createLink(options: CreateLinkOptions): Promise<CreateLinkResult> {
|
|
@@ -178,10 +185,11 @@ export class LinkFortySDK {
|
|
|
178
185
|
throw new Error('API key required to create links. Pass apiKey in init().');
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
const body: Record<string, unknown> = {
|
|
182
|
-
templateId: options.templateId,
|
|
183
|
-
};
|
|
188
|
+
const body: Record<string, unknown> = {};
|
|
184
189
|
|
|
190
|
+
if (options.templateId) {
|
|
191
|
+
body.templateId = options.templateId;
|
|
192
|
+
}
|
|
185
193
|
if (options.deepLinkParameters) {
|
|
186
194
|
body.deepLinkParameters = options.deepLinkParameters;
|
|
187
195
|
}
|
|
@@ -198,16 +206,32 @@ export class LinkFortySDK {
|
|
|
198
206
|
body.utmParameters = options.utmParameters;
|
|
199
207
|
}
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
209
|
+
// Use the simplified SDK endpoint when no templateId is provided
|
|
210
|
+
const useSimplifiedEndpoint = !options.templateId;
|
|
211
|
+
const endpoint = useSimplifiedEndpoint ? '/api/sdk/v1/links' : '/api/links';
|
|
212
|
+
|
|
213
|
+
const response = await this.apiRequest<{ id: string; short_code: string; url?: string; shortCode?: string; linkId?: string }>(
|
|
214
|
+
endpoint,
|
|
203
215
|
{
|
|
204
216
|
method: 'POST',
|
|
205
217
|
body: JSON.stringify(body),
|
|
206
218
|
},
|
|
207
219
|
);
|
|
208
220
|
|
|
221
|
+
// The SDK endpoint returns { url, shortCode, linkId } directly
|
|
222
|
+
if (useSimplifiedEndpoint && response.url) {
|
|
223
|
+
return {
|
|
224
|
+
url: response.url,
|
|
225
|
+
shortCode: response.shortCode || response.short_code,
|
|
226
|
+
linkId: response.linkId || response.id,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The dashboard endpoint returns { id, short_code, ... } — build URL from parts
|
|
209
231
|
const shortCode = response.short_code;
|
|
210
|
-
const url =
|
|
232
|
+
const url = options.templateSlug
|
|
233
|
+
? `${this.config.baseUrl}/${options.templateSlug}/${shortCode}`
|
|
234
|
+
: `${this.config.baseUrl}/${shortCode}`;
|
|
211
235
|
|
|
212
236
|
if (this.config.debug) {
|
|
213
237
|
console.log('[LinkForty] Created link:', url);
|
|
@@ -286,7 +310,7 @@ export class LinkFortySDK {
|
|
|
286
310
|
// Parse screen resolution (e.g., "1080x1920" -> [1080, 1920])
|
|
287
311
|
const [screenWidth, screenHeight] = fingerprint.screenResolution
|
|
288
312
|
.split('x')
|
|
289
|
-
.map(Number);
|
|
313
|
+
.map((v) => Math.round(Number(v)));
|
|
290
314
|
|
|
291
315
|
// Convert attribution window from days to hours
|
|
292
316
|
const attributionWindowHours = (this.config.attributionWindow || 7) * 24;
|
package/src/types.ts
CHANGED
|
@@ -123,13 +123,17 @@ export type DeepLinkCallback = (url: string, deepLinkData: DeepLinkData | null)
|
|
|
123
123
|
export type ResolveFunction = (path: string) => Promise<DeepLinkData | null>;
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* Options for creating a new short link via the LinkForty API
|
|
126
|
+
* Options for creating a new short link via the LinkForty API.
|
|
127
|
+
*
|
|
128
|
+
* When `templateId` and `templateSlug` are omitted the SDK uses
|
|
129
|
+
* `POST /api/sdk/v1/links` which auto-selects the organization's
|
|
130
|
+
* default template and returns the full URL server-side.
|
|
127
131
|
*/
|
|
128
132
|
export interface CreateLinkOptions {
|
|
129
|
-
/** Template ID (UUID) —
|
|
130
|
-
templateId
|
|
131
|
-
/** Template slug —
|
|
132
|
-
templateSlug
|
|
133
|
+
/** Template ID (UUID) — optional; auto-selected when omitted */
|
|
134
|
+
templateId?: string;
|
|
135
|
+
/** Template slug — only needed when templateId is provided (for URL construction) */
|
|
136
|
+
templateSlug?: string;
|
|
133
137
|
/** Custom parameters embedded in the link (e.g., { route: 'VIDEO_VIEWER', id: '...' }) */
|
|
134
138
|
deepLinkParameters?: Record<string, string>;
|
|
135
139
|
/** Link title (for internal reference) */
|