@sendoracloud/sdk-react-native 0.15.0 → 0.16.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.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 Error(`[sendoracloud] ${ctx}: network error`);
884
+ if (!res) throw new LinkError("NETWORK", `${ctx}: network error`);
849
885
  if (!res.ok) {
850
- let msg = `HTTP ${res.status}`;
886
+ let body = null;
851
887
  try {
852
- const parsed2 = await res.clone().json();
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 new Error(`[sendoracloud] ${ctx}: ${msg}`);
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
- function extractShortcodeFromUrl(url) {
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
- const tail = segs[segs.length - 1];
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 unsubscribe fn. */
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
- this.listeners.push(handler);
1133
+ const widened = handler;
1134
+ this.listeners.push(widened);
883
1135
  return () => {
884
- const idx = this.listeners.indexOf(handler);
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
- /** Mint a new short link. Returns the share URL + shortcode. */
898
- async create(input) {
899
- if (!input.title) throw new Error("[sendoracloud] links.create: title is required");
900
- if (!input.fallbackUrl) throw new Error("[sendoracloud] links.create: fallbackUrl is required");
901
- const body = { title: input.title, fallbackUrl: input.fallbackUrl };
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;
@@ -922,17 +1221,18 @@ var Links = class {
922
1221
  );
923
1222
  const data = await unwrap(res, "links.create");
924
1223
  if (!data?.shortcode) {
925
- throw new Error("[sendoracloud] links.create returned no shortcode");
1224
+ throw new LinkError("UNKNOWN", "links.create returned no shortcode");
926
1225
  }
927
1226
  return data;
928
1227
  }
1228
+ /* -------------------- warm + deferred resolve ---------------------- */
929
1229
  /**
930
1230
  * Warm-path: app received a universal/app-link delivery. Resolves the
931
1231
  * shortcode against the backend and fires `onLinkOpened`. No-op (and
932
1232
  * returns false) when the URL isn't a Sendora link.
933
1233
  */
934
1234
  async handleUniversalLink(url) {
935
- const shortcode = extractShortcodeFromUrl(url);
1235
+ const shortcode = extractShortcodeFromUrl(url, this.deps.linkDomain);
936
1236
  if (!shortcode) return false;
937
1237
  try {
938
1238
  const res = await getJson(
@@ -951,24 +1251,41 @@ var Links = class {
951
1251
  return true;
952
1252
  } catch (err) {
953
1253
  if (this.deps.debug) console.warn("[sendoracloud] handleUniversalLink failed:", err);
1254
+ if (err instanceof LinkError) throw err;
954
1255
  return false;
955
1256
  }
956
1257
  }
957
1258
  /**
958
- * Cold-launch deferred deep link match. Pass the Android Play Install
959
- * Referrer string (preferred 100% accurate) and/or a precomputed
960
- * fingerprint hash (iOSprobabilistic).
1259
+ * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
1260
+ * `installReferrer` is supplied, the SDK auto-discovers:
1261
+ * - Android: probes `react-native-play-install-referrer` (preferred100%
1262
+ * accurate when present).
1263
+ * - Otherwise: computes the canonical device fingerprint hash.
961
1264
  *
962
- * SDK fires `onLinkOpened` with `isDeferred: true` on success.
963
- * Returns the event payload on match, `null` on no match.
1265
+ * Fires `onLinkOpened` with `isDeferred: true` on success.
964
1266
  */
965
- async matchDeferred(input) {
966
- if (!input.fingerprintHash && !input.installReferrer) {
967
- throw new Error("[sendoracloud] links.matchDeferred: pass fingerprintHash or installReferrer");
1267
+ async matchDeferred(input = {}) {
1268
+ let fingerprintHash = input.fingerprintHash;
1269
+ let installReferrer = input.installReferrer;
1270
+ if (!fingerprintHash && !installReferrer) {
1271
+ installReferrer = await getPlayInstallReferrer() ?? void 0;
1272
+ if (!installReferrer) {
1273
+ try {
1274
+ fingerprintHash = await computeDeviceFingerprint();
1275
+ } catch (err) {
1276
+ if (this.deps.debug) console.warn("[sendoracloud] fingerprint compute failed:", err);
1277
+ }
1278
+ }
1279
+ }
1280
+ if (!fingerprintHash && !installReferrer) {
1281
+ throw new LinkError(
1282
+ "INVALID_INPUT",
1283
+ "links.matchDeferred: install `react-native-play-install-referrer` for Android, or ensure web crypto / expo-crypto is available for iOS fingerprint compute."
1284
+ );
968
1285
  }
969
1286
  const body = {};
970
- if (input.fingerprintHash) body.fingerprintHash = input.fingerprintHash;
971
- if (input.installReferrer) body.installReferrer = input.installReferrer;
1287
+ if (fingerprintHash) body.fingerprintHash = fingerprintHash;
1288
+ if (installReferrer) body.installReferrer = installReferrer;
972
1289
  const ios = input.iosBundleId ?? this.deps.iosBundleId;
973
1290
  const android = input.androidPackageName ?? this.deps.androidPackageName;
974
1291
  if (ios) body.iosBundleId = ios;
@@ -990,6 +1307,99 @@ var Links = class {
990
1307
  this.emit(event);
991
1308
  return event;
992
1309
  }
1310
+ /* --------------------------- revoke ------------------------------- */
1311
+ /** Soft-delete a link. Idempotent — repeated calls succeed. */
1312
+ async revoke(shortcode) {
1313
+ if (!SHORTCODE_RE.test(shortcode)) {
1314
+ throw new LinkError("INVALID_INPUT", `links.revoke: '${shortcode}' is not a valid shortcode`);
1315
+ }
1316
+ const res = await post(
1317
+ { apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
1318
+ `/api/v1/sdk/links/${encodeURIComponent(shortcode)}/revoke`,
1319
+ {}
1320
+ );
1321
+ const data = await unwrap(res, "links.revoke");
1322
+ if (!data) throw new LinkError("UNKNOWN", "links.revoke returned empty body");
1323
+ return { shortcode: data.shortcode, revoked: true };
1324
+ }
1325
+ /* ---------------------------- stats ------------------------------- */
1326
+ /** Click totals + breakdowns for a link the SDK owns. */
1327
+ async getStats(shortcode) {
1328
+ if (!SHORTCODE_RE.test(shortcode)) {
1329
+ throw new LinkError("INVALID_INPUT", `links.getStats: '${shortcode}' is not a valid shortcode`);
1330
+ }
1331
+ const res = await getJson(
1332
+ { apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
1333
+ `/api/v1/sdk/links/${encodeURIComponent(shortcode)}/stats`
1334
+ );
1335
+ const data = await unwrap(res, "links.getStats");
1336
+ if (!data) throw new LinkError("UNKNOWN", "links.getStats returned empty body");
1337
+ return data;
1338
+ }
1339
+ /* ----------------------- Linking-API wiring ----------------------- */
1340
+ /**
1341
+ * One-call replacement for the standard cold + warm URL boilerplate:
1342
+ *
1343
+ * const detach = await sendora.links.attachLinkingApi({
1344
+ * onLegacyUrl: (url) => myRouter.handle(url),
1345
+ * });
1346
+ * // …later in unmount:
1347
+ * detach();
1348
+ *
1349
+ * Internally:
1350
+ * - Reads `Linking.getInitialURL()` once.
1351
+ * - Subscribes to `Linking.addEventListener('url', …)`.
1352
+ * - For each URL: checks `extractShortcodeFromUrl(url, linkDomain)`.
1353
+ * Sendora-shaped → calls `handleUniversalLink`.
1354
+ * Otherwise → forwards to `onLegacyUrl` (if supplied).
1355
+ *
1356
+ * Idempotent: re-attaching while a subscription is active replaces it.
1357
+ */
1358
+ async attachLinkingApi(opts = {}) {
1359
+ const rn = loadRn();
1360
+ if (!rn) throw new LinkError("UNKNOWN", "react-native Linking is not available in this runtime");
1361
+ const Linking = rn.Linking;
1362
+ if (!Linking) throw new LinkError("UNKNOWN", "react-native Linking module is undefined");
1363
+ if (this.linkingSub) {
1364
+ try {
1365
+ this.linkingSub.remove();
1366
+ } catch {
1367
+ }
1368
+ this.linkingSub = null;
1369
+ }
1370
+ const handle = async (url) => {
1371
+ if (!url) return;
1372
+ const sc = extractShortcodeFromUrl(url, this.deps.linkDomain);
1373
+ if (sc) {
1374
+ try {
1375
+ await this.handleUniversalLink(url);
1376
+ } catch (err) {
1377
+ if (this.deps.debug) console.warn("[sendoracloud] attachLinkingApi resolve failed:", err);
1378
+ }
1379
+ } else if (opts.onLegacyUrl) {
1380
+ try {
1381
+ opts.onLegacyUrl(url);
1382
+ } catch (err) {
1383
+ if (this.deps.debug) console.warn("[sendoracloud] onLegacyUrl handler threw:", err);
1384
+ }
1385
+ }
1386
+ };
1387
+ try {
1388
+ await handle(await Linking.getInitialURL());
1389
+ } catch (err) {
1390
+ if (this.deps.debug) console.warn("[sendoracloud] getInitialURL failed:", err);
1391
+ }
1392
+ this.linkingSub = Linking.addEventListener("url", (ev) => {
1393
+ void handle(ev?.url ?? null);
1394
+ });
1395
+ return () => {
1396
+ try {
1397
+ this.linkingSub?.remove();
1398
+ } catch {
1399
+ }
1400
+ this.linkingSub = null;
1401
+ };
1402
+ }
993
1403
  };
994
1404
 
995
1405
  // src/auto-track.ts
@@ -1075,7 +1485,7 @@ function installAutoTrack(args) {
1075
1485
  }
1076
1486
 
1077
1487
  // src/index.ts
1078
- var SDK_VERSION = "0.15.0";
1488
+ var SDK_VERSION = "0.16.0";
1079
1489
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
1080
1490
  var ANON_KEY = "anon_id";
1081
1491
  var USER_ID_KEY = "user_id";
@@ -1219,7 +1629,8 @@ var SendoraSDK = class {
1219
1629
  publicKey: this.config.publicKey,
1220
1630
  debug: this.debug,
1221
1631
  iosBundleId: this.config.iosBundleId,
1222
- androidPackageName: this.config.androidPackageName
1632
+ androidPackageName: this.config.androidPackageName,
1633
+ linkDomain: this.config.linkDomain
1223
1634
  });
1224
1635
  this.ready = true;
1225
1636
  if (config.autoTrack !== false) {
@@ -1415,7 +1826,10 @@ var index_default = SendoraCloud;
1415
1826
  Auth,
1416
1827
  AuthError,
1417
1828
  EmailAlreadyTakenError,
1829
+ LinkError,
1418
1830
  Links,
1419
1831
  SendoraCloud,
1420
- extractShortcodeFromUrl
1832
+ computeDeviceFingerprint,
1833
+ extractShortcodeFromUrl,
1834
+ getPlayInstallReferrer
1421
1835
  });