@sendoracloud/sdk-react-native 0.15.1 → 0.16.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 +117 -78
- package/dist/index.cjs +453 -37
- package/dist/index.d.cts +176 -56
- package/dist/index.d.ts +176 -56
- package/dist/index.js +449 -36
- package/examples/links-router.tsx +79 -0
- package/examples/links-share.tsx +99 -0
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -33,10 +33,13 @@ __export(index_exports, {
|
|
|
33
33
|
Auth: () => Auth,
|
|
34
34
|
AuthError: () => AuthError,
|
|
35
35
|
EmailAlreadyTakenError: () => EmailAlreadyTakenError,
|
|
36
|
+
LinkError: () => LinkError,
|
|
36
37
|
Links: () => Links,
|
|
37
38
|
SendoraCloud: () => SendoraCloud,
|
|
39
|
+
computeDeviceFingerprint: () => computeDeviceFingerprint,
|
|
38
40
|
default: () => index_default,
|
|
39
|
-
extractShortcodeFromUrl: () => extractShortcodeFromUrl
|
|
41
|
+
extractShortcodeFromUrl: () => extractShortcodeFromUrl,
|
|
42
|
+
getPlayInstallReferrer: () => getPlayInstallReferrer
|
|
40
43
|
});
|
|
41
44
|
module.exports = __toCommonJS(index_exports);
|
|
42
45
|
|
|
@@ -844,44 +847,293 @@ function decodeJwtPayload(token) {
|
|
|
844
847
|
}
|
|
845
848
|
|
|
846
849
|
// src/links.ts
|
|
850
|
+
var LinkError = class extends Error {
|
|
851
|
+
constructor(code, message, statusCode = 0, details) {
|
|
852
|
+
super(message);
|
|
853
|
+
this.name = "LinkError";
|
|
854
|
+
this.code = code;
|
|
855
|
+
this.statusCode = statusCode;
|
|
856
|
+
this.details = details;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
function mapToLinkError(status, body, fallback) {
|
|
860
|
+
const code = body?.error?.code ?? void 0;
|
|
861
|
+
const msg = body?.error?.message ?? fallback;
|
|
862
|
+
if (status === 0) return new LinkError("NETWORK", msg || "Network unavailable", 0);
|
|
863
|
+
if (status === 401) return new LinkError("UNAUTHORIZED", msg, 401);
|
|
864
|
+
if (status === 403) {
|
|
865
|
+
return new LinkError("UNAUTHORIZED", msg, 403);
|
|
866
|
+
}
|
|
867
|
+
if (status === 404) return new LinkError("NOT_FOUND", msg, 404);
|
|
868
|
+
if (status === 410) return new LinkError("EXPIRED", msg, 410);
|
|
869
|
+
if (status === 429) return new LinkError("RATE_LIMITED", msg, 429, body?.error?.details);
|
|
870
|
+
if (status === 412) return new LinkError("INVALID_INPUT", msg, 412);
|
|
871
|
+
if (status === 422) {
|
|
872
|
+
if (/iOS bundle|Android package/i.test(msg)) return new LinkError("BUNDLE_MISMATCH", msg, 422);
|
|
873
|
+
if (/2KB|10KB|linkData/i.test(msg)) return new LinkError("DATA_TOO_LARGE", msg, 422);
|
|
874
|
+
if (/fallbackUrl/i.test(msg) && /apps/i.test(msg)) return new LinkError("FALLBACK_REQUIRED", msg, 422);
|
|
875
|
+
return new LinkError("INVALID_INPUT", msg, 422);
|
|
876
|
+
}
|
|
877
|
+
if (status === 402 || code === "ENTITLEMENT_ERROR" || /plan limit/i.test(msg)) {
|
|
878
|
+
return new LinkError("PLAN_LIMIT", msg, status);
|
|
879
|
+
}
|
|
880
|
+
if (status >= 500) return new LinkError("SERVER", msg || "Server error", status);
|
|
881
|
+
return new LinkError("UNKNOWN", msg || fallback, status);
|
|
882
|
+
}
|
|
847
883
|
async function unwrap(res, ctx) {
|
|
848
|
-
if (!res) throw new
|
|
884
|
+
if (!res) throw new LinkError("NETWORK", `${ctx}: network error`);
|
|
849
885
|
if (!res.ok) {
|
|
850
|
-
let
|
|
886
|
+
let body = null;
|
|
851
887
|
try {
|
|
852
|
-
|
|
853
|
-
if (parsed2.error?.code || parsed2.error?.message) {
|
|
854
|
-
msg = `${parsed2.error.code ?? ""}${parsed2.error.code && parsed2.error.message ? ": " : ""}${parsed2.error.message ?? ""}`;
|
|
855
|
-
}
|
|
888
|
+
body = await res.clone().json();
|
|
856
889
|
} catch {
|
|
857
890
|
}
|
|
858
|
-
throw
|
|
891
|
+
throw mapToLinkError(res.status, body, `${ctx}: HTTP ${res.status}`);
|
|
859
892
|
}
|
|
860
893
|
const parsed = await res.json();
|
|
861
894
|
return parsed.data ?? null;
|
|
862
895
|
}
|
|
863
896
|
var SHORTCODE_RE = /^[a-z0-9-]{3,20}$/;
|
|
864
|
-
|
|
897
|
+
var RESERVED_TAILS = /* @__PURE__ */ new Set(["link"]);
|
|
898
|
+
function extractShortcodeFromUrl(url, linkDomain) {
|
|
865
899
|
try {
|
|
866
900
|
const u = new URL(url);
|
|
901
|
+
if (linkDomain) {
|
|
902
|
+
const wantHost = linkDomain.toLowerCase();
|
|
903
|
+
const gotHost = u.hostname.toLowerCase();
|
|
904
|
+
if (gotHost !== wantHost && !gotHost.endsWith(`.${wantHost}`)) return null;
|
|
905
|
+
}
|
|
867
906
|
const segs = u.pathname.split("/").filter(Boolean);
|
|
868
907
|
if (segs.length === 0) return null;
|
|
869
|
-
|
|
908
|
+
let tail = segs[segs.length - 1];
|
|
909
|
+
if (segs.length >= 2 && RESERVED_TAILS.has(segs[0]) && SHORTCODE_RE.test(tail)) {
|
|
910
|
+
return tail;
|
|
911
|
+
}
|
|
870
912
|
return SHORTCODE_RE.test(tail) ? tail : null;
|
|
871
913
|
} catch {
|
|
872
914
|
return null;
|
|
873
915
|
}
|
|
874
916
|
}
|
|
917
|
+
function dynRequire(id) {
|
|
918
|
+
const dyn = globalThis.require;
|
|
919
|
+
if (typeof dyn !== "function") return null;
|
|
920
|
+
try {
|
|
921
|
+
return dyn(id);
|
|
922
|
+
} catch {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function loadRn() {
|
|
927
|
+
return dynRequire("react-native");
|
|
928
|
+
}
|
|
929
|
+
function readTimezone() {
|
|
930
|
+
try {
|
|
931
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
932
|
+
return tz || "UTC";
|
|
933
|
+
} catch {
|
|
934
|
+
return "UTC";
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function readLocale() {
|
|
938
|
+
const loc = dynRequire("expo-localization");
|
|
939
|
+
if (loc) {
|
|
940
|
+
const tag = loc.getLocales?.()[0]?.languageTag || loc.locale;
|
|
941
|
+
if (tag) return tag;
|
|
942
|
+
}
|
|
943
|
+
const rn = loadRn();
|
|
944
|
+
if (rn) {
|
|
945
|
+
const nm = rn.NativeModules;
|
|
946
|
+
const ios = nm.SettingsManager;
|
|
947
|
+
if (ios) {
|
|
948
|
+
const settings = ios.settings;
|
|
949
|
+
if (settings?.AppleLocale) return settings.AppleLocale;
|
|
950
|
+
if (Array.isArray(settings?.AppleLanguages) && settings.AppleLanguages[0]) return settings.AppleLanguages[0];
|
|
951
|
+
}
|
|
952
|
+
const android = nm.I18nManager;
|
|
953
|
+
if (android?.localeIdentifier) return android.localeIdentifier;
|
|
954
|
+
}
|
|
955
|
+
return "en-US";
|
|
956
|
+
}
|
|
957
|
+
async function sha256Hex(input) {
|
|
958
|
+
const g = globalThis;
|
|
959
|
+
if (g.crypto?.subtle?.digest) {
|
|
960
|
+
const enc = new TextEncoder().encode(input);
|
|
961
|
+
const buf = await g.crypto.subtle.digest("SHA-256", enc);
|
|
962
|
+
const view = new Uint8Array(buf);
|
|
963
|
+
return Array.from(view, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
964
|
+
}
|
|
965
|
+
const c = dynRequire("expo-crypto");
|
|
966
|
+
if (c?.digestStringAsync) {
|
|
967
|
+
const hex = await c.digestStringAsync("SHA-256", input);
|
|
968
|
+
return hex.toLowerCase();
|
|
969
|
+
}
|
|
970
|
+
return sha256HexPureJs(input);
|
|
971
|
+
}
|
|
972
|
+
function sha256HexPureJs(message) {
|
|
973
|
+
const utf8 = unescape(encodeURIComponent(message));
|
|
974
|
+
const bytes = new Uint8Array(utf8.length);
|
|
975
|
+
for (let i = 0; i < utf8.length; i++) bytes[i] = utf8.charCodeAt(i);
|
|
976
|
+
const bitLen = bytes.length * 8;
|
|
977
|
+
const padLen = bytes.length + 9 + 63 & ~63;
|
|
978
|
+
const buf = new Uint8Array(padLen);
|
|
979
|
+
buf.set(bytes);
|
|
980
|
+
buf[bytes.length] = 128;
|
|
981
|
+
const dv = new DataView(buf.buffer);
|
|
982
|
+
dv.setUint32(padLen - 4, bitLen >>> 0, false);
|
|
983
|
+
dv.setUint32(padLen - 8, Math.floor(bitLen / 4294967296), false);
|
|
984
|
+
const K = [
|
|
985
|
+
1116352408,
|
|
986
|
+
1899447441,
|
|
987
|
+
3049323471,
|
|
988
|
+
3921009573,
|
|
989
|
+
961987163,
|
|
990
|
+
1508970993,
|
|
991
|
+
2453635748,
|
|
992
|
+
2870763221,
|
|
993
|
+
3624381080,
|
|
994
|
+
310598401,
|
|
995
|
+
607225278,
|
|
996
|
+
1426881987,
|
|
997
|
+
1925078388,
|
|
998
|
+
2162078206,
|
|
999
|
+
2614888103,
|
|
1000
|
+
3248222580,
|
|
1001
|
+
3835390401,
|
|
1002
|
+
4022224774,
|
|
1003
|
+
264347078,
|
|
1004
|
+
604807628,
|
|
1005
|
+
770255983,
|
|
1006
|
+
1249150122,
|
|
1007
|
+
1555081692,
|
|
1008
|
+
1996064986,
|
|
1009
|
+
2554220882,
|
|
1010
|
+
2821834349,
|
|
1011
|
+
2952996808,
|
|
1012
|
+
3210313671,
|
|
1013
|
+
3336571891,
|
|
1014
|
+
3584528711,
|
|
1015
|
+
113926993,
|
|
1016
|
+
338241895,
|
|
1017
|
+
666307205,
|
|
1018
|
+
773529912,
|
|
1019
|
+
1294757372,
|
|
1020
|
+
1396182291,
|
|
1021
|
+
1695183700,
|
|
1022
|
+
1986661051,
|
|
1023
|
+
2177026350,
|
|
1024
|
+
2456956037,
|
|
1025
|
+
2730485921,
|
|
1026
|
+
2820302411,
|
|
1027
|
+
3259730800,
|
|
1028
|
+
3345764771,
|
|
1029
|
+
3516065817,
|
|
1030
|
+
3600352804,
|
|
1031
|
+
4094571909,
|
|
1032
|
+
275423344,
|
|
1033
|
+
430227734,
|
|
1034
|
+
506948616,
|
|
1035
|
+
659060556,
|
|
1036
|
+
883997877,
|
|
1037
|
+
958139571,
|
|
1038
|
+
1322822218,
|
|
1039
|
+
1537002063,
|
|
1040
|
+
1747873779,
|
|
1041
|
+
1955562222,
|
|
1042
|
+
2024104815,
|
|
1043
|
+
2227730452,
|
|
1044
|
+
2361852424,
|
|
1045
|
+
2428436474,
|
|
1046
|
+
2756734187,
|
|
1047
|
+
3204031479,
|
|
1048
|
+
3329325298
|
|
1049
|
+
];
|
|
1050
|
+
const H = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
1051
|
+
const W = new Uint32Array(64);
|
|
1052
|
+
for (let chunk = 0; chunk < padLen; chunk += 64) {
|
|
1053
|
+
for (let i = 0; i < 16; i++) W[i] = dv.getUint32(chunk + i * 4, false);
|
|
1054
|
+
for (let i = 16; i < 64; i++) {
|
|
1055
|
+
const s0 = ror(W[i - 15], 7) ^ ror(W[i - 15], 18) ^ W[i - 15] >>> 3;
|
|
1056
|
+
const s1 = ror(W[i - 2], 17) ^ ror(W[i - 2], 19) ^ W[i - 2] >>> 10;
|
|
1057
|
+
W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
|
|
1058
|
+
}
|
|
1059
|
+
let [a, b, c, d, e, f, g, h] = H;
|
|
1060
|
+
for (let i = 0; i < 64; i++) {
|
|
1061
|
+
const S1 = ror(e, 6) ^ ror(e, 11) ^ ror(e, 25);
|
|
1062
|
+
const ch = e & f ^ ~e & g;
|
|
1063
|
+
const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
|
|
1064
|
+
const S0 = ror(a, 2) ^ ror(a, 13) ^ ror(a, 22);
|
|
1065
|
+
const mj = a & b ^ a & c ^ b & c;
|
|
1066
|
+
const t2 = S0 + mj >>> 0;
|
|
1067
|
+
h = g;
|
|
1068
|
+
g = f;
|
|
1069
|
+
f = e;
|
|
1070
|
+
e = d + t1 >>> 0;
|
|
1071
|
+
d = c;
|
|
1072
|
+
c = b;
|
|
1073
|
+
b = a;
|
|
1074
|
+
a = t1 + t2 >>> 0;
|
|
1075
|
+
}
|
|
1076
|
+
H[0] = H[0] + a >>> 0;
|
|
1077
|
+
H[1] = H[1] + b >>> 0;
|
|
1078
|
+
H[2] = H[2] + c >>> 0;
|
|
1079
|
+
H[3] = H[3] + d >>> 0;
|
|
1080
|
+
H[4] = H[4] + e >>> 0;
|
|
1081
|
+
H[5] = H[5] + f >>> 0;
|
|
1082
|
+
H[6] = H[6] + g >>> 0;
|
|
1083
|
+
H[7] = H[7] + h >>> 0;
|
|
1084
|
+
}
|
|
1085
|
+
return H.map((x) => x.toString(16).padStart(8, "0")).join("");
|
|
1086
|
+
}
|
|
1087
|
+
function ror(x, n) {
|
|
1088
|
+
return (x >>> n | x << 32 - n) >>> 0;
|
|
1089
|
+
}
|
|
1090
|
+
async function computeDeviceFingerprint() {
|
|
1091
|
+
const rn = loadRn();
|
|
1092
|
+
const platform = rn?.Platform.OS ?? "unknown";
|
|
1093
|
+
let screen = "0x0";
|
|
1094
|
+
if (rn) {
|
|
1095
|
+
try {
|
|
1096
|
+
const d = rn.Dimensions.get("window");
|
|
1097
|
+
screen = `${Math.round(d.width)}x${Math.round(d.height)}`;
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const tz = readTimezone();
|
|
1102
|
+
const locale = readLocale();
|
|
1103
|
+
return sha256Hex(`${platform}|${screen}|${tz}|${locale}`);
|
|
1104
|
+
}
|
|
1105
|
+
async function getPlayInstallReferrer() {
|
|
1106
|
+
const rn = loadRn();
|
|
1107
|
+
if (!rn || rn.Platform.OS !== "android") return null;
|
|
1108
|
+
const mod = dynRequire("react-native-play-install-referrer");
|
|
1109
|
+
const cli = mod?.PlayInstallReferrer;
|
|
1110
|
+
if (!cli?.getInstallReferrerInfo) return null;
|
|
1111
|
+
return await new Promise((resolve) => {
|
|
1112
|
+
cli.getInstallReferrerInfo((err, info) => {
|
|
1113
|
+
if (err) {
|
|
1114
|
+
resolve(null);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
resolve(info?.installReferrer ?? null);
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
var PREWARM_TTL_MS = 5 * 6e4;
|
|
1122
|
+
var PREWARM_MAX = 50;
|
|
875
1123
|
var Links = class {
|
|
876
1124
|
constructor(deps) {
|
|
877
1125
|
this.deps = deps;
|
|
878
1126
|
this.listeners = [];
|
|
1127
|
+
this.prewarmCache = /* @__PURE__ */ new Map();
|
|
1128
|
+
this.linkingSub = null;
|
|
879
1129
|
}
|
|
880
|
-
/** Register a callback for warm + deferred deep-link opens. Returns
|
|
1130
|
+
/** Register a callback for warm + deferred deep-link opens. Returns
|
|
1131
|
+
* unsubscribe fn. Generic `T` lets callers type `linkData` per-app. */
|
|
881
1132
|
onLinkOpened(handler) {
|
|
882
|
-
|
|
1133
|
+
const widened = handler;
|
|
1134
|
+
this.listeners.push(widened);
|
|
883
1135
|
return () => {
|
|
884
|
-
const idx = this.listeners.indexOf(
|
|
1136
|
+
const idx = this.listeners.indexOf(widened);
|
|
885
1137
|
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
886
1138
|
};
|
|
887
1139
|
}
|
|
@@ -894,11 +1146,58 @@ var Links = class {
|
|
|
894
1146
|
}
|
|
895
1147
|
}
|
|
896
1148
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (
|
|
901
|
-
const
|
|
1149
|
+
/* ------------------------ create + prewarm ------------------------- */
|
|
1150
|
+
/** Stable cache key. Customer can override per call with `opts.key`. */
|
|
1151
|
+
cacheKey(input, override) {
|
|
1152
|
+
if (override) return `k:${override}`;
|
|
1153
|
+
const sorted = {};
|
|
1154
|
+
const flat = input;
|
|
1155
|
+
for (const k of Object.keys(flat).sort()) sorted[k] = flat[k];
|
|
1156
|
+
return `s:${JSON.stringify(sorted)}`;
|
|
1157
|
+
}
|
|
1158
|
+
evictExpired() {
|
|
1159
|
+
const now = Date.now();
|
|
1160
|
+
for (const [k, v] of this.prewarmCache) {
|
|
1161
|
+
if (now - v.ts > PREWARM_TTL_MS) this.prewarmCache.delete(k);
|
|
1162
|
+
}
|
|
1163
|
+
while (this.prewarmCache.size > PREWARM_MAX) {
|
|
1164
|
+
const first = this.prewarmCache.keys().next().value;
|
|
1165
|
+
if (first === void 0) break;
|
|
1166
|
+
this.prewarmCache.delete(first);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Background-mint a link + cache the promise so a subsequent `create()`
|
|
1171
|
+
* with the same input (or matching `key`) returns instantly.
|
|
1172
|
+
*
|
|
1173
|
+
* Fire-and-forget. Errors are swallowed; the matching `create()` call
|
|
1174
|
+
* will surface them on demand instead.
|
|
1175
|
+
*/
|
|
1176
|
+
prewarm(input, opts) {
|
|
1177
|
+
this.evictExpired();
|
|
1178
|
+
const key = this.cacheKey(input, opts?.key);
|
|
1179
|
+
if (this.prewarmCache.has(key)) return;
|
|
1180
|
+
const promise = this.doCreate(input).catch((err) => {
|
|
1181
|
+
this.prewarmCache.delete(key);
|
|
1182
|
+
throw err;
|
|
1183
|
+
});
|
|
1184
|
+
this.prewarmCache.set(key, { promise, ts: Date.now() });
|
|
1185
|
+
}
|
|
1186
|
+
/** Mint a new short link. Uses prewarm cache when the input matches. */
|
|
1187
|
+
async create(input, opts) {
|
|
1188
|
+
if (!input.title) throw new LinkError("INVALID_INPUT", "links.create: title is required");
|
|
1189
|
+
this.evictExpired();
|
|
1190
|
+
const key = this.cacheKey(input, opts?.key);
|
|
1191
|
+
const cached = this.prewarmCache.get(key);
|
|
1192
|
+
if (cached) {
|
|
1193
|
+
this.prewarmCache.delete(key);
|
|
1194
|
+
return cached.promise;
|
|
1195
|
+
}
|
|
1196
|
+
return this.doCreate(input);
|
|
1197
|
+
}
|
|
1198
|
+
async doCreate(input) {
|
|
1199
|
+
const body = { title: input.title };
|
|
1200
|
+
if (input.fallbackUrl) body.fallbackUrl = input.fallbackUrl;
|
|
902
1201
|
if (input.iosDeepLinkPath) body.iosDeepLinkPath = input.iosDeepLinkPath;
|
|
903
1202
|
if (input.androidDeepLinkPath) body.androidDeepLinkPath = input.androidDeepLinkPath;
|
|
904
1203
|
if (input.linkData) body.linkData = input.linkData;
|
|
@@ -911,8 +1210,9 @@ var Links = class {
|
|
|
911
1210
|
if (input.channel) body.channel = input.channel;
|
|
912
1211
|
if (input.tags) body.tags = input.tags;
|
|
913
1212
|
if (input.expiresAt) body.expiresAt = input.expiresAt;
|
|
914
|
-
const
|
|
915
|
-
const
|
|
1213
|
+
const rnPlatform = loadRn()?.Platform.OS;
|
|
1214
|
+
const ios = input.iosBundleId ?? (rnPlatform === "ios" ? this.deps.iosBundleId : void 0);
|
|
1215
|
+
const android = input.androidPackageName ?? (rnPlatform === "android" ? this.deps.androidPackageName : void 0);
|
|
916
1216
|
if (ios) body.iosBundleId = ios;
|
|
917
1217
|
if (android) body.androidPackageName = android;
|
|
918
1218
|
const res = await post(
|
|
@@ -922,17 +1222,18 @@ var Links = class {
|
|
|
922
1222
|
);
|
|
923
1223
|
const data = await unwrap(res, "links.create");
|
|
924
1224
|
if (!data?.shortcode) {
|
|
925
|
-
throw new
|
|
1225
|
+
throw new LinkError("UNKNOWN", "links.create returned no shortcode");
|
|
926
1226
|
}
|
|
927
1227
|
return data;
|
|
928
1228
|
}
|
|
1229
|
+
/* -------------------- warm + deferred resolve ---------------------- */
|
|
929
1230
|
/**
|
|
930
1231
|
* Warm-path: app received a universal/app-link delivery. Resolves the
|
|
931
1232
|
* shortcode against the backend and fires `onLinkOpened`. No-op (and
|
|
932
1233
|
* returns false) when the URL isn't a Sendora link.
|
|
933
1234
|
*/
|
|
934
1235
|
async handleUniversalLink(url) {
|
|
935
|
-
const shortcode = extractShortcodeFromUrl(url);
|
|
1236
|
+
const shortcode = extractShortcodeFromUrl(url, this.deps.linkDomain);
|
|
936
1237
|
if (!shortcode) return false;
|
|
937
1238
|
try {
|
|
938
1239
|
const res = await getJson(
|
|
@@ -951,26 +1252,44 @@ var Links = class {
|
|
|
951
1252
|
return true;
|
|
952
1253
|
} catch (err) {
|
|
953
1254
|
if (this.deps.debug) console.warn("[sendoracloud] handleUniversalLink failed:", err);
|
|
1255
|
+
if (err instanceof LinkError) throw err;
|
|
954
1256
|
return false;
|
|
955
1257
|
}
|
|
956
1258
|
}
|
|
957
1259
|
/**
|
|
958
|
-
* Cold-launch deferred deep link match.
|
|
959
|
-
*
|
|
960
|
-
*
|
|
1260
|
+
* Cold-launch deferred deep link match. When neither `fingerprintHash` nor
|
|
1261
|
+
* `installReferrer` is supplied, the SDK auto-discovers:
|
|
1262
|
+
* - Android: probes `react-native-play-install-referrer` (preferred — 100%
|
|
1263
|
+
* accurate when present).
|
|
1264
|
+
* - Otherwise: computes the canonical device fingerprint hash.
|
|
961
1265
|
*
|
|
962
|
-
*
|
|
963
|
-
* Returns the event payload on match, `null` on no match.
|
|
1266
|
+
* Fires `onLinkOpened` with `isDeferred: true` on success.
|
|
964
1267
|
*/
|
|
965
|
-
async matchDeferred(input) {
|
|
966
|
-
|
|
967
|
-
|
|
1268
|
+
async matchDeferred(input = {}) {
|
|
1269
|
+
let fingerprintHash = input.fingerprintHash;
|
|
1270
|
+
let installReferrer = input.installReferrer;
|
|
1271
|
+
if (!fingerprintHash && !installReferrer) {
|
|
1272
|
+
installReferrer = await getPlayInstallReferrer() ?? void 0;
|
|
1273
|
+
if (!installReferrer) {
|
|
1274
|
+
try {
|
|
1275
|
+
fingerprintHash = await computeDeviceFingerprint();
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
if (this.deps.debug) console.warn("[sendoracloud] fingerprint compute failed:", err);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (!fingerprintHash && !installReferrer) {
|
|
1282
|
+
throw new LinkError(
|
|
1283
|
+
"INVALID_INPUT",
|
|
1284
|
+
"links.matchDeferred: install `react-native-play-install-referrer` for Android, or ensure web crypto / expo-crypto is available for iOS fingerprint compute."
|
|
1285
|
+
);
|
|
968
1286
|
}
|
|
969
1287
|
const body = {};
|
|
970
|
-
if (
|
|
971
|
-
if (
|
|
972
|
-
const
|
|
973
|
-
const
|
|
1288
|
+
if (fingerprintHash) body.fingerprintHash = fingerprintHash;
|
|
1289
|
+
if (installReferrer) body.installReferrer = installReferrer;
|
|
1290
|
+
const rnPlatform = loadRn()?.Platform.OS;
|
|
1291
|
+
const ios = input.iosBundleId ?? (rnPlatform === "ios" ? this.deps.iosBundleId : void 0);
|
|
1292
|
+
const android = input.androidPackageName ?? (rnPlatform === "android" ? this.deps.androidPackageName : void 0);
|
|
974
1293
|
if (ios) body.iosBundleId = ios;
|
|
975
1294
|
if (android) body.androidPackageName = android;
|
|
976
1295
|
const res = await post(
|
|
@@ -990,6 +1309,99 @@ var Links = class {
|
|
|
990
1309
|
this.emit(event);
|
|
991
1310
|
return event;
|
|
992
1311
|
}
|
|
1312
|
+
/* --------------------------- revoke ------------------------------- */
|
|
1313
|
+
/** Soft-delete a link. Idempotent — repeated calls succeed. */
|
|
1314
|
+
async revoke(shortcode) {
|
|
1315
|
+
if (!SHORTCODE_RE.test(shortcode)) {
|
|
1316
|
+
throw new LinkError("INVALID_INPUT", `links.revoke: '${shortcode}' is not a valid shortcode`);
|
|
1317
|
+
}
|
|
1318
|
+
const res = await post(
|
|
1319
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
1320
|
+
`/api/v1/sdk/links/${encodeURIComponent(shortcode)}/revoke`,
|
|
1321
|
+
{}
|
|
1322
|
+
);
|
|
1323
|
+
const data = await unwrap(res, "links.revoke");
|
|
1324
|
+
if (!data) throw new LinkError("UNKNOWN", "links.revoke returned empty body");
|
|
1325
|
+
return { shortcode: data.shortcode, revoked: true };
|
|
1326
|
+
}
|
|
1327
|
+
/* ---------------------------- stats ------------------------------- */
|
|
1328
|
+
/** Click totals + breakdowns for a link the SDK owns. */
|
|
1329
|
+
async getStats(shortcode) {
|
|
1330
|
+
if (!SHORTCODE_RE.test(shortcode)) {
|
|
1331
|
+
throw new LinkError("INVALID_INPUT", `links.getStats: '${shortcode}' is not a valid shortcode`);
|
|
1332
|
+
}
|
|
1333
|
+
const res = await getJson(
|
|
1334
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
1335
|
+
`/api/v1/sdk/links/${encodeURIComponent(shortcode)}/stats`
|
|
1336
|
+
);
|
|
1337
|
+
const data = await unwrap(res, "links.getStats");
|
|
1338
|
+
if (!data) throw new LinkError("UNKNOWN", "links.getStats returned empty body");
|
|
1339
|
+
return data;
|
|
1340
|
+
}
|
|
1341
|
+
/* ----------------------- Linking-API wiring ----------------------- */
|
|
1342
|
+
/**
|
|
1343
|
+
* One-call replacement for the standard cold + warm URL boilerplate:
|
|
1344
|
+
*
|
|
1345
|
+
* const detach = await sendora.links.attachLinkingApi({
|
|
1346
|
+
* onLegacyUrl: (url) => myRouter.handle(url),
|
|
1347
|
+
* });
|
|
1348
|
+
* // …later in unmount:
|
|
1349
|
+
* detach();
|
|
1350
|
+
*
|
|
1351
|
+
* Internally:
|
|
1352
|
+
* - Reads `Linking.getInitialURL()` once.
|
|
1353
|
+
* - Subscribes to `Linking.addEventListener('url', …)`.
|
|
1354
|
+
* - For each URL: checks `extractShortcodeFromUrl(url, linkDomain)`.
|
|
1355
|
+
* Sendora-shaped → calls `handleUniversalLink`.
|
|
1356
|
+
* Otherwise → forwards to `onLegacyUrl` (if supplied).
|
|
1357
|
+
*
|
|
1358
|
+
* Idempotent: re-attaching while a subscription is active replaces it.
|
|
1359
|
+
*/
|
|
1360
|
+
async attachLinkingApi(opts = {}) {
|
|
1361
|
+
const rn = loadRn();
|
|
1362
|
+
if (!rn) throw new LinkError("UNKNOWN", "react-native Linking is not available in this runtime");
|
|
1363
|
+
const Linking = rn.Linking;
|
|
1364
|
+
if (!Linking) throw new LinkError("UNKNOWN", "react-native Linking module is undefined");
|
|
1365
|
+
if (this.linkingSub) {
|
|
1366
|
+
try {
|
|
1367
|
+
this.linkingSub.remove();
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
this.linkingSub = null;
|
|
1371
|
+
}
|
|
1372
|
+
const handle = async (url) => {
|
|
1373
|
+
if (!url) return;
|
|
1374
|
+
const sc = extractShortcodeFromUrl(url, this.deps.linkDomain);
|
|
1375
|
+
if (sc) {
|
|
1376
|
+
try {
|
|
1377
|
+
await this.handleUniversalLink(url);
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
if (this.deps.debug) console.warn("[sendoracloud] attachLinkingApi resolve failed:", err);
|
|
1380
|
+
}
|
|
1381
|
+
} else if (opts.onLegacyUrl) {
|
|
1382
|
+
try {
|
|
1383
|
+
opts.onLegacyUrl(url);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
if (this.deps.debug) console.warn("[sendoracloud] onLegacyUrl handler threw:", err);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
try {
|
|
1390
|
+
await handle(await Linking.getInitialURL());
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
if (this.deps.debug) console.warn("[sendoracloud] getInitialURL failed:", err);
|
|
1393
|
+
}
|
|
1394
|
+
this.linkingSub = Linking.addEventListener("url", (ev) => {
|
|
1395
|
+
void handle(ev?.url ?? null);
|
|
1396
|
+
});
|
|
1397
|
+
return () => {
|
|
1398
|
+
try {
|
|
1399
|
+
this.linkingSub?.remove();
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
this.linkingSub = null;
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
993
1405
|
};
|
|
994
1406
|
|
|
995
1407
|
// src/auto-track.ts
|
|
@@ -1075,7 +1487,7 @@ function installAutoTrack(args) {
|
|
|
1075
1487
|
}
|
|
1076
1488
|
|
|
1077
1489
|
// src/index.ts
|
|
1078
|
-
var SDK_VERSION = "0.
|
|
1490
|
+
var SDK_VERSION = "0.16.1";
|
|
1079
1491
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
1080
1492
|
var ANON_KEY = "anon_id";
|
|
1081
1493
|
var USER_ID_KEY = "user_id";
|
|
@@ -1219,7 +1631,8 @@ var SendoraSDK = class {
|
|
|
1219
1631
|
publicKey: this.config.publicKey,
|
|
1220
1632
|
debug: this.debug,
|
|
1221
1633
|
iosBundleId: this.config.iosBundleId,
|
|
1222
|
-
androidPackageName: this.config.androidPackageName
|
|
1634
|
+
androidPackageName: this.config.androidPackageName,
|
|
1635
|
+
linkDomain: this.config.linkDomain
|
|
1223
1636
|
});
|
|
1224
1637
|
this.ready = true;
|
|
1225
1638
|
if (config.autoTrack !== false) {
|
|
@@ -1415,7 +1828,10 @@ var index_default = SendoraCloud;
|
|
|
1415
1828
|
Auth,
|
|
1416
1829
|
AuthError,
|
|
1417
1830
|
EmailAlreadyTakenError,
|
|
1831
|
+
LinkError,
|
|
1418
1832
|
Links,
|
|
1419
1833
|
SendoraCloud,
|
|
1420
|
-
|
|
1834
|
+
computeDeviceFingerprint,
|
|
1835
|
+
extractShortcodeFromUrl,
|
|
1836
|
+
getPlayInstallReferrer
|
|
1421
1837
|
});
|