@tolinku/react-native-sdk 0.3.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/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { StyleSheet, AppState, Dimensions, Platform, PixelRatio, Modal, Pressable, TouchableOpacity, Text, ScrollView, View, ImageBackground, Image, Linking } from 'react-native';
1
+ import { StyleSheet, Platform, NativeModules, AppState, Dimensions, PixelRatio, Modal, Pressable, TouchableOpacity, Text, ScrollView, View, ImageBackground, Image, Linking } from 'react-native';
2
2
  import AsyncStorage from '@react-native-async-storage/async-storage';
3
3
  import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
5
 
6
6
  // src/types.ts
7
- var SDK_VERSION = "0.1.0";
7
+ var SDK_VERSION = "0.4.0";
8
8
 
9
9
  // src/debug.ts
10
10
  var _debugEnabled = false;
@@ -624,32 +624,126 @@ var Referrals = class {
624
624
  });
625
625
  }
626
626
  };
627
+ var TOKEN_KEY = "tolk_token";
628
+ function parseInstallReferrer(referrer) {
629
+ if (!referrer || !referrer.trim()) return null;
630
+ let decoded = referrer;
631
+ try {
632
+ decoded = decodeURIComponent(referrer);
633
+ } catch {
634
+ }
635
+ const pair = decoded.split("&").map((p) => p.trim()).find((p) => p.startsWith(`${TOKEN_KEY}=`));
636
+ if (!pair) return null;
637
+ const token = pair.slice(TOKEN_KEY.length + 1);
638
+ return token.trim() ? token : null;
639
+ }
640
+ function nativeProvider() {
641
+ const native = NativeModules?.TolinkuInstallReferrer;
642
+ if (!native || typeof native.getInstallReferrer !== "function") return null;
643
+ return () => native.getInstallReferrer();
644
+ }
645
+ async function getInstallReferrerToken(provider) {
646
+ if (Platform.OS !== "android") return null;
647
+ const source = provider ?? nativeProvider();
648
+ if (!source) return null;
649
+ try {
650
+ const referrer = await source();
651
+ return parseInstallReferrer(referrer);
652
+ } catch (err) {
653
+ debugWarn(`Install referrer lookup failed: ${err.message}`);
654
+ return null;
655
+ }
656
+ }
657
+
658
+ // src/deferred.ts
659
+ var CLAIMED_KEY = "tolinku_deferred_claimed";
627
660
  var Deferred = class {
628
661
  constructor(client) {
629
662
  this.client = client;
630
663
  }
631
664
  /** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
632
- async claimByToken(token) {
665
+ async claimByToken(token, appspaceId) {
633
666
  if (!token || !token.trim()) {
634
667
  throw new Error("Tolinku: token is required and must not be blank for claimByToken.");
635
668
  }
636
669
  try {
637
- return await this.client.getPublic("/v1/api/deferred/claim", { token });
670
+ return await this.client.getPublic("/v1/api/deferred/claim", {
671
+ token,
672
+ ...appspaceId ? { appspace_id: appspaceId } : {}
673
+ });
638
674
  } catch (err) {
639
675
  debugWarn(`Deferred claimByToken failed: ${err.message}`);
640
676
  return null;
641
677
  }
642
678
  }
679
+ /**
680
+ * Recover the link that led to this install, trying both mechanisms.
681
+ *
682
+ * The Play Install Referrer is asked first on Android: it names the exact
683
+ * click, survives for days, and does not care which network the device was
684
+ * on. Device signals are the fallback, and the only option on iOS, where no
685
+ * equivalent exists.
686
+ *
687
+ * Call once on first launch. Safe to call again, but a claim is consumed the
688
+ * first time it succeeds, so a second call returns null.
689
+ *
690
+ * Reading the referrer needs a native Play Services binding, which this
691
+ * package deliberately does not bundle. Pass `referrerProvider`, or install a
692
+ * supported referrer package and it is used automatically. Without either,
693
+ * Android falls back to signal matching.
694
+ */
695
+ async claimDeferredLink(options) {
696
+ if (!options.appspaceId || !options.appspaceId.trim()) {
697
+ throw new Error("Tolinku: appspaceId is required and must not be blank for claimDeferredLink.");
698
+ }
699
+ if (!options.force && await this.alreadyAttempted()) return null;
700
+ const token = await getInstallReferrerToken(options.referrerProvider);
701
+ if (token) {
702
+ const byToken = await this.claimByToken(token, options.appspaceId).catch(() => null);
703
+ if (byToken) {
704
+ await this.rememberAttempt();
705
+ return byToken;
706
+ }
707
+ }
708
+ const { link, settled } = await this.attemptSignals({ appspaceId: options.appspaceId });
709
+ if (settled) await this.rememberAttempt();
710
+ return link;
711
+ }
712
+ async alreadyAttempted() {
713
+ try {
714
+ return await AsyncStorage.getItem(CLAIMED_KEY) !== null;
715
+ } catch {
716
+ return false;
717
+ }
718
+ }
719
+ async rememberAttempt() {
720
+ try {
721
+ await AsyncStorage.setItem(CLAIMED_KEY, (/* @__PURE__ */ new Date()).toISOString());
722
+ } catch {
723
+ }
724
+ }
643
725
  /** Claim a deferred deep link by device signal matching */
644
726
  async claimBySignals(options) {
645
727
  if (!options.appspaceId || !options.appspaceId.trim()) {
646
728
  throw new Error("Tolinku: appspaceId is required and must not be blank for claimBySignals.");
647
729
  }
730
+ return (await this.attemptSignals(options)).link;
731
+ }
732
+ /**
733
+ * The signal claim, with whether the server actually answered.
734
+ *
735
+ * `settled` separates "nothing is waiting for this device", which no amount
736
+ * of asking will change, from "the request never got there". Both surface as
737
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
738
+ * apart: recording an attempt that never reached the server would spend an
739
+ * install's one chance at attribution on a dropped connection.
740
+ */
741
+ async attemptSignals(options) {
648
742
  try {
649
743
  const { width, height } = Dimensions.get("screen");
650
744
  const resolvedTimezone = options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
651
745
  const resolvedLanguage = options.language || (typeof Intl !== "undefined" && typeof Intl.DateTimeFormat === "function" ? Intl.DateTimeFormat().resolvedOptions().locale : void 0) || "en";
652
- return await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
746
+ const link = await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
653
747
  appspace_id: options.appspaceId,
654
748
  timezone: resolvedTimezone,
655
749
  language: resolvedLanguage,
@@ -659,20 +753,21 @@ var Deferred = class {
659
753
  device_pixel_ratio: options.devicePixelRatio || PixelRatio.get(),
660
754
  os_version: options.osVersion || String(Platform.Version)
661
755
  });
756
+ return { link, settled: true };
662
757
  } catch (err) {
663
758
  const status = err?.statusCode ?? err?.status;
664
759
  if (status === 404) {
665
760
  debugWarn("Deferred claimBySignals: no match for this device.");
666
- return null;
761
+ return { link: null, settled: true };
667
762
  }
668
763
  if (status === 403) {
669
764
  console.warn(
670
765
  `[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}`
671
766
  );
672
- return null;
767
+ return { link: null, settled: false };
673
768
  }
674
769
  debugWarn(`Deferred claimBySignals failed: ${err.message}`);
675
- return null;
770
+ return { link: null, settled: false };
676
771
  }
677
772
  }
678
773
  };
@@ -762,6 +857,17 @@ var _Tolinku = class _Tolinku {
762
857
  * If init() is called a second time without calling destroy() first,
763
858
  * a warning is logged and the existing instance is returned.
764
859
  */
860
+ /**
861
+ * Configure the SDK.
862
+ *
863
+ * The name the Android, iOS and Flutter SDKs use for this. {@link init} does
864
+ * the same thing and still works; it is what this package shipped and
865
+ * breaking it would serve nobody. It is meant for deprecation later, once
866
+ * moving off it is a one-line change rather than a surprise.
867
+ */
868
+ static configure(config) {
869
+ _Tolinku.init(config);
870
+ }
765
871
  static init(config) {
766
872
  if (!config.apiKey) throw new Error("Tolinku: apiKey is required");
767
873
  if (_Tolinku._initialized) {
@@ -1196,6 +1302,6 @@ function TolinkuMessages({
1196
1302
  );
1197
1303
  }
1198
1304
 
1199
- export { Tolinku, TolinkuError, TolinkuMessages, isSafeUrl };
1305
+ export { Tolinku, TolinkuError, TolinkuMessages, getInstallReferrerToken, isSafeUrl, parseInstallReferrer };
1200
1306
  //# sourceMappingURL=index.mjs.map
1201
1307
  //# sourceMappingURL=index.mjs.map