@li-nk.me/react-native-sdk 0.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/README.md +103 -0
- package/lib/index.d.ts +56 -0
- package/lib/index.js +507 -0
- package/package.json +54 -0
- package/plugin/app.plugin.js +131 -0
- package/src/index.ts +580 -0
- package/src/react-native-stub.d.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# LinkMe React Native SDK
|
|
2
|
+
|
|
3
|
+
Pure TypeScript React Native SDK for LinkMe — deep linking and attribution. No native modules required.
|
|
4
|
+
|
|
5
|
+
- Quick Start: See `QUICK_START.md`
|
|
6
|
+
- Repo docs: ../../docs/help/docs/setup/react-native.md
|
|
7
|
+
- Hosted docs: https://li-nk.me/resources/developer/setup/react-native
|
|
8
|
+
- Example app: See `example-expo/` directory
|
|
9
|
+
- Android troubleshooting: See [Android Troubleshooting](https://li-nk.me/resources/developer/setup/android#troubleshooting) section in Android setup docs
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @li-nk.me/react-native-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Optional: iOS Pasteboard Support
|
|
18
|
+
|
|
19
|
+
For more reliable deferred deep linking on iOS, install `expo-clipboard`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx expo install expo-clipboard
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When installed, the SDK will automatically check the iOS pasteboard for a li-nk.me URL before falling back to fingerprint matching. Pasteboard must also be enabled in the Portal (App Settings → iOS).
|
|
26
|
+
|
|
27
|
+
### Expo Configuration
|
|
28
|
+
|
|
29
|
+
Add the config plugin to your `app.json` or `app.config.js`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"expo": {
|
|
34
|
+
"plugins": [
|
|
35
|
+
[
|
|
36
|
+
"@li-nk.me/react-native-sdk/plugin/app.plugin.js",
|
|
37
|
+
{
|
|
38
|
+
"hosts": ["link.example.com"],
|
|
39
|
+
"associatedDomains": ["applinks:link.example.com"],
|
|
40
|
+
"schemes": ["com.example.app"]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Initialize the SDK in your root layout (e.g., `app/_layout.tsx`):
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { configure, onLink, getInitialLink, claimDeferredIfAvailable, track } from '@li-nk.me/react-native-sdk';
|
|
54
|
+
|
|
55
|
+
// ... inside your component
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
configure({
|
|
58
|
+
appId: 'your-app-id',
|
|
59
|
+
appKey: 'your-app-key',
|
|
60
|
+
// ... other options
|
|
61
|
+
}).then(() => {
|
|
62
|
+
// 1. Listen for deep links (foreground)
|
|
63
|
+
onLink((payload) => handlePayload(payload));
|
|
64
|
+
|
|
65
|
+
// 2. Check for initial link (cold start)
|
|
66
|
+
getInitialLink().then((payload) => {
|
|
67
|
+
if (payload) handlePayload(payload);
|
|
68
|
+
else {
|
|
69
|
+
// 3. Check for deferred link (first install)
|
|
70
|
+
claimDeferredIfAvailable().then((payload) => {
|
|
71
|
+
if (payload) handlePayload(payload);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 4. Track open
|
|
77
|
+
track('open');
|
|
78
|
+
});
|
|
79
|
+
}, []);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## UTM & Analytics
|
|
83
|
+
|
|
84
|
+
LinkMe automatically normalizes UTM parameters from deep links and deferred links. You can map these to your analytics provider (e.g., Firebase):
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
function handlePayload(payload: LinkMePayload) {
|
|
88
|
+
if (payload.utm) {
|
|
89
|
+
// Example: Map to Firebase Analytics
|
|
90
|
+
// analytics().logEvent('campaign_details', {
|
|
91
|
+
// source: payload.utm.source,
|
|
92
|
+
// medium: payload.utm.medium,
|
|
93
|
+
// campaign: payload.utm.campaign,
|
|
94
|
+
// term: payload.utm.term,
|
|
95
|
+
// content: payload.utm.content,
|
|
96
|
+
// });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The SDK uses React Native's built-in `Linking` API and requires an Expo config plugin for deep link configuration.
|
|
102
|
+
|
|
103
|
+
License: MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Linking } from 'react-native';
|
|
2
|
+
export type LinkMePayload = {
|
|
3
|
+
linkId?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
params?: Record<string, string>;
|
|
6
|
+
utm?: Record<string, string>;
|
|
7
|
+
custom?: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
export type LinkMeConfig = {
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
appId?: string;
|
|
12
|
+
appKey?: string;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Pasteboard is now controlled from the Portal (App Settings → iOS).
|
|
15
|
+
* The SDK automatically checks pasteboard on iOS if expo-clipboard is installed.
|
|
16
|
+
* This parameter is ignored.
|
|
17
|
+
*/
|
|
18
|
+
enablePasteboard?: boolean;
|
|
19
|
+
sendDeviceInfo?: boolean;
|
|
20
|
+
includeVendorId?: boolean;
|
|
21
|
+
includeAdvertisingId?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type Listener = (payload: LinkMePayload) => void;
|
|
24
|
+
type LinkingLike = typeof Linking;
|
|
25
|
+
type FetchLike = typeof fetch;
|
|
26
|
+
type ControllerDeps = {
|
|
27
|
+
fetchImpl?: FetchLike;
|
|
28
|
+
linking?: LinkingLike;
|
|
29
|
+
};
|
|
30
|
+
export declare function configure(config: LinkMeConfig): Promise<void>;
|
|
31
|
+
export declare function getInitialLink(): Promise<LinkMePayload | null>;
|
|
32
|
+
export declare function handleUrl(url: string): Promise<boolean>;
|
|
33
|
+
export declare function claimDeferredIfAvailable(): Promise<LinkMePayload | null>;
|
|
34
|
+
export declare function setUserId(userId: string): Promise<void>;
|
|
35
|
+
export declare function setAdvertisingConsent(granted: boolean): Promise<void>;
|
|
36
|
+
export declare function setReady(): Promise<void>;
|
|
37
|
+
export declare function track(event: string, properties?: Record<string, any>): Promise<void>;
|
|
38
|
+
export declare function onLink(listener: Listener): {
|
|
39
|
+
remove: () => void;
|
|
40
|
+
};
|
|
41
|
+
export declare class LinkMeClient {
|
|
42
|
+
private readonly controller;
|
|
43
|
+
constructor(deps?: ControllerDeps);
|
|
44
|
+
configure(config: LinkMeConfig): Promise<void>;
|
|
45
|
+
getInitialLink(): Promise<LinkMePayload | null>;
|
|
46
|
+
handleUrl(url: string): Promise<boolean>;
|
|
47
|
+
claimDeferredIfAvailable(): Promise<LinkMePayload | null>;
|
|
48
|
+
setUserId(userId: string): Promise<void>;
|
|
49
|
+
setAdvertisingConsent(granted: boolean): Promise<void>;
|
|
50
|
+
setReady(): Promise<void>;
|
|
51
|
+
track(event: string, properties?: Record<string, any>): Promise<void>;
|
|
52
|
+
onLink(listener: Listener): {
|
|
53
|
+
remove: () => void;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export default LinkMeClient;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { Linking, Platform } from 'react-native';
|
|
2
|
+
// Optional expo-clipboard for pasteboard support on iOS
|
|
3
|
+
let Clipboard = null;
|
|
4
|
+
try {
|
|
5
|
+
// Dynamic import to make expo-clipboard optional
|
|
6
|
+
Clipboard = require('expo-clipboard');
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// expo-clipboard not installed - pasteboard will be skipped
|
|
10
|
+
}
|
|
11
|
+
class LinkMeController {
|
|
12
|
+
constructor(deps) {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
this.ready = false;
|
|
15
|
+
this.advertisingConsent = false;
|
|
16
|
+
this.lastPayload = null;
|
|
17
|
+
this.listeners = new Set();
|
|
18
|
+
this.pendingUrls = [];
|
|
19
|
+
this.linkingSubscription = null;
|
|
20
|
+
this.initialUrlChecked = false;
|
|
21
|
+
const impl = (_a = deps === null || deps === void 0 ? void 0 : deps.fetchImpl) !== null && _a !== void 0 ? _a : globalThis === null || globalThis === void 0 ? void 0 : globalThis.fetch;
|
|
22
|
+
if (typeof impl !== 'function') {
|
|
23
|
+
throw new Error('fetch is not available; provide deps.fetchImpl');
|
|
24
|
+
}
|
|
25
|
+
this.fetchImpl = impl.bind(globalThis);
|
|
26
|
+
this.linking = (_b = deps === null || deps === void 0 ? void 0 : deps.linking) !== null && _b !== void 0 ? _b : Linking;
|
|
27
|
+
}
|
|
28
|
+
async configure(config) {
|
|
29
|
+
const normalized = normalizeConfig(config);
|
|
30
|
+
this.config = normalized;
|
|
31
|
+
this.advertisingConsent = !!config.includeAdvertisingId;
|
|
32
|
+
this.subscribeToLinking();
|
|
33
|
+
this.ready = true;
|
|
34
|
+
await this.drainPending();
|
|
35
|
+
}
|
|
36
|
+
async getInitialLink() {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
if (this.lastPayload) {
|
|
39
|
+
return this.lastPayload;
|
|
40
|
+
}
|
|
41
|
+
if (this.initialUrlChecked) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
this.initialUrlChecked = true;
|
|
45
|
+
try {
|
|
46
|
+
const url = await ((_b = (_a = this.linking) === null || _a === void 0 ? void 0 : _a.getInitialURL) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
47
|
+
if (url) {
|
|
48
|
+
return await this.processUrl(url);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* noop */
|
|
53
|
+
}
|
|
54
|
+
return this.lastPayload;
|
|
55
|
+
}
|
|
56
|
+
async handleUrl(url) {
|
|
57
|
+
if (!url) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!this.ready || !this.config) {
|
|
61
|
+
this.pendingUrls.push(url);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const payload = await this.processUrl(url);
|
|
65
|
+
return payload != null;
|
|
66
|
+
}
|
|
67
|
+
async claimDeferredIfAvailable() {
|
|
68
|
+
const cfg = this.config;
|
|
69
|
+
if (!cfg) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// 1. On iOS, try to read CID from pasteboard first (if expo-clipboard is available)
|
|
73
|
+
if (Platform.OS === 'ios' && (Clipboard === null || Clipboard === void 0 ? void 0 : Clipboard.getStringAsync)) {
|
|
74
|
+
const pasteboardPayload = await this.tryClaimFromPasteboard(cfg);
|
|
75
|
+
if (pasteboardPayload) {
|
|
76
|
+
return pasteboardPayload;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 2. Fallback to probabilistic fingerprint matching
|
|
80
|
+
try {
|
|
81
|
+
const body = {
|
|
82
|
+
platform: Platform.OS,
|
|
83
|
+
};
|
|
84
|
+
const device = cfg.sendDeviceInfo === false ? undefined : this.buildDevicePayload();
|
|
85
|
+
if (device) {
|
|
86
|
+
body.device = device;
|
|
87
|
+
}
|
|
88
|
+
const res = await this.fetchImpl(`${cfg.apiBaseUrl}/deferred/claim`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: this.buildHeaders(true),
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const payload = await this.parsePayload(res);
|
|
97
|
+
if (payload) {
|
|
98
|
+
this.emit(payload);
|
|
99
|
+
}
|
|
100
|
+
return payload;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async tryClaimFromPasteboard(cfg) {
|
|
107
|
+
try {
|
|
108
|
+
if (!(Clipboard === null || Clipboard === void 0 ? void 0 : Clipboard.getStringAsync)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const pasteStr = await Clipboard.getStringAsync();
|
|
112
|
+
if (!pasteStr) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Check if the clipboard contains a li-nk.me URL with a cid parameter
|
|
116
|
+
const cid = this.extractCidFromUrl(pasteStr, cfg.baseUrl);
|
|
117
|
+
if (!cid) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
// Resolve the CID to get the payload
|
|
121
|
+
const payload = await this.resolveCidWithConfig(cfg, cid);
|
|
122
|
+
if (payload) {
|
|
123
|
+
this.emit(payload);
|
|
124
|
+
// Track pasteboard claim
|
|
125
|
+
this.track('claim', { claim_type: 'pasteboard' });
|
|
126
|
+
}
|
|
127
|
+
return payload;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
extractCidFromUrl(str, baseUrl) {
|
|
134
|
+
try {
|
|
135
|
+
const url = new URL(str);
|
|
136
|
+
// Check if the URL is from our domain
|
|
137
|
+
const baseHost = new URL(baseUrl).host;
|
|
138
|
+
if (!url.host.endsWith(baseHost) && url.host !== baseHost.replace(/^www\./, '')) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
// Extract the cid parameter
|
|
142
|
+
const cid = url.searchParams.get('cid');
|
|
143
|
+
return cid || null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async resolveCidWithConfig(cfg, cid) {
|
|
150
|
+
try {
|
|
151
|
+
const res = await this.fetchImpl(`${cfg.apiBaseUrl}/deeplink?cid=${encodeURIComponent(cid)}`, {
|
|
152
|
+
method: 'GET',
|
|
153
|
+
headers: this.buildHeaders(false),
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return this.parsePayload(res);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
setUserId(userId) {
|
|
165
|
+
this.userId = userId;
|
|
166
|
+
}
|
|
167
|
+
setAdvertisingConsent(granted) {
|
|
168
|
+
this.advertisingConsent = granted;
|
|
169
|
+
}
|
|
170
|
+
async setReady() {
|
|
171
|
+
this.ready = true;
|
|
172
|
+
await this.drainPending();
|
|
173
|
+
}
|
|
174
|
+
async track(event, properties) {
|
|
175
|
+
const cfg = this.config;
|
|
176
|
+
if (!cfg || !event) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const body = {
|
|
181
|
+
event,
|
|
182
|
+
platform: Platform.OS,
|
|
183
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
184
|
+
};
|
|
185
|
+
if (this.userId) {
|
|
186
|
+
body.userId = this.userId;
|
|
187
|
+
}
|
|
188
|
+
if (properties) {
|
|
189
|
+
body.props = properties;
|
|
190
|
+
}
|
|
191
|
+
const res = await this.fetchImpl(`${cfg.apiBaseUrl}/app-events`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: this.buildHeaders(true),
|
|
194
|
+
body: JSON.stringify(body),
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
await res.text().catch(() => undefined);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* noop */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
onLink(listener) {
|
|
205
|
+
this.listeners.add(listener);
|
|
206
|
+
return {
|
|
207
|
+
remove: () => {
|
|
208
|
+
this.listeners.delete(listener);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
subscribeToLinking() {
|
|
213
|
+
if (this.linkingSubscription || !this.linking || typeof this.linking.addEventListener !== 'function') {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const handler = (event) => {
|
|
217
|
+
const incoming = typeof event === 'string' ? event : event === null || event === void 0 ? void 0 : event.url;
|
|
218
|
+
if (incoming) {
|
|
219
|
+
this.handleIncomingUrl(incoming);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const maybeSubscription = this.linking.addEventListener('url', handler);
|
|
223
|
+
if (maybeSubscription && typeof maybeSubscription.remove === 'function') {
|
|
224
|
+
this.linkingSubscription = maybeSubscription;
|
|
225
|
+
}
|
|
226
|
+
else if (typeof this.linking.removeEventListener === 'function') {
|
|
227
|
+
this.linkingSubscription = { remove: () => this.linking.removeEventListener('url', handler) };
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.linkingSubscription = { remove: () => { } };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
handleIncomingUrl(url) {
|
|
234
|
+
if (!url) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!this.ready || !this.config) {
|
|
238
|
+
this.pendingUrls.push(url);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
void this.processUrl(url);
|
|
242
|
+
}
|
|
243
|
+
async drainPending() {
|
|
244
|
+
if (!this.ready || !this.config) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
while (this.pendingUrls.length > 0) {
|
|
248
|
+
const url = this.pendingUrls.shift();
|
|
249
|
+
if (url) {
|
|
250
|
+
await this.processUrl(url);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async processUrl(url) {
|
|
255
|
+
var _a;
|
|
256
|
+
const cfg = this.config;
|
|
257
|
+
if (!cfg) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const parsed = this.parseUrl(url);
|
|
261
|
+
if (!parsed) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const cid = (_a = parsed.searchParams) === null || _a === void 0 ? void 0 : _a.get('cid');
|
|
265
|
+
let payload = null;
|
|
266
|
+
if (cid) {
|
|
267
|
+
payload = await this.resolveCid(cid);
|
|
268
|
+
}
|
|
269
|
+
else if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
270
|
+
payload = await this.resolveUniversalLink(url);
|
|
271
|
+
}
|
|
272
|
+
if (payload) {
|
|
273
|
+
this.emit(payload);
|
|
274
|
+
}
|
|
275
|
+
return payload;
|
|
276
|
+
}
|
|
277
|
+
parseUrl(url) {
|
|
278
|
+
try {
|
|
279
|
+
return new URL(url);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
try {
|
|
283
|
+
return new URL(url, 'https://placeholder.local');
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async resolveCid(cid) {
|
|
291
|
+
const cfg = this.config;
|
|
292
|
+
if (!cfg) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const headers = this.buildHeaders(false);
|
|
297
|
+
const device = cfg.sendDeviceInfo === false ? undefined : this.buildDevicePayload();
|
|
298
|
+
if (device) {
|
|
299
|
+
headers['x-linkme-device'] = JSON.stringify(device);
|
|
300
|
+
}
|
|
301
|
+
const res = await this.fetchImpl(`${cfg.apiBaseUrl}/deeplink?cid=${encodeURIComponent(cid)}`, {
|
|
302
|
+
method: 'GET',
|
|
303
|
+
headers,
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return await this.parsePayload(res);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async resolveUniversalLink(url) {
|
|
315
|
+
const cfg = this.config;
|
|
316
|
+
if (!cfg) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const body = { url };
|
|
321
|
+
const device = cfg.sendDeviceInfo === false ? undefined : this.buildDevicePayload();
|
|
322
|
+
if (device) {
|
|
323
|
+
body.device = device;
|
|
324
|
+
}
|
|
325
|
+
const res = await this.fetchImpl(`${cfg.apiBaseUrl}/deeplink/resolve-url`, {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: this.buildHeaders(true),
|
|
328
|
+
body: JSON.stringify(body),
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return await this.parsePayload(res);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
buildHeaders(includeContentType) {
|
|
340
|
+
var _a, _b;
|
|
341
|
+
const headers = { Accept: 'application/json' };
|
|
342
|
+
if (includeContentType) {
|
|
343
|
+
headers['Content-Type'] = 'application/json';
|
|
344
|
+
}
|
|
345
|
+
if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.appId) {
|
|
346
|
+
headers['x-app-id'] = this.config.appId;
|
|
347
|
+
}
|
|
348
|
+
if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.appKey) {
|
|
349
|
+
headers['x-api-key'] = this.config.appKey;
|
|
350
|
+
}
|
|
351
|
+
return headers;
|
|
352
|
+
}
|
|
353
|
+
buildDevicePayload() {
|
|
354
|
+
const cfg = this.config;
|
|
355
|
+
if (!cfg) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const device = {
|
|
359
|
+
platform: Platform.OS,
|
|
360
|
+
};
|
|
361
|
+
const version = Platform.Version;
|
|
362
|
+
if (version !== undefined) {
|
|
363
|
+
device.osVersion = typeof version === 'string' ? version : String(version);
|
|
364
|
+
}
|
|
365
|
+
const locale = getLocale();
|
|
366
|
+
if (locale) {
|
|
367
|
+
device.locale = locale;
|
|
368
|
+
}
|
|
369
|
+
const timezone = getTimezone();
|
|
370
|
+
if (timezone) {
|
|
371
|
+
device.timezone = timezone;
|
|
372
|
+
}
|
|
373
|
+
const consent = {};
|
|
374
|
+
if (cfg.includeVendorId) {
|
|
375
|
+
consent.vendor = true;
|
|
376
|
+
}
|
|
377
|
+
if (this.advertisingConsent) {
|
|
378
|
+
consent.advertising = true;
|
|
379
|
+
}
|
|
380
|
+
device.consent = consent;
|
|
381
|
+
return device;
|
|
382
|
+
}
|
|
383
|
+
async parsePayload(res) {
|
|
384
|
+
try {
|
|
385
|
+
const json = (await res.json());
|
|
386
|
+
return json !== null && json !== void 0 ? json : null;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
emit(payload) {
|
|
393
|
+
this.lastPayload = payload;
|
|
394
|
+
for (const listener of this.listeners) {
|
|
395
|
+
try {
|
|
396
|
+
listener(payload);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* noop */
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function normalizeConfig(config) {
|
|
405
|
+
const baseUrl = (config === null || config === void 0 ? void 0 : config.baseUrl) || 'https://li-nk.me';
|
|
406
|
+
const trimmed = baseUrl.replace(/\/$/, '');
|
|
407
|
+
return {
|
|
408
|
+
...config,
|
|
409
|
+
baseUrl: trimmed,
|
|
410
|
+
apiBaseUrl: `${trimmed}/api`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function getLocale() {
|
|
414
|
+
var _a;
|
|
415
|
+
try {
|
|
416
|
+
const intl = Intl === null || Intl === void 0 ? void 0 : Intl.DateTimeFormat;
|
|
417
|
+
if (typeof intl === 'function') {
|
|
418
|
+
const resolved = new intl().resolvedOptions();
|
|
419
|
+
const locale = (_a = resolved === null || resolved === void 0 ? void 0 : resolved.locale) !== null && _a !== void 0 ? _a : resolved === null || resolved === void 0 ? void 0 : resolved.localeMatcher;
|
|
420
|
+
return typeof locale === 'string' ? locale : undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
/* noop */
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
function getTimezone() {
|
|
429
|
+
var _a;
|
|
430
|
+
try {
|
|
431
|
+
const intl = Intl === null || Intl === void 0 ? void 0 : Intl.DateTimeFormat;
|
|
432
|
+
if (typeof intl === 'function') {
|
|
433
|
+
const resolved = new intl().resolvedOptions();
|
|
434
|
+
const tz = (_a = resolved === null || resolved === void 0 ? void 0 : resolved.timeZone) !== null && _a !== void 0 ? _a : resolved === null || resolved === void 0 ? void 0 : resolved.timeZoneName;
|
|
435
|
+
return typeof tz === 'string' ? tz : undefined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
/* noop */
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
const defaultController = new LinkMeController();
|
|
444
|
+
export async function configure(config) {
|
|
445
|
+
await defaultController.configure(config);
|
|
446
|
+
}
|
|
447
|
+
export function getInitialLink() {
|
|
448
|
+
return defaultController.getInitialLink();
|
|
449
|
+
}
|
|
450
|
+
export function handleUrl(url) {
|
|
451
|
+
return defaultController.handleUrl(url);
|
|
452
|
+
}
|
|
453
|
+
export function claimDeferredIfAvailable() {
|
|
454
|
+
return defaultController.claimDeferredIfAvailable();
|
|
455
|
+
}
|
|
456
|
+
export function setUserId(userId) {
|
|
457
|
+
defaultController.setUserId(userId);
|
|
458
|
+
return Promise.resolve();
|
|
459
|
+
}
|
|
460
|
+
export function setAdvertisingConsent(granted) {
|
|
461
|
+
defaultController.setAdvertisingConsent(granted);
|
|
462
|
+
return Promise.resolve();
|
|
463
|
+
}
|
|
464
|
+
export function setReady() {
|
|
465
|
+
return defaultController.setReady();
|
|
466
|
+
}
|
|
467
|
+
export function track(event, properties) {
|
|
468
|
+
return defaultController.track(event, properties);
|
|
469
|
+
}
|
|
470
|
+
export function onLink(listener) {
|
|
471
|
+
return defaultController.onLink(listener);
|
|
472
|
+
}
|
|
473
|
+
export class LinkMeClient {
|
|
474
|
+
constructor(deps) {
|
|
475
|
+
this.controller = new LinkMeController(deps);
|
|
476
|
+
}
|
|
477
|
+
configure(config) {
|
|
478
|
+
return this.controller.configure(config);
|
|
479
|
+
}
|
|
480
|
+
getInitialLink() {
|
|
481
|
+
return this.controller.getInitialLink();
|
|
482
|
+
}
|
|
483
|
+
handleUrl(url) {
|
|
484
|
+
return this.controller.handleUrl(url);
|
|
485
|
+
}
|
|
486
|
+
claimDeferredIfAvailable() {
|
|
487
|
+
return this.controller.claimDeferredIfAvailable();
|
|
488
|
+
}
|
|
489
|
+
setUserId(userId) {
|
|
490
|
+
this.controller.setUserId(userId);
|
|
491
|
+
return Promise.resolve();
|
|
492
|
+
}
|
|
493
|
+
setAdvertisingConsent(granted) {
|
|
494
|
+
this.controller.setAdvertisingConsent(granted);
|
|
495
|
+
return Promise.resolve();
|
|
496
|
+
}
|
|
497
|
+
setReady() {
|
|
498
|
+
return this.controller.setReady();
|
|
499
|
+
}
|
|
500
|
+
track(event, properties) {
|
|
501
|
+
return this.controller.track(event, properties);
|
|
502
|
+
}
|
|
503
|
+
onLink(listener) {
|
|
504
|
+
return this.controller.onLink(listener);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
export default LinkMeClient;
|