@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/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { StyleSheet, AppState, Dimensions, 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,41 +624,150 @@ 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
- const resolvedLanguage = options.language || "en";
652
- return await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
745
+ const resolvedLanguage = options.language || (typeof Intl !== "undefined" && typeof Intl.DateTimeFormat === "function" ? Intl.DateTimeFormat().resolvedOptions().locale : void 0) || "en";
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,
656
750
  screen_width: options.screenWidth || width,
657
- screen_height: options.screenHeight || height
751
+ screen_height: options.screenHeight || height,
752
+ // Separates devices reporting identical dp dimensions.
753
+ device_pixel_ratio: options.devicePixelRatio || PixelRatio.get(),
754
+ os_version: options.osVersion || String(Platform.Version)
658
755
  });
756
+ return { link, settled: true };
659
757
  } catch (err) {
758
+ const status = err?.statusCode ?? err?.status;
759
+ if (status === 404) {
760
+ debugWarn("Deferred claimBySignals: no match for this device.");
761
+ return { link: null, settled: true };
762
+ }
763
+ if (status === 403) {
764
+ console.warn(
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}`
766
+ );
767
+ return { link: null, settled: false };
768
+ }
660
769
  debugWarn(`Deferred claimBySignals failed: ${err.message}`);
661
- return null;
770
+ return { link: null, settled: false };
662
771
  }
663
772
  }
664
773
  };
@@ -748,6 +857,17 @@ var _Tolinku = class _Tolinku {
748
857
  * If init() is called a second time without calling destroy() first,
749
858
  * a warning is logged and the existing instance is returned.
750
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
+ }
751
871
  static init(config) {
752
872
  if (!config.apiKey) throw new Error("Tolinku: apiKey is required");
753
873
  if (_Tolinku._initialized) {
@@ -1182,6 +1302,6 @@ function TolinkuMessages({
1182
1302
  );
1183
1303
  }
1184
1304
 
1185
- export { Tolinku, TolinkuError, TolinkuMessages, isSafeUrl };
1305
+ export { Tolinku, TolinkuError, TolinkuMessages, getInstallReferrerToken, isSafeUrl, parseInstallReferrer };
1186
1306
  //# sourceMappingURL=index.mjs.map
1187
1307
  //# sourceMappingURL=index.mjs.map