@sendoracloud/sdk-react-native 0.15.1 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -305,35 +305,62 @@ declare class Auth {
305
305
  }
306
306
 
307
307
  /**
308
- * Sendora Deep Links surface for React Native.
308
+ * Sendora Deep Links surface for React Native (s58.50 — Pulse DX rewrite).
309
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`.
310
+ * Surface:
311
+ * 1. `create<T>(...)` — mint a Sendora short link from inside the app.
312
+ * 2. `prewarm<T>(...)` — background-mint + cache so share row taps
313
+ * skip the HTTPS round-trip ActivityIndicator.
314
+ * 3. `handleUniversalLink(url)` — resolve a delivered universal / app link
315
+ * and fire `onLinkOpened`.
316
+ * 4. `matchDeferred({ ... })` cold-launch deferred match.
317
+ * 5. `attachLinkingApi({ ... })` — one-call replacement for the
318
+ * `Linking.getInitialURL` + `addEventListener`
319
+ * + extract-pre-check + fallthrough boilerplate.
320
+ * 6. `computeDeviceFingerprint()` canonical recipe (matches iOS + Android),
321
+ * no client-side boilerplate required.
322
+ * 7. `revoke(shortcode)` — soft-delete (private-content unsend).
323
+ * 8. `getStats(shortcode)` — totals + breakdowns; no dashboard scraping.
324
+ * 9. `onLinkOpened<T>(cb)` — generic-typed handler.
325
+ * 10. `LinkError` — typed errors with codes
326
+ * (`instanceof` + `err.code` rather than
327
+ * `err.message.includes(...)`).
321
328
  *
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.
329
+ * Bundle-id gate: SDK auto-supplies `iosBundleId` / `androidPackageName`
330
+ * from `init()` config host app never has to thread them through per call.
331
+ * Sendora-side `apps` table is the source of truth a leaked public key
332
+ * + wrong bundle = 422 from the backend mapped to
333
+ * `LinkError(code: "BUNDLE_MISMATCH")` here.
325
334
  */
326
- interface LinkCreateInput {
335
+ /**
336
+ * Error code taxonomy. Mirrors `apps/backend/src/modules/links/*` outcomes.
337
+ * Callers branch on `err.code` (string compare) or `err instanceof LinkError`.
338
+ */
339
+ type LinkErrorCode = "BUNDLE_MISMATCH" | "DATA_TOO_LARGE" | "EXPIRED" | "NETWORK" | "RATE_LIMITED" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_INPUT" | "PLAN_LIMIT" | "FALLBACK_REQUIRED" | "SERVER" | "UNKNOWN";
340
+ declare class LinkError extends Error {
341
+ readonly code: LinkErrorCode;
342
+ readonly statusCode: number;
343
+ readonly details?: unknown;
344
+ constructor(code: LinkErrorCode, message: string, statusCode?: number, details?: unknown);
345
+ }
346
+ /** Default linkData shape — opaque record. Override with a discriminated
347
+ * union or product-specific interface in the host app for full type-safety. */
348
+ type LinkData = Record<string, unknown>;
349
+ interface LinkCreateInput<T extends LinkData = LinkData> {
327
350
  /** Customer-facing title. Shown in dashboard list views. */
328
351
  title: string;
329
352
  /** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
330
353
  iosDeepLinkPath?: string;
331
354
  /** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
332
355
  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>;
356
+ /**
357
+ * Web fallback URL. **Optional as of 0.16.0** — backend defaults from
358
+ * the project's apps registry (web origin > iOS App Store URL > Android
359
+ * Play Store URL). Pass explicitly for per-link override.
360
+ */
361
+ fallbackUrl?: string;
362
+ /** Custom typed JSON delivered to onLinkOpened. Max 2KB serialized. */
363
+ linkData?: T;
337
364
  /** Optional OG preview overrides for richer share cards. */
338
365
  ogTitle?: string;
339
366
  ogDescription?: string;
@@ -344,72 +371,130 @@ interface LinkCreateInput {
344
371
  medium?: string;
345
372
  channel?: string;
346
373
  tags?: string[];
347
- /** ISO timestamp when the link stops resolving. */
374
+ /** ISO timestamp when the link stops resolving. Must be future + within 5y. */
348
375
  expiresAt?: string;
349
- /** Bundle ID / package name for the bundle-id gate. */
376
+ /** Bundle ID / package name for the bundle-id gate. Defaults from init. */
350
377
  iosBundleId?: string;
351
378
  androidPackageName?: string;
352
379
  }
353
- interface LinkCreateResult {
380
+ interface LinkCreateResult<T extends LinkData = LinkData> {
354
381
  id: string;
355
382
  shortcode: string;
356
- /** Fully qualified share URL — what you pass to RN `Share.share({ message: ... })`. */
383
+ /** Fully qualified share URL — pass to RN `Share.share({ message: ... })`. */
357
384
  url: string;
358
385
  iosDeepLinkPath: string | null;
359
386
  androidDeepLinkPath: string | null;
360
387
  fallbackUrl: string;
361
- linkData: Record<string, unknown>;
388
+ linkData: T;
362
389
  expiresAt: string | null;
363
390
  }
364
- interface LinkOpenedEvent {
391
+ interface LinkOpenedEvent<T extends LinkData = LinkData> {
365
392
  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. */
393
+ /** Custom typed JSON the link was created with. */
394
+ linkData: T;
369
395
  iosDeepLinkPath: string | null;
370
- /** Server's Android deep-link path. */
371
396
  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
- */
397
+ /** `true` when this event came from the deferred deep link path. */
378
398
  isDeferred: boolean;
379
399
  }
380
- type LinkOpenedHandler = (event: LinkOpenedEvent) => void;
400
+ type LinkOpenedHandler<T extends LinkData = LinkData> = (event: LinkOpenedEvent<T>) => void;
381
401
  interface LinkMatchInput {
382
- /** Hex-encoded SHA-256 of (ip + ua + screen + tz + locale). Pre-hashed by caller. */
402
+ /** Hex-encoded SHA-256 pass output of `computeDeviceFingerprint()`. */
383
403
  fingerprintHash?: string;
384
- /** Raw Play Install Referrer (`PlayInstallReferrerClient.getInstallReferrer().installReferrer`). */
404
+ /** Raw Play Install Referrer. Sendora probes
405
+ * `react-native-play-install-referrer` automatically when this is omitted. */
385
406
  installReferrer?: string;
386
407
  iosBundleId?: string;
387
408
  androidPackageName?: string;
388
409
  }
410
+ interface LinkStats {
411
+ totalClicks: number;
412
+ uniqueClicks: number;
413
+ deferredMatches: number;
414
+ byDevice: Array<{
415
+ deviceType: string | null;
416
+ count: number;
417
+ }>;
418
+ byCountry: Array<{
419
+ country: string | null;
420
+ count: number;
421
+ }>;
422
+ byOs: Array<{
423
+ os: string | null;
424
+ count: number;
425
+ }>;
426
+ }
427
+ /**
428
+ * Extract a Sendora shortcode from a URL.
429
+ *
430
+ * - When `linkDomain` is supplied, only matches URLs whose host equals it
431
+ * (or is a subdomain) — prevents firing `handleUniversalLink` on unrelated
432
+ * inbound URLs from other deep-link providers.
433
+ * - When omitted, host-agnostic (matches any tail segment that looks like a
434
+ * shortcode — back-compat with 0.15.x).
435
+ * - Accepts both `https://<host>/<shortcode>` and `https://<host>/link/<shortcode>`.
436
+ */
437
+ declare function extractShortcodeFromUrl(url: string, linkDomain?: string): string | null;
389
438
  /**
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`).
439
+ * Canonical device-fingerprint recipe. Identical byte-for-byte across the
440
+ * RN / iOS / Android SDKs so a click logged from one platform and an
441
+ * install completing on another won't accidentally match.
442
+ *
443
+ * Input: `${platform}|${screenW}x${screenH}|${timezone}|${locale}`
444
+ * Output: lowercase hex SHA-256.
395
445
  */
396
- declare function extractShortcodeFromUrl(url: string): string | null;
446
+ declare function computeDeviceFingerprint(): Promise<string>;
447
+ /** Probe optional `react-native-play-install-referrer` peer. Returns the
448
+ * raw referrer string (which Play passes Sendora's `?sd_sc=<shortcode>`
449
+ * inside) when the peer is installed, or `null` when not. Safe to call
450
+ * on iOS — returns null. */
451
+ declare function getPlayInstallReferrer(): Promise<string | null>;
452
+ interface AttachLinkingApiOptions {
453
+ /** Called with URLs that aren't Sendora-shaped so the host app can hand
454
+ * them to its existing router (other deep-link providers, OAuth callbacks,
455
+ * etc). */
456
+ onLegacyUrl?: (url: string) => void;
457
+ }
397
458
  interface LinksDeps {
398
459
  apiUrl: string;
399
460
  publicKey: string;
400
461
  debug: boolean;
401
462
  iosBundleId?: string;
402
463
  androidPackageName?: string;
464
+ /** Custom share domain (e.g. `pulse.link`). Used by `attachLinkingApi` to
465
+ * filter Sendora vs legacy URLs. When unset, falls back to the default
466
+ * Sendora host pattern (host-agnostic — any URL whose tail looks like a
467
+ * shortcode is treated as Sendora). */
468
+ linkDomain?: string;
403
469
  }
404
470
  declare class Links {
405
471
  private deps;
406
472
  private listeners;
473
+ private prewarmCache;
474
+ private linkingSub;
407
475
  constructor(deps: LinksDeps);
408
- /** Register a callback for warm + deferred deep-link opens. Returns unsubscribe fn. */
409
- onLinkOpened(handler: LinkOpenedHandler): () => void;
476
+ /** Register a callback for warm + deferred deep-link opens. Returns
477
+ * unsubscribe fn. Generic `T` lets callers type `linkData` per-app. */
478
+ onLinkOpened<T extends LinkData = LinkData>(handler: LinkOpenedHandler<T>): () => void;
410
479
  private emit;
411
- /** Mint a new short link. Returns the share URL + shortcode. */
412
- create(input: LinkCreateInput): Promise<LinkCreateResult>;
480
+ /** Stable cache key. Customer can override per call with `opts.key`. */
481
+ private cacheKey;
482
+ private evictExpired;
483
+ /**
484
+ * Background-mint a link + cache the promise so a subsequent `create()`
485
+ * with the same input (or matching `key`) returns instantly.
486
+ *
487
+ * Fire-and-forget. Errors are swallowed; the matching `create()` call
488
+ * will surface them on demand instead.
489
+ */
490
+ prewarm<T extends LinkData = LinkData>(input: LinkCreateInput<T>, opts?: {
491
+ key?: string;
492
+ }): void;
493
+ /** Mint a new short link. Uses prewarm cache when the input matches. */
494
+ create<T extends LinkData = LinkData>(input: LinkCreateInput<T>, opts?: {
495
+ key?: string;
496
+ }): Promise<LinkCreateResult<T>>;
497
+ private doCreate;
413
498
  /**
414
499
  * Warm-path: app received a universal/app-link delivery. Resolves the
415
500
  * shortcode against the backend and fires `onLinkOpened`. No-op (and
@@ -417,14 +502,41 @@ declare class Links {
417
502
  */
418
503
  handleUniversalLink(url: string): Promise<boolean>;
419
504
  /**
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 (iOSprobabilistic).
505
+ * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
506
+ * `installReferrer` is supplied, the SDK auto-discovers:
507
+ * - Android: probes `react-native-play-install-referrer` (preferred100%
508
+ * accurate when present).
509
+ * - Otherwise: computes the canonical device fingerprint hash.
510
+ *
511
+ * Fires `onLinkOpened` with `isDeferred: true` on success.
512
+ */
513
+ matchDeferred(input?: LinkMatchInput): Promise<LinkOpenedEvent<LinkData> | null>;
514
+ /** Soft-delete a link. Idempotent — repeated calls succeed. */
515
+ revoke(shortcode: string): Promise<{
516
+ shortcode: string;
517
+ revoked: true;
518
+ }>;
519
+ /** Click totals + breakdowns for a link the SDK owns. */
520
+ getStats(shortcode: string): Promise<LinkStats>;
521
+ /**
522
+ * One-call replacement for the standard cold + warm URL boilerplate:
523
+ *
524
+ * const detach = await sendora.links.attachLinkingApi({
525
+ * onLegacyUrl: (url) => myRouter.handle(url),
526
+ * });
527
+ * // …later in unmount:
528
+ * detach();
423
529
  *
424
- * SDK fires `onLinkOpened` with `isDeferred: true` on success.
425
- * Returns the event payload on match, `null` on no match.
530
+ * Internally:
531
+ * - Reads `Linking.getInitialURL()` once.
532
+ * - Subscribes to `Linking.addEventListener('url', …)`.
533
+ * - For each URL: checks `extractShortcodeFromUrl(url, linkDomain)`.
534
+ * Sendora-shaped → calls `handleUniversalLink`.
535
+ * Otherwise → forwards to `onLegacyUrl` (if supplied).
536
+ *
537
+ * Idempotent: re-attaching while a subscription is active replaces it.
426
538
  */
427
- matchDeferred(input: LinkMatchInput): Promise<LinkOpenedEvent | null>;
539
+ attachLinkingApi(opts?: AttachLinkingApiOptions): Promise<() => void>;
428
540
  }
429
541
 
430
542
  interface SendoraConfig {
@@ -492,6 +604,14 @@ interface SendoraConfig {
492
604
  * `iosBundleId` above.
493
605
  */
494
606
  androidPackageName?: string;
607
+ /**
608
+ * Optional share-link host (e.g. `pulse.link`). When set, the deep
609
+ * links module's URL extractor only treats Sendora-routed URLs whose
610
+ * host matches this domain (or a subdomain). Leave unset on apps that
611
+ * use the default Sendora-hosted share domain (`go.sendoracloud.com`).
612
+ * Added 0.16.0.
613
+ */
614
+ linkDomain?: string;
495
615
  }
496
616
  type TraitValue = string | number | boolean | null | undefined;
497
617
  interface IdentifyTraits {
@@ -612,4 +732,4 @@ declare class SendoraSDK {
612
732
  }
613
733
  declare const SendoraCloud: SendoraSDK;
614
734
 
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 };
735
+ export { type AttachLinkingApiOptions, Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
package/dist/index.d.ts CHANGED
@@ -305,35 +305,62 @@ declare class Auth {
305
305
  }
306
306
 
307
307
  /**
308
- * Sendora Deep Links surface for React Native.
308
+ * Sendora Deep Links surface for React Native (s58.50 — Pulse DX rewrite).
309
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`.
310
+ * Surface:
311
+ * 1. `create<T>(...)` — mint a Sendora short link from inside the app.
312
+ * 2. `prewarm<T>(...)` — background-mint + cache so share row taps
313
+ * skip the HTTPS round-trip ActivityIndicator.
314
+ * 3. `handleUniversalLink(url)` — resolve a delivered universal / app link
315
+ * and fire `onLinkOpened`.
316
+ * 4. `matchDeferred({ ... })` cold-launch deferred match.
317
+ * 5. `attachLinkingApi({ ... })` — one-call replacement for the
318
+ * `Linking.getInitialURL` + `addEventListener`
319
+ * + extract-pre-check + fallthrough boilerplate.
320
+ * 6. `computeDeviceFingerprint()` canonical recipe (matches iOS + Android),
321
+ * no client-side boilerplate required.
322
+ * 7. `revoke(shortcode)` — soft-delete (private-content unsend).
323
+ * 8. `getStats(shortcode)` — totals + breakdowns; no dashboard scraping.
324
+ * 9. `onLinkOpened<T>(cb)` — generic-typed handler.
325
+ * 10. `LinkError` — typed errors with codes
326
+ * (`instanceof` + `err.code` rather than
327
+ * `err.message.includes(...)`).
321
328
  *
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.
329
+ * Bundle-id gate: SDK auto-supplies `iosBundleId` / `androidPackageName`
330
+ * from `init()` config host app never has to thread them through per call.
331
+ * Sendora-side `apps` table is the source of truth a leaked public key
332
+ * + wrong bundle = 422 from the backend mapped to
333
+ * `LinkError(code: "BUNDLE_MISMATCH")` here.
325
334
  */
326
- interface LinkCreateInput {
335
+ /**
336
+ * Error code taxonomy. Mirrors `apps/backend/src/modules/links/*` outcomes.
337
+ * Callers branch on `err.code` (string compare) or `err instanceof LinkError`.
338
+ */
339
+ type LinkErrorCode = "BUNDLE_MISMATCH" | "DATA_TOO_LARGE" | "EXPIRED" | "NETWORK" | "RATE_LIMITED" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_INPUT" | "PLAN_LIMIT" | "FALLBACK_REQUIRED" | "SERVER" | "UNKNOWN";
340
+ declare class LinkError extends Error {
341
+ readonly code: LinkErrorCode;
342
+ readonly statusCode: number;
343
+ readonly details?: unknown;
344
+ constructor(code: LinkErrorCode, message: string, statusCode?: number, details?: unknown);
345
+ }
346
+ /** Default linkData shape — opaque record. Override with a discriminated
347
+ * union or product-specific interface in the host app for full type-safety. */
348
+ type LinkData = Record<string, unknown>;
349
+ interface LinkCreateInput<T extends LinkData = LinkData> {
327
350
  /** Customer-facing title. Shown in dashboard list views. */
328
351
  title: string;
329
352
  /** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
330
353
  iosDeepLinkPath?: string;
331
354
  /** In-app deep-link path the host app routes to (e.g. `/articles/123`). */
332
355
  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>;
356
+ /**
357
+ * Web fallback URL. **Optional as of 0.16.0** — backend defaults from
358
+ * the project's apps registry (web origin > iOS App Store URL > Android
359
+ * Play Store URL). Pass explicitly for per-link override.
360
+ */
361
+ fallbackUrl?: string;
362
+ /** Custom typed JSON delivered to onLinkOpened. Max 2KB serialized. */
363
+ linkData?: T;
337
364
  /** Optional OG preview overrides for richer share cards. */
338
365
  ogTitle?: string;
339
366
  ogDescription?: string;
@@ -344,72 +371,130 @@ interface LinkCreateInput {
344
371
  medium?: string;
345
372
  channel?: string;
346
373
  tags?: string[];
347
- /** ISO timestamp when the link stops resolving. */
374
+ /** ISO timestamp when the link stops resolving. Must be future + within 5y. */
348
375
  expiresAt?: string;
349
- /** Bundle ID / package name for the bundle-id gate. */
376
+ /** Bundle ID / package name for the bundle-id gate. Defaults from init. */
350
377
  iosBundleId?: string;
351
378
  androidPackageName?: string;
352
379
  }
353
- interface LinkCreateResult {
380
+ interface LinkCreateResult<T extends LinkData = LinkData> {
354
381
  id: string;
355
382
  shortcode: string;
356
- /** Fully qualified share URL — what you pass to RN `Share.share({ message: ... })`. */
383
+ /** Fully qualified share URL — pass to RN `Share.share({ message: ... })`. */
357
384
  url: string;
358
385
  iosDeepLinkPath: string | null;
359
386
  androidDeepLinkPath: string | null;
360
387
  fallbackUrl: string;
361
- linkData: Record<string, unknown>;
388
+ linkData: T;
362
389
  expiresAt: string | null;
363
390
  }
364
- interface LinkOpenedEvent {
391
+ interface LinkOpenedEvent<T extends LinkData = LinkData> {
365
392
  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. */
393
+ /** Custom typed JSON the link was created with. */
394
+ linkData: T;
369
395
  iosDeepLinkPath: string | null;
370
- /** Server's Android deep-link path. */
371
396
  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
- */
397
+ /** `true` when this event came from the deferred deep link path. */
378
398
  isDeferred: boolean;
379
399
  }
380
- type LinkOpenedHandler = (event: LinkOpenedEvent) => void;
400
+ type LinkOpenedHandler<T extends LinkData = LinkData> = (event: LinkOpenedEvent<T>) => void;
381
401
  interface LinkMatchInput {
382
- /** Hex-encoded SHA-256 of (ip + ua + screen + tz + locale). Pre-hashed by caller. */
402
+ /** Hex-encoded SHA-256 pass output of `computeDeviceFingerprint()`. */
383
403
  fingerprintHash?: string;
384
- /** Raw Play Install Referrer (`PlayInstallReferrerClient.getInstallReferrer().installReferrer`). */
404
+ /** Raw Play Install Referrer. Sendora probes
405
+ * `react-native-play-install-referrer` automatically when this is omitted. */
385
406
  installReferrer?: string;
386
407
  iosBundleId?: string;
387
408
  androidPackageName?: string;
388
409
  }
410
+ interface LinkStats {
411
+ totalClicks: number;
412
+ uniqueClicks: number;
413
+ deferredMatches: number;
414
+ byDevice: Array<{
415
+ deviceType: string | null;
416
+ count: number;
417
+ }>;
418
+ byCountry: Array<{
419
+ country: string | null;
420
+ count: number;
421
+ }>;
422
+ byOs: Array<{
423
+ os: string | null;
424
+ count: number;
425
+ }>;
426
+ }
427
+ /**
428
+ * Extract a Sendora shortcode from a URL.
429
+ *
430
+ * - When `linkDomain` is supplied, only matches URLs whose host equals it
431
+ * (or is a subdomain) — prevents firing `handleUniversalLink` on unrelated
432
+ * inbound URLs from other deep-link providers.
433
+ * - When omitted, host-agnostic (matches any tail segment that looks like a
434
+ * shortcode — back-compat with 0.15.x).
435
+ * - Accepts both `https://<host>/<shortcode>` and `https://<host>/link/<shortcode>`.
436
+ */
437
+ declare function extractShortcodeFromUrl(url: string, linkDomain?: string): string | null;
389
438
  /**
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`).
439
+ * Canonical device-fingerprint recipe. Identical byte-for-byte across the
440
+ * RN / iOS / Android SDKs so a click logged from one platform and an
441
+ * install completing on another won't accidentally match.
442
+ *
443
+ * Input: `${platform}|${screenW}x${screenH}|${timezone}|${locale}`
444
+ * Output: lowercase hex SHA-256.
395
445
  */
396
- declare function extractShortcodeFromUrl(url: string): string | null;
446
+ declare function computeDeviceFingerprint(): Promise<string>;
447
+ /** Probe optional `react-native-play-install-referrer` peer. Returns the
448
+ * raw referrer string (which Play passes Sendora's `?sd_sc=<shortcode>`
449
+ * inside) when the peer is installed, or `null` when not. Safe to call
450
+ * on iOS — returns null. */
451
+ declare function getPlayInstallReferrer(): Promise<string | null>;
452
+ interface AttachLinkingApiOptions {
453
+ /** Called with URLs that aren't Sendora-shaped so the host app can hand
454
+ * them to its existing router (other deep-link providers, OAuth callbacks,
455
+ * etc). */
456
+ onLegacyUrl?: (url: string) => void;
457
+ }
397
458
  interface LinksDeps {
398
459
  apiUrl: string;
399
460
  publicKey: string;
400
461
  debug: boolean;
401
462
  iosBundleId?: string;
402
463
  androidPackageName?: string;
464
+ /** Custom share domain (e.g. `pulse.link`). Used by `attachLinkingApi` to
465
+ * filter Sendora vs legacy URLs. When unset, falls back to the default
466
+ * Sendora host pattern (host-agnostic — any URL whose tail looks like a
467
+ * shortcode is treated as Sendora). */
468
+ linkDomain?: string;
403
469
  }
404
470
  declare class Links {
405
471
  private deps;
406
472
  private listeners;
473
+ private prewarmCache;
474
+ private linkingSub;
407
475
  constructor(deps: LinksDeps);
408
- /** Register a callback for warm + deferred deep-link opens. Returns unsubscribe fn. */
409
- onLinkOpened(handler: LinkOpenedHandler): () => void;
476
+ /** Register a callback for warm + deferred deep-link opens. Returns
477
+ * unsubscribe fn. Generic `T` lets callers type `linkData` per-app. */
478
+ onLinkOpened<T extends LinkData = LinkData>(handler: LinkOpenedHandler<T>): () => void;
410
479
  private emit;
411
- /** Mint a new short link. Returns the share URL + shortcode. */
412
- create(input: LinkCreateInput): Promise<LinkCreateResult>;
480
+ /** Stable cache key. Customer can override per call with `opts.key`. */
481
+ private cacheKey;
482
+ private evictExpired;
483
+ /**
484
+ * Background-mint a link + cache the promise so a subsequent `create()`
485
+ * with the same input (or matching `key`) returns instantly.
486
+ *
487
+ * Fire-and-forget. Errors are swallowed; the matching `create()` call
488
+ * will surface them on demand instead.
489
+ */
490
+ prewarm<T extends LinkData = LinkData>(input: LinkCreateInput<T>, opts?: {
491
+ key?: string;
492
+ }): void;
493
+ /** Mint a new short link. Uses prewarm cache when the input matches. */
494
+ create<T extends LinkData = LinkData>(input: LinkCreateInput<T>, opts?: {
495
+ key?: string;
496
+ }): Promise<LinkCreateResult<T>>;
497
+ private doCreate;
413
498
  /**
414
499
  * Warm-path: app received a universal/app-link delivery. Resolves the
415
500
  * shortcode against the backend and fires `onLinkOpened`. No-op (and
@@ -417,14 +502,41 @@ declare class Links {
417
502
  */
418
503
  handleUniversalLink(url: string): Promise<boolean>;
419
504
  /**
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 (iOSprobabilistic).
505
+ * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
506
+ * `installReferrer` is supplied, the SDK auto-discovers:
507
+ * - Android: probes `react-native-play-install-referrer` (preferred100%
508
+ * accurate when present).
509
+ * - Otherwise: computes the canonical device fingerprint hash.
510
+ *
511
+ * Fires `onLinkOpened` with `isDeferred: true` on success.
512
+ */
513
+ matchDeferred(input?: LinkMatchInput): Promise<LinkOpenedEvent<LinkData> | null>;
514
+ /** Soft-delete a link. Idempotent — repeated calls succeed. */
515
+ revoke(shortcode: string): Promise<{
516
+ shortcode: string;
517
+ revoked: true;
518
+ }>;
519
+ /** Click totals + breakdowns for a link the SDK owns. */
520
+ getStats(shortcode: string): Promise<LinkStats>;
521
+ /**
522
+ * One-call replacement for the standard cold + warm URL boilerplate:
523
+ *
524
+ * const detach = await sendora.links.attachLinkingApi({
525
+ * onLegacyUrl: (url) => myRouter.handle(url),
526
+ * });
527
+ * // …later in unmount:
528
+ * detach();
423
529
  *
424
- * SDK fires `onLinkOpened` with `isDeferred: true` on success.
425
- * Returns the event payload on match, `null` on no match.
530
+ * Internally:
531
+ * - Reads `Linking.getInitialURL()` once.
532
+ * - Subscribes to `Linking.addEventListener('url', …)`.
533
+ * - For each URL: checks `extractShortcodeFromUrl(url, linkDomain)`.
534
+ * Sendora-shaped → calls `handleUniversalLink`.
535
+ * Otherwise → forwards to `onLegacyUrl` (if supplied).
536
+ *
537
+ * Idempotent: re-attaching while a subscription is active replaces it.
426
538
  */
427
- matchDeferred(input: LinkMatchInput): Promise<LinkOpenedEvent | null>;
539
+ attachLinkingApi(opts?: AttachLinkingApiOptions): Promise<() => void>;
428
540
  }
429
541
 
430
542
  interface SendoraConfig {
@@ -492,6 +604,14 @@ interface SendoraConfig {
492
604
  * `iosBundleId` above.
493
605
  */
494
606
  androidPackageName?: string;
607
+ /**
608
+ * Optional share-link host (e.g. `pulse.link`). When set, the deep
609
+ * links module's URL extractor only treats Sendora-routed URLs whose
610
+ * host matches this domain (or a subdomain). Leave unset on apps that
611
+ * use the default Sendora-hosted share domain (`go.sendoracloud.com`).
612
+ * Added 0.16.0.
613
+ */
614
+ linkDomain?: string;
495
615
  }
496
616
  type TraitValue = string | number | boolean | null | undefined;
497
617
  interface IdentifyTraits {
@@ -612,4 +732,4 @@ declare class SendoraSDK {
612
732
  }
613
733
  declare const SendoraCloud: SendoraSDK;
614
734
 
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 };
735
+ export { type AttachLinkingApiOptions, Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };