@linkforty/mobile-sdk-react-native 1.0.1 → 1.1.1
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 +23 -123
- package/README.md +3 -3
- package/dist/DeepLinkHandler.d.ts +23 -4
- package/dist/DeepLinkHandler.d.ts.map +1 -1
- package/dist/DeepLinkHandler.js +82 -8
- package/dist/LinkFortySDK.d.ts +21 -2
- package/dist/LinkFortySDK.d.ts.map +1 -1
- package/dist/LinkFortySDK.js +82 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +47 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/DeepLinkHandler.ts +89 -9
- package/src/LinkFortySDK.ts +96 -4
- package/src/index.ts +3 -0
- package/src/types.ts +50 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,137 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.1] - 2026-02-12
|
|
10
11
|
### Added
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- **Event Tracking** - Track in-app events with attribution
|
|
16
|
-
- **Privacy-Focused** - No persistent device IDs required
|
|
17
|
-
- **TypeScript Support** - Full type definitions included
|
|
18
|
-
- **Works with Core & Cloud** - Compatible with self-hosted and Cloud instances
|
|
19
|
-
|
|
20
|
-
### Core Features
|
|
21
|
-
|
|
22
|
-
#### SDK Initialization
|
|
23
|
-
- `init(config)` - Initialize SDK with base URL and optional API key
|
|
24
|
-
- Support for debug logging during development
|
|
25
|
-
- Customizable attribution window (default: 7 days)
|
|
26
|
-
|
|
27
|
-
#### Deep Linking
|
|
28
|
-
- `onDeferredDeepLink(callback)` - Handle deferred deep links for new installs
|
|
29
|
-
- `onDeepLink(callback)` - Handle direct deep links for existing users
|
|
30
|
-
- Automatic Universal Links (iOS) and App Links (Android) configuration
|
|
31
|
-
- Deep link data includes UTM parameters and custom parameters
|
|
32
|
-
|
|
33
|
-
#### Event Tracking
|
|
34
|
-
- `trackEvent(name, properties)` - Track in-app events with optional properties
|
|
35
|
-
- Automatic attribution to install source
|
|
36
|
-
- Webhook triggers for conversion events
|
|
37
|
-
- Support for revenue tracking and custom event properties
|
|
38
|
-
|
|
39
|
-
#### Data Management
|
|
40
|
-
- `getInstallData()` - Retrieve cached install attribution data
|
|
41
|
-
- `getInstallId()` - Get unique install ID for this app installation
|
|
42
|
-
- `clearData()` - Clear all cached SDK data (useful for testing)
|
|
43
|
-
|
|
44
|
-
### Device Fingerprinting
|
|
45
|
-
|
|
46
|
-
Collects the following non-invasive signals for probabilistic attribution:
|
|
47
|
-
- User agent string
|
|
48
|
-
- Device timezone
|
|
49
|
-
- Device language
|
|
50
|
-
- Screen resolution
|
|
51
|
-
- Platform (iOS/Android)
|
|
52
|
-
- Device model and OS version
|
|
53
|
-
- IP address (server-side only)
|
|
54
|
-
|
|
55
|
-
### API Endpoints
|
|
56
|
-
|
|
57
|
-
Uses the following LinkForty Core API endpoints:
|
|
58
|
-
- `POST /api/sdk/v1/install` - Report app installation
|
|
59
|
-
- `POST /api/sdk/v1/event` - Track in-app events
|
|
60
|
-
- `GET /api/sdk/v1/attribution/:fingerprint` - Debug attribution data
|
|
61
|
-
- `GET /api/sdk/v1/health` - Health check
|
|
62
|
-
|
|
63
|
-
### Dependencies
|
|
64
|
-
|
|
65
|
-
**Peer Dependencies:**
|
|
66
|
-
- `react` >= 16.8.0
|
|
67
|
-
- `react-native` >= 0.60.0
|
|
68
|
-
|
|
69
|
-
**Runtime Dependencies:**
|
|
70
|
-
- `@react-native-async-storage/async-storage` ^1.21.0
|
|
71
|
-
- `react-native-device-info` ^10.12.0
|
|
72
|
-
|
|
73
|
-
### Platform Support
|
|
74
|
-
|
|
75
|
-
- ✅ iOS 12+
|
|
76
|
-
- ✅ Android 5.0+ (API level 21+)
|
|
77
|
-
- ✅ React Native 0.60+
|
|
78
|
-
- ✅ TypeScript 5.3+
|
|
79
|
-
- ✅ Expo (with custom native modules)
|
|
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
|
+
- `CreateLinkResult` type — returns `url` (full shareable URL), `shortCode`, and `linkId`
|
|
15
|
+
- Exported `CreateLinkOptions` and `CreateLinkResult` types from package entry point
|
|
80
16
|
|
|
81
|
-
|
|
17
|
+
## [1.1.0] - 2026-02-11
|
|
82
18
|
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
### Documentation
|
|
90
|
-
|
|
91
|
-
- Comprehensive README with setup instructions
|
|
92
|
-
- TypeScript type definitions included
|
|
93
|
-
- iOS Universal Links setup guide
|
|
94
|
-
- Android App Links setup guide
|
|
95
|
-
- Troubleshooting guide
|
|
96
|
-
- API reference with examples
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## [Unreleased]
|
|
19
|
+
### Added
|
|
20
|
+
- Server-side URL resolution for deep links intercepted by App Links (Android) and Universal Links (iOS) — when the OS opens the app directly, the SDK now calls the resolve endpoint to retrieve custom parameters, UTM data, and link metadata
|
|
21
|
+
- Device fingerprint collection sent with resolve requests for click attribution and deferred deep linking analytics
|
|
22
|
+
- `deepLinkPath` and `appScheme` optional fields on `DeepLinkData` type
|
|
23
|
+
- `ResolveFunction` type export for advanced SDK consumers
|
|
101
24
|
|
|
102
25
|
### Changed
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
- Android SDK (Native Kotlin) - Q2 2025
|
|
109
|
-
- React Native Expo compatibility mode
|
|
110
|
-
- Offline event queue for poor network conditions
|
|
111
|
-
- Advanced debugging tools
|
|
112
|
-
- Custom domain support
|
|
113
|
-
- A/B testing integration
|
|
114
|
-
- User segmentation
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Version History
|
|
119
|
-
|
|
120
|
-
- **1.0.0** - Initial release (2024-11-22)
|
|
121
|
-
|
|
122
|
-
## Migration Guides
|
|
123
|
-
|
|
124
|
-
### Upgrading to 1.0.0
|
|
125
|
-
|
|
126
|
-
This is the initial release, so no migration is necessary.
|
|
127
|
-
|
|
128
|
-
## Support
|
|
26
|
+
- `DeepLinkHandler.handleDeepLink` is now async to support server-side URL resolution
|
|
27
|
+
- `DeepLinkHandler.initialize` accepts an optional `resolveFn` parameter for URL resolution
|
|
28
|
+
- `DeepLinkHandler.parseURL` now correctly extracts the short code from template URLs (e.g., `/templateSlug/shortCode`)
|
|
29
|
+
- `LinkFortySDK.onDeepLink` automatically provides a resolver that calls the backend resolve endpoint
|
|
30
|
+
- Deferred deep link responses now normalize `deepLinkParameters` from the backend into `customParameters` for a consistent interface across direct and deferred deep link paths
|
|
129
31
|
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
- Documentation: https://docs.linkforty.com
|
|
133
|
-
- Discord Community: https://discord.gg/linkforty
|
|
32
|
+
### Fixed
|
|
33
|
+
- Fixed deep links returning empty `customParameters` when the app was opened via App Links or Universal Links, where the OS intercepted the URL before the LinkForty server could append parameters via redirect
|
|
134
34
|
|
|
135
|
-
##
|
|
35
|
+
## [1.0.1] - 2025-12-11
|
|
136
36
|
|
|
137
|
-
|
|
37
|
+
- Initial release
|
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ Official React Native SDK for [LinkForty](https://github.com/linkforty/core) - O
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm
|
|
18
|
+
npm i @linkforty/mobile-sdk-react-native
|
|
19
19
|
# or
|
|
20
|
-
yarn add @linkforty/react-native
|
|
20
|
+
yarn add @linkforty/mobile-sdk-react-native
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### Requirements
|
|
@@ -104,7 +104,7 @@ cd ios && pod install
|
|
|
104
104
|
## Quick Start
|
|
105
105
|
|
|
106
106
|
```typescript
|
|
107
|
-
import LinkForty from '@linkforty/react-native
|
|
107
|
+
import LinkForty from '@linkforty/mobile-sdk-react-native';
|
|
108
108
|
|
|
109
109
|
// Initialize SDK (call in App.tsx or index.js)
|
|
110
110
|
await LinkForty.init({
|
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepLinkHandler - Handles Universal Links (iOS) and App Links (Android)
|
|
3
|
+
*
|
|
4
|
+
* When the OS intercepts a LinkForty URL via App Links or Universal Links,
|
|
5
|
+
* the redirect server is bypassed. This handler detects LinkForty URLs and
|
|
6
|
+
* resolves them via the server API to retrieve the full deep link data
|
|
7
|
+
* (custom parameters, UTM params, etc.) that would normally be appended
|
|
8
|
+
* during the redirect.
|
|
3
9
|
*/
|
|
4
|
-
import type { DeepLinkData, DeepLinkCallback } from './types';
|
|
10
|
+
import type { DeepLinkData, DeepLinkCallback, ResolveFunction } from './types';
|
|
5
11
|
export declare class DeepLinkHandler {
|
|
6
12
|
private callback;
|
|
7
13
|
private baseUrl;
|
|
14
|
+
private resolveFn;
|
|
8
15
|
/**
|
|
9
16
|
* Initialize deep link listener
|
|
17
|
+
* @param baseUrl - LinkForty instance base URL for detecting LinkForty URLs
|
|
18
|
+
* @param callback - Callback invoked with parsed/resolved deep link data
|
|
19
|
+
* @param resolveFn - Optional function to resolve LinkForty URLs via the server API
|
|
10
20
|
*/
|
|
11
|
-
initialize(baseUrl: string, callback: DeepLinkCallback): void;
|
|
21
|
+
initialize(baseUrl: string, callback: DeepLinkCallback, resolveFn?: ResolveFunction): void;
|
|
12
22
|
/**
|
|
13
23
|
* Clean up listeners
|
|
14
24
|
*/
|
|
15
25
|
cleanup(): void;
|
|
16
26
|
/**
|
|
17
|
-
* Handle incoming deep link
|
|
27
|
+
* Handle incoming deep link.
|
|
28
|
+
* If the URL is a LinkForty URL and a resolver is available, resolves via the server.
|
|
29
|
+
* Falls back to local URL parsing on failure.
|
|
18
30
|
*/
|
|
19
31
|
private handleDeepLink;
|
|
20
32
|
/**
|
|
21
|
-
*
|
|
33
|
+
* Resolve a LinkForty URL via the server API.
|
|
34
|
+
* Extracts the path from the URL, collects fingerprint data for click attribution,
|
|
35
|
+
* and calls the resolve endpoint.
|
|
36
|
+
*/
|
|
37
|
+
private resolveURL;
|
|
38
|
+
/**
|
|
39
|
+
* Parse deep link URL and extract data locally (without server call).
|
|
40
|
+
* Used as fallback when server resolution fails or for non-LinkForty URLs.
|
|
22
41
|
*/
|
|
23
42
|
parseURL(url: string): DeepLinkData | null;
|
|
24
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeepLinkHandler.d.ts","sourceRoot":"","sources":["../src/DeepLinkHandler.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/DeepLinkHandler.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepLinkHandler - Handles Universal Links (iOS) and App Links (Android)
|
|
3
|
+
*
|
|
4
|
+
* When the OS intercepts a LinkForty URL via App Links or Universal Links,
|
|
5
|
+
* the redirect server is bypassed. This handler detects LinkForty URLs and
|
|
6
|
+
* resolves them via the server API to retrieve the full deep link data
|
|
7
|
+
* (custom parameters, UTM params, etc.) that would normally be appended
|
|
8
|
+
* during the redirect.
|
|
3
9
|
*/
|
|
4
10
|
import { Linking } from 'react-native';
|
|
11
|
+
import { FingerprintCollector } from './FingerprintCollector';
|
|
5
12
|
export class DeepLinkHandler {
|
|
6
13
|
callback = null;
|
|
7
14
|
baseUrl = null;
|
|
15
|
+
resolveFn = null;
|
|
8
16
|
/**
|
|
9
17
|
* Initialize deep link listener
|
|
18
|
+
* @param baseUrl - LinkForty instance base URL for detecting LinkForty URLs
|
|
19
|
+
* @param callback - Callback invoked with parsed/resolved deep link data
|
|
20
|
+
* @param resolveFn - Optional function to resolve LinkForty URLs via the server API
|
|
10
21
|
*/
|
|
11
|
-
initialize(baseUrl, callback) {
|
|
22
|
+
initialize(baseUrl, callback, resolveFn) {
|
|
12
23
|
this.baseUrl = baseUrl;
|
|
13
24
|
this.callback = callback;
|
|
25
|
+
this.resolveFn = resolveFn || null;
|
|
14
26
|
// Listen for deep links when app is already open
|
|
15
27
|
Linking.addEventListener('url', this.handleDeepLink);
|
|
16
28
|
// Check if app was opened via deep link
|
|
@@ -28,17 +40,78 @@ export class DeepLinkHandler {
|
|
|
28
40
|
this.callback = null;
|
|
29
41
|
}
|
|
30
42
|
/**
|
|
31
|
-
* Handle incoming deep link
|
|
43
|
+
* Handle incoming deep link.
|
|
44
|
+
* If the URL is a LinkForty URL and a resolver is available, resolves via the server.
|
|
45
|
+
* Falls back to local URL parsing on failure.
|
|
32
46
|
*/
|
|
33
|
-
handleDeepLink = ({ url }) => {
|
|
47
|
+
handleDeepLink = async ({ url }) => {
|
|
34
48
|
if (!this.callback || !url) {
|
|
35
49
|
return;
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
this.
|
|
51
|
+
// Parse locally first (for fallback and to detect LinkForty URLs)
|
|
52
|
+
const localData = this.parseURL(url);
|
|
53
|
+
// If this is a LinkForty URL and we have a resolver, try the server
|
|
54
|
+
if (localData && this.resolveFn && this.baseUrl && url.startsWith(this.baseUrl)) {
|
|
55
|
+
try {
|
|
56
|
+
const resolvedData = await this.resolveURL(url);
|
|
57
|
+
if (resolvedData) {
|
|
58
|
+
this.callback(url, resolvedData);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.warn('[LinkForty] Failed to resolve URL from server, falling back to local parse:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Fallback to locally-parsed data
|
|
67
|
+
this.callback(url, localData);
|
|
39
68
|
};
|
|
40
69
|
/**
|
|
41
|
-
*
|
|
70
|
+
* Resolve a LinkForty URL via the server API.
|
|
71
|
+
* Extracts the path from the URL, collects fingerprint data for click attribution,
|
|
72
|
+
* and calls the resolve endpoint.
|
|
73
|
+
*/
|
|
74
|
+
async resolveURL(url) {
|
|
75
|
+
if (!this.resolveFn) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const parsedUrl = new URL(url);
|
|
79
|
+
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
|
|
80
|
+
if (pathSegments.length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
// Build resolve endpoint path (handles both /shortCode and /templateSlug/shortCode)
|
|
84
|
+
let resolvePath;
|
|
85
|
+
if (pathSegments.length >= 2) {
|
|
86
|
+
resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}/${pathSegments[1]}`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}`;
|
|
90
|
+
}
|
|
91
|
+
// Collect fingerprint data for click attribution
|
|
92
|
+
try {
|
|
93
|
+
const fingerprint = await FingerprintCollector.collect();
|
|
94
|
+
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);
|
|
101
|
+
if (fingerprint.osVersion) {
|
|
102
|
+
queryParams.set('fp_pv', fingerprint.osVersion);
|
|
103
|
+
}
|
|
104
|
+
resolvePath += `?${queryParams.toString()}`;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// If fingerprint collection fails, still resolve without it
|
|
108
|
+
console.warn('[LinkForty] Fingerprint collection failed, resolving without fingerprint:', error);
|
|
109
|
+
}
|
|
110
|
+
return this.resolveFn(resolvePath);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse deep link URL and extract data locally (without server call).
|
|
114
|
+
* Used as fallback when server resolution fails or for non-LinkForty URLs.
|
|
42
115
|
*/
|
|
43
116
|
parseURL(url) {
|
|
44
117
|
try {
|
|
@@ -47,8 +120,9 @@ export class DeepLinkHandler {
|
|
|
47
120
|
if (this.baseUrl && !url.startsWith(this.baseUrl)) {
|
|
48
121
|
return null;
|
|
49
122
|
}
|
|
50
|
-
// Extract short code from path
|
|
51
|
-
const
|
|
123
|
+
// Extract short code from path (last segment handles both /shortCode and /templateSlug/shortCode)
|
|
124
|
+
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
|
|
125
|
+
const shortCode = pathSegments[pathSegments.length - 1];
|
|
52
126
|
if (!shortCode) {
|
|
53
127
|
return null;
|
|
54
128
|
}
|
package/dist/LinkFortySDK.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LinkFortySDK - Main SDK class for LinkForty deep linking and attribution
|
|
3
3
|
*/
|
|
4
|
-
import type { LinkFortyConfig, DeepLinkData, DeferredDeepLinkCallback, DeepLinkCallback } from './types';
|
|
4
|
+
import type { LinkFortyConfig, DeepLinkData, DeferredDeepLinkCallback, DeepLinkCallback, CreateLinkOptions, CreateLinkResult } from './types';
|
|
5
5
|
export declare class LinkFortySDK {
|
|
6
6
|
private config;
|
|
7
7
|
private deepLinkHandler;
|
|
@@ -21,13 +21,32 @@ export declare class LinkFortySDK {
|
|
|
21
21
|
*/
|
|
22
22
|
onDeferredDeepLink(callback: DeferredDeepLinkCallback): void;
|
|
23
23
|
/**
|
|
24
|
-
* Register callback for direct deep links (existing users)
|
|
24
|
+
* Register callback for direct deep links (existing users).
|
|
25
|
+
* When a LinkForty URL is detected, the SDK will resolve it via the server
|
|
26
|
+
* to retrieve the full deep link data (custom parameters, UTM params, etc.).
|
|
25
27
|
*/
|
|
26
28
|
onDeepLink(callback: DeepLinkCallback): void;
|
|
27
29
|
/**
|
|
28
30
|
* Track in-app event
|
|
29
31
|
*/
|
|
30
32
|
trackEvent(name: string, properties?: Record<string, any>): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Create a new short link via the LinkForty API.
|
|
35
|
+
*
|
|
36
|
+
* Requires an API key to be configured in the SDK init options.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const result = await LinkFortySDK.createLink({
|
|
41
|
+
* templateId: 'uuid-of-template',
|
|
42
|
+
* templateSlug: 'ToQs',
|
|
43
|
+
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
44
|
+
* title: 'My Video',
|
|
45
|
+
* });
|
|
46
|
+
* // result.url → 'https://go.example.com/ToQs/abc123'
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
createLink(options: CreateLinkOptions): Promise<CreateLinkResult>;
|
|
31
50
|
/**
|
|
32
51
|
* Get install ID
|
|
33
52
|
*/
|
|
@@ -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,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
|
|
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"}
|
package/dist/LinkFortySDK.js
CHANGED
|
@@ -67,7 +67,9 @@ export class LinkFortySDK {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
|
-
* Register callback for direct deep links (existing users)
|
|
70
|
+
* Register callback for direct deep links (existing users).
|
|
71
|
+
* When a LinkForty URL is detected, the SDK will resolve it via the server
|
|
72
|
+
* to retrieve the full deep link data (custom parameters, UTM params, etc.).
|
|
71
73
|
*/
|
|
72
74
|
onDeepLink(callback) {
|
|
73
75
|
if (!this.config) {
|
|
@@ -76,7 +78,19 @@ export class LinkFortySDK {
|
|
|
76
78
|
if (!this.deepLinkHandler) {
|
|
77
79
|
this.deepLinkHandler = new DeepLinkHandler();
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
// Create a resolver that wraps the private apiRequest method
|
|
82
|
+
const resolveFn = async (path) => {
|
|
83
|
+
try {
|
|
84
|
+
return await this.apiRequest(path);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (this.config?.debug) {
|
|
88
|
+
console.log('[LinkForty] URL resolve failed:', error);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
this.deepLinkHandler.initialize(this.config.baseUrl, callback, resolveFn);
|
|
80
94
|
}
|
|
81
95
|
/**
|
|
82
96
|
* Track in-app event
|
|
@@ -109,6 +123,62 @@ export class LinkFortySDK {
|
|
|
109
123
|
console.error('[LinkForty] Failed to track event:', error);
|
|
110
124
|
}
|
|
111
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Create a new short link via the LinkForty API.
|
|
128
|
+
*
|
|
129
|
+
* Requires an API key to be configured in the SDK init options.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const result = await LinkFortySDK.createLink({
|
|
134
|
+
* templateId: 'uuid-of-template',
|
|
135
|
+
* templateSlug: 'ToQs',
|
|
136
|
+
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
137
|
+
* title: 'My Video',
|
|
138
|
+
* });
|
|
139
|
+
* // result.url → 'https://go.example.com/ToQs/abc123'
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
async createLink(options) {
|
|
143
|
+
if (!this.config) {
|
|
144
|
+
throw new Error('SDK not initialized. Call init() first.');
|
|
145
|
+
}
|
|
146
|
+
if (!this.config.apiKey) {
|
|
147
|
+
throw new Error('API key required to create links. Pass apiKey in init().');
|
|
148
|
+
}
|
|
149
|
+
const body = {
|
|
150
|
+
templateId: options.templateId,
|
|
151
|
+
};
|
|
152
|
+
if (options.deepLinkParameters) {
|
|
153
|
+
body.deepLinkParameters = options.deepLinkParameters;
|
|
154
|
+
}
|
|
155
|
+
if (options.title) {
|
|
156
|
+
body.title = options.title;
|
|
157
|
+
}
|
|
158
|
+
if (options.description) {
|
|
159
|
+
body.description = options.description;
|
|
160
|
+
}
|
|
161
|
+
if (options.customCode) {
|
|
162
|
+
body.customCode = options.customCode;
|
|
163
|
+
}
|
|
164
|
+
if (options.utmParameters) {
|
|
165
|
+
body.utmParameters = options.utmParameters;
|
|
166
|
+
}
|
|
167
|
+
const response = await this.apiRequest('/api/links', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
});
|
|
171
|
+
const shortCode = response.short_code;
|
|
172
|
+
const url = `${this.config.baseUrl}/${options.templateSlug}/${shortCode}`;
|
|
173
|
+
if (this.config.debug) {
|
|
174
|
+
console.log('[LinkForty] Created link:', url);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
url,
|
|
178
|
+
shortCode,
|
|
179
|
+
linkId: response.id,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
112
182
|
/**
|
|
113
183
|
* Get install ID
|
|
114
184
|
*/
|
|
@@ -191,10 +261,18 @@ export class LinkFortySDK {
|
|
|
191
261
|
}
|
|
192
262
|
// If attributed, store deep link data
|
|
193
263
|
if (response.attributed && response.deepLinkData) {
|
|
194
|
-
|
|
264
|
+
// Normalize backend response to match DeepLinkData type
|
|
265
|
+
// Populate customParameters from deepLinkParameters so app developers
|
|
266
|
+
// get a consistent field regardless of direct vs deferred deep link path
|
|
267
|
+
const deepLinkData = {
|
|
268
|
+
...response.deepLinkData,
|
|
269
|
+
customParameters: response.deepLinkData.deepLinkParameters
|
|
270
|
+
|| response.deepLinkData.customParameters,
|
|
271
|
+
};
|
|
272
|
+
await AsyncStorage.setItem(STORAGE_KEYS.INSTALL_DATA, JSON.stringify(deepLinkData));
|
|
195
273
|
// Call deferred deep link callback if registered
|
|
196
274
|
if (this.deferredDeepLinkCallback) {
|
|
197
|
-
this.deferredDeepLinkCallback(
|
|
275
|
+
this.deferredDeepLinkCallback(deepLinkData);
|
|
198
276
|
}
|
|
199
277
|
if (this.config.debug) {
|
|
200
278
|
console.log('[LinkForty] Install attributed with confidence:', response.confidenceScore);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export { default } from './LinkFortySDK';
|
|
|
9
9
|
export { LinkFortySDK } from './LinkFortySDK';
|
|
10
10
|
export { FingerprintCollector } from './FingerprintCollector';
|
|
11
11
|
export { DeepLinkHandler } from './DeepLinkHandler';
|
|
12
|
-
export type { LinkFortyConfig, DeviceFingerprint, DeepLinkData, InstallAttributionResponse, EventData, DeferredDeepLinkCallback, DeepLinkCallback, } from './types';
|
|
12
|
+
export type { LinkFortyConfig, DeviceFingerprint, DeepLinkData, InstallAttributionResponse, EventData, DeferredDeepLinkCallback, DeepLinkCallback, ResolveFunction, CreateLinkOptions, CreateLinkResult, } from './types';
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGzC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,0BAA0B,EAC1B,SAAS,EACT,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGzC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,0BAA0B,EAC1B,SAAS,EACT,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -57,8 +57,12 @@ export interface DeepLinkData {
|
|
|
57
57
|
term?: string;
|
|
58
58
|
content?: string;
|
|
59
59
|
};
|
|
60
|
-
/** Custom query
|
|
60
|
+
/** Custom parameters (from URL query params on direct links, or from link configuration on deferred links) */
|
|
61
61
|
customParameters?: Record<string, string>;
|
|
62
|
+
/** In-app destination path (e.g., '/video/123') */
|
|
63
|
+
deepLinkPath?: string;
|
|
64
|
+
/** Custom URI scheme (e.g., 'myapp') */
|
|
65
|
+
appScheme?: string;
|
|
62
66
|
/** Click timestamp */
|
|
63
67
|
clickedAt?: string;
|
|
64
68
|
/** Link ID */
|
|
@@ -103,4 +107,46 @@ export type DeferredDeepLinkCallback = (deepLinkData: DeepLinkData | null) => vo
|
|
|
103
107
|
* Callback function for direct deep links
|
|
104
108
|
*/
|
|
105
109
|
export type DeepLinkCallback = (url: string, deepLinkData: DeepLinkData | null) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Function that resolves a LinkForty URL path to deep link data via the server.
|
|
112
|
+
* Used internally by DeepLinkHandler when the OS intercepts a LinkForty URL
|
|
113
|
+
* before the server can process the redirect.
|
|
114
|
+
*/
|
|
115
|
+
export type ResolveFunction = (path: string) => Promise<DeepLinkData | null>;
|
|
116
|
+
/**
|
|
117
|
+
* Options for creating a new short link via the LinkForty API
|
|
118
|
+
*/
|
|
119
|
+
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
|
+
/** Custom parameters embedded in the link (e.g., { route: 'VIDEO_VIEWER', id: '...' }) */
|
|
125
|
+
deepLinkParameters?: Record<string, string>;
|
|
126
|
+
/** Link title (for internal reference) */
|
|
127
|
+
title?: string;
|
|
128
|
+
/** Link description */
|
|
129
|
+
description?: string;
|
|
130
|
+
/** Custom short code (auto-generated if omitted) */
|
|
131
|
+
customCode?: string;
|
|
132
|
+
/** UTM parameters */
|
|
133
|
+
utmParameters?: {
|
|
134
|
+
source?: string;
|
|
135
|
+
medium?: string;
|
|
136
|
+
campaign?: string;
|
|
137
|
+
term?: string;
|
|
138
|
+
content?: string;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Result of creating a short link
|
|
143
|
+
*/
|
|
144
|
+
export interface CreateLinkResult {
|
|
145
|
+
/** Full shareable URL (e.g., 'https://go.example.com/tmpl/abc123') */
|
|
146
|
+
url: string;
|
|
147
|
+
/** The generated short code */
|
|
148
|
+
shortCode: string;
|
|
149
|
+
/** Link UUID */
|
|
150
|
+
linkId: string;
|
|
151
|
+
}
|
|
106
152
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,+BAA+B;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sBAAsB;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/DeepLinkHandler.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepLinkHandler - Handles Universal Links (iOS) and App Links (Android)
|
|
3
|
+
*
|
|
4
|
+
* When the OS intercepts a LinkForty URL via App Links or Universal Links,
|
|
5
|
+
* the redirect server is bypassed. This handler detects LinkForty URLs and
|
|
6
|
+
* resolves them via the server API to retrieve the full deep link data
|
|
7
|
+
* (custom parameters, UTM params, etc.) that would normally be appended
|
|
8
|
+
* during the redirect.
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
11
|
import { Linking } from 'react-native';
|
|
6
|
-
import
|
|
12
|
+
import { FingerprintCollector } from './FingerprintCollector';
|
|
13
|
+
import type { DeepLinkData, DeepLinkCallback, ResolveFunction } from './types';
|
|
7
14
|
|
|
8
15
|
export class DeepLinkHandler {
|
|
9
16
|
private callback: DeepLinkCallback | null = null;
|
|
10
17
|
private baseUrl: string | null = null;
|
|
18
|
+
private resolveFn: ResolveFunction | null = null;
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Initialize deep link listener
|
|
22
|
+
* @param baseUrl - LinkForty instance base URL for detecting LinkForty URLs
|
|
23
|
+
* @param callback - Callback invoked with parsed/resolved deep link data
|
|
24
|
+
* @param resolveFn - Optional function to resolve LinkForty URLs via the server API
|
|
14
25
|
*/
|
|
15
|
-
initialize(baseUrl: string, callback: DeepLinkCallback): void {
|
|
26
|
+
initialize(baseUrl: string, callback: DeepLinkCallback, resolveFn?: ResolveFunction): void {
|
|
16
27
|
this.baseUrl = baseUrl;
|
|
17
28
|
this.callback = callback;
|
|
29
|
+
this.resolveFn = resolveFn || null;
|
|
18
30
|
|
|
19
31
|
// Listen for deep links when app is already open
|
|
20
32
|
Linking.addEventListener('url', this.handleDeepLink);
|
|
@@ -36,19 +48,86 @@ export class DeepLinkHandler {
|
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
|
-
* Handle incoming deep link
|
|
51
|
+
* Handle incoming deep link.
|
|
52
|
+
* If the URL is a LinkForty URL and a resolver is available, resolves via the server.
|
|
53
|
+
* Falls back to local URL parsing on failure.
|
|
40
54
|
*/
|
|
41
|
-
private handleDeepLink = ({ url }: { url: string }): void => {
|
|
55
|
+
private handleDeepLink = async ({ url }: { url: string }): Promise<void> => {
|
|
42
56
|
if (!this.callback || !url) {
|
|
43
57
|
return;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
this.
|
|
60
|
+
// Parse locally first (for fallback and to detect LinkForty URLs)
|
|
61
|
+
const localData = this.parseURL(url);
|
|
62
|
+
|
|
63
|
+
// If this is a LinkForty URL and we have a resolver, try the server
|
|
64
|
+
if (localData && this.resolveFn && this.baseUrl && url.startsWith(this.baseUrl)) {
|
|
65
|
+
try {
|
|
66
|
+
const resolvedData = await this.resolveURL(url);
|
|
67
|
+
if (resolvedData) {
|
|
68
|
+
this.callback(url, resolvedData);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn('[LinkForty] Failed to resolve URL from server, falling back to local parse:', error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fallback to locally-parsed data
|
|
77
|
+
this.callback(url, localData);
|
|
48
78
|
};
|
|
49
79
|
|
|
50
80
|
/**
|
|
51
|
-
*
|
|
81
|
+
* Resolve a LinkForty URL via the server API.
|
|
82
|
+
* Extracts the path from the URL, collects fingerprint data for click attribution,
|
|
83
|
+
* and calls the resolve endpoint.
|
|
84
|
+
*/
|
|
85
|
+
private async resolveURL(url: string): Promise<DeepLinkData | null> {
|
|
86
|
+
if (!this.resolveFn) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parsedUrl = new URL(url);
|
|
91
|
+
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
|
|
92
|
+
|
|
93
|
+
if (pathSegments.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build resolve endpoint path (handles both /shortCode and /templateSlug/shortCode)
|
|
98
|
+
let resolvePath: string;
|
|
99
|
+
if (pathSegments.length >= 2) {
|
|
100
|
+
resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}/${pathSegments[1]}`;
|
|
101
|
+
} else {
|
|
102
|
+
resolvePath = `/api/sdk/v1/resolve/${pathSegments[0]}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Collect fingerprint data for click attribution
|
|
106
|
+
try {
|
|
107
|
+
const fingerprint = await FingerprintCollector.collect();
|
|
108
|
+
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);
|
|
115
|
+
if (fingerprint.osVersion) {
|
|
116
|
+
queryParams.set('fp_pv', fingerprint.osVersion);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
resolvePath += `?${queryParams.toString()}`;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// If fingerprint collection fails, still resolve without it
|
|
122
|
+
console.warn('[LinkForty] Fingerprint collection failed, resolving without fingerprint:', error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.resolveFn(resolvePath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse deep link URL and extract data locally (without server call).
|
|
130
|
+
* Used as fallback when server resolution fails or for non-LinkForty URLs.
|
|
52
131
|
*/
|
|
53
132
|
parseURL(url: string): DeepLinkData | null {
|
|
54
133
|
try {
|
|
@@ -59,8 +138,9 @@ export class DeepLinkHandler {
|
|
|
59
138
|
return null;
|
|
60
139
|
}
|
|
61
140
|
|
|
62
|
-
// Extract short code from path
|
|
63
|
-
const
|
|
141
|
+
// Extract short code from path (last segment handles both /shortCode and /templateSlug/shortCode)
|
|
142
|
+
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);
|
|
143
|
+
const shortCode = pathSegments[pathSegments.length - 1];
|
|
64
144
|
|
|
65
145
|
if (!shortCode) {
|
|
66
146
|
return null;
|
package/src/LinkFortySDK.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
DeepLinkData,
|
|
12
12
|
DeferredDeepLinkCallback,
|
|
13
13
|
DeepLinkCallback,
|
|
14
|
+
CreateLinkOptions,
|
|
15
|
+
CreateLinkResult,
|
|
14
16
|
} from './types';
|
|
15
17
|
|
|
16
18
|
const STORAGE_KEYS = {
|
|
@@ -88,7 +90,9 @@ export class LinkFortySDK {
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
|
-
* Register callback for direct deep links (existing users)
|
|
93
|
+
* Register callback for direct deep links (existing users).
|
|
94
|
+
* When a LinkForty URL is detected, the SDK will resolve it via the server
|
|
95
|
+
* to retrieve the full deep link data (custom parameters, UTM params, etc.).
|
|
92
96
|
*/
|
|
93
97
|
onDeepLink(callback: DeepLinkCallback): void {
|
|
94
98
|
if (!this.config) {
|
|
@@ -99,7 +103,19 @@ export class LinkFortySDK {
|
|
|
99
103
|
this.deepLinkHandler = new DeepLinkHandler();
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
// Create a resolver that wraps the private apiRequest method
|
|
107
|
+
const resolveFn = async (path: string): Promise<DeepLinkData | null> => {
|
|
108
|
+
try {
|
|
109
|
+
return await this.apiRequest<DeepLinkData>(path);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (this.config?.debug) {
|
|
112
|
+
console.log('[LinkForty] URL resolve failed:', error);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.deepLinkHandler.initialize(this.config.baseUrl, callback, resolveFn);
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
/**
|
|
@@ -137,6 +153,73 @@ export class LinkFortySDK {
|
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Create a new short link via the LinkForty API.
|
|
158
|
+
*
|
|
159
|
+
* Requires an API key to be configured in the SDK init options.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const result = await LinkFortySDK.createLink({
|
|
164
|
+
* templateId: 'uuid-of-template',
|
|
165
|
+
* templateSlug: 'ToQs',
|
|
166
|
+
* deepLinkParameters: { route: 'VIDEO_VIEWER', id: 'video-uuid' },
|
|
167
|
+
* title: 'My Video',
|
|
168
|
+
* });
|
|
169
|
+
* // result.url → 'https://go.example.com/ToQs/abc123'
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
async createLink(options: CreateLinkOptions): Promise<CreateLinkResult> {
|
|
173
|
+
if (!this.config) {
|
|
174
|
+
throw new Error('SDK not initialized. Call init() first.');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!this.config.apiKey) {
|
|
178
|
+
throw new Error('API key required to create links. Pass apiKey in init().');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const body: Record<string, unknown> = {
|
|
182
|
+
templateId: options.templateId,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (options.deepLinkParameters) {
|
|
186
|
+
body.deepLinkParameters = options.deepLinkParameters;
|
|
187
|
+
}
|
|
188
|
+
if (options.title) {
|
|
189
|
+
body.title = options.title;
|
|
190
|
+
}
|
|
191
|
+
if (options.description) {
|
|
192
|
+
body.description = options.description;
|
|
193
|
+
}
|
|
194
|
+
if (options.customCode) {
|
|
195
|
+
body.customCode = options.customCode;
|
|
196
|
+
}
|
|
197
|
+
if (options.utmParameters) {
|
|
198
|
+
body.utmParameters = options.utmParameters;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const response = await this.apiRequest<{ id: string; short_code: string }>(
|
|
202
|
+
'/api/links',
|
|
203
|
+
{
|
|
204
|
+
method: 'POST',
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const shortCode = response.short_code;
|
|
210
|
+
const url = `${this.config.baseUrl}/${options.templateSlug}/${shortCode}`;
|
|
211
|
+
|
|
212
|
+
if (this.config.debug) {
|
|
213
|
+
console.log('[LinkForty] Created link:', url);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
url,
|
|
218
|
+
shortCode,
|
|
219
|
+
linkId: response.id,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
140
223
|
/**
|
|
141
224
|
* Get install ID
|
|
142
225
|
*/
|
|
@@ -239,14 +322,23 @@ export class LinkFortySDK {
|
|
|
239
322
|
|
|
240
323
|
// If attributed, store deep link data
|
|
241
324
|
if (response.attributed && response.deepLinkData) {
|
|
325
|
+
// Normalize backend response to match DeepLinkData type
|
|
326
|
+
// Populate customParameters from deepLinkParameters so app developers
|
|
327
|
+
// get a consistent field regardless of direct vs deferred deep link path
|
|
328
|
+
const deepLinkData: DeepLinkData = {
|
|
329
|
+
...response.deepLinkData as DeepLinkData,
|
|
330
|
+
customParameters: (response.deepLinkData as any).deepLinkParameters
|
|
331
|
+
|| (response.deepLinkData as DeepLinkData).customParameters,
|
|
332
|
+
};
|
|
333
|
+
|
|
242
334
|
await AsyncStorage.setItem(
|
|
243
335
|
STORAGE_KEYS.INSTALL_DATA,
|
|
244
|
-
JSON.stringify(
|
|
336
|
+
JSON.stringify(deepLinkData)
|
|
245
337
|
);
|
|
246
338
|
|
|
247
339
|
// Call deferred deep link callback if registered
|
|
248
340
|
if (this.deferredDeepLinkCallback) {
|
|
249
|
-
this.deferredDeepLinkCallback(
|
|
341
|
+
this.deferredDeepLinkCallback(deepLinkData);
|
|
250
342
|
}
|
|
251
343
|
|
|
252
344
|
if (this.config.debug) {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -60,8 +60,12 @@ export interface DeepLinkData {
|
|
|
60
60
|
term?: string;
|
|
61
61
|
content?: string;
|
|
62
62
|
};
|
|
63
|
-
/** Custom query
|
|
63
|
+
/** Custom parameters (from URL query params on direct links, or from link configuration on deferred links) */
|
|
64
64
|
customParameters?: Record<string, string>;
|
|
65
|
+
/** In-app destination path (e.g., '/video/123') */
|
|
66
|
+
deepLinkPath?: string;
|
|
67
|
+
/** Custom URI scheme (e.g., 'myapp') */
|
|
68
|
+
appScheme?: string;
|
|
65
69
|
/** Click timestamp */
|
|
66
70
|
clickedAt?: string;
|
|
67
71
|
/** Link ID */
|
|
@@ -110,3 +114,48 @@ export type DeferredDeepLinkCallback = (deepLinkData: DeepLinkData | null) => vo
|
|
|
110
114
|
* Callback function for direct deep links
|
|
111
115
|
*/
|
|
112
116
|
export type DeepLinkCallback = (url: string, deepLinkData: DeepLinkData | null) => void;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Function that resolves a LinkForty URL path to deep link data via the server.
|
|
120
|
+
* Used internally by DeepLinkHandler when the OS intercepts a LinkForty URL
|
|
121
|
+
* before the server can process the redirect.
|
|
122
|
+
*/
|
|
123
|
+
export type ResolveFunction = (path: string) => Promise<DeepLinkData | null>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Options for creating a new short link via the LinkForty API
|
|
127
|
+
*/
|
|
128
|
+
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
|
+
/** Custom parameters embedded in the link (e.g., { route: 'VIDEO_VIEWER', id: '...' }) */
|
|
134
|
+
deepLinkParameters?: Record<string, string>;
|
|
135
|
+
/** Link title (for internal reference) */
|
|
136
|
+
title?: string;
|
|
137
|
+
/** Link description */
|
|
138
|
+
description?: string;
|
|
139
|
+
/** Custom short code (auto-generated if omitted) */
|
|
140
|
+
customCode?: string;
|
|
141
|
+
/** UTM parameters */
|
|
142
|
+
utmParameters?: {
|
|
143
|
+
source?: string;
|
|
144
|
+
medium?: string;
|
|
145
|
+
campaign?: string;
|
|
146
|
+
term?: string;
|
|
147
|
+
content?: string;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Result of creating a short link
|
|
153
|
+
*/
|
|
154
|
+
export interface CreateLinkResult {
|
|
155
|
+
/** Full shareable URL (e.g., 'https://go.example.com/tmpl/abc123') */
|
|
156
|
+
url: string;
|
|
157
|
+
/** The generated short code */
|
|
158
|
+
shortCode: string;
|
|
159
|
+
/** Link UUID */
|
|
160
|
+
linkId: string;
|
|
161
|
+
}
|