@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 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.11.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
- SendoraCloud
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.11.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.14.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",