@tolinku/web-sdk 0.3.0 → 0.4.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.d.mts CHANGED
@@ -393,8 +393,41 @@ declare class Deferred {
393
393
  constructor(client: HttpClient);
394
394
  /** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
395
395
  claimByToken(token: string): Promise<DeferredLink | null>;
396
+ /**
397
+ * Recover the link that led here, once.
398
+ *
399
+ * There is no Play Install Referrer on the web, so this is signal matching
400
+ * with the bookkeeping that makes calling it safe. That bookkeeping is the
401
+ * point: a claim is consumed the first time it succeeds, so an app calling
402
+ * claimBySignals on every page load asks again after the answer is already
403
+ * spent, and every one of those asks is recorded as a miss. The match rate on
404
+ * the dashboard then falls towards zero while the integration is working
405
+ * correctly, which is a hard thing to diagnose from the outside.
406
+ *
407
+ * Call it once on first run. Calling it again is free after the first.
408
+ *
409
+ * Named to match the Android, React Native and Flutter SDKs, where the same
410
+ * call also tries the install referrer before falling back to here.
411
+ */
412
+ claimDeferredLink(options: {
413
+ appspaceId: string;
414
+ /** Claim again even if an attempt was already recorded. For tests. */
415
+ force?: boolean;
416
+ }): Promise<DeferredLink | null>;
417
+ private alreadyAttempted;
418
+ private rememberAttempt;
396
419
  /** Claim a deferred deep link by device signal matching */
397
420
  claimBySignals(options: ClaimBySignalsOptions): Promise<DeferredLink | null>;
421
+ /**
422
+ * The signal claim, with whether the server actually answered.
423
+ *
424
+ * `settled` separates "nothing is waiting for this device", which no amount
425
+ * of asking will change, from "the request never got there". Both surface as
426
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
427
+ * apart: recording an attempt that never reached the server would spend an
428
+ * install's one chance at attribution on a dropped connection.
429
+ */
430
+ private attemptSignals;
398
431
  }
399
432
 
400
433
  declare class Tolinku {
package/dist/index.d.ts CHANGED
@@ -393,8 +393,41 @@ declare class Deferred {
393
393
  constructor(client: HttpClient);
394
394
  /** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
395
395
  claimByToken(token: string): Promise<DeferredLink | null>;
396
+ /**
397
+ * Recover the link that led here, once.
398
+ *
399
+ * There is no Play Install Referrer on the web, so this is signal matching
400
+ * with the bookkeeping that makes calling it safe. That bookkeeping is the
401
+ * point: a claim is consumed the first time it succeeds, so an app calling
402
+ * claimBySignals on every page load asks again after the answer is already
403
+ * spent, and every one of those asks is recorded as a miss. The match rate on
404
+ * the dashboard then falls towards zero while the integration is working
405
+ * correctly, which is a hard thing to diagnose from the outside.
406
+ *
407
+ * Call it once on first run. Calling it again is free after the first.
408
+ *
409
+ * Named to match the Android, React Native and Flutter SDKs, where the same
410
+ * call also tries the install referrer before falling back to here.
411
+ */
412
+ claimDeferredLink(options: {
413
+ appspaceId: string;
414
+ /** Claim again even if an attempt was already recorded. For tests. */
415
+ force?: boolean;
416
+ }): Promise<DeferredLink | null>;
417
+ private alreadyAttempted;
418
+ private rememberAttempt;
396
419
  /** Claim a deferred deep link by device signal matching */
397
420
  claimBySignals(options: ClaimBySignalsOptions): Promise<DeferredLink | null>;
421
+ /**
422
+ * The signal claim, with whether the server actually answered.
423
+ *
424
+ * `settled` separates "nothing is waiting for this device", which no amount
425
+ * of asking will change, from "the request never got there". Both surface as
426
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
427
+ * apart: recording an attempt that never reached the server would spend an
428
+ * install's one chance at attribution on a dropped connection.
429
+ */
430
+ private attemptSignals;
398
431
  }
399
432
 
400
433
  declare class Tolinku {
package/dist/index.js CHANGED
@@ -169,7 +169,11 @@ var HttpClient = class {
169
169
  }
170
170
  headers() {
171
171
  return {
172
- "X-API-Key": this.apiKey
172
+ "X-API-Key": this.apiKey,
173
+ // Browsers default to "Accept: */*", which a server content-negotiating an
174
+ // error page reads as "HTML is fine". Saying so explicitly keeps an error
175
+ // parseable instead of arriving as a rendered page.
176
+ "Accept": "application/json"
173
177
  };
174
178
  }
175
179
  };
@@ -527,6 +531,7 @@ var Referrals = class {
527
531
  };
528
532
 
529
533
  // src/deferred.ts
534
+ var CLAIMED_KEY = "tolinku_deferred_claimed";
530
535
  var Deferred = class {
531
536
  constructor(client) {
532
537
  this.client = client;
@@ -540,10 +545,60 @@ var Deferred = class {
540
545
  return null;
541
546
  }
542
547
  }
548
+ /**
549
+ * Recover the link that led here, once.
550
+ *
551
+ * There is no Play Install Referrer on the web, so this is signal matching
552
+ * with the bookkeeping that makes calling it safe. That bookkeeping is the
553
+ * point: a claim is consumed the first time it succeeds, so an app calling
554
+ * claimBySignals on every page load asks again after the answer is already
555
+ * spent, and every one of those asks is recorded as a miss. The match rate on
556
+ * the dashboard then falls towards zero while the integration is working
557
+ * correctly, which is a hard thing to diagnose from the outside.
558
+ *
559
+ * Call it once on first run. Calling it again is free after the first.
560
+ *
561
+ * Named to match the Android, React Native and Flutter SDKs, where the same
562
+ * call also tries the install referrer before falling back to here.
563
+ */
564
+ async claimDeferredLink(options) {
565
+ if (!options.appspaceId || !options.appspaceId.trim()) {
566
+ throw new Error("Tolinku: appspaceId is required and must not be blank for claimDeferredLink.");
567
+ }
568
+ if (!options.force && this.alreadyAttempted()) return null;
569
+ const { link, settled } = await this.attemptSignals({ appspaceId: options.appspaceId });
570
+ if (settled) this.rememberAttempt();
571
+ return link;
572
+ }
573
+ alreadyAttempted() {
574
+ try {
575
+ return window.localStorage.getItem(CLAIMED_KEY) !== null;
576
+ } catch {
577
+ return false;
578
+ }
579
+ }
580
+ rememberAttempt() {
581
+ try {
582
+ window.localStorage.setItem(CLAIMED_KEY, (/* @__PURE__ */ new Date()).toISOString());
583
+ } catch {
584
+ }
585
+ }
543
586
  /** Claim a deferred deep link by device signal matching */
544
587
  async claimBySignals(options) {
588
+ return (await this.attemptSignals(options)).link;
589
+ }
590
+ /**
591
+ * The signal claim, with whether the server actually answered.
592
+ *
593
+ * `settled` separates "nothing is waiting for this device", which no amount
594
+ * of asking will change, from "the request never got there". Both surface as
595
+ * null to callers of claimBySignals, but claimDeferredLink has to tell them
596
+ * apart: recording an attempt that never reached the server would spend an
597
+ * install's one chance at attribution on a dropped connection.
598
+ */
599
+ async attemptSignals(options) {
545
600
  try {
546
- return await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
601
+ const link = await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
547
602
  appspace_id: options.appspaceId,
548
603
  timezone: options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
549
604
  language: options.language || navigator.language,
@@ -552,10 +607,11 @@ var Deferred = class {
552
607
  // Separates devices reporting identical logical dimensions.
553
608
  device_pixel_ratio: options.devicePixelRatio || window.devicePixelRatio || 1
554
609
  });
610
+ return { link, settled: true };
555
611
  } catch (err) {
556
612
  const status = err?.statusCode ?? err?.status;
557
613
  if (status === 404) {
558
- return null;
614
+ return { link: null, settled: true };
559
615
  }
560
616
  if (status === 403) {
561
617
  console.warn(
@@ -564,10 +620,10 @@ var Deferred = class {
564
620
  "under Settings), not your subdomain or slug.",
565
621
  err
566
622
  );
567
- return null;
623
+ return { link: null, settled: false };
568
624
  }
569
625
  console.warn("[Tolinku] Failed to claim deferred link by signals:", err);
570
- return null;
626
+ return { link: null, settled: false };
571
627
  }
572
628
  }
573
629
  };