@li-nk.me/react-native-sdk 0.1.1 → 0.2.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 CHANGED
@@ -1,103 +1,37 @@
1
1
  # LinkMe React Native SDK
2
2
 
3
- Pure TypeScript React Native SDK for LinkMe — deep linking and attribution. No native modules required.
3
+ React Native SDK for LinkMe — deep linking and attribution.
4
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
5
+ - **Main Site**: [li-nk.me](https://li-nk.me)
6
+ - **Documentation**: [React Native Setup](https://li-nk.me/docs/developer/setup/react-native)
7
+ - **Package**: [npm](https://www.npmjs.com/package/@linkme/react-native-sdk)
10
8
 
11
9
  ## Installation
12
10
 
13
11
  ```bash
14
- npm install @li-nk.me/react-native-sdk
12
+ npm install @linkme/react-native-sdk
15
13
  ```
16
14
 
17
- ### Optional: iOS Pasteboard Support
15
+ ## Basic Usage
18
16
 
19
- For more reliable deferred deep linking on iOS, install `expo-clipboard`:
17
+ ```ts
18
+ import { configure } from '@linkme/react-native-sdk';
20
19
 
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
- }, []);
20
+ await configure({
21
+ appId: 'your_app_id',
22
+ appKey: 'your_app_key',
23
+ debug: __DEV__, // Optional: surface verbose logs for deferred/pasteboard flows
24
+ });
80
25
  ```
81
26
 
82
- ## UTM & Analytics
27
+ For full documentation, guides, and API reference, please visit our [Help Center](https://li-nk.me/docs/help).
83
28
 
84
- LinkMe automatically normalizes UTM parameters from deep links and deferred links. You can map these to your analytics provider (e.g., Firebase):
29
+ ## Debugging Deferred Links
85
30
 
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
- ```
31
+ - Pass `debug: true` (or `__DEV__`) to `configure` to emit `[LinkMe SDK]` logs for pasteboard and fingerprint claims.
32
+ - Check that Expo Clipboard is installed if you expect pasteboard-based iOS claims.
33
+ - Review server logs for `/api/deferred/claim` to verify fingerprint matches.
100
34
 
101
- The SDK uses React Native's built-in `Linking` API and requires an Expo config plugin for deep link configuration.
35
+ ## License
102
36
 
103
- License: MIT
37
+ Apache-2.0
package/lib/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type LinkMeConfig = {
10
10
  baseUrl?: string;
11
11
  appId?: string;
12
12
  appKey?: string;
13
+ debug?: boolean;
13
14
  /**
14
15
  * @deprecated Pasteboard is now controlled from the Portal (App Settings → iOS).
15
16
  * The SDK automatically checks pasteboard on iOS if expo-clipboard is installed.
package/lib/index.js CHANGED
@@ -13,6 +13,7 @@ class LinkMeController {
13
13
  var _a, _b;
14
14
  this.ready = false;
15
15
  this.advertisingConsent = false;
16
+ this.debug = false;
16
17
  this.lastPayload = null;
17
18
  this.listeners = new Set();
18
19
  this.pendingUrls = [];
@@ -26,9 +27,13 @@ class LinkMeController {
26
27
  this.linking = (_b = deps === null || deps === void 0 ? void 0 : deps.linking) !== null && _b !== void 0 ? _b : Linking;
27
28
  }
28
29
  async configure(config) {
30
+ var _a;
29
31
  const normalized = normalizeConfig(config);
30
32
  this.config = normalized;
31
33
  this.advertisingConsent = !!config.includeAdvertisingId;
34
+ const fallbackDev = Boolean(globalThis === null || globalThis === void 0 ? void 0 : globalThis.__DEV__);
35
+ this.debug = (_a = normalized.debug) !== null && _a !== void 0 ? _a : fallbackDev;
36
+ this.logDebug('configure', { baseUrl: normalized.baseUrl, debug: this.debug });
32
37
  this.subscribeToLinking();
33
38
  this.ready = true;
34
39
  await this.drainPending();
@@ -67,14 +72,19 @@ class LinkMeController {
67
72
  async claimDeferredIfAvailable() {
68
73
  const cfg = this.config;
69
74
  if (!cfg) {
75
+ this.logDebug('deferred.skip_no_config');
70
76
  return null;
71
77
  }
78
+ this.logDebug('deferred.claim.start', { platform: Platform.OS });
72
79
  // 1. On iOS, try to read CID from pasteboard first (if expo-clipboard is available)
73
80
  if (Platform.OS === 'ios' && (Clipboard === null || Clipboard === void 0 ? void 0 : Clipboard.getStringAsync)) {
81
+ this.logDebug('deferred.pasteboard.check');
74
82
  const pasteboardPayload = await this.tryClaimFromPasteboard(cfg);
75
83
  if (pasteboardPayload) {
84
+ this.logDebug('deferred.pasteboard.payload');
76
85
  return pasteboardPayload;
77
86
  }
87
+ this.logDebug('deferred.pasteboard.no_match');
78
88
  }
79
89
  // 2. Fallback to probabilistic fingerprint matching
80
90
  try {
@@ -85,48 +95,65 @@ class LinkMeController {
85
95
  if (device) {
86
96
  body.device = device;
87
97
  }
98
+ this.logDebug('deferred.fingerprint.request');
88
99
  const res = await this.fetchImpl(`${cfg.apiBaseUrl}/deferred/claim`, {
89
100
  method: 'POST',
90
101
  headers: this.buildHeaders(true),
91
102
  body: JSON.stringify(body),
92
103
  });
93
104
  if (!res.ok) {
105
+ this.logDebug('deferred.fingerprint.http_error', { status: res.status });
94
106
  return null;
95
107
  }
96
108
  const payload = await this.parsePayload(res);
97
109
  if (payload) {
110
+ this.logDebug('deferred.fingerprint.payload', { linkId: payload.linkId, duplicate: payload === null || payload === void 0 ? void 0 : payload.duplicate });
98
111
  this.emit(payload);
99
112
  }
113
+ else {
114
+ this.logDebug('deferred.fingerprint.no_match');
115
+ }
100
116
  return payload;
101
117
  }
102
- catch {
118
+ catch (err) {
119
+ this.logDebug('deferred.fingerprint.error', { message: err instanceof Error ? err.message : String(err) });
103
120
  return null;
104
121
  }
105
122
  }
106
123
  async tryClaimFromPasteboard(cfg) {
107
124
  try {
108
125
  if (!(Clipboard === null || Clipboard === void 0 ? void 0 : Clipboard.getStringAsync)) {
126
+ this.logDebug('pasteboard.skip_module');
109
127
  return null;
110
128
  }
129
+ this.logDebug('pasteboard.read');
111
130
  const pasteStr = await Clipboard.getStringAsync();
112
131
  if (!pasteStr) {
132
+ this.logDebug('pasteboard.empty');
113
133
  return null;
114
134
  }
115
135
  // Check if the clipboard contains a li-nk.me URL with a cid parameter
116
136
  const cid = this.extractCidFromUrl(pasteStr, cfg.baseUrl);
117
137
  if (!cid) {
138
+ this.logDebug('pasteboard.no_cid', { hasClipboard: true });
118
139
  return null;
119
140
  }
141
+ this.logDebug('pasteboard.cid_found');
120
142
  // Resolve the CID to get the payload
121
143
  const payload = await this.resolveCidWithConfig(cfg, cid);
122
144
  if (payload) {
123
145
  this.emit(payload);
124
146
  // Track pasteboard claim
125
- this.track('claim', { claim_type: 'pasteboard' });
147
+ this.logDebug('pasteboard.payload', { linkId: payload.linkId });
148
+ void this.track('claim', { claim_type: 'pasteboard' });
149
+ }
150
+ else {
151
+ this.logDebug('pasteboard.resolve_empty');
126
152
  }
127
153
  return payload;
128
154
  }
129
- catch {
155
+ catch (err) {
156
+ this.logDebug('pasteboard.error', { message: err instanceof Error ? err.message : String(err) });
130
157
  return null;
131
158
  }
132
159
  }
@@ -136,13 +163,21 @@ class LinkMeController {
136
163
  // Check if the URL is from our domain
137
164
  const baseHost = new URL(baseUrl).host;
138
165
  if (!url.host.endsWith(baseHost) && url.host !== baseHost.replace(/^www\./, '')) {
166
+ this.logDebug('pasteboard.url_mismatch', { host: url.host });
139
167
  return null;
140
168
  }
141
169
  // Extract the cid parameter
142
170
  const cid = url.searchParams.get('cid');
171
+ if (cid) {
172
+ this.logDebug('pasteboard.url_cid_present');
173
+ }
174
+ else {
175
+ this.logDebug('pasteboard.url_no_cid');
176
+ }
143
177
  return cid || null;
144
178
  }
145
- catch {
179
+ catch (err) {
180
+ this.logDebug('pasteboard.url_parse_error', { message: err instanceof Error ? err.message : String(err) });
146
181
  return null;
147
182
  }
148
183
  }
@@ -194,11 +229,14 @@ class LinkMeController {
194
229
  body: JSON.stringify(body),
195
230
  });
196
231
  if (!res.ok) {
197
- await res.text().catch(() => undefined);
232
+ const text = await res.text().catch(() => undefined);
233
+ this.logDebug('app_events.http_error', { status: res.status, body: text });
234
+ console.warn('[LinkMe SDK] app_events.http_error', { status: res.status, body: text });
198
235
  }
199
236
  }
200
237
  catch {
201
- /* noop */
238
+ this.logDebug('app_events.network_error');
239
+ console.warn('[LinkMe SDK] app_events.network_error');
202
240
  }
203
241
  }
204
242
  onLink(listener) {
@@ -385,7 +423,8 @@ class LinkMeController {
385
423
  const json = (await res.json());
386
424
  return json !== null && json !== void 0 ? json : null;
387
425
  }
388
- catch {
426
+ catch (err) {
427
+ this.logDebug('payload.parse_error', { message: err instanceof Error ? err.message : String(err) });
389
428
  return null;
390
429
  }
391
430
  }
@@ -400,6 +439,22 @@ class LinkMeController {
400
439
  }
401
440
  }
402
441
  }
442
+ logDebug(event, details) {
443
+ if (!this.debug) {
444
+ return;
445
+ }
446
+ try {
447
+ if (details) {
448
+ console.log(`[LinkMe SDK] ${event}`, details);
449
+ }
450
+ else {
451
+ console.log(`[LinkMe SDK] ${event}`);
452
+ }
453
+ }
454
+ catch {
455
+ /* noop */
456
+ }
457
+ }
403
458
  }
404
459
  function normalizeConfig(config) {
405
460
  const baseUrl = (config === null || config === void 0 ? void 0 : config.baseUrl) || 'https://li-nk.me';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@li-nk.me/react-native-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Pure TypeScript React Native SDK for LinkMe deep and deferred links.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -24,6 +24,10 @@
24
24
  "sdk"
25
25
  ],
26
26
  "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/r-dev-limited/li-nk.me-react-native-sdk"
30
+ },
27
31
  "peerDependencies": {
28
32
  "react": ">=18",
29
33
  "react-native": ">=0.72.0",
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export type LinkMeConfig = {
21
21
  baseUrl?: string;
22
22
  appId?: string;
23
23
  appKey?: string;
24
+ debug?: boolean;
24
25
  /**
25
26
  * @deprecated Pasteboard is now controlled from the Portal (App Settings → iOS).
26
27
  * The SDK automatically checks pasteboard on iOS if expo-clipboard is installed.
@@ -52,6 +53,7 @@ class LinkMeController {
52
53
  private config: NormalizedConfig | undefined;
53
54
  private ready = false;
54
55
  private advertisingConsent = false;
56
+ private debug = false;
55
57
  private userId: string | undefined;
56
58
  private lastPayload: LinkMePayload | null = null;
57
59
  private readonly listeners = new Set<Listener>();
@@ -74,6 +76,9 @@ class LinkMeController {
74
76
  const normalized = normalizeConfig(config);
75
77
  this.config = normalized;
76
78
  this.advertisingConsent = !!config.includeAdvertisingId;
79
+ const fallbackDev = Boolean((globalThis as any)?.__DEV__);
80
+ this.debug = normalized.debug ?? fallbackDev;
81
+ this.logDebug('configure', { baseUrl: normalized.baseUrl, debug: this.debug });
77
82
  this.subscribeToLinking();
78
83
  this.ready = true;
79
84
  await this.drainPending();
@@ -113,15 +118,20 @@ class LinkMeController {
113
118
  async claimDeferredIfAvailable(): Promise<LinkMePayload | null> {
114
119
  const cfg = this.config;
115
120
  if (!cfg) {
121
+ this.logDebug('deferred.skip_no_config');
116
122
  return null;
117
123
  }
124
+ this.logDebug('deferred.claim.start', { platform: Platform.OS });
118
125
 
119
126
  // 1. On iOS, try to read CID from pasteboard first (if expo-clipboard is available)
120
127
  if (Platform.OS === 'ios' && Clipboard?.getStringAsync) {
128
+ this.logDebug('deferred.pasteboard.check');
121
129
  const pasteboardPayload = await this.tryClaimFromPasteboard(cfg);
122
130
  if (pasteboardPayload) {
131
+ this.logDebug('deferred.pasteboard.payload');
123
132
  return pasteboardPayload;
124
133
  }
134
+ this.logDebug('deferred.pasteboard.no_match');
125
135
  }
126
136
 
127
137
  // 2. Fallback to probabilistic fingerprint matching
@@ -133,20 +143,26 @@ class LinkMeController {
133
143
  if (device) {
134
144
  body.device = device;
135
145
  }
146
+ this.logDebug('deferred.fingerprint.request');
136
147
  const res = await this.fetchImpl(`${cfg.apiBaseUrl}/deferred/claim`, {
137
148
  method: 'POST',
138
149
  headers: this.buildHeaders(true),
139
150
  body: JSON.stringify(body),
140
151
  });
141
152
  if (!res.ok) {
153
+ this.logDebug('deferred.fingerprint.http_error', { status: res.status });
142
154
  return null;
143
155
  }
144
156
  const payload = await this.parsePayload(res);
145
157
  if (payload) {
158
+ this.logDebug('deferred.fingerprint.payload', { linkId: payload.linkId, duplicate: (payload as any)?.duplicate });
146
159
  this.emit(payload);
160
+ } else {
161
+ this.logDebug('deferred.fingerprint.no_match');
147
162
  }
148
163
  return payload;
149
- } catch {
164
+ } catch (err) {
165
+ this.logDebug('deferred.fingerprint.error', { message: err instanceof Error ? err.message : String(err) });
150
166
  return null;
151
167
  }
152
168
  }
@@ -154,26 +170,35 @@ class LinkMeController {
154
170
  private async tryClaimFromPasteboard(cfg: NormalizedConfig): Promise<LinkMePayload | null> {
155
171
  try {
156
172
  if (!Clipboard?.getStringAsync) {
173
+ this.logDebug('pasteboard.skip_module');
157
174
  return null;
158
175
  }
176
+ this.logDebug('pasteboard.read');
159
177
  const pasteStr = await Clipboard.getStringAsync();
160
178
  if (!pasteStr) {
179
+ this.logDebug('pasteboard.empty');
161
180
  return null;
162
181
  }
163
182
  // Check if the clipboard contains a li-nk.me URL with a cid parameter
164
183
  const cid = this.extractCidFromUrl(pasteStr, cfg.baseUrl);
165
184
  if (!cid) {
185
+ this.logDebug('pasteboard.no_cid', { hasClipboard: true });
166
186
  return null;
167
187
  }
188
+ this.logDebug('pasteboard.cid_found');
168
189
  // Resolve the CID to get the payload
169
190
  const payload = await this.resolveCidWithConfig(cfg, cid);
170
191
  if (payload) {
171
192
  this.emit(payload);
172
193
  // Track pasteboard claim
173
- this.track('claim', { claim_type: 'pasteboard' });
194
+ this.logDebug('pasteboard.payload', { linkId: payload.linkId });
195
+ void this.track('claim', { claim_type: 'pasteboard' });
196
+ } else {
197
+ this.logDebug('pasteboard.resolve_empty');
174
198
  }
175
199
  return payload;
176
- } catch {
200
+ } catch (err) {
201
+ this.logDebug('pasteboard.error', { message: err instanceof Error ? err.message : String(err) });
177
202
  return null;
178
203
  }
179
204
  }
@@ -184,12 +209,19 @@ class LinkMeController {
184
209
  // Check if the URL is from our domain
185
210
  const baseHost = new URL(baseUrl).host;
186
211
  if (!url.host.endsWith(baseHost) && url.host !== baseHost.replace(/^www\./, '')) {
212
+ this.logDebug('pasteboard.url_mismatch', { host: url.host });
187
213
  return null;
188
214
  }
189
215
  // Extract the cid parameter
190
216
  const cid = url.searchParams.get('cid');
217
+ if (cid) {
218
+ this.logDebug('pasteboard.url_cid_present');
219
+ } else {
220
+ this.logDebug('pasteboard.url_no_cid');
221
+ }
191
222
  return cid || null;
192
- } catch {
223
+ } catch (err) {
224
+ this.logDebug('pasteboard.url_parse_error', { message: err instanceof Error ? err.message : String(err) });
193
225
  return null;
194
226
  }
195
227
  }
@@ -245,10 +277,13 @@ class LinkMeController {
245
277
  body: JSON.stringify(body),
246
278
  });
247
279
  if (!res.ok) {
248
- await res.text().catch(() => undefined);
280
+ const text = await res.text().catch(() => undefined);
281
+ this.logDebug('app_events.http_error', { status: res.status, body: text });
282
+ console.warn('[LinkMe SDK] app_events.http_error', { status: res.status, body: text });
249
283
  }
250
284
  } catch {
251
- /* noop */
285
+ this.logDebug('app_events.network_error');
286
+ console.warn('[LinkMe SDK] app_events.network_error');
252
287
  }
253
288
  }
254
289
 
@@ -436,7 +471,8 @@ class LinkMeController {
436
471
  try {
437
472
  const json = (await res.json()) as LinkMePayload;
438
473
  return json ?? null;
439
- } catch {
474
+ } catch (err) {
475
+ this.logDebug('payload.parse_error', { message: err instanceof Error ? err.message : String(err) });
440
476
  return null;
441
477
  }
442
478
  }
@@ -451,6 +487,21 @@ class LinkMeController {
451
487
  }
452
488
  }
453
489
  }
490
+
491
+ private logDebug(event: string, details?: Record<string, any>): void {
492
+ if (!this.debug) {
493
+ return;
494
+ }
495
+ try {
496
+ if (details) {
497
+ console.log(`[LinkMe SDK] ${event}`, details);
498
+ } else {
499
+ console.log(`[LinkMe SDK] ${event}`);
500
+ }
501
+ } catch {
502
+ /* noop */
503
+ }
504
+ }
454
505
  }
455
506
 
456
507
  function normalizeConfig(config: LinkMeConfig): NormalizedConfig {