@tolinku/react-native-sdk 0.2.0 → 0.4.0

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
@@ -142,12 +142,41 @@ if (link) {
142
142
  console.log(link.deep_link_path); // e.g. "/merchant/xyz"
143
143
  }
144
144
 
145
- // Claim by device signal matching (auto-detects timezone, language, screen size)
145
+ // Claim by device signal matching. Timezone, language, screen size, pixel ratio and
146
+ // OS version are detected for you.
146
147
  const link = await Tolinku.deferred.claimBySignals({
147
- appspaceId: 'your_appspace_id',
148
+ appspaceId: '64f0a1b2c3d4e5f60718',
148
149
  });
149
150
  ```
150
151
 
152
+ On Android the Play Install Referrer is the deterministic mechanism: a Tolinku
153
+ link attaches a token to the store URL, Play keeps it through the install, and
154
+ this SDK reads it back on first launch. It names the exact click, survives for
155
+ days, and does not depend on the network the device was on. Device signals are
156
+ the fallback, and the only option on iOS, where no equivalent exists.
157
+
158
+ Prefer `claimDeferredLink()` over choosing a mechanism yourself:
159
+
160
+ ```ts
161
+ // Referrer first, device signals as the fallback.
162
+ const link = await Tolinku.deferred.claimDeferredLink({
163
+ appspaceId: '64f0a1b2c3d4e5f60718',
164
+ });
165
+ if (link) routeTo(link.deep_link_path);
166
+ ```
167
+
168
+ Call it once on first launch. Calling again is safe, but a claim is consumed the
169
+ first time it succeeds, so a second call returns nothing.
170
+
171
+ The Android side is bundled with this package, so there is nothing else to
172
+ install. Autolinking is per-platform, so an iOS-only app never builds it. In
173
+ Expo Go the native module is absent and Android falls back to signal matching;
174
+ a development build gets the referrer.
175
+
176
+ `appspaceId` is your Appspace ID, not your subdomain or slug. Copy it from the dashboard
177
+ under **Integrate** or **Settings**. It looks like `64f0a1b2c3d4e5f60718`.
178
+
179
+
151
180
  ### In-App Messages (React Native Component)
152
181
 
153
182
  Display server-configured in-app messages using the `<TolinkuMessages>` component. Drop it anywhere in your React Native component tree to automatically fetch and render messages as a modal overlay. Messages are created and managed from the Tolinku dashboard without shipping app updates.
@@ -0,0 +1,74 @@
1
+ // Android side of @tolinku/react-native-sdk.
2
+ //
3
+ // Exists only to read the Play Install Referrer, which is the deterministic way
4
+ // a deferred link survives an install on Android. It is bundled rather than
5
+ // delegated to a third-party package because the referrer token is Tolinku's
6
+ // own mechanism: we mint it on the link and read it back here.
7
+ //
8
+ // Autolinking is per-platform, so this is not built when compiling for iOS. An
9
+ // iOS-only app pays nothing for its presence.
10
+ //
11
+ // To verify compilation outside a host app:
12
+ // printf 'rootProject.name = "t"\n' > android/settings.gradle
13
+ // ../../android-sdk/gradlew -p android assembleRelease \
14
+ // -PtolinkuReactAndroidVersion=0.73.11 -Pandroid.useAndroidX=true
15
+ // rm android/settings.gradle
16
+
17
+ buildscript {
18
+ repositories {
19
+ google()
20
+ mavenCentral()
21
+ }
22
+ dependencies {
23
+ classpath("com.android.tools.build:gradle:8.1.4")
24
+ }
25
+ }
26
+
27
+ apply plugin: "com.android.library"
28
+
29
+ def safeExtGet(prop, fallback) {
30
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
31
+ }
32
+
33
+ android {
34
+ // The peer range starts at React Native 0.72, whose AGP 7.4 supports
35
+ // namespace, so the old manifest package attribute is not needed.
36
+ namespace "com.tolinku.rnsdk"
37
+
38
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
39
+
40
+ defaultConfig {
41
+ // The Install Referrer library needs 21; the host app decides above that.
42
+ minSdkVersion safeExtGet("minSdkVersion", 21)
43
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
44
+ }
45
+
46
+ compileOptions {
47
+ sourceCompatibility JavaVersion.VERSION_1_8
48
+ targetCompatibility JavaVersion.VERSION_1_8
49
+ }
50
+
51
+ lintOptions {
52
+ abortOnError false
53
+ }
54
+ }
55
+
56
+ repositories {
57
+ google()
58
+ mavenCentral()
59
+ }
60
+
61
+ dependencies {
62
+ // Unversioned on purpose: the React Native Gradle plugin in the host app
63
+ // pins this to whatever version the app is on, so the library follows the
64
+ // app rather than forcing one. A version is supplied only for the standalone
65
+ // build used to verify compilation outside an app.
66
+ def rnVersion = project.findProperty("tolinkuReactAndroidVersion")
67
+ if (rnVersion) {
68
+ implementation "com.facebook.react:react-android:${rnVersion}"
69
+ } else {
70
+ implementation "com.facebook.react:react-android"
71
+ }
72
+
73
+ implementation "com.android.installreferrer:installreferrer:2.2"
74
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,107 @@
1
+ package com.tolinku.rnsdk;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.android.installreferrer.api.InstallReferrerClient;
6
+ import com.android.installreferrer.api.InstallReferrerStateListener;
7
+ import com.facebook.react.bridge.Promise;
8
+ import com.facebook.react.bridge.ReactApplicationContext;
9
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
10
+ import com.facebook.react.bridge.ReactMethod;
11
+
12
+ import java.util.concurrent.atomic.AtomicBoolean;
13
+
14
+ /**
15
+ * Reads the Play Install Referrer.
16
+ *
17
+ * A Tolinku link sends an Android visitor to the store with
18
+ * {@code referrer=tolk_token=<token>} attached. Play keeps that string through
19
+ * the install and hands it back on first launch, which names the exact click
20
+ * instead of inferring it from device signals.
21
+ *
22
+ * Only the raw referrer string is returned. Finding our token inside it is done
23
+ * in JavaScript, where it is covered by tests that run on every platform rather
24
+ * than only where an Android toolchain exists.
25
+ */
26
+ public class TolinkuInstallReferrerModule extends ReactContextBaseJavaModule {
27
+
28
+ public static final String NAME = "TolinkuInstallReferrer";
29
+
30
+ public TolinkuInstallReferrerModule(ReactApplicationContext reactContext) {
31
+ super(reactContext);
32
+ }
33
+
34
+ @Override
35
+ @NonNull
36
+ public String getName() {
37
+ return NAME;
38
+ }
39
+
40
+ /**
41
+ * Resolves with the raw referrer string, or null when there is nothing to
42
+ * report.
43
+ *
44
+ * Never rejects. An organic install, a device without Play Services, and a
45
+ * store other than Play are all ordinary outcomes rather than errors, and
46
+ * the caller falls back to signal matching for every one of them. Rejecting
47
+ * would turn a routine absence into an unhandled promise on first launch.
48
+ */
49
+ @ReactMethod
50
+ public void getInstallReferrer(final Promise promise) {
51
+ final InstallReferrerClient client;
52
+ try {
53
+ client = InstallReferrerClient.newBuilder(getReactApplicationContext()).build();
54
+ } catch (Throwable t) {
55
+ promise.resolve(null);
56
+ return;
57
+ }
58
+
59
+ // The Play listener can fire more than once on some devices, and
60
+ // resolving a promise twice is an error in React Native.
61
+ final AtomicBoolean settled = new AtomicBoolean(false);
62
+
63
+ try {
64
+ client.startConnection(new InstallReferrerStateListener() {
65
+ @Override
66
+ public void onInstallReferrerSetupFinished(int responseCode) {
67
+ if (!settled.compareAndSet(false, true)) {
68
+ return;
69
+ }
70
+ String referrer = null;
71
+ try {
72
+ if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
73
+ referrer = client.getInstallReferrer().getInstallReferrer();
74
+ }
75
+ } catch (Throwable t) {
76
+ referrer = null;
77
+ } finally {
78
+ endQuietly(client);
79
+ }
80
+ promise.resolve(referrer);
81
+ }
82
+
83
+ @Override
84
+ public void onInstallReferrerServiceDisconnected() {
85
+ if (!settled.compareAndSet(false, true)) {
86
+ return;
87
+ }
88
+ endQuietly(client);
89
+ promise.resolve(null);
90
+ }
91
+ });
92
+ } catch (Throwable t) {
93
+ if (settled.compareAndSet(false, true)) {
94
+ endQuietly(client);
95
+ promise.resolve(null);
96
+ }
97
+ }
98
+ }
99
+
100
+ private static void endQuietly(InstallReferrerClient client) {
101
+ try {
102
+ client.endConnection();
103
+ } catch (Throwable ignored) {
104
+ // Already gone.
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,30 @@
1
+ package com.tolinku.rnsdk;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.ReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.uimanager.ViewManager;
9
+
10
+ import java.util.ArrayList;
11
+ import java.util.Collections;
12
+ import java.util.List;
13
+
14
+ /** Registers the install referrer module with React Native's autolinking. */
15
+ public class TolinkuInstallReferrerPackage implements ReactPackage {
16
+
17
+ @Override
18
+ @NonNull
19
+ public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
20
+ List<NativeModule> modules = new ArrayList<>();
21
+ modules.add(new TolinkuInstallReferrerModule(reactContext));
22
+ return modules;
23
+ }
24
+
25
+ @Override
26
+ @NonNull
27
+ public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
28
+ return Collections.emptyList();
29
+ }
30
+ }
package/dist/index.d.mts CHANGED
@@ -111,6 +111,10 @@ interface ClaimBySignalsOptions {
111
111
  language?: string;
112
112
  screenWidth?: number;
113
113
  screenHeight?: number;
114
+ /** Defaults to `PixelRatio.get()`. Override only if you report a custom screen size. */
115
+ devicePixelRatio?: number;
116
+ /** Defaults to `Platform.Version`. Compared on the major component only. */
117
+ osVersion?: string;
114
118
  }
115
119
  /** In-app message from the API */
116
120
  interface Message {
@@ -315,13 +319,68 @@ declare class Referrals {
315
319
  }>;
316
320
  }
317
321
 
322
+ /**
323
+ * Pull our token out of a Play referrer string.
324
+ *
325
+ * The referrer is shared. A developer's own `utm_source` and anything else they
326
+ * attached sit in the same string, so the token is found among the pairs rather
327
+ * than assumed to be the whole value. A percent-encoded `%3D` is tolerated
328
+ * because Play normally decodes it, and that assumption is not worth a lost
329
+ * install if it is ever wrong.
330
+ */
331
+ declare function parseInstallReferrer(referrer: string | null | undefined): string | null;
332
+ /** Anything that can hand back a Play referrer string. */
333
+ type ReferrerProvider = () => Promise<string | null> | string | null;
334
+ /**
335
+ * Best effort at the referrer token for this install.
336
+ *
337
+ * Android only, and null everywhere else: there is no equivalent on iOS, which
338
+ * is why signal matching exists at all. Never throws, because attribution must
339
+ * not be able to fail a first launch.
340
+ */
341
+ declare function getInstallReferrerToken(provider?: ReferrerProvider): Promise<string | null>;
342
+
318
343
  declare class Deferred {
319
344
  private client;
320
345
  constructor(client: HttpClient);
321
346
  /** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
322
- claimByToken(token: string): Promise<DeferredLink | null>;
347
+ claimByToken(token: string, appspaceId?: string): Promise<DeferredLink | null>;
348
+ /**
349
+ * Recover the link that led to this install, trying both mechanisms.
350
+ *
351
+ * The Play Install Referrer is asked first on Android: it names the exact
352
+ * click, survives for days, and does not care which network the device was
353
+ * on. Device signals are the fallback, and the only option on iOS, where no
354
+ * equivalent exists.
355
+ *
356
+ * Call once on first launch. Safe to call again, but a claim is consumed the
357
+ * first time it succeeds, so a second call returns null.
358
+ *
359
+ * Reading the referrer needs a native Play Services binding, which this
360
+ * package deliberately does not bundle. Pass `referrerProvider`, or install a
361
+ * supported referrer package and it is used automatically. Without either,
362
+ * Android falls back to signal matching.
363
+ */
364
+ claimDeferredLink(options: {
365
+ appspaceId: string;
366
+ referrerProvider?: ReferrerProvider;
367
+ /** Claim again even if an attempt was already recorded. For tests. */
368
+ force?: boolean;
369
+ }): Promise<DeferredLink | null>;
370
+ private alreadyAttempted;
371
+ private rememberAttempt;
323
372
  /** Claim a deferred deep link by device signal matching */
324
373
  claimBySignals(options: ClaimBySignalsOptions): Promise<DeferredLink | null>;
374
+ /**
375
+ * The signal claim, with whether the server actually answered.
376
+ *
377
+ * `settled` separates "nothing is waiting for this device", which no amount
378
+ * of asking will change, from "the request never got there". Both surface as
379
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
380
+ * apart: recording an attempt that never reached the server would spend an
381
+ * install's one chance at attribution on a dropped connection.
382
+ */
383
+ private attemptSignals;
325
384
  }
326
385
 
327
386
  /**
@@ -334,7 +393,7 @@ declare class Deferred {
334
393
  * await Tolinku.track('signup', { source: 'onboarding' });
335
394
  */
336
395
  declare class Tolinku {
337
- static readonly VERSION = "0.1.0";
396
+ static readonly VERSION = "0.4.0";
338
397
  private static client;
339
398
  private static analyticsInstance;
340
399
  private static ecommerceInstance;
@@ -348,6 +407,15 @@ declare class Tolinku {
348
407
  * If init() is called a second time without calling destroy() first,
349
408
  * a warning is logged and the existing instance is returned.
350
409
  */
410
+ /**
411
+ * Configure the SDK.
412
+ *
413
+ * The name the Android, iOS and Flutter SDKs use for this. {@link init} does
414
+ * the same thing and still works; it is what this package shipped and
415
+ * breaking it would serve nobody. It is meant for deprecation later, once
416
+ * moving off it is a one-line change rather than a surprise.
417
+ */
418
+ static configure(config: TolinkuConfig): void;
351
419
  static init(config: TolinkuConfig): void;
352
420
  /** Check whether the SDK has been initialized. */
353
421
  static isConfigured(): boolean;
@@ -413,4 +481,4 @@ declare function TolinkuMessages({ trigger, triggerValue, onDismiss, onButtonPre
413
481
  */
414
482
  declare function isSafeUrl(url: string): boolean;
415
483
 
416
- export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, isSafeUrl };
484
+ export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type ReferrerProvider, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, getInstallReferrerToken, isSafeUrl, parseInstallReferrer };
package/dist/index.d.ts CHANGED
@@ -111,6 +111,10 @@ interface ClaimBySignalsOptions {
111
111
  language?: string;
112
112
  screenWidth?: number;
113
113
  screenHeight?: number;
114
+ /** Defaults to `PixelRatio.get()`. Override only if you report a custom screen size. */
115
+ devicePixelRatio?: number;
116
+ /** Defaults to `Platform.Version`. Compared on the major component only. */
117
+ osVersion?: string;
114
118
  }
115
119
  /** In-app message from the API */
116
120
  interface Message {
@@ -315,13 +319,68 @@ declare class Referrals {
315
319
  }>;
316
320
  }
317
321
 
322
+ /**
323
+ * Pull our token out of a Play referrer string.
324
+ *
325
+ * The referrer is shared. A developer's own `utm_source` and anything else they
326
+ * attached sit in the same string, so the token is found among the pairs rather
327
+ * than assumed to be the whole value. A percent-encoded `%3D` is tolerated
328
+ * because Play normally decodes it, and that assumption is not worth a lost
329
+ * install if it is ever wrong.
330
+ */
331
+ declare function parseInstallReferrer(referrer: string | null | undefined): string | null;
332
+ /** Anything that can hand back a Play referrer string. */
333
+ type ReferrerProvider = () => Promise<string | null> | string | null;
334
+ /**
335
+ * Best effort at the referrer token for this install.
336
+ *
337
+ * Android only, and null everywhere else: there is no equivalent on iOS, which
338
+ * is why signal matching exists at all. Never throws, because attribution must
339
+ * not be able to fail a first launch.
340
+ */
341
+ declare function getInstallReferrerToken(provider?: ReferrerProvider): Promise<string | null>;
342
+
318
343
  declare class Deferred {
319
344
  private client;
320
345
  constructor(client: HttpClient);
321
346
  /** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
322
- claimByToken(token: string): Promise<DeferredLink | null>;
347
+ claimByToken(token: string, appspaceId?: string): Promise<DeferredLink | null>;
348
+ /**
349
+ * Recover the link that led to this install, trying both mechanisms.
350
+ *
351
+ * The Play Install Referrer is asked first on Android: it names the exact
352
+ * click, survives for days, and does not care which network the device was
353
+ * on. Device signals are the fallback, and the only option on iOS, where no
354
+ * equivalent exists.
355
+ *
356
+ * Call once on first launch. Safe to call again, but a claim is consumed the
357
+ * first time it succeeds, so a second call returns null.
358
+ *
359
+ * Reading the referrer needs a native Play Services binding, which this
360
+ * package deliberately does not bundle. Pass `referrerProvider`, or install a
361
+ * supported referrer package and it is used automatically. Without either,
362
+ * Android falls back to signal matching.
363
+ */
364
+ claimDeferredLink(options: {
365
+ appspaceId: string;
366
+ referrerProvider?: ReferrerProvider;
367
+ /** Claim again even if an attempt was already recorded. For tests. */
368
+ force?: boolean;
369
+ }): Promise<DeferredLink | null>;
370
+ private alreadyAttempted;
371
+ private rememberAttempt;
323
372
  /** Claim a deferred deep link by device signal matching */
324
373
  claimBySignals(options: ClaimBySignalsOptions): Promise<DeferredLink | null>;
374
+ /**
375
+ * The signal claim, with whether the server actually answered.
376
+ *
377
+ * `settled` separates "nothing is waiting for this device", which no amount
378
+ * of asking will change, from "the request never got there". Both surface as
379
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
380
+ * apart: recording an attempt that never reached the server would spend an
381
+ * install's one chance at attribution on a dropped connection.
382
+ */
383
+ private attemptSignals;
325
384
  }
326
385
 
327
386
  /**
@@ -334,7 +393,7 @@ declare class Deferred {
334
393
  * await Tolinku.track('signup', { source: 'onboarding' });
335
394
  */
336
395
  declare class Tolinku {
337
- static readonly VERSION = "0.1.0";
396
+ static readonly VERSION = "0.4.0";
338
397
  private static client;
339
398
  private static analyticsInstance;
340
399
  private static ecommerceInstance;
@@ -348,6 +407,15 @@ declare class Tolinku {
348
407
  * If init() is called a second time without calling destroy() first,
349
408
  * a warning is logged and the existing instance is returned.
350
409
  */
410
+ /**
411
+ * Configure the SDK.
412
+ *
413
+ * The name the Android, iOS and Flutter SDKs use for this. {@link init} does
414
+ * the same thing and still works; it is what this package shipped and
415
+ * breaking it would serve nobody. It is meant for deprecation later, once
416
+ * moving off it is a one-line change rather than a surprise.
417
+ */
418
+ static configure(config: TolinkuConfig): void;
351
419
  static init(config: TolinkuConfig): void;
352
420
  /** Check whether the SDK has been initialized. */
353
421
  static isConfigured(): boolean;
@@ -413,4 +481,4 @@ declare function TolinkuMessages({ trigger, triggerValue, onDismiss, onButtonPre
413
481
  */
414
482
  declare function isSafeUrl(url: string): boolean;
415
483
 
416
- export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, isSafeUrl };
484
+ export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type ReferrerProvider, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, getInstallReferrerToken, isSafeUrl, parseInstallReferrer };
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
  var AsyncStorage__default = /*#__PURE__*/_interopDefault(AsyncStorage);
11
11
 
12
12
  // src/types.ts
13
- var SDK_VERSION = "0.1.0";
13
+ var SDK_VERSION = "0.4.0";
14
14
 
15
15
  // src/debug.ts
16
16
  var _debugEnabled = false;
@@ -630,41 +630,150 @@ var Referrals = class {
630
630
  });
631
631
  }
632
632
  };
633
+ var TOKEN_KEY = "tolk_token";
634
+ function parseInstallReferrer(referrer) {
635
+ if (!referrer || !referrer.trim()) return null;
636
+ let decoded = referrer;
637
+ try {
638
+ decoded = decodeURIComponent(referrer);
639
+ } catch {
640
+ }
641
+ const pair = decoded.split("&").map((p) => p.trim()).find((p) => p.startsWith(`${TOKEN_KEY}=`));
642
+ if (!pair) return null;
643
+ const token = pair.slice(TOKEN_KEY.length + 1);
644
+ return token.trim() ? token : null;
645
+ }
646
+ function nativeProvider() {
647
+ const native = reactNative.NativeModules?.TolinkuInstallReferrer;
648
+ if (!native || typeof native.getInstallReferrer !== "function") return null;
649
+ return () => native.getInstallReferrer();
650
+ }
651
+ async function getInstallReferrerToken(provider) {
652
+ if (reactNative.Platform.OS !== "android") return null;
653
+ const source = provider ?? nativeProvider();
654
+ if (!source) return null;
655
+ try {
656
+ const referrer = await source();
657
+ return parseInstallReferrer(referrer);
658
+ } catch (err) {
659
+ debugWarn(`Install referrer lookup failed: ${err.message}`);
660
+ return null;
661
+ }
662
+ }
663
+
664
+ // src/deferred.ts
665
+ var CLAIMED_KEY = "tolinku_deferred_claimed";
633
666
  var Deferred = class {
634
667
  constructor(client) {
635
668
  this.client = client;
636
669
  }
637
670
  /** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
638
- async claimByToken(token) {
671
+ async claimByToken(token, appspaceId) {
639
672
  if (!token || !token.trim()) {
640
673
  throw new Error("Tolinku: token is required and must not be blank for claimByToken.");
641
674
  }
642
675
  try {
643
- return await this.client.getPublic("/v1/api/deferred/claim", { token });
676
+ return await this.client.getPublic("/v1/api/deferred/claim", {
677
+ token,
678
+ ...appspaceId ? { appspace_id: appspaceId } : {}
679
+ });
644
680
  } catch (err) {
645
681
  debugWarn(`Deferred claimByToken failed: ${err.message}`);
646
682
  return null;
647
683
  }
648
684
  }
685
+ /**
686
+ * Recover the link that led to this install, trying both mechanisms.
687
+ *
688
+ * The Play Install Referrer is asked first on Android: it names the exact
689
+ * click, survives for days, and does not care which network the device was
690
+ * on. Device signals are the fallback, and the only option on iOS, where no
691
+ * equivalent exists.
692
+ *
693
+ * Call once on first launch. Safe to call again, but a claim is consumed the
694
+ * first time it succeeds, so a second call returns null.
695
+ *
696
+ * Reading the referrer needs a native Play Services binding, which this
697
+ * package deliberately does not bundle. Pass `referrerProvider`, or install a
698
+ * supported referrer package and it is used automatically. Without either,
699
+ * Android falls back to signal matching.
700
+ */
701
+ async claimDeferredLink(options) {
702
+ if (!options.appspaceId || !options.appspaceId.trim()) {
703
+ throw new Error("Tolinku: appspaceId is required and must not be blank for claimDeferredLink.");
704
+ }
705
+ if (!options.force && await this.alreadyAttempted()) return null;
706
+ const token = await getInstallReferrerToken(options.referrerProvider);
707
+ if (token) {
708
+ const byToken = await this.claimByToken(token, options.appspaceId).catch(() => null);
709
+ if (byToken) {
710
+ await this.rememberAttempt();
711
+ return byToken;
712
+ }
713
+ }
714
+ const { link, settled } = await this.attemptSignals({ appspaceId: options.appspaceId });
715
+ if (settled) await this.rememberAttempt();
716
+ return link;
717
+ }
718
+ async alreadyAttempted() {
719
+ try {
720
+ return await AsyncStorage__default.default.getItem(CLAIMED_KEY) !== null;
721
+ } catch {
722
+ return false;
723
+ }
724
+ }
725
+ async rememberAttempt() {
726
+ try {
727
+ await AsyncStorage__default.default.setItem(CLAIMED_KEY, (/* @__PURE__ */ new Date()).toISOString());
728
+ } catch {
729
+ }
730
+ }
649
731
  /** Claim a deferred deep link by device signal matching */
650
732
  async claimBySignals(options) {
651
733
  if (!options.appspaceId || !options.appspaceId.trim()) {
652
734
  throw new Error("Tolinku: appspaceId is required and must not be blank for claimBySignals.");
653
735
  }
736
+ return (await this.attemptSignals(options)).link;
737
+ }
738
+ /**
739
+ * The signal claim, with whether the server actually answered.
740
+ *
741
+ * `settled` separates "nothing is waiting for this device", which no amount
742
+ * of asking will change, from "the request never got there". Both surface as
743
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
744
+ * apart: recording an attempt that never reached the server would spend an
745
+ * install's one chance at attribution on a dropped connection.
746
+ */
747
+ async attemptSignals(options) {
654
748
  try {
655
749
  const { width, height } = reactNative.Dimensions.get("screen");
656
750
  const resolvedTimezone = options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
657
- const resolvedLanguage = options.language || "en";
658
- return await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
751
+ const resolvedLanguage = options.language || (typeof Intl !== "undefined" && typeof Intl.DateTimeFormat === "function" ? Intl.DateTimeFormat().resolvedOptions().locale : void 0) || "en";
752
+ const link = await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
659
753
  appspace_id: options.appspaceId,
660
754
  timezone: resolvedTimezone,
661
755
  language: resolvedLanguage,
662
756
  screen_width: options.screenWidth || width,
663
- screen_height: options.screenHeight || height
757
+ screen_height: options.screenHeight || height,
758
+ // Separates devices reporting identical dp dimensions.
759
+ device_pixel_ratio: options.devicePixelRatio || reactNative.PixelRatio.get(),
760
+ os_version: options.osVersion || String(reactNative.Platform.Version)
664
761
  });
762
+ return { link, settled: true };
665
763
  } catch (err) {
764
+ const status = err?.statusCode ?? err?.status;
765
+ if (status === 404) {
766
+ debugWarn("Deferred claimBySignals: no match for this device.");
767
+ return { link: null, settled: true };
768
+ }
769
+ if (status === 403) {
770
+ console.warn(
771
+ `[Tolinku] claimBySignals failed with HTTP 403. Check that appspaceId is your Appspace ID (copy it from the dashboard under Settings), not your subdomain or slug. ${err.message}`
772
+ );
773
+ return { link: null, settled: false };
774
+ }
666
775
  debugWarn(`Deferred claimBySignals failed: ${err.message}`);
667
- return null;
776
+ return { link: null, settled: false };
668
777
  }
669
778
  }
670
779
  };
@@ -754,6 +863,17 @@ var _Tolinku = class _Tolinku {
754
863
  * If init() is called a second time without calling destroy() first,
755
864
  * a warning is logged and the existing instance is returned.
756
865
  */
866
+ /**
867
+ * Configure the SDK.
868
+ *
869
+ * The name the Android, iOS and Flutter SDKs use for this. {@link init} does
870
+ * the same thing and still works; it is what this package shipped and
871
+ * breaking it would serve nobody. It is meant for deprecation later, once
872
+ * moving off it is a one-line change rather than a surprise.
873
+ */
874
+ static configure(config) {
875
+ _Tolinku.init(config);
876
+ }
757
877
  static init(config) {
758
878
  if (!config.apiKey) throw new Error("Tolinku: apiKey is required");
759
879
  if (_Tolinku._initialized) {
@@ -1191,6 +1311,8 @@ function TolinkuMessages({
1191
1311
  exports.Tolinku = Tolinku;
1192
1312
  exports.TolinkuError = TolinkuError;
1193
1313
  exports.TolinkuMessages = TolinkuMessages;
1314
+ exports.getInstallReferrerToken = getInstallReferrerToken;
1194
1315
  exports.isSafeUrl = isSafeUrl;
1316
+ exports.parseInstallReferrer = parseInstallReferrer;
1195
1317
  //# sourceMappingURL=index.js.map
1196
1318
  //# sourceMappingURL=index.js.map