@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 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;IAgB1F;;OAEG;IACH,OAAO,IAAI,IAAI;IAKf;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAuBpB;IAEF;;;;OAIG;YACW,UAAU;IA2CxB;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;CA4C3C"}
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"}
@@ -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
- // Fallback to locally-parsed data
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 parsedUrl = new URL(url);
79
- const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
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 queryParams = new URLSearchParams();
96
- queryParams.set('fp_tz', fingerprint.timezone);
97
- queryParams.set('fp_lang', fingerprint.language);
98
- queryParams.set('fp_sw', sw);
99
- queryParams.set('fp_sh', sh);
100
- queryParams.set('fp_platform', fingerprint.platform);
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
- queryParams.set('fp_pv', fingerprint.osVersion);
166
+ fpParams.fp_pv = fingerprint.osVersion;
103
167
  }
104
- resolvePath += `?${queryParams.toString()}`;
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 = parsedUrl.pathname.split('/').filter(Boolean);
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: parsedUrl.searchParams.get('utm_source') || undefined,
132
- medium: parsedUrl.searchParams.get('utm_medium') || undefined,
133
- campaign: parsedUrl.searchParams.get('utm_campaign') || undefined,
134
- term: parsedUrl.searchParams.get('utm_term') || undefined,
135
- content: parsedUrl.searchParams.get('utm_content') || undefined,
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
- parsedUrl.searchParams.forEach((value, key) => {
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;IAmBlD;;OAEG;WACU,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC;IAiBnD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAS1B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;CAuB3B"}
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
- const params = new URLSearchParams({
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
- ...(fingerprint.deviceModel && { model: fingerprint.deviceModel }),
38
- ...(fingerprint.osVersion && { os: fingerprint.osVersion }),
39
- ...(fingerprint.appVersion && { app_version: fingerprint.appVersion }),
40
- });
41
- return params.toString();
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
@@ -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;;;;;;;;;;;;;;;OAeG;IACG,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmDvE;;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"}
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"}
@@ -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
- templateId: options.templateId,
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
- const response = await this.apiRequest('/api/links', {
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 = `${this.config.baseUrl}/${options.templateSlug}/${shortCode}`;
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) — required by the API */
121
- templateId: string;
122
- /** Template slug — used to construct the full shareable URL */
123
- templateSlug: string;
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) */
@@ -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.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
  },
@@ -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 parsedUrl = new URL(url);
91
- const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
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 queryParams = new URLSearchParams();
110
- queryParams.set('fp_tz', fingerprint.timezone);
111
- queryParams.set('fp_lang', fingerprint.language);
112
- queryParams.set('fp_sw', sw);
113
- queryParams.set('fp_sh', sh);
114
- queryParams.set('fp_platform', fingerprint.platform);
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
- queryParams.set('fp_pv', fingerprint.osVersion);
188
+ fpParams.fp_pv = fingerprint.osVersion;
117
189
  }
118
190
 
119
- resolvePath += `?${queryParams.toString()}`;
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 = parsedUrl.pathname.split('/').filter(Boolean);
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: parsedUrl.searchParams.get('utm_source') || undefined,
152
- medium: parsedUrl.searchParams.get('utm_medium') || undefined,
153
- campaign: parsedUrl.searchParams.get('utm_campaign') || undefined,
154
- term: parsedUrl.searchParams.get('utm_term') || undefined,
155
- content: parsedUrl.searchParams.get('utm_content') || undefined,
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
- parsedUrl.searchParams.forEach((value, key) => {
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
- const params = new URLSearchParams({
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
- ...(fingerprint.deviceModel && { model: fingerprint.deviceModel }),
45
- ...(fingerprint.osVersion && { os: fingerprint.osVersion }),
46
- ...(fingerprint.appVersion && { app_version: fingerprint.appVersion }),
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 params.toString();
51
+ return Object.entries(params)
52
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
53
+ .join('&');
50
54
  }
51
55
 
52
56
  /**
@@ -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
- const response = await this.apiRequest<{ id: string; short_code: string }>(
202
- '/api/links',
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 = `${this.config.baseUrl}/${options.templateSlug}/${shortCode}`;
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) — required by the API */
130
- templateId: string;
131
- /** Template slug — used to construct the full shareable URL */
132
- templateSlug: string;
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) */