@sendoracloud/sdk-react-native 0.14.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 +163 -3
- package/dist/index.d.cts +137 -1
- package/dist/index.d.ts +137 -1
- package/dist/index.js +160 -2
- 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
|
|
|
@@ -841,6 +843,155 @@ function decodeJwtPayload(token) {
|
|
|
841
843
|
}
|
|
842
844
|
}
|
|
843
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
|
+
|
|
844
995
|
// src/auto-track.ts
|
|
845
996
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
846
997
|
function resolveFlags(cfg) {
|
|
@@ -924,7 +1075,7 @@ function installAutoTrack(args) {
|
|
|
924
1075
|
}
|
|
925
1076
|
|
|
926
1077
|
// src/index.ts
|
|
927
|
-
var SDK_VERSION = "0.
|
|
1078
|
+
var SDK_VERSION = "0.15.0";
|
|
928
1079
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
929
1080
|
var ANON_KEY = "anon_id";
|
|
930
1081
|
var USER_ID_KEY = "user_id";
|
|
@@ -1063,6 +1214,13 @@ var SendoraSDK = class {
|
|
|
1063
1214
|
}
|
|
1064
1215
|
});
|
|
1065
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
|
+
});
|
|
1066
1224
|
this.ready = true;
|
|
1067
1225
|
if (config.autoTrack !== false) {
|
|
1068
1226
|
this.autoTrackCleanup = installAutoTrack({
|
|
@@ -1257,5 +1415,7 @@ var index_default = SendoraCloud;
|
|
|
1257
1415
|
Auth,
|
|
1258
1416
|
AuthError,
|
|
1259
1417
|
EmailAlreadyTakenError,
|
|
1260
|
-
|
|
1418
|
+
Links,
|
|
1419
|
+
SendoraCloud,
|
|
1420
|
+
extractShortcodeFromUrl
|
|
1261
1421
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -304,6 +304,129 @@ declare class Auth {
|
|
|
304
304
|
private wipeLocalIdentity;
|
|
305
305
|
}
|
|
306
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
|
+
|
|
307
430
|
interface SendoraConfig {
|
|
308
431
|
/**
|
|
309
432
|
* Public API key (`pk_live_…` or `pk_test_…`). Secret keys are refused.
|
|
@@ -358,6 +481,17 @@ interface SendoraConfig {
|
|
|
358
481
|
};
|
|
359
482
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
360
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;
|
|
361
495
|
}
|
|
362
496
|
type TraitValue = string | number | boolean | null | undefined;
|
|
363
497
|
interface IdentifyTraits {
|
|
@@ -430,6 +564,8 @@ declare class SendoraSDK {
|
|
|
430
564
|
private autoTrackCleanup;
|
|
431
565
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
432
566
|
auth: Auth;
|
|
567
|
+
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
568
|
+
links: Links;
|
|
433
569
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
434
570
|
init(config: SendoraConfig): Promise<void>;
|
|
435
571
|
/** Current logical session id, if auto-track is enabled. */
|
|
@@ -476,4 +612,4 @@ declare class SendoraSDK {
|
|
|
476
612
|
}
|
|
477
613
|
declare const SendoraCloud: SendoraSDK;
|
|
478
614
|
|
|
479
|
-
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
|
@@ -304,6 +304,129 @@ declare class Auth {
|
|
|
304
304
|
private wipeLocalIdentity;
|
|
305
305
|
}
|
|
306
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
|
+
|
|
307
430
|
interface SendoraConfig {
|
|
308
431
|
/**
|
|
309
432
|
* Public API key (`pk_live_…` or `pk_test_…`). Secret keys are refused.
|
|
@@ -358,6 +481,17 @@ interface SendoraConfig {
|
|
|
358
481
|
};
|
|
359
482
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
360
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;
|
|
361
495
|
}
|
|
362
496
|
type TraitValue = string | number | boolean | null | undefined;
|
|
363
497
|
interface IdentifyTraits {
|
|
@@ -430,6 +564,8 @@ declare class SendoraSDK {
|
|
|
430
564
|
private autoTrackCleanup;
|
|
431
565
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
432
566
|
auth: Auth;
|
|
567
|
+
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
568
|
+
links: Links;
|
|
433
569
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
434
570
|
init(config: SendoraConfig): Promise<void>;
|
|
435
571
|
/** Current logical session id, if auto-track is enabled. */
|
|
@@ -476,4 +612,4 @@ declare class SendoraSDK {
|
|
|
476
612
|
}
|
|
477
613
|
declare const SendoraCloud: SendoraSDK;
|
|
478
614
|
|
|
479
|
-
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
|
@@ -801,6 +801,155 @@ function decodeJwtPayload(token) {
|
|
|
801
801
|
}
|
|
802
802
|
}
|
|
803
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
|
+
|
|
804
953
|
// src/auto-track.ts
|
|
805
954
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
806
955
|
function resolveFlags(cfg) {
|
|
@@ -884,7 +1033,7 @@ function installAutoTrack(args) {
|
|
|
884
1033
|
}
|
|
885
1034
|
|
|
886
1035
|
// src/index.ts
|
|
887
|
-
var SDK_VERSION = "0.
|
|
1036
|
+
var SDK_VERSION = "0.15.0";
|
|
888
1037
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
889
1038
|
var ANON_KEY = "anon_id";
|
|
890
1039
|
var USER_ID_KEY = "user_id";
|
|
@@ -1023,6 +1172,13 @@ var SendoraSDK = class {
|
|
|
1023
1172
|
}
|
|
1024
1173
|
});
|
|
1025
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
|
+
});
|
|
1026
1182
|
this.ready = true;
|
|
1027
1183
|
if (config.autoTrack !== false) {
|
|
1028
1184
|
this.autoTrackCleanup = installAutoTrack({
|
|
@@ -1216,6 +1372,8 @@ export {
|
|
|
1216
1372
|
Auth,
|
|
1217
1373
|
AuthError,
|
|
1218
1374
|
EmailAlreadyTakenError,
|
|
1375
|
+
Links,
|
|
1219
1376
|
SendoraCloud,
|
|
1220
|
-
index_default as default
|
|
1377
|
+
index_default as default,
|
|
1378
|
+
extractShortcodeFromUrl
|
|
1221
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",
|