@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 +31 -2
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/tolinku/rnsdk/TolinkuInstallReferrerModule.java +107 -0
- package/android/src/main/java/com/tolinku/rnsdk/TolinkuInstallReferrerPackage.java +30 -0
- package/dist/index.d.mts +71 -3
- package/dist/index.d.ts +71 -3
- package/dist/index.js +129 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
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.
|
|
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", {
|
|
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
|
-
|
|
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
|