@swype-org/deposit 0.3.16 → 0.3.19

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
@@ -526,15 +526,19 @@ var STYLES = `
526
526
  [data-blink-overlay][data-blink-mobile-sheet][data-blink-closing] [data-blink-container]{opacity:1;animation:blink-slide-down-full ${CLOSE_DURATION_MS}ms ease-in forwards}
527
527
  @media(max-width:${MOBILE_SHEET_MAX_VIEWPORT_PX}px){[data-blink-overlay]{align-items:flex-end;touch-action:none;overscroll-behavior:contain}[data-blink-container]{width:100%;max-width:100%;height:79vh;border-radius:24px 24px 0 0;box-shadow:0 -8px 40px rgba(0,0,0,.25);animation:blink-slide-up-full .35s cubic-bezier(.32,.72,0,1);padding-bottom:env(safe-area-inset-bottom,0px)}[data-blink-overlay][data-blink-closing] [data-blink-container]{opacity:1;animation:blink-slide-down-full ${CLOSE_DURATION_MS}ms ease-in forwards}}
528
528
  `;
529
- function createIframe(url, containerElement) {
529
+ function createIframe(url, containerElement, options) {
530
530
  ensureStyles();
531
531
  let closed = false;
532
+ let hidden = options?.hidden === true;
532
533
  let closeCallback = null;
533
534
  const overlay = document.createElement("div");
534
535
  overlay.setAttribute("data-blink-overlay", "");
535
536
  if (shouldUseMobileSheetLayout()) {
536
537
  overlay.setAttribute("data-blink-mobile-sheet", "");
537
538
  }
539
+ if (hidden) {
540
+ overlay.style.display = "none";
541
+ }
538
542
  const container = document.createElement("div");
539
543
  container.setAttribute("data-blink-container", "");
540
544
  const iframe = document.createElement("iframe");
@@ -560,8 +564,8 @@ function createIframe(url, containerElement) {
560
564
  };
561
565
  attachBridge();
562
566
  iframe.addEventListener("load", attachBridge);
563
- const savedOverflow = document.body.style.overflow;
564
- document.body.style.overflow = "hidden";
567
+ let savedOverflow = "";
568
+ let scrollLocked = false;
565
569
  const onBackdropClick = (event) => {
566
570
  if (event.target === overlay) triggerClose();
567
571
  };
@@ -571,15 +575,29 @@ function createIframe(url, containerElement) {
571
575
  const onTouchMove = (event) => {
572
576
  if (event.target === overlay) event.preventDefault();
573
577
  };
574
- overlay.addEventListener("click", onBackdropClick);
575
- overlay.addEventListener("touchmove", onTouchMove, { passive: false });
576
- document.addEventListener("keydown", onKeyDown);
578
+ function lockScroll() {
579
+ if (scrollLocked) return;
580
+ scrollLocked = true;
581
+ savedOverflow = document.body.style.overflow;
582
+ document.body.style.overflow = "hidden";
583
+ }
584
+ function attachDismissalListeners() {
585
+ overlay.addEventListener("click", onBackdropClick);
586
+ overlay.addEventListener("touchmove", onTouchMove, { passive: false });
587
+ document.addEventListener("keydown", onKeyDown);
588
+ }
589
+ if (!hidden) {
590
+ lockScroll();
591
+ attachDismissalListeners();
592
+ }
577
593
  function removeListeners() {
578
594
  overlay.removeEventListener("click", onBackdropClick);
579
595
  overlay.removeEventListener("touchmove", onTouchMove);
580
596
  document.removeEventListener("keydown", onKeyDown);
581
597
  }
582
598
  function unlockScroll() {
599
+ if (!scrollLocked) return;
600
+ scrollLocked = false;
583
601
  document.body.style.overflow = savedOverflow;
584
602
  }
585
603
  function triggerClose() {
@@ -617,6 +635,21 @@ function createIframe(url, containerElement) {
617
635
  isClosed() {
618
636
  return closed;
619
637
  },
638
+ isHidden() {
639
+ return hidden;
640
+ },
641
+ reveal() {
642
+ if (closed || !hidden) return;
643
+ hidden = false;
644
+ if (shouldUseMobileSheetLayout()) {
645
+ overlay.setAttribute("data-blink-mobile-sheet", "");
646
+ } else {
647
+ overlay.removeAttribute("data-blink-mobile-sheet");
648
+ }
649
+ overlay.style.display = "";
650
+ lockScroll();
651
+ attachDismissalListeners();
652
+ },
620
653
  onClose(callback) {
621
654
  closeCallback = callback;
622
655
  },
@@ -788,6 +821,13 @@ function buildSignerRequest(request, webviewBaseUrl) {
788
821
 
789
822
  // src/checkout.ts
790
823
  var DEFAULT_WEBVIEW_BASE_URL = "https://pay.blink.cash";
824
+ var SANDBOX_WEBVIEW_BASE_URL = "https://pay-sandbox.blink.cash";
825
+ var IFRAME_READY_TIMEOUT_MS = 2e3;
826
+ var WARM_IFRAME_READY_TIMEOUT_MS = 6e4;
827
+ function resolveWebviewBaseUrl(config) {
828
+ if (config.webviewBaseUrl) return config.webviewBaseUrl;
829
+ return config.environment === "sandbox" ? SANDBOX_WEBVIEW_BASE_URL : DEFAULT_WEBVIEW_BASE_URL;
830
+ }
791
831
  var Deposit = class {
792
832
  config;
793
833
  iframe = null;
@@ -800,6 +840,9 @@ var Deposit = class {
800
840
  _error = null;
801
841
  flowTimer = null;
802
842
  lastSignerResponse = null;
843
+ warmIframe = null;
844
+ warmIframeReady = null;
845
+ warmIframeReadyCancel = null;
803
846
  listeners = {
804
847
  complete: /* @__PURE__ */ new Set(),
805
848
  error: /* @__PURE__ */ new Set(),
@@ -830,6 +873,93 @@ var Deposit = class {
830
873
  this.log("Deposit instance created", {
831
874
  signer: typeof config.signer === "string" ? config.signer : "<function>"
832
875
  });
876
+ if (config.preload !== false) {
877
+ this.schedulePreload();
878
+ }
879
+ }
880
+ /**
881
+ * Warm up the hosted payment flow in a hidden iframe so `requestDeposit`
882
+ * opens an already-loaded page (JS bundle, auth session restore, and —
883
+ * when {@link DepositConfig.merchantId} is set — merchant config prefetch
884
+ * all happen in the background).
885
+ *
886
+ * Runs automatically after construction unless `preload: false`; call it
887
+ * manually to control the timing (e.g. on deposit-button hover). No-op
888
+ * when a warm iframe already exists or a deposit flow is active.
889
+ */
890
+ preload() {
891
+ if (this.destroyed || this.isActive) return;
892
+ if (this.warmIframe && !this.warmIframe.isClosed()) return;
893
+ if (typeof document === "undefined" || typeof window === "undefined") return;
894
+ const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
895
+ this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
896
+ const preloadUrl = this.buildPreloadUrl(webviewBaseUrl);
897
+ const iframe = createIframe(preloadUrl, this.config.containerElement, { hidden: true });
898
+ const ready = this.waitForIframeReady(iframe, WARM_IFRAME_READY_TIMEOUT_MS);
899
+ this.warmIframe = iframe;
900
+ this.warmIframeReady = ready.promise;
901
+ this.warmIframeReadyCancel = ready.cancel;
902
+ this.log("Warm-up iframe created", { url: preloadUrl });
903
+ }
904
+ // Defer the automatic warm-up until the host page has finished its own
905
+ // load and the main thread is idle — constructing a Deposit instance must
906
+ // never slow the merchant page down.
907
+ schedulePreload() {
908
+ if (typeof window === "undefined" || typeof document === "undefined") return;
909
+ const start = () => {
910
+ if (!this.destroyed && !this.isActive) this.preload();
911
+ };
912
+ const whenIdle = () => {
913
+ const w = window;
914
+ if (typeof w.requestIdleCallback === "function") {
915
+ w.requestIdleCallback(start, { timeout: 3e3 });
916
+ } else {
917
+ setTimeout(start, 250);
918
+ }
919
+ };
920
+ if (document.readyState === "complete") {
921
+ whenIdle();
922
+ } else {
923
+ window.addEventListener("load", whenIdle, { once: true });
924
+ }
925
+ }
926
+ buildPreloadUrl(webviewBaseUrl) {
927
+ const preloadUrl = new URL(webviewBaseUrl);
928
+ preloadUrl.searchParams.set("preload", "true");
929
+ if (this.config.merchantId) {
930
+ preloadUrl.searchParams.set("merchantId", this.config.merchantId);
931
+ }
932
+ this.applyFullWidgetParam(preloadUrl);
933
+ return preloadUrl.toString();
934
+ }
935
+ /**
936
+ * Detach the warm iframe for use by a starting flow. Returns null (and
937
+ * cleans up) when there is none or it was closed in the meantime.
938
+ */
939
+ takeWarmIframe() {
940
+ const iframe = this.warmIframe;
941
+ const ready = this.warmIframeReady;
942
+ const cancelReady = this.warmIframeReadyCancel ?? (() => {
943
+ });
944
+ this.warmIframe = null;
945
+ this.warmIframeReady = null;
946
+ this.warmIframeReadyCancel = null;
947
+ if (!iframe || !ready) return null;
948
+ if (iframe.isClosed()) {
949
+ cancelReady();
950
+ iframe.destroy();
951
+ return null;
952
+ }
953
+ return { iframe, ready, cancelReady };
954
+ }
955
+ discardWarmIframe() {
956
+ this.warmIframeReadyCancel?.();
957
+ this.warmIframeReadyCancel = null;
958
+ this.warmIframeReady = null;
959
+ if (this.warmIframe) {
960
+ this.warmIframe.destroy();
961
+ this.warmIframe = null;
962
+ }
833
963
  }
834
964
  /**
835
965
  * Open the hosted payment flow for the given deposit.
@@ -917,9 +1047,10 @@ var Deposit = class {
917
1047
  /** Tear down the instance and release all resources. */
918
1048
  destroy() {
919
1049
  this.log("destroy() called");
1050
+ this.destroyed = true;
920
1051
  this.cleanup();
1052
+ this.discardWarmIframe();
921
1053
  this.setStatus("idle");
922
- this.destroyed = true;
923
1054
  for (const set of Object.values(this.listeners)) {
924
1055
  set.clear();
925
1056
  }
@@ -933,6 +1064,17 @@ var Deposit = class {
933
1064
  console.debug(`[BlinkDeposit] ${message}`);
934
1065
  }
935
1066
  }
1067
+ /**
1068
+ * Appends `enableFullWidget=false` to the hosted-flow URL when the merchant
1069
+ * integration disables the full widget. Absence of the param means enabled
1070
+ * (the default) — the hosted flow ANDs this with the merchant's backend
1071
+ * config, so the full widget renders only when both allow it.
1072
+ */
1073
+ applyFullWidgetParam(url) {
1074
+ if (this.config.enableFullWidget === false) {
1075
+ url.searchParams.set("enableFullWidget", "false");
1076
+ }
1077
+ }
936
1078
  setStatus(status) {
937
1079
  if (this._status === status) return;
938
1080
  this.log(`Status: ${this._status} \u2192 ${status}`);
@@ -941,15 +1083,35 @@ var Deposit = class {
941
1083
  }
942
1084
  async runSignerFlow(request, requestId, onComplete, onError) {
943
1085
  try {
944
- const webviewBaseUrl = this.config.webviewBaseUrl ?? DEFAULT_WEBVIEW_BASE_URL;
1086
+ const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
945
1087
  this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
946
1088
  this.log("Calling signer", {
947
1089
  signer: typeof this.config.signer === "string" ? this.config.signer : "<function>"
948
1090
  });
949
1091
  const signerRequest = buildSignerRequest(request, webviewBaseUrl);
950
- const preloadUrl = new URL(webviewBaseUrl);
951
- preloadUrl.searchParams.set("preload", "true");
952
- const preloadIframe = createIframe(preloadUrl.toString(), this.config.containerElement);
1092
+ let preloadIframe;
1093
+ let iframeReadyPromise;
1094
+ const warm = this.takeWarmIframe();
1095
+ if (warm) {
1096
+ warm.iframe.reveal();
1097
+ preloadIframe = warm.iframe;
1098
+ iframeReadyPromise = Promise.race([
1099
+ warm.ready,
1100
+ new Promise(
1101
+ (resolve) => setTimeout(() => resolve(false), IFRAME_READY_TIMEOUT_MS)
1102
+ )
1103
+ ]).then((ready) => {
1104
+ warm.cancelReady();
1105
+ return ready;
1106
+ });
1107
+ this.log("Reusing warm preload iframe");
1108
+ } else {
1109
+ preloadIframe = createIframe(
1110
+ this.buildPreloadUrl(webviewBaseUrl),
1111
+ this.config.containerElement
1112
+ );
1113
+ iframeReadyPromise = this.waitForIframeReady(preloadIframe).promise;
1114
+ }
953
1115
  this.iframe = preloadIframe;
954
1116
  preloadIframe.onClose(() => {
955
1117
  onError(
@@ -966,7 +1128,6 @@ var Deposit = class {
966
1128
  signerRequest,
967
1129
  this.config.signerTimeoutMs
968
1130
  );
969
- const iframeReadyPromise = this.waitForIframeReady(preloadIframe);
970
1131
  const [signerResponse, iframeReady] = await Promise.all([
971
1132
  signerPromise,
972
1133
  iframeReadyPromise
@@ -998,6 +1159,7 @@ var Deposit = class {
998
1159
  hostedUrl.searchParams.set("merchantId", signerResponse.merchantId);
999
1160
  hostedUrl.searchParams.set("payload", signerResponse.payload);
1000
1161
  hostedUrl.searchParams.set("signature", signerResponse.signature);
1162
+ this.applyFullWidgetParam(hostedUrl);
1001
1163
  const targetUrl = hostedUrl.toString();
1002
1164
  const iframeHandle = createIframe(targetUrl, this.config.containerElement);
1003
1165
  this.iframe = iframeHandle;
@@ -1030,24 +1192,38 @@ var Deposit = class {
1030
1192
  }
1031
1193
  }
1032
1194
  }
1033
- waitForIframeReady(iframeHandle) {
1034
- return new Promise((resolve) => {
1195
+ /**
1196
+ * Resolves true when the iframe posts `blink:iframe-ready`, false on
1197
+ * timeout. `cancel()` settles the promise as false immediately and releases
1198
+ * the listener + timer — used to reap the generous warm-up wait once it no
1199
+ * longer matters (warm iframe consumed or discarded).
1200
+ */
1201
+ waitForIframeReady(iframeHandle, timeoutMs = IFRAME_READY_TIMEOUT_MS) {
1202
+ let finish = () => {
1203
+ };
1204
+ const promise = new Promise((resolve) => {
1035
1205
  const handler = (event) => {
1036
1206
  if (event.origin !== this.hostedOrigin) return;
1037
1207
  if (event.source !== iframeHandle.contentWindow) return;
1038
1208
  if (parseIframeReady(event.data)) {
1039
- window.removeEventListener("message", handler);
1040
- clearTimeout(timeout);
1041
- resolve(true);
1209
+ finish(true);
1042
1210
  }
1043
1211
  };
1044
1212
  window.addEventListener("message", handler);
1045
1213
  const timeout = setTimeout(() => {
1046
- window.removeEventListener("message", handler);
1047
1214
  this.log("Iframe ready timeout \u2014 falling back to URL params");
1048
- resolve(false);
1049
- }, 2e3);
1215
+ finish(false);
1216
+ }, timeoutMs);
1217
+ let settled = false;
1218
+ finish = (ready) => {
1219
+ if (settled) return;
1220
+ settled = true;
1221
+ window.removeEventListener("message", handler);
1222
+ clearTimeout(timeout);
1223
+ resolve(ready);
1224
+ };
1050
1225
  });
1226
+ return { promise, cancel: () => finish(false) };
1051
1227
  }
1052
1228
  startMessageListener(iframeHandle, onComplete) {
1053
1229
  const handler = (event) => {
@@ -1107,6 +1283,9 @@ var Deposit = class {
1107
1283
  }
1108
1284
  this.hostedOrigin = null;
1109
1285
  this.lastSignerResponse = null;
1286
+ if (!this.destroyed && this.config.preload !== false) {
1287
+ this.schedulePreload();
1288
+ }
1110
1289
  }
1111
1290
  emit(event, ...args) {
1112
1291
  for (const handler of this.listeners[event]) {
@@ -1129,6 +1308,7 @@ exports.CheckoutError = CheckoutError;
1129
1308
  exports.DEFAULT_WEBVIEW_BASE_URL = DEFAULT_WEBVIEW_BASE_URL;
1130
1309
  exports.Deposit = Deposit;
1131
1310
  exports.DepositError = DepositError;
1311
+ exports.SANDBOX_WEBVIEW_BASE_URL = SANDBOX_WEBVIEW_BASE_URL;
1132
1312
  exports.VERSION = VERSION;
1133
1313
  exports.attachRpcHost = attachRpcHost;
1134
1314
  exports.createWalletDiscoverer = createWalletDiscoverer;