@sendoracloud/sdk-react-native 0.13.0 → 0.15.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 +190 -10
- package/dist/index.d.cts +155 -5
- package/dist/index.d.ts +155 -5
- package/dist/index.js +187 -9
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,8 +33,10 @@ __export(index_exports, {
|
|
|
33
33
|
Auth: () => Auth,
|
|
34
34
|
AuthError: () => AuthError,
|
|
35
35
|
EmailAlreadyTakenError: () => EmailAlreadyTakenError,
|
|
36
|
+
Links: () => Links,
|
|
36
37
|
SendoraCloud: () => SendoraCloud,
|
|
37
|
-
default: () => index_default
|
|
38
|
+
default: () => index_default,
|
|
39
|
+
extractShortcodeFromUrl: () => extractShortcodeFromUrl
|
|
38
40
|
});
|
|
39
41
|
module.exports = __toCommonJS(index_exports);
|
|
40
42
|
|
|
@@ -235,21 +237,41 @@ var Auth = class {
|
|
|
235
237
|
this.resolveReady = res;
|
|
236
238
|
});
|
|
237
239
|
}
|
|
238
|
-
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
240
|
+
/**
|
|
241
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
242
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
243
|
+
* promise as resolved so queued auth calls unblock.
|
|
244
|
+
*
|
|
245
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
246
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
247
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
248
|
+
* caused a cold-start regression: if the access token rotated out
|
|
249
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
250
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
251
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
252
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
253
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
254
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
255
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
256
|
+
* access token without disturbing identity.
|
|
257
|
+
*/
|
|
242
258
|
hydrate() {
|
|
243
259
|
const cachedUser = this.hooks.storage.get(USER_KEY);
|
|
244
260
|
const cachedToken = this.hooks.storage.get(TOKEN_KEY);
|
|
261
|
+
const cachedRefresh = this.hooks.storage.get(REFRESH_KEY);
|
|
245
262
|
const cachedExpires = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
|
|
246
|
-
if (cachedUser && cachedToken) {
|
|
263
|
+
if (cachedUser && (cachedToken || cachedRefresh)) {
|
|
247
264
|
try {
|
|
248
265
|
const parsed = JSON.parse(cachedUser);
|
|
249
266
|
if (typeof parsed.id !== "string" || parsed.id.length === 0) throw new Error("invalid cache");
|
|
250
267
|
this.user = parsed;
|
|
251
|
-
|
|
252
|
-
|
|
268
|
+
if (cachedToken) {
|
|
269
|
+
this.accessToken = cachedToken;
|
|
270
|
+
this.accessExpiresAt = cachedExpires ? Number(cachedExpires) : 0;
|
|
271
|
+
} else {
|
|
272
|
+
this.accessToken = null;
|
|
273
|
+
this.accessExpiresAt = 0;
|
|
274
|
+
}
|
|
253
275
|
this.hooks.onIdentityChange(this.user.id);
|
|
254
276
|
} catch {
|
|
255
277
|
this.hooks.storage.remove(USER_KEY);
|
|
@@ -821,6 +843,155 @@ function decodeJwtPayload(token) {
|
|
|
821
843
|
}
|
|
822
844
|
}
|
|
823
845
|
|
|
846
|
+
// src/links.ts
|
|
847
|
+
async function unwrap(res, ctx) {
|
|
848
|
+
if (!res) throw new Error(`[sendoracloud] ${ctx}: network error`);
|
|
849
|
+
if (!res.ok) {
|
|
850
|
+
let msg = `HTTP ${res.status}`;
|
|
851
|
+
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
|
+
}
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
throw new Error(`[sendoracloud] ${ctx}: ${msg}`);
|
|
859
|
+
}
|
|
860
|
+
const parsed = await res.json();
|
|
861
|
+
return parsed.data ?? null;
|
|
862
|
+
}
|
|
863
|
+
var SHORTCODE_RE = /^[a-z0-9-]{3,20}$/;
|
|
864
|
+
function extractShortcodeFromUrl(url) {
|
|
865
|
+
try {
|
|
866
|
+
const u = new URL(url);
|
|
867
|
+
const segs = u.pathname.split("/").filter(Boolean);
|
|
868
|
+
if (segs.length === 0) return null;
|
|
869
|
+
const tail = segs[segs.length - 1];
|
|
870
|
+
return SHORTCODE_RE.test(tail) ? tail : null;
|
|
871
|
+
} catch {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
var Links = class {
|
|
876
|
+
constructor(deps) {
|
|
877
|
+
this.deps = deps;
|
|
878
|
+
this.listeners = [];
|
|
879
|
+
}
|
|
880
|
+
/** Register a callback for warm + deferred deep-link opens. Returns unsubscribe fn. */
|
|
881
|
+
onLinkOpened(handler) {
|
|
882
|
+
this.listeners.push(handler);
|
|
883
|
+
return () => {
|
|
884
|
+
const idx = this.listeners.indexOf(handler);
|
|
885
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
emit(event) {
|
|
889
|
+
for (const fn of this.listeners) {
|
|
890
|
+
try {
|
|
891
|
+
fn(event);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
if (this.deps.debug) console.warn("[sendoracloud] onLinkOpened handler threw:", err);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
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 };
|
|
902
|
+
if (input.iosDeepLinkPath) body.iosDeepLinkPath = input.iosDeepLinkPath;
|
|
903
|
+
if (input.androidDeepLinkPath) body.androidDeepLinkPath = input.androidDeepLinkPath;
|
|
904
|
+
if (input.linkData) body.linkData = input.linkData;
|
|
905
|
+
if (input.ogTitle) body.ogTitle = input.ogTitle;
|
|
906
|
+
if (input.ogDescription) body.ogDescription = input.ogDescription;
|
|
907
|
+
if (input.ogImageUrl) body.ogImageUrl = input.ogImageUrl;
|
|
908
|
+
if (input.campaign) body.campaign = input.campaign;
|
|
909
|
+
if (input.source) body.source = input.source;
|
|
910
|
+
if (input.medium) body.medium = input.medium;
|
|
911
|
+
if (input.channel) body.channel = input.channel;
|
|
912
|
+
if (input.tags) body.tags = input.tags;
|
|
913
|
+
if (input.expiresAt) body.expiresAt = input.expiresAt;
|
|
914
|
+
const ios = input.iosBundleId ?? this.deps.iosBundleId;
|
|
915
|
+
const android = input.androidPackageName ?? this.deps.androidPackageName;
|
|
916
|
+
if (ios) body.iosBundleId = ios;
|
|
917
|
+
if (android) body.androidPackageName = android;
|
|
918
|
+
const res = await post(
|
|
919
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
920
|
+
"/api/v1/sdk/links",
|
|
921
|
+
body
|
|
922
|
+
);
|
|
923
|
+
const data = await unwrap(res, "links.create");
|
|
924
|
+
if (!data?.shortcode) {
|
|
925
|
+
throw new Error("[sendoracloud] links.create returned no shortcode");
|
|
926
|
+
}
|
|
927
|
+
return data;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Warm-path: app received a universal/app-link delivery. Resolves the
|
|
931
|
+
* shortcode against the backend and fires `onLinkOpened`. No-op (and
|
|
932
|
+
* returns false) when the URL isn't a Sendora link.
|
|
933
|
+
*/
|
|
934
|
+
async handleUniversalLink(url) {
|
|
935
|
+
const shortcode = extractShortcodeFromUrl(url);
|
|
936
|
+
if (!shortcode) return false;
|
|
937
|
+
try {
|
|
938
|
+
const res = await getJson(
|
|
939
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
940
|
+
`/api/v1/sdk/links/${encodeURIComponent(shortcode)}`
|
|
941
|
+
);
|
|
942
|
+
const data = await unwrap(res, "links.handleUniversalLink");
|
|
943
|
+
if (!data) return false;
|
|
944
|
+
this.emit({
|
|
945
|
+
shortcode: data.shortcode,
|
|
946
|
+
linkData: data.linkData ?? {},
|
|
947
|
+
iosDeepLinkPath: data.iosDeepLinkPath,
|
|
948
|
+
androidDeepLinkPath: data.androidDeepLinkPath,
|
|
949
|
+
isDeferred: false
|
|
950
|
+
});
|
|
951
|
+
return true;
|
|
952
|
+
} catch (err) {
|
|
953
|
+
if (this.deps.debug) console.warn("[sendoracloud] handleUniversalLink failed:", err);
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
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 (iOS — probabilistic).
|
|
961
|
+
*
|
|
962
|
+
* SDK fires `onLinkOpened` with `isDeferred: true` on success.
|
|
963
|
+
* Returns the event payload on match, `null` on no match.
|
|
964
|
+
*/
|
|
965
|
+
async matchDeferred(input) {
|
|
966
|
+
if (!input.fingerprintHash && !input.installReferrer) {
|
|
967
|
+
throw new Error("[sendoracloud] links.matchDeferred: pass fingerprintHash or installReferrer");
|
|
968
|
+
}
|
|
969
|
+
const body = {};
|
|
970
|
+
if (input.fingerprintHash) body.fingerprintHash = input.fingerprintHash;
|
|
971
|
+
if (input.installReferrer) body.installReferrer = input.installReferrer;
|
|
972
|
+
const ios = input.iosBundleId ?? this.deps.iosBundleId;
|
|
973
|
+
const android = input.androidPackageName ?? this.deps.androidPackageName;
|
|
974
|
+
if (ios) body.iosBundleId = ios;
|
|
975
|
+
if (android) body.androidPackageName = android;
|
|
976
|
+
const res = await post(
|
|
977
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
978
|
+
"/api/v1/sdk/links/match",
|
|
979
|
+
body
|
|
980
|
+
);
|
|
981
|
+
const data = await unwrap(res, "links.matchDeferred");
|
|
982
|
+
if (!data || !data.shortcode) return null;
|
|
983
|
+
const event = {
|
|
984
|
+
shortcode: data.shortcode,
|
|
985
|
+
linkData: data.linkData ?? {},
|
|
986
|
+
iosDeepLinkPath: data.iosDeepLinkPath,
|
|
987
|
+
androidDeepLinkPath: data.androidDeepLinkPath,
|
|
988
|
+
isDeferred: true
|
|
989
|
+
};
|
|
990
|
+
this.emit(event);
|
|
991
|
+
return event;
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
824
995
|
// src/auto-track.ts
|
|
825
996
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
826
997
|
function resolveFlags(cfg) {
|
|
@@ -904,7 +1075,7 @@ function installAutoTrack(args) {
|
|
|
904
1075
|
}
|
|
905
1076
|
|
|
906
1077
|
// src/index.ts
|
|
907
|
-
var SDK_VERSION = "0.
|
|
1078
|
+
var SDK_VERSION = "0.15.0";
|
|
908
1079
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
909
1080
|
var ANON_KEY = "anon_id";
|
|
910
1081
|
var USER_ID_KEY = "user_id";
|
|
@@ -1043,6 +1214,13 @@ var SendoraSDK = class {
|
|
|
1043
1214
|
}
|
|
1044
1215
|
});
|
|
1045
1216
|
this.auth.hydrate();
|
|
1217
|
+
this.links = new Links({
|
|
1218
|
+
apiUrl: this.config.apiUrl,
|
|
1219
|
+
publicKey: this.config.publicKey,
|
|
1220
|
+
debug: this.debug,
|
|
1221
|
+
iosBundleId: this.config.iosBundleId,
|
|
1222
|
+
androidPackageName: this.config.androidPackageName
|
|
1223
|
+
});
|
|
1046
1224
|
this.ready = true;
|
|
1047
1225
|
if (config.autoTrack !== false) {
|
|
1048
1226
|
this.autoTrackCleanup = installAutoTrack({
|
|
@@ -1237,5 +1415,7 @@ var index_default = SendoraCloud;
|
|
|
1237
1415
|
Auth,
|
|
1238
1416
|
AuthError,
|
|
1239
1417
|
EmailAlreadyTakenError,
|
|
1240
|
-
|
|
1418
|
+
Links,
|
|
1419
|
+
SendoraCloud,
|
|
1420
|
+
extractShortcodeFromUrl
|
|
1241
1421
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -122,10 +122,24 @@ declare class Auth {
|
|
|
122
122
|
private readyPromise;
|
|
123
123
|
private resolveReady;
|
|
124
124
|
constructor(hooks: AuthHooks);
|
|
125
|
-
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
/**
|
|
126
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
127
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
128
|
+
* promise as resolved so queued auth calls unblock.
|
|
129
|
+
*
|
|
130
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
131
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
132
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
133
|
+
* caused a cold-start regression: if the access token rotated out
|
|
134
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
135
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
136
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
137
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
138
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
139
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
140
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
141
|
+
* access token without disturbing identity.
|
|
142
|
+
*/
|
|
129
143
|
hydrate(): void;
|
|
130
144
|
getCurrentUser(): AuthUser | null;
|
|
131
145
|
/**
|
|
@@ -290,6 +304,129 @@ declare class Auth {
|
|
|
290
304
|
private wipeLocalIdentity;
|
|
291
305
|
}
|
|
292
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Sendora Deep Links surface for React Native.
|
|
309
|
+
*
|
|
310
|
+
* Three core moves:
|
|
311
|
+
* 1. `create(...)` — mint a Sendora short link from inside the app
|
|
312
|
+
* (Pulse-style "share article" UX).
|
|
313
|
+
* 2. `handleUniversalLink(url)` — call when iOS / Android delivers a Universal
|
|
314
|
+
* Link or App Link to the app. SDK resolves the
|
|
315
|
+
* shortcode against backend and fires
|
|
316
|
+
* `onLinkOpened` with the link's `linkData`.
|
|
317
|
+
* 3. `matchDeferred({ ... })` — call once on cold launch after install. If the
|
|
318
|
+
* device fingerprint OR Android Play Install
|
|
319
|
+
* Referrer match a recent click, SDK fires
|
|
320
|
+
* `onLinkOpened` with `isDeferred: true`.
|
|
321
|
+
*
|
|
322
|
+
* The host app supplies the iOS bundle id / Android package via `init()` of
|
|
323
|
+
* the main SDK, OR per-call. Sendora-side `apps` table is the source of
|
|
324
|
+
* truth — a leaked public key + wrong bundle = 400 from the backend.
|
|
325
|
+
*/
|
|
326
|
+
interface LinkCreateInput {
|
|
327
|
+
/** Customer-facing title. Shown in dashboard list views. */
|
|
328
|
+
title: string;
|
|
329
|
+
/** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
|
|
330
|
+
iosDeepLinkPath?: string;
|
|
331
|
+
/** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
|
|
332
|
+
androidDeepLinkPath?: string;
|
|
333
|
+
/** Web fallback URL — shown when app is not installed + Play Store fallback. */
|
|
334
|
+
fallbackUrl: string;
|
|
335
|
+
/** Custom JSON delivered to onLinkOpened. Max 2KB serialized. */
|
|
336
|
+
linkData?: Record<string, unknown>;
|
|
337
|
+
/** Optional OG preview overrides for richer share cards. */
|
|
338
|
+
ogTitle?: string;
|
|
339
|
+
ogDescription?: string;
|
|
340
|
+
ogImageUrl?: string;
|
|
341
|
+
/** Attribution tags. */
|
|
342
|
+
campaign?: string;
|
|
343
|
+
source?: string;
|
|
344
|
+
medium?: string;
|
|
345
|
+
channel?: string;
|
|
346
|
+
tags?: string[];
|
|
347
|
+
/** ISO timestamp when the link stops resolving. */
|
|
348
|
+
expiresAt?: string;
|
|
349
|
+
/** Bundle ID / package name for the bundle-id gate. */
|
|
350
|
+
iosBundleId?: string;
|
|
351
|
+
androidPackageName?: string;
|
|
352
|
+
}
|
|
353
|
+
interface LinkCreateResult {
|
|
354
|
+
id: string;
|
|
355
|
+
shortcode: string;
|
|
356
|
+
/** Fully qualified share URL — what you pass to RN `Share.share({ message: ... })`. */
|
|
357
|
+
url: string;
|
|
358
|
+
iosDeepLinkPath: string | null;
|
|
359
|
+
androidDeepLinkPath: string | null;
|
|
360
|
+
fallbackUrl: string;
|
|
361
|
+
linkData: Record<string, unknown>;
|
|
362
|
+
expiresAt: string | null;
|
|
363
|
+
}
|
|
364
|
+
interface LinkOpenedEvent {
|
|
365
|
+
shortcode: string;
|
|
366
|
+
/** Custom JSON the link was created with. Pulse navigates off `articleId` here. */
|
|
367
|
+
linkData: Record<string, unknown>;
|
|
368
|
+
/** Server's iOS deep-link path. Useful when host app's URL parser needs a hint. */
|
|
369
|
+
iosDeepLinkPath: string | null;
|
|
370
|
+
/** Server's Android deep-link path. */
|
|
371
|
+
androidDeepLinkPath: string | null;
|
|
372
|
+
/**
|
|
373
|
+
* `true` when this event came from the deferred deep link path —
|
|
374
|
+
* i.e. user installed the app from the App / Play Store after
|
|
375
|
+
* clicking the link, and the SDK matched their fingerprint /
|
|
376
|
+
* install-referrer to the prior click.
|
|
377
|
+
*/
|
|
378
|
+
isDeferred: boolean;
|
|
379
|
+
}
|
|
380
|
+
type LinkOpenedHandler = (event: LinkOpenedEvent) => void;
|
|
381
|
+
interface LinkMatchInput {
|
|
382
|
+
/** Hex-encoded SHA-256 of (ip + ua + screen + tz + locale). Pre-hashed by caller. */
|
|
383
|
+
fingerprintHash?: string;
|
|
384
|
+
/** Raw Play Install Referrer (`PlayInstallReferrerClient.getInstallReferrer().installReferrer`). */
|
|
385
|
+
installReferrer?: string;
|
|
386
|
+
iosBundleId?: string;
|
|
387
|
+
androidPackageName?: string;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Extract a Sendora shortcode from a universal link URL. Accepts either:
|
|
391
|
+
* - https://go.sendoracloud.com/<shortcode>
|
|
392
|
+
* - https://go.sendoracloud.com/link/<shortcode> (Worker rewrites this path)
|
|
393
|
+
* - any custom host the customer pointed at the Sendora Worker (lookup by tail
|
|
394
|
+
* segment matches `SHORTCODE_RE`).
|
|
395
|
+
*/
|
|
396
|
+
declare function extractShortcodeFromUrl(url: string): string | null;
|
|
397
|
+
interface LinksDeps {
|
|
398
|
+
apiUrl: string;
|
|
399
|
+
publicKey: string;
|
|
400
|
+
debug: boolean;
|
|
401
|
+
iosBundleId?: string;
|
|
402
|
+
androidPackageName?: string;
|
|
403
|
+
}
|
|
404
|
+
declare class Links {
|
|
405
|
+
private deps;
|
|
406
|
+
private listeners;
|
|
407
|
+
constructor(deps: LinksDeps);
|
|
408
|
+
/** Register a callback for warm + deferred deep-link opens. Returns unsubscribe fn. */
|
|
409
|
+
onLinkOpened(handler: LinkOpenedHandler): () => void;
|
|
410
|
+
private emit;
|
|
411
|
+
/** Mint a new short link. Returns the share URL + shortcode. */
|
|
412
|
+
create(input: LinkCreateInput): Promise<LinkCreateResult>;
|
|
413
|
+
/**
|
|
414
|
+
* Warm-path: app received a universal/app-link delivery. Resolves the
|
|
415
|
+
* shortcode against the backend and fires `onLinkOpened`. No-op (and
|
|
416
|
+
* returns false) when the URL isn't a Sendora link.
|
|
417
|
+
*/
|
|
418
|
+
handleUniversalLink(url: string): Promise<boolean>;
|
|
419
|
+
/**
|
|
420
|
+
* Cold-launch deferred deep link match. Pass the Android Play Install
|
|
421
|
+
* Referrer string (preferred — 100% accurate) and/or a precomputed
|
|
422
|
+
* fingerprint hash (iOS — probabilistic).
|
|
423
|
+
*
|
|
424
|
+
* SDK fires `onLinkOpened` with `isDeferred: true` on success.
|
|
425
|
+
* Returns the event payload on match, `null` on no match.
|
|
426
|
+
*/
|
|
427
|
+
matchDeferred(input: LinkMatchInput): Promise<LinkOpenedEvent | null>;
|
|
428
|
+
}
|
|
429
|
+
|
|
293
430
|
interface SendoraConfig {
|
|
294
431
|
/**
|
|
295
432
|
* Public API key (`pk_live_…` or `pk_test_…`). Secret keys are refused.
|
|
@@ -344,6 +481,17 @@ interface SendoraConfig {
|
|
|
344
481
|
};
|
|
345
482
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
346
483
|
sessionIdleMs?: number;
|
|
484
|
+
/**
|
|
485
|
+
* iOS bundle id (e.g. `com.pulse.news`). Forwarded to backend on
|
|
486
|
+
* `links.create()` / `links.matchDeferred()` for the bundle-id gate —
|
|
487
|
+
* blocks a leaked public key from being used by a different app.
|
|
488
|
+
*/
|
|
489
|
+
iosBundleId?: string;
|
|
490
|
+
/**
|
|
491
|
+
* Android package name (e.g. `com.pulse.news`). Same purpose as
|
|
492
|
+
* `iosBundleId` above.
|
|
493
|
+
*/
|
|
494
|
+
androidPackageName?: string;
|
|
347
495
|
}
|
|
348
496
|
type TraitValue = string | number | boolean | null | undefined;
|
|
349
497
|
interface IdentifyTraits {
|
|
@@ -416,6 +564,8 @@ declare class SendoraSDK {
|
|
|
416
564
|
private autoTrackCleanup;
|
|
417
565
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
418
566
|
auth: Auth;
|
|
567
|
+
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
568
|
+
links: Links;
|
|
419
569
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
420
570
|
init(config: SendoraConfig): Promise<void>;
|
|
421
571
|
/** Current logical session id, if auto-track is enabled. */
|
|
@@ -462,4 +612,4 @@ declare class SendoraSDK {
|
|
|
462
612
|
}
|
|
463
613
|
declare const SendoraCloud: SendoraSDK;
|
|
464
614
|
|
|
465
|
-
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
|
|
615
|
+
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default, extractShortcodeFromUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -122,10 +122,24 @@ declare class Auth {
|
|
|
122
122
|
private readyPromise;
|
|
123
123
|
private resolveReady;
|
|
124
124
|
constructor(hooks: AuthHooks);
|
|
125
|
-
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
/**
|
|
126
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
127
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
128
|
+
* promise as resolved so queued auth calls unblock.
|
|
129
|
+
*
|
|
130
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
131
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
132
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
133
|
+
* caused a cold-start regression: if the access token rotated out
|
|
134
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
135
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
136
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
137
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
138
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
139
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
140
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
141
|
+
* access token without disturbing identity.
|
|
142
|
+
*/
|
|
129
143
|
hydrate(): void;
|
|
130
144
|
getCurrentUser(): AuthUser | null;
|
|
131
145
|
/**
|
|
@@ -290,6 +304,129 @@ declare class Auth {
|
|
|
290
304
|
private wipeLocalIdentity;
|
|
291
305
|
}
|
|
292
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Sendora Deep Links surface for React Native.
|
|
309
|
+
*
|
|
310
|
+
* Three core moves:
|
|
311
|
+
* 1. `create(...)` — mint a Sendora short link from inside the app
|
|
312
|
+
* (Pulse-style "share article" UX).
|
|
313
|
+
* 2. `handleUniversalLink(url)` — call when iOS / Android delivers a Universal
|
|
314
|
+
* Link or App Link to the app. SDK resolves the
|
|
315
|
+
* shortcode against backend and fires
|
|
316
|
+
* `onLinkOpened` with the link's `linkData`.
|
|
317
|
+
* 3. `matchDeferred({ ... })` — call once on cold launch after install. If the
|
|
318
|
+
* device fingerprint OR Android Play Install
|
|
319
|
+
* Referrer match a recent click, SDK fires
|
|
320
|
+
* `onLinkOpened` with `isDeferred: true`.
|
|
321
|
+
*
|
|
322
|
+
* The host app supplies the iOS bundle id / Android package via `init()` of
|
|
323
|
+
* the main SDK, OR per-call. Sendora-side `apps` table is the source of
|
|
324
|
+
* truth — a leaked public key + wrong bundle = 400 from the backend.
|
|
325
|
+
*/
|
|
326
|
+
interface LinkCreateInput {
|
|
327
|
+
/** Customer-facing title. Shown in dashboard list views. */
|
|
328
|
+
title: string;
|
|
329
|
+
/** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
|
|
330
|
+
iosDeepLinkPath?: string;
|
|
331
|
+
/** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
|
|
332
|
+
androidDeepLinkPath?: string;
|
|
333
|
+
/** Web fallback URL — shown when app is not installed + Play Store fallback. */
|
|
334
|
+
fallbackUrl: string;
|
|
335
|
+
/** Custom JSON delivered to onLinkOpened. Max 2KB serialized. */
|
|
336
|
+
linkData?: Record<string, unknown>;
|
|
337
|
+
/** Optional OG preview overrides for richer share cards. */
|
|
338
|
+
ogTitle?: string;
|
|
339
|
+
ogDescription?: string;
|
|
340
|
+
ogImageUrl?: string;
|
|
341
|
+
/** Attribution tags. */
|
|
342
|
+
campaign?: string;
|
|
343
|
+
source?: string;
|
|
344
|
+
medium?: string;
|
|
345
|
+
channel?: string;
|
|
346
|
+
tags?: string[];
|
|
347
|
+
/** ISO timestamp when the link stops resolving. */
|
|
348
|
+
expiresAt?: string;
|
|
349
|
+
/** Bundle ID / package name for the bundle-id gate. */
|
|
350
|
+
iosBundleId?: string;
|
|
351
|
+
androidPackageName?: string;
|
|
352
|
+
}
|
|
353
|
+
interface LinkCreateResult {
|
|
354
|
+
id: string;
|
|
355
|
+
shortcode: string;
|
|
356
|
+
/** Fully qualified share URL — what you pass to RN `Share.share({ message: ... })`. */
|
|
357
|
+
url: string;
|
|
358
|
+
iosDeepLinkPath: string | null;
|
|
359
|
+
androidDeepLinkPath: string | null;
|
|
360
|
+
fallbackUrl: string;
|
|
361
|
+
linkData: Record<string, unknown>;
|
|
362
|
+
expiresAt: string | null;
|
|
363
|
+
}
|
|
364
|
+
interface LinkOpenedEvent {
|
|
365
|
+
shortcode: string;
|
|
366
|
+
/** Custom JSON the link was created with. Pulse navigates off `articleId` here. */
|
|
367
|
+
linkData: Record<string, unknown>;
|
|
368
|
+
/** Server's iOS deep-link path. Useful when host app's URL parser needs a hint. */
|
|
369
|
+
iosDeepLinkPath: string | null;
|
|
370
|
+
/** Server's Android deep-link path. */
|
|
371
|
+
androidDeepLinkPath: string | null;
|
|
372
|
+
/**
|
|
373
|
+
* `true` when this event came from the deferred deep link path —
|
|
374
|
+
* i.e. user installed the app from the App / Play Store after
|
|
375
|
+
* clicking the link, and the SDK matched their fingerprint /
|
|
376
|
+
* install-referrer to the prior click.
|
|
377
|
+
*/
|
|
378
|
+
isDeferred: boolean;
|
|
379
|
+
}
|
|
380
|
+
type LinkOpenedHandler = (event: LinkOpenedEvent) => void;
|
|
381
|
+
interface LinkMatchInput {
|
|
382
|
+
/** Hex-encoded SHA-256 of (ip + ua + screen + tz + locale). Pre-hashed by caller. */
|
|
383
|
+
fingerprintHash?: string;
|
|
384
|
+
/** Raw Play Install Referrer (`PlayInstallReferrerClient.getInstallReferrer().installReferrer`). */
|
|
385
|
+
installReferrer?: string;
|
|
386
|
+
iosBundleId?: string;
|
|
387
|
+
androidPackageName?: string;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Extract a Sendora shortcode from a universal link URL. Accepts either:
|
|
391
|
+
* - https://go.sendoracloud.com/<shortcode>
|
|
392
|
+
* - https://go.sendoracloud.com/link/<shortcode> (Worker rewrites this path)
|
|
393
|
+
* - any custom host the customer pointed at the Sendora Worker (lookup by tail
|
|
394
|
+
* segment matches `SHORTCODE_RE`).
|
|
395
|
+
*/
|
|
396
|
+
declare function extractShortcodeFromUrl(url: string): string | null;
|
|
397
|
+
interface LinksDeps {
|
|
398
|
+
apiUrl: string;
|
|
399
|
+
publicKey: string;
|
|
400
|
+
debug: boolean;
|
|
401
|
+
iosBundleId?: string;
|
|
402
|
+
androidPackageName?: string;
|
|
403
|
+
}
|
|
404
|
+
declare class Links {
|
|
405
|
+
private deps;
|
|
406
|
+
private listeners;
|
|
407
|
+
constructor(deps: LinksDeps);
|
|
408
|
+
/** Register a callback for warm + deferred deep-link opens. Returns unsubscribe fn. */
|
|
409
|
+
onLinkOpened(handler: LinkOpenedHandler): () => void;
|
|
410
|
+
private emit;
|
|
411
|
+
/** Mint a new short link. Returns the share URL + shortcode. */
|
|
412
|
+
create(input: LinkCreateInput): Promise<LinkCreateResult>;
|
|
413
|
+
/**
|
|
414
|
+
* Warm-path: app received a universal/app-link delivery. Resolves the
|
|
415
|
+
* shortcode against the backend and fires `onLinkOpened`. No-op (and
|
|
416
|
+
* returns false) when the URL isn't a Sendora link.
|
|
417
|
+
*/
|
|
418
|
+
handleUniversalLink(url: string): Promise<boolean>;
|
|
419
|
+
/**
|
|
420
|
+
* Cold-launch deferred deep link match. Pass the Android Play Install
|
|
421
|
+
* Referrer string (preferred — 100% accurate) and/or a precomputed
|
|
422
|
+
* fingerprint hash (iOS — probabilistic).
|
|
423
|
+
*
|
|
424
|
+
* SDK fires `onLinkOpened` with `isDeferred: true` on success.
|
|
425
|
+
* Returns the event payload on match, `null` on no match.
|
|
426
|
+
*/
|
|
427
|
+
matchDeferred(input: LinkMatchInput): Promise<LinkOpenedEvent | null>;
|
|
428
|
+
}
|
|
429
|
+
|
|
293
430
|
interface SendoraConfig {
|
|
294
431
|
/**
|
|
295
432
|
* Public API key (`pk_live_…` or `pk_test_…`). Secret keys are refused.
|
|
@@ -344,6 +481,17 @@ interface SendoraConfig {
|
|
|
344
481
|
};
|
|
345
482
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
346
483
|
sessionIdleMs?: number;
|
|
484
|
+
/**
|
|
485
|
+
* iOS bundle id (e.g. `com.pulse.news`). Forwarded to backend on
|
|
486
|
+
* `links.create()` / `links.matchDeferred()` for the bundle-id gate —
|
|
487
|
+
* blocks a leaked public key from being used by a different app.
|
|
488
|
+
*/
|
|
489
|
+
iosBundleId?: string;
|
|
490
|
+
/**
|
|
491
|
+
* Android package name (e.g. `com.pulse.news`). Same purpose as
|
|
492
|
+
* `iosBundleId` above.
|
|
493
|
+
*/
|
|
494
|
+
androidPackageName?: string;
|
|
347
495
|
}
|
|
348
496
|
type TraitValue = string | number | boolean | null | undefined;
|
|
349
497
|
interface IdentifyTraits {
|
|
@@ -416,6 +564,8 @@ declare class SendoraSDK {
|
|
|
416
564
|
private autoTrackCleanup;
|
|
417
565
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
418
566
|
auth: Auth;
|
|
567
|
+
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
568
|
+
links: Links;
|
|
419
569
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
420
570
|
init(config: SendoraConfig): Promise<void>;
|
|
421
571
|
/** Current logical session id, if auto-track is enabled. */
|
|
@@ -462,4 +612,4 @@ declare class SendoraSDK {
|
|
|
462
612
|
}
|
|
463
613
|
declare const SendoraCloud: SendoraSDK;
|
|
464
614
|
|
|
465
|
-
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
|
|
615
|
+
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default, extractShortcodeFromUrl };
|
package/dist/index.js
CHANGED
|
@@ -195,21 +195,41 @@ var Auth = class {
|
|
|
195
195
|
this.resolveReady = res;
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
|
-
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
198
|
+
/**
|
|
199
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
200
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
201
|
+
* promise as resolved so queued auth calls unblock.
|
|
202
|
+
*
|
|
203
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
204
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
205
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
206
|
+
* caused a cold-start regression: if the access token rotated out
|
|
207
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
208
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
209
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
210
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
211
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
212
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
213
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
214
|
+
* access token without disturbing identity.
|
|
215
|
+
*/
|
|
202
216
|
hydrate() {
|
|
203
217
|
const cachedUser = this.hooks.storage.get(USER_KEY);
|
|
204
218
|
const cachedToken = this.hooks.storage.get(TOKEN_KEY);
|
|
219
|
+
const cachedRefresh = this.hooks.storage.get(REFRESH_KEY);
|
|
205
220
|
const cachedExpires = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
|
|
206
|
-
if (cachedUser && cachedToken) {
|
|
221
|
+
if (cachedUser && (cachedToken || cachedRefresh)) {
|
|
207
222
|
try {
|
|
208
223
|
const parsed = JSON.parse(cachedUser);
|
|
209
224
|
if (typeof parsed.id !== "string" || parsed.id.length === 0) throw new Error("invalid cache");
|
|
210
225
|
this.user = parsed;
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
if (cachedToken) {
|
|
227
|
+
this.accessToken = cachedToken;
|
|
228
|
+
this.accessExpiresAt = cachedExpires ? Number(cachedExpires) : 0;
|
|
229
|
+
} else {
|
|
230
|
+
this.accessToken = null;
|
|
231
|
+
this.accessExpiresAt = 0;
|
|
232
|
+
}
|
|
213
233
|
this.hooks.onIdentityChange(this.user.id);
|
|
214
234
|
} catch {
|
|
215
235
|
this.hooks.storage.remove(USER_KEY);
|
|
@@ -781,6 +801,155 @@ function decodeJwtPayload(token) {
|
|
|
781
801
|
}
|
|
782
802
|
}
|
|
783
803
|
|
|
804
|
+
// src/links.ts
|
|
805
|
+
async function unwrap(res, ctx) {
|
|
806
|
+
if (!res) throw new Error(`[sendoracloud] ${ctx}: network error`);
|
|
807
|
+
if (!res.ok) {
|
|
808
|
+
let msg = `HTTP ${res.status}`;
|
|
809
|
+
try {
|
|
810
|
+
const parsed2 = await res.clone().json();
|
|
811
|
+
if (parsed2.error?.code || parsed2.error?.message) {
|
|
812
|
+
msg = `${parsed2.error.code ?? ""}${parsed2.error.code && parsed2.error.message ? ": " : ""}${parsed2.error.message ?? ""}`;
|
|
813
|
+
}
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
throw new Error(`[sendoracloud] ${ctx}: ${msg}`);
|
|
817
|
+
}
|
|
818
|
+
const parsed = await res.json();
|
|
819
|
+
return parsed.data ?? null;
|
|
820
|
+
}
|
|
821
|
+
var SHORTCODE_RE = /^[a-z0-9-]{3,20}$/;
|
|
822
|
+
function extractShortcodeFromUrl(url) {
|
|
823
|
+
try {
|
|
824
|
+
const u = new URL(url);
|
|
825
|
+
const segs = u.pathname.split("/").filter(Boolean);
|
|
826
|
+
if (segs.length === 0) return null;
|
|
827
|
+
const tail = segs[segs.length - 1];
|
|
828
|
+
return SHORTCODE_RE.test(tail) ? tail : null;
|
|
829
|
+
} catch {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
var Links = class {
|
|
834
|
+
constructor(deps) {
|
|
835
|
+
this.deps = deps;
|
|
836
|
+
this.listeners = [];
|
|
837
|
+
}
|
|
838
|
+
/** Register a callback for warm + deferred deep-link opens. Returns unsubscribe fn. */
|
|
839
|
+
onLinkOpened(handler) {
|
|
840
|
+
this.listeners.push(handler);
|
|
841
|
+
return () => {
|
|
842
|
+
const idx = this.listeners.indexOf(handler);
|
|
843
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
emit(event) {
|
|
847
|
+
for (const fn of this.listeners) {
|
|
848
|
+
try {
|
|
849
|
+
fn(event);
|
|
850
|
+
} catch (err) {
|
|
851
|
+
if (this.deps.debug) console.warn("[sendoracloud] onLinkOpened handler threw:", err);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/** Mint a new short link. Returns the share URL + shortcode. */
|
|
856
|
+
async create(input) {
|
|
857
|
+
if (!input.title) throw new Error("[sendoracloud] links.create: title is required");
|
|
858
|
+
if (!input.fallbackUrl) throw new Error("[sendoracloud] links.create: fallbackUrl is required");
|
|
859
|
+
const body = { title: input.title, fallbackUrl: input.fallbackUrl };
|
|
860
|
+
if (input.iosDeepLinkPath) body.iosDeepLinkPath = input.iosDeepLinkPath;
|
|
861
|
+
if (input.androidDeepLinkPath) body.androidDeepLinkPath = input.androidDeepLinkPath;
|
|
862
|
+
if (input.linkData) body.linkData = input.linkData;
|
|
863
|
+
if (input.ogTitle) body.ogTitle = input.ogTitle;
|
|
864
|
+
if (input.ogDescription) body.ogDescription = input.ogDescription;
|
|
865
|
+
if (input.ogImageUrl) body.ogImageUrl = input.ogImageUrl;
|
|
866
|
+
if (input.campaign) body.campaign = input.campaign;
|
|
867
|
+
if (input.source) body.source = input.source;
|
|
868
|
+
if (input.medium) body.medium = input.medium;
|
|
869
|
+
if (input.channel) body.channel = input.channel;
|
|
870
|
+
if (input.tags) body.tags = input.tags;
|
|
871
|
+
if (input.expiresAt) body.expiresAt = input.expiresAt;
|
|
872
|
+
const ios = input.iosBundleId ?? this.deps.iosBundleId;
|
|
873
|
+
const android = input.androidPackageName ?? this.deps.androidPackageName;
|
|
874
|
+
if (ios) body.iosBundleId = ios;
|
|
875
|
+
if (android) body.androidPackageName = android;
|
|
876
|
+
const res = await post(
|
|
877
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
878
|
+
"/api/v1/sdk/links",
|
|
879
|
+
body
|
|
880
|
+
);
|
|
881
|
+
const data = await unwrap(res, "links.create");
|
|
882
|
+
if (!data?.shortcode) {
|
|
883
|
+
throw new Error("[sendoracloud] links.create returned no shortcode");
|
|
884
|
+
}
|
|
885
|
+
return data;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Warm-path: app received a universal/app-link delivery. Resolves the
|
|
889
|
+
* shortcode against the backend and fires `onLinkOpened`. No-op (and
|
|
890
|
+
* returns false) when the URL isn't a Sendora link.
|
|
891
|
+
*/
|
|
892
|
+
async handleUniversalLink(url) {
|
|
893
|
+
const shortcode = extractShortcodeFromUrl(url);
|
|
894
|
+
if (!shortcode) return false;
|
|
895
|
+
try {
|
|
896
|
+
const res = await getJson(
|
|
897
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
898
|
+
`/api/v1/sdk/links/${encodeURIComponent(shortcode)}`
|
|
899
|
+
);
|
|
900
|
+
const data = await unwrap(res, "links.handleUniversalLink");
|
|
901
|
+
if (!data) return false;
|
|
902
|
+
this.emit({
|
|
903
|
+
shortcode: data.shortcode,
|
|
904
|
+
linkData: data.linkData ?? {},
|
|
905
|
+
iosDeepLinkPath: data.iosDeepLinkPath,
|
|
906
|
+
androidDeepLinkPath: data.androidDeepLinkPath,
|
|
907
|
+
isDeferred: false
|
|
908
|
+
});
|
|
909
|
+
return true;
|
|
910
|
+
} catch (err) {
|
|
911
|
+
if (this.deps.debug) console.warn("[sendoracloud] handleUniversalLink failed:", err);
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Cold-launch deferred deep link match. Pass the Android Play Install
|
|
917
|
+
* Referrer string (preferred — 100% accurate) and/or a precomputed
|
|
918
|
+
* fingerprint hash (iOS — probabilistic).
|
|
919
|
+
*
|
|
920
|
+
* SDK fires `onLinkOpened` with `isDeferred: true` on success.
|
|
921
|
+
* Returns the event payload on match, `null` on no match.
|
|
922
|
+
*/
|
|
923
|
+
async matchDeferred(input) {
|
|
924
|
+
if (!input.fingerprintHash && !input.installReferrer) {
|
|
925
|
+
throw new Error("[sendoracloud] links.matchDeferred: pass fingerprintHash or installReferrer");
|
|
926
|
+
}
|
|
927
|
+
const body = {};
|
|
928
|
+
if (input.fingerprintHash) body.fingerprintHash = input.fingerprintHash;
|
|
929
|
+
if (input.installReferrer) body.installReferrer = input.installReferrer;
|
|
930
|
+
const ios = input.iosBundleId ?? this.deps.iosBundleId;
|
|
931
|
+
const android = input.androidPackageName ?? this.deps.androidPackageName;
|
|
932
|
+
if (ios) body.iosBundleId = ios;
|
|
933
|
+
if (android) body.androidPackageName = android;
|
|
934
|
+
const res = await post(
|
|
935
|
+
{ apiUrl: this.deps.apiUrl, publicKey: this.deps.publicKey, debug: this.deps.debug },
|
|
936
|
+
"/api/v1/sdk/links/match",
|
|
937
|
+
body
|
|
938
|
+
);
|
|
939
|
+
const data = await unwrap(res, "links.matchDeferred");
|
|
940
|
+
if (!data || !data.shortcode) return null;
|
|
941
|
+
const event = {
|
|
942
|
+
shortcode: data.shortcode,
|
|
943
|
+
linkData: data.linkData ?? {},
|
|
944
|
+
iosDeepLinkPath: data.iosDeepLinkPath,
|
|
945
|
+
androidDeepLinkPath: data.androidDeepLinkPath,
|
|
946
|
+
isDeferred: true
|
|
947
|
+
};
|
|
948
|
+
this.emit(event);
|
|
949
|
+
return event;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
784
953
|
// src/auto-track.ts
|
|
785
954
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
786
955
|
function resolveFlags(cfg) {
|
|
@@ -864,7 +1033,7 @@ function installAutoTrack(args) {
|
|
|
864
1033
|
}
|
|
865
1034
|
|
|
866
1035
|
// src/index.ts
|
|
867
|
-
var SDK_VERSION = "0.
|
|
1036
|
+
var SDK_VERSION = "0.15.0";
|
|
868
1037
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
869
1038
|
var ANON_KEY = "anon_id";
|
|
870
1039
|
var USER_ID_KEY = "user_id";
|
|
@@ -1003,6 +1172,13 @@ var SendoraSDK = class {
|
|
|
1003
1172
|
}
|
|
1004
1173
|
});
|
|
1005
1174
|
this.auth.hydrate();
|
|
1175
|
+
this.links = new Links({
|
|
1176
|
+
apiUrl: this.config.apiUrl,
|
|
1177
|
+
publicKey: this.config.publicKey,
|
|
1178
|
+
debug: this.debug,
|
|
1179
|
+
iosBundleId: this.config.iosBundleId,
|
|
1180
|
+
androidPackageName: this.config.androidPackageName
|
|
1181
|
+
});
|
|
1006
1182
|
this.ready = true;
|
|
1007
1183
|
if (config.autoTrack !== false) {
|
|
1008
1184
|
this.autoTrackCleanup = installAutoTrack({
|
|
@@ -1196,6 +1372,8 @@ export {
|
|
|
1196
1372
|
Auth,
|
|
1197
1373
|
AuthError,
|
|
1198
1374
|
EmailAlreadyTakenError,
|
|
1375
|
+
Links,
|
|
1199
1376
|
SendoraCloud,
|
|
1200
|
-
index_default as default
|
|
1377
|
+
index_default as default,
|
|
1378
|
+
extractShortcodeFromUrl
|
|
1201
1379
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|