@swype-org/deposit 0.3.16 → 0.3.20

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/README.md CHANGED
@@ -98,10 +98,20 @@ const deposit = new Deposit({
98
98
  containerElement: document.getElementById('deposit-root')!,
99
99
  signerTimeoutMs: 15_000,
100
100
  flowTimeoutMs: 300_000,
101
+ enableFullWidget: true,
101
102
  debug: false,
102
103
  });
103
104
  ```
104
105
 
106
+ ### `enableFullWidget`
107
+
108
+ Controls whether the hosted flow may show the full widget (the Deposit Options
109
+ entry screen for unauthenticated users). Defaults to `true`. The full widget
110
+ only appears when this option is enabled **and** the merchant's backend config
111
+ has it enabled — disabling either turns it off. Set it to `false` to force the
112
+ hosted flow straight to the standard deposit flow regardless of merchant
113
+ config.
114
+
105
115
  ## Error Handling
106
116
 
107
117
  Every error is a `DepositError` with a machine-readable `code`:
@@ -164,6 +174,27 @@ import type {
164
174
  import type { DepositErrorCode } from '@swype-org/deposit';
165
175
  ```
166
176
 
177
+ ## Security & content integrity
178
+
179
+ The deposit UI loads live from `https://pay.blink.cash` every render — the
180
+ standard model for embedded payment UIs (it lets us ship security/fraud fixes
181
+ instantly). We do not version-pin the iframe; instead:
182
+
183
+ - **Verifiable build manifest** — every deploy publishes
184
+ [`https://pay.blink.cash/manifest.json`](https://pay.blink.cash/manifest.json)
185
+ with the build version, source commit, and a SHA-256 of every served file, so
186
+ you can confirm exactly what we serve.
187
+ - **Lock the iframe origin** — add a CSP to your page so the iframe can only ever
188
+ load from Swype:
189
+
190
+ ```
191
+ Content-Security-Policy: frame-src https://pay.blink.cash; script-src 'self';
192
+ ```
193
+
194
+ See [docs/iframe-security.md](../docs/iframe-security.md) for the full security
195
+ model (CSP, postMessage hardening, Privy origin allow-listing, passkey
196
+ delegation, and SRI rationale).
197
+
167
198
  ## Backward Compatibility
168
199
 
169
200
  The previous `Checkout`-named symbols are re-exported as deprecated aliases:
@@ -524,15 +524,19 @@ var STYLES = `
524
524
  [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}
525
525
  @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}}
526
526
  `;
527
- function createIframe(url, containerElement) {
527
+ function createIframe(url, containerElement, options) {
528
528
  ensureStyles();
529
529
  let closed = false;
530
+ let hidden = options?.hidden === true;
530
531
  let closeCallback = null;
531
532
  const overlay = document.createElement("div");
532
533
  overlay.setAttribute("data-blink-overlay", "");
533
534
  if (shouldUseMobileSheetLayout()) {
534
535
  overlay.setAttribute("data-blink-mobile-sheet", "");
535
536
  }
537
+ if (hidden) {
538
+ overlay.style.display = "none";
539
+ }
536
540
  const container = document.createElement("div");
537
541
  container.setAttribute("data-blink-container", "");
538
542
  const iframe = document.createElement("iframe");
@@ -558,8 +562,8 @@ function createIframe(url, containerElement) {
558
562
  };
559
563
  attachBridge();
560
564
  iframe.addEventListener("load", attachBridge);
561
- const savedOverflow = document.body.style.overflow;
562
- document.body.style.overflow = "hidden";
565
+ let savedOverflow = "";
566
+ let scrollLocked = false;
563
567
  const onBackdropClick = (event) => {
564
568
  if (event.target === overlay) triggerClose();
565
569
  };
@@ -569,15 +573,29 @@ function createIframe(url, containerElement) {
569
573
  const onTouchMove = (event) => {
570
574
  if (event.target === overlay) event.preventDefault();
571
575
  };
572
- overlay.addEventListener("click", onBackdropClick);
573
- overlay.addEventListener("touchmove", onTouchMove, { passive: false });
574
- document.addEventListener("keydown", onKeyDown);
576
+ function lockScroll() {
577
+ if (scrollLocked) return;
578
+ scrollLocked = true;
579
+ savedOverflow = document.body.style.overflow;
580
+ document.body.style.overflow = "hidden";
581
+ }
582
+ function attachDismissalListeners() {
583
+ overlay.addEventListener("click", onBackdropClick);
584
+ overlay.addEventListener("touchmove", onTouchMove, { passive: false });
585
+ document.addEventListener("keydown", onKeyDown);
586
+ }
587
+ if (!hidden) {
588
+ lockScroll();
589
+ attachDismissalListeners();
590
+ }
575
591
  function removeListeners() {
576
592
  overlay.removeEventListener("click", onBackdropClick);
577
593
  overlay.removeEventListener("touchmove", onTouchMove);
578
594
  document.removeEventListener("keydown", onKeyDown);
579
595
  }
580
596
  function unlockScroll() {
597
+ if (!scrollLocked) return;
598
+ scrollLocked = false;
581
599
  document.body.style.overflow = savedOverflow;
582
600
  }
583
601
  function triggerClose() {
@@ -615,6 +633,21 @@ function createIframe(url, containerElement) {
615
633
  isClosed() {
616
634
  return closed;
617
635
  },
636
+ isHidden() {
637
+ return hidden;
638
+ },
639
+ reveal() {
640
+ if (closed || !hidden) return;
641
+ hidden = false;
642
+ if (shouldUseMobileSheetLayout()) {
643
+ overlay.setAttribute("data-blink-mobile-sheet", "");
644
+ } else {
645
+ overlay.removeAttribute("data-blink-mobile-sheet");
646
+ }
647
+ overlay.style.display = "";
648
+ lockScroll();
649
+ attachDismissalListeners();
650
+ },
618
651
  onClose(callback) {
619
652
  closeCallback = callback;
620
653
  },
@@ -786,6 +819,13 @@ function buildSignerRequest(request, webviewBaseUrl) {
786
819
 
787
820
  // src/checkout.ts
788
821
  var DEFAULT_WEBVIEW_BASE_URL = "https://pay.blink.cash";
822
+ var SANDBOX_WEBVIEW_BASE_URL = "https://pay-sandbox.blink.cash";
823
+ var IFRAME_READY_TIMEOUT_MS = 2e3;
824
+ var WARM_IFRAME_READY_TIMEOUT_MS = 6e4;
825
+ function resolveWebviewBaseUrl(config) {
826
+ if (config.webviewBaseUrl) return config.webviewBaseUrl;
827
+ return config.environment === "sandbox" ? SANDBOX_WEBVIEW_BASE_URL : DEFAULT_WEBVIEW_BASE_URL;
828
+ }
789
829
  var Deposit = class {
790
830
  config;
791
831
  iframe = null;
@@ -798,6 +838,9 @@ var Deposit = class {
798
838
  _error = null;
799
839
  flowTimer = null;
800
840
  lastSignerResponse = null;
841
+ warmIframe = null;
842
+ warmIframeReady = null;
843
+ warmIframeReadyCancel = null;
801
844
  listeners = {
802
845
  complete: /* @__PURE__ */ new Set(),
803
846
  error: /* @__PURE__ */ new Set(),
@@ -828,6 +871,93 @@ var Deposit = class {
828
871
  this.log("Deposit instance created", {
829
872
  signer: typeof config.signer === "string" ? config.signer : "<function>"
830
873
  });
874
+ if (config.preload !== false) {
875
+ this.schedulePreload();
876
+ }
877
+ }
878
+ /**
879
+ * Warm up the hosted payment flow in a hidden iframe so `requestDeposit`
880
+ * opens an already-loaded page (JS bundle, auth session restore, and —
881
+ * when {@link DepositConfig.merchantId} is set — merchant config prefetch
882
+ * all happen in the background).
883
+ *
884
+ * Runs automatically after construction unless `preload: false`; call it
885
+ * manually to control the timing (e.g. on deposit-button hover). No-op
886
+ * when a warm iframe already exists or a deposit flow is active.
887
+ */
888
+ preload() {
889
+ if (this.destroyed || this.isActive) return;
890
+ if (this.warmIframe && !this.warmIframe.isClosed()) return;
891
+ if (typeof document === "undefined" || typeof window === "undefined") return;
892
+ const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
893
+ this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
894
+ const preloadUrl = this.buildPreloadUrl(webviewBaseUrl);
895
+ const iframe = createIframe(preloadUrl, this.config.containerElement, { hidden: true });
896
+ const ready = this.waitForIframeReady(iframe, WARM_IFRAME_READY_TIMEOUT_MS);
897
+ this.warmIframe = iframe;
898
+ this.warmIframeReady = ready.promise;
899
+ this.warmIframeReadyCancel = ready.cancel;
900
+ this.log("Warm-up iframe created", { url: preloadUrl });
901
+ }
902
+ // Defer the automatic warm-up until the host page has finished its own
903
+ // load and the main thread is idle — constructing a Deposit instance must
904
+ // never slow the merchant page down.
905
+ schedulePreload() {
906
+ if (typeof window === "undefined" || typeof document === "undefined") return;
907
+ const start = () => {
908
+ if (!this.destroyed && !this.isActive) this.preload();
909
+ };
910
+ const whenIdle = () => {
911
+ const w = window;
912
+ if (typeof w.requestIdleCallback === "function") {
913
+ w.requestIdleCallback(start, { timeout: 3e3 });
914
+ } else {
915
+ setTimeout(start, 250);
916
+ }
917
+ };
918
+ if (document.readyState === "complete") {
919
+ whenIdle();
920
+ } else {
921
+ window.addEventListener("load", whenIdle, { once: true });
922
+ }
923
+ }
924
+ buildPreloadUrl(webviewBaseUrl) {
925
+ const preloadUrl = new URL(webviewBaseUrl);
926
+ preloadUrl.searchParams.set("preload", "true");
927
+ if (this.config.merchantId) {
928
+ preloadUrl.searchParams.set("merchantId", this.config.merchantId);
929
+ }
930
+ this.applyFullWidgetParam(preloadUrl);
931
+ return preloadUrl.toString();
932
+ }
933
+ /**
934
+ * Detach the warm iframe for use by a starting flow. Returns null (and
935
+ * cleans up) when there is none or it was closed in the meantime.
936
+ */
937
+ takeWarmIframe() {
938
+ const iframe = this.warmIframe;
939
+ const ready = this.warmIframeReady;
940
+ const cancelReady = this.warmIframeReadyCancel ?? (() => {
941
+ });
942
+ this.warmIframe = null;
943
+ this.warmIframeReady = null;
944
+ this.warmIframeReadyCancel = null;
945
+ if (!iframe || !ready) return null;
946
+ if (iframe.isClosed()) {
947
+ cancelReady();
948
+ iframe.destroy();
949
+ return null;
950
+ }
951
+ return { iframe, ready, cancelReady };
952
+ }
953
+ discardWarmIframe() {
954
+ this.warmIframeReadyCancel?.();
955
+ this.warmIframeReadyCancel = null;
956
+ this.warmIframeReady = null;
957
+ if (this.warmIframe) {
958
+ this.warmIframe.destroy();
959
+ this.warmIframe = null;
960
+ }
831
961
  }
832
962
  /**
833
963
  * Open the hosted payment flow for the given deposit.
@@ -915,9 +1045,10 @@ var Deposit = class {
915
1045
  /** Tear down the instance and release all resources. */
916
1046
  destroy() {
917
1047
  this.log("destroy() called");
1048
+ this.destroyed = true;
918
1049
  this.cleanup();
1050
+ this.discardWarmIframe();
919
1051
  this.setStatus("idle");
920
- this.destroyed = true;
921
1052
  for (const set of Object.values(this.listeners)) {
922
1053
  set.clear();
923
1054
  }
@@ -931,6 +1062,17 @@ var Deposit = class {
931
1062
  console.debug(`[BlinkDeposit] ${message}`);
932
1063
  }
933
1064
  }
1065
+ /**
1066
+ * Appends `enableFullWidget=false` to the hosted-flow URL when the merchant
1067
+ * integration disables the full widget. Absence of the param means enabled
1068
+ * (the default) — the hosted flow ANDs this with the merchant's backend
1069
+ * config, so the full widget renders only when both allow it.
1070
+ */
1071
+ applyFullWidgetParam(url) {
1072
+ if (this.config.enableFullWidget === false) {
1073
+ url.searchParams.set("enableFullWidget", "false");
1074
+ }
1075
+ }
934
1076
  setStatus(status) {
935
1077
  if (this._status === status) return;
936
1078
  this.log(`Status: ${this._status} \u2192 ${status}`);
@@ -939,15 +1081,35 @@ var Deposit = class {
939
1081
  }
940
1082
  async runSignerFlow(request, requestId, onComplete, onError) {
941
1083
  try {
942
- const webviewBaseUrl = this.config.webviewBaseUrl ?? DEFAULT_WEBVIEW_BASE_URL;
1084
+ const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
943
1085
  this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
944
1086
  this.log("Calling signer", {
945
1087
  signer: typeof this.config.signer === "string" ? this.config.signer : "<function>"
946
1088
  });
947
1089
  const signerRequest = buildSignerRequest(request, webviewBaseUrl);
948
- const preloadUrl = new URL(webviewBaseUrl);
949
- preloadUrl.searchParams.set("preload", "true");
950
- const preloadIframe = createIframe(preloadUrl.toString(), this.config.containerElement);
1090
+ let preloadIframe;
1091
+ let iframeReadyPromise;
1092
+ const warm = this.takeWarmIframe();
1093
+ if (warm) {
1094
+ warm.iframe.reveal();
1095
+ preloadIframe = warm.iframe;
1096
+ iframeReadyPromise = Promise.race([
1097
+ warm.ready,
1098
+ new Promise(
1099
+ (resolve) => setTimeout(() => resolve(false), IFRAME_READY_TIMEOUT_MS)
1100
+ )
1101
+ ]).then((ready) => {
1102
+ warm.cancelReady();
1103
+ return ready;
1104
+ });
1105
+ this.log("Reusing warm preload iframe");
1106
+ } else {
1107
+ preloadIframe = createIframe(
1108
+ this.buildPreloadUrl(webviewBaseUrl),
1109
+ this.config.containerElement
1110
+ );
1111
+ iframeReadyPromise = this.waitForIframeReady(preloadIframe).promise;
1112
+ }
951
1113
  this.iframe = preloadIframe;
952
1114
  preloadIframe.onClose(() => {
953
1115
  onError(
@@ -964,7 +1126,6 @@ var Deposit = class {
964
1126
  signerRequest,
965
1127
  this.config.signerTimeoutMs
966
1128
  );
967
- const iframeReadyPromise = this.waitForIframeReady(preloadIframe);
968
1129
  const [signerResponse, iframeReady] = await Promise.all([
969
1130
  signerPromise,
970
1131
  iframeReadyPromise
@@ -996,6 +1157,7 @@ var Deposit = class {
996
1157
  hostedUrl.searchParams.set("merchantId", signerResponse.merchantId);
997
1158
  hostedUrl.searchParams.set("payload", signerResponse.payload);
998
1159
  hostedUrl.searchParams.set("signature", signerResponse.signature);
1160
+ this.applyFullWidgetParam(hostedUrl);
999
1161
  const targetUrl = hostedUrl.toString();
1000
1162
  const iframeHandle = createIframe(targetUrl, this.config.containerElement);
1001
1163
  this.iframe = iframeHandle;
@@ -1028,24 +1190,38 @@ var Deposit = class {
1028
1190
  }
1029
1191
  }
1030
1192
  }
1031
- waitForIframeReady(iframeHandle) {
1032
- return new Promise((resolve) => {
1193
+ /**
1194
+ * Resolves true when the iframe posts `blink:iframe-ready`, false on
1195
+ * timeout. `cancel()` settles the promise as false immediately and releases
1196
+ * the listener + timer — used to reap the generous warm-up wait once it no
1197
+ * longer matters (warm iframe consumed or discarded).
1198
+ */
1199
+ waitForIframeReady(iframeHandle, timeoutMs = IFRAME_READY_TIMEOUT_MS) {
1200
+ let finish = () => {
1201
+ };
1202
+ const promise = new Promise((resolve) => {
1033
1203
  const handler = (event) => {
1034
1204
  if (event.origin !== this.hostedOrigin) return;
1035
1205
  if (event.source !== iframeHandle.contentWindow) return;
1036
1206
  if (parseIframeReady(event.data)) {
1037
- window.removeEventListener("message", handler);
1038
- clearTimeout(timeout);
1039
- resolve(true);
1207
+ finish(true);
1040
1208
  }
1041
1209
  };
1042
1210
  window.addEventListener("message", handler);
1043
1211
  const timeout = setTimeout(() => {
1044
- window.removeEventListener("message", handler);
1045
1212
  this.log("Iframe ready timeout \u2014 falling back to URL params");
1046
- resolve(false);
1047
- }, 2e3);
1213
+ finish(false);
1214
+ }, timeoutMs);
1215
+ let settled = false;
1216
+ finish = (ready) => {
1217
+ if (settled) return;
1218
+ settled = true;
1219
+ window.removeEventListener("message", handler);
1220
+ clearTimeout(timeout);
1221
+ resolve(ready);
1222
+ };
1048
1223
  });
1224
+ return { promise, cancel: () => finish(false) };
1049
1225
  }
1050
1226
  startMessageListener(iframeHandle, onComplete) {
1051
1227
  const handler = (event) => {
@@ -1105,6 +1281,9 @@ var Deposit = class {
1105
1281
  }
1106
1282
  this.hostedOrigin = null;
1107
1283
  this.lastSignerResponse = null;
1284
+ if (!this.destroyed && this.config.preload !== false) {
1285
+ this.schedulePreload();
1286
+ }
1108
1287
  }
1109
1288
  emit(event, ...args) {
1110
1289
  for (const handler of this.listeners[event]) {
@@ -1117,6 +1296,6 @@ var Deposit = class {
1117
1296
  };
1118
1297
  var Checkout = Deposit;
1119
1298
 
1120
- export { ALLOWED_RPC_METHODS, BRIDGE_PROTOCOL_VERSION, Checkout, CheckoutError, DEFAULT_WEBVIEW_BASE_URL, Deposit, DepositError, attachRpcHost, createWalletDiscoverer, getDisplayMessage, parseBridgeMessage };
1121
- //# sourceMappingURL=chunk-WD6DVQHT.js.map
1122
- //# sourceMappingURL=chunk-WD6DVQHT.js.map
1299
+ export { ALLOWED_RPC_METHODS, BRIDGE_PROTOCOL_VERSION, Checkout, CheckoutError, DEFAULT_WEBVIEW_BASE_URL, Deposit, DepositError, SANDBOX_WEBVIEW_BASE_URL, attachRpcHost, createWalletDiscoverer, getDisplayMessage, parseBridgeMessage };
1300
+ //# sourceMappingURL=chunk-VMXCXXVN.js.map
1301
+ //# sourceMappingURL=chunk-VMXCXXVN.js.map