@swype-org/deposit 0.3.23 → 0.3.31

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.
@@ -507,8 +507,43 @@ function parseCapabilities(value) {
507
507
  }
508
508
  return value.filter((entry) => typeof entry === "string");
509
509
  }
510
- function buildViewportMessage(viewportLvh, safeAreaBottom) {
511
- return { type: "blink:viewport", viewportLvh, safeAreaBottom };
510
+ function buildViewportMessage(viewportLvh, safeAreaBottom, embedMaxHeightPx) {
511
+ return {
512
+ type: "blink:viewport",
513
+ viewportLvh,
514
+ safeAreaBottom,
515
+ ...embedMaxHeightPx === void 0 ? {} : { embedMaxHeightPx }
516
+ };
517
+ }
518
+ var MAX_CONTENT_HEIGHT_PX = 2e4;
519
+ function parseContentHeight(data) {
520
+ if (!data || typeof data !== "object") {
521
+ return null;
522
+ }
523
+ const msg = data;
524
+ if (msg.type !== "blink:content-height") {
525
+ return null;
526
+ }
527
+ const raw = msg.heightPx;
528
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
529
+ return null;
530
+ }
531
+ const heightPx = Math.round(raw);
532
+ if (heightPx <= 0 || heightPx > MAX_CONTENT_HEIGHT_PX) {
533
+ return null;
534
+ }
535
+ return {
536
+ type: "blink:content-height",
537
+ heightPx,
538
+ ...parseWidthHint("preferredWidthPx", msg.preferredWidthPx),
539
+ ...parseWidthHint("minWidthPx", msg.minWidthPx)
540
+ };
541
+ }
542
+ function parseWidthHint(key, value) {
543
+ if (typeof value !== "number" || !Number.isFinite(value)) return {};
544
+ const px = Math.round(value);
545
+ if (px <= 0 || px > MAX_CONTENT_HEIGHT_PX) return {};
546
+ return { [key]: px };
512
547
  }
513
548
  function buildRevealMessage() {
514
549
  return { type: "blink:reveal" };
@@ -522,6 +557,23 @@ function buildSignedPayloadMessage(merchantId, payload, signature) {
522
557
  };
523
558
  }
524
559
 
560
+ // src/brandParam.ts
561
+ var BRAND_COLOR_PARAM_KEYS = {
562
+ colorPrimary: "p",
563
+ colorBackground: "bg",
564
+ colorText: "t",
565
+ colorDanger: "d",
566
+ colorBorder: "b"
567
+ };
568
+ function normalizeBrandHex(value) {
569
+ if (typeof value !== "string") return null;
570
+ const hex = value.trim().toLowerCase();
571
+ if (!/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/.test(hex)) return null;
572
+ const digits = hex.slice(1);
573
+ if (digits.length === 6) return `#${digits}`;
574
+ return `#${digits[0]}${digits[0]}${digits[1]}${digits[1]}${digits[2]}${digits[2]}`;
575
+ }
576
+
525
577
  // src/viewportMetrics.ts
526
578
  function measureViewportMetrics() {
527
579
  const unavailable = { viewportLvh: 0, safeAreaBottom: 0 };
@@ -550,6 +602,84 @@ function measureViewportMetrics() {
550
602
  }
551
603
  }
552
604
 
605
+ // src/iframeCore.ts
606
+ function createFrameElement(url) {
607
+ const iframe = document.createElement("iframe");
608
+ iframe.src = url;
609
+ const iframeOrigin = new URL(url).origin;
610
+ iframe.allow = `publickey-credentials-get ${iframeOrigin}; publickey-credentials-create ${iframeOrigin}; clipboard-write ${iframeOrigin}; web-share ${iframeOrigin}`;
611
+ return { iframe, iframeOrigin };
612
+ }
613
+ function attachFrameBridges(options) {
614
+ const { iframe, iframeOrigin, postViewport, getEmbedMaxHeightPx, shouldRevealOnLoad } = options;
615
+ let detached = false;
616
+ const discoverer = createWalletDiscoverer();
617
+ let rpcHostHandle = null;
618
+ const attachBridge = () => {
619
+ if (rpcHostHandle) return;
620
+ if (!iframe.contentWindow) return;
621
+ rpcHostHandle = attachRpcHost({
622
+ iframeWindow: iframe.contentWindow,
623
+ iframeOrigin,
624
+ discoverer
625
+ });
626
+ };
627
+ attachBridge();
628
+ iframe.addEventListener("load", attachBridge);
629
+ function postReveal() {
630
+ if (detached) return;
631
+ iframe.contentWindow?.postMessage(buildRevealMessage(), iframeOrigin);
632
+ }
633
+ function postViewportMetrics() {
634
+ if (detached || !postViewport) return;
635
+ const metrics = measureViewportMetrics();
636
+ if (metrics.viewportLvh <= 0) return;
637
+ iframe.contentWindow?.postMessage(
638
+ buildViewportMessage(
639
+ metrics.viewportLvh,
640
+ metrics.safeAreaBottom,
641
+ getEmbedMaxHeightPx?.()
642
+ ),
643
+ iframeOrigin
644
+ );
645
+ }
646
+ let viewportPostScheduled = false;
647
+ const onViewportChange = () => {
648
+ if (viewportPostScheduled) return;
649
+ viewportPostScheduled = true;
650
+ requestAnimationFrame(() => {
651
+ viewportPostScheduled = false;
652
+ postViewportMetrics();
653
+ });
654
+ };
655
+ if (postViewport) {
656
+ window.addEventListener("resize", onViewportChange);
657
+ window.addEventListener("orientationchange", onViewportChange);
658
+ }
659
+ const onLoadReveal = () => {
660
+ postViewportMetrics();
661
+ if (shouldRevealOnLoad()) postReveal();
662
+ };
663
+ iframe.addEventListener("load", onLoadReveal);
664
+ return {
665
+ postReveal,
666
+ postViewportMetrics,
667
+ detach() {
668
+ if (detached) return;
669
+ detached = true;
670
+ iframe.removeEventListener("load", attachBridge);
671
+ iframe.removeEventListener("load", onLoadReveal);
672
+ window.removeEventListener("resize", onViewportChange);
673
+ window.removeEventListener("orientationchange", onViewportChange);
674
+ if (rpcHostHandle) {
675
+ rpcHostHandle.detach();
676
+ rpcHostHandle = null;
677
+ }
678
+ discoverer.destroy();
679
+ }
680
+ };
681
+ }
682
+
553
683
  // src/iframe.ts
554
684
  var STYLE_ID = "blink-deposit-styles";
555
685
  var CLOSE_DURATION_MS = 280;
@@ -566,7 +696,7 @@ var STYLES = `
566
696
  [data-blink-overlay][data-blink-mobile-sheet]{align-items:flex-end;touch-action:none;overscroll-behavior:contain}
567
697
  [data-blink-container]{width:min(420px,92vw);height:min(600px,85vh);border-radius:24px;overflow:hidden;box-shadow:0 24px 80px rgba(0,0,0,.4);animation:blink-slide-up .25s ease-out;display:flex;flex-direction:column;background:transparent}
568
698
  [data-blink-overlay][data-blink-closing] [data-blink-container]{opacity:0;transition:opacity ${CLOSE_DURATION_MS}ms ease-in}
569
- [data-blink-container] iframe{width:100%;flex:1;border:none;display:block;background:transparent;color-scheme:normal;border-radius:inherit}
699
+ [data-blink-container] iframe{width:100%;flex:1;border:none;display:block;background:transparent;color-scheme:light;border-radius:inherit}
570
700
  [data-blink-overlay][data-blink-mobile-sheet] [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)}
571
701
  [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}
572
702
  @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}}
@@ -591,60 +721,20 @@ function createIframe(url, containerElement, options) {
591
721
  }
592
722
  const container = document.createElement("div");
593
723
  container.setAttribute("data-blink-container", "");
594
- const iframe = document.createElement("iframe");
595
- iframe.src = url;
596
- const iframeOrigin = new URL(url).origin;
597
- iframe.allow = `publickey-credentials-get ${iframeOrigin}; publickey-credentials-create ${iframeOrigin}; clipboard-write ${iframeOrigin}`;
724
+ const { iframe, iframeOrigin } = createFrameElement(url);
598
725
  const handle = document.createElement("div");
599
726
  container.appendChild(handle);
600
727
  container.appendChild(iframe);
601
728
  overlay.appendChild(container);
602
729
  const mountTarget = containerElement ?? document.body;
603
730
  mountTarget.appendChild(overlay);
604
- const discoverer = createWalletDiscoverer();
605
- let rpcHostHandle = null;
606
- const attachBridge = () => {
607
- if (rpcHostHandle) return;
608
- if (!iframe.contentWindow) return;
609
- rpcHostHandle = attachRpcHost({
610
- iframeWindow: iframe.contentWindow,
611
- iframeOrigin,
612
- discoverer
613
- });
614
- };
615
- attachBridge();
616
- iframe.addEventListener("load", attachBridge);
617
- function postReveal() {
618
- if (closed) return;
619
- iframe.contentWindow?.postMessage(buildRevealMessage(), iframeOrigin);
620
- }
621
- function postViewportMetrics() {
622
- if (closed || !options?.fluid) return;
623
- const metrics = measureViewportMetrics();
624
- if (metrics.viewportLvh <= 0) return;
625
- iframe.contentWindow?.postMessage(
626
- buildViewportMessage(metrics.viewportLvh, metrics.safeAreaBottom),
627
- iframeOrigin
628
- );
629
- }
630
- let viewportPostScheduled = false;
631
- const onViewportChange = () => {
632
- if (viewportPostScheduled) return;
633
- viewportPostScheduled = true;
634
- requestAnimationFrame(() => {
635
- viewportPostScheduled = false;
636
- postViewportMetrics();
637
- });
638
- };
639
- if (options?.fluid) {
640
- window.addEventListener("resize", onViewportChange);
641
- window.addEventListener("orientationchange", onViewportChange);
642
- }
643
- const onLoadReveal = () => {
644
- postViewportMetrics();
645
- if (!hidden) postReveal();
646
- };
647
- iframe.addEventListener("load", onLoadReveal);
731
+ const bridges = attachFrameBridges({
732
+ iframe,
733
+ iframeOrigin,
734
+ // Only fluid needs them: the legacy fixed overlay sizes the iframe itself.
735
+ postViewport: options?.fluid === true,
736
+ shouldRevealOnLoad: () => !hidden
737
+ });
648
738
  let savedOverflow = "";
649
739
  let scrollLocked = false;
650
740
  const onBackdropClick = (event) => {
@@ -675,8 +765,6 @@ function createIframe(url, containerElement, options) {
675
765
  overlay.removeEventListener("click", onBackdropClick);
676
766
  overlay.removeEventListener("touchmove", onTouchMove);
677
767
  document.removeEventListener("keydown", onKeyDown);
678
- window.removeEventListener("resize", onViewportChange);
679
- window.removeEventListener("orientationchange", onViewportChange);
680
768
  }
681
769
  function unlockScroll() {
682
770
  if (!scrollLocked) return;
@@ -687,7 +775,7 @@ function createIframe(url, containerElement, options) {
687
775
  if (closed) return;
688
776
  closed = true;
689
777
  removeListeners();
690
- detachBridge();
778
+ bridges.detach();
691
779
  overlay.setAttribute("data-blink-closing", "");
692
780
  let removed = false;
693
781
  const removeOverlay = () => {
@@ -700,15 +788,6 @@ function createIframe(url, containerElement, options) {
700
788
  container.addEventListener("animationend", removeOverlay, { once: true });
701
789
  setTimeout(removeOverlay, CLOSE_DURATION_MS + 50);
702
790
  }
703
- function detachBridge() {
704
- iframe.removeEventListener("load", attachBridge);
705
- iframe.removeEventListener("load", onLoadReveal);
706
- if (rpcHostHandle) {
707
- rpcHostHandle.detach();
708
- rpcHostHandle = null;
709
- }
710
- discoverer.destroy();
711
- }
712
791
  return {
713
792
  get contentWindow() {
714
793
  return iframe.contentWindow;
@@ -733,15 +812,17 @@ function createIframe(url, containerElement, options) {
733
812
  overlay.style.display = "";
734
813
  lockScroll();
735
814
  attachDismissalListeners();
736
- postViewportMetrics();
737
- postReveal();
815
+ bridges.postViewportMetrics();
816
+ bridges.postReveal();
738
817
  },
739
818
  postReveal() {
740
- postReveal();
819
+ bridges.postReveal();
741
820
  },
742
821
  downgradeToFixed() {
743
822
  overlay.removeAttribute("data-blink-fluid");
744
823
  },
824
+ applyContentHeight() {
825
+ },
745
826
  onClose(callback) {
746
827
  closeCallback = callback;
747
828
  },
@@ -750,7 +831,7 @@ function createIframe(url, containerElement, options) {
750
831
  if (!closed) {
751
832
  closed = true;
752
833
  removeListeners();
753
- detachBridge();
834
+ bridges.detach();
754
835
  overlay.remove();
755
836
  unlockScroll();
756
837
  }
@@ -787,6 +868,117 @@ function shouldUseMobileSheetLayout() {
787
868
  return shortestScreenSide > 0 && shortestScreenSide <= MOBILE_SHEET_MAX_SCREEN_PX;
788
869
  }
789
870
 
871
+ // src/embeddedIframe.ts
872
+ var EMBED_STYLE_ID = "blink-deposit-embed-styles";
873
+ var EMBED_STYLES = `
874
+ [data-blink-embed]{display:block;width:100%;position:relative}
875
+ [data-blink-embed][data-blink-embed-hidden]{height:0;overflow:hidden;visibility:hidden;pointer-events:none}
876
+ [data-blink-embed] iframe{display:block;width:100%;height:0;border:none;background:transparent;color-scheme:light}
877
+ `;
878
+ function createEmbeddedIframe(url, containerElement, options) {
879
+ ensureEmbedStyles();
880
+ if (!containerElement.isConnected) {
881
+ console.warn(
882
+ "[blink] The containerElement passed to Deposit is not in the document. Embedded mode renders into it for the life of the instance, so it must be a stable node \u2014 render the slot once and hide it with `visibility: hidden` (NOT `display: none`, which removes its layout and leaves the warming flow unable to measure itself) instead of unmounting it, or construct a new Deposit against the new node."
883
+ );
884
+ }
885
+ let closed = false;
886
+ let hidden = options?.hidden === true;
887
+ let closeCallback = null;
888
+ const readEmbedMaxHeightPx = () => typeof options?.embedMaxHeightPx === "function" ? options.embedMaxHeightPx() : options?.embedMaxHeightPx;
889
+ const wrapper = document.createElement("div");
890
+ wrapper.setAttribute("data-blink-embed", "");
891
+ if (hidden) {
892
+ wrapper.setAttribute("data-blink-embed-hidden", "");
893
+ }
894
+ const { iframe, iframeOrigin } = createFrameElement(url);
895
+ iframe.setAttribute("scrolling", "no");
896
+ const initialBudget = readEmbedMaxHeightPx();
897
+ if (initialBudget !== void 0) {
898
+ iframe.style.height = `${initialBudget}px`;
899
+ }
900
+ wrapper.appendChild(iframe);
901
+ containerElement.appendChild(wrapper);
902
+ const bridges = attachFrameBridges({
903
+ iframe,
904
+ iframeOrigin,
905
+ // Embedded always needs them: the flow has no usable viewport of its own,
906
+ // so the basis it sizes against can only come from here.
907
+ postViewport: true,
908
+ getEmbedMaxHeightPx: readEmbedMaxHeightPx,
909
+ shouldRevealOnLoad: () => !hidden
910
+ });
911
+ function teardown() {
912
+ closed = true;
913
+ bridges.detach();
914
+ wrapper.remove();
915
+ }
916
+ return {
917
+ get contentWindow() {
918
+ return iframe.contentWindow;
919
+ },
920
+ close() {
921
+ if (closed) return;
922
+ teardown();
923
+ closeCallback?.();
924
+ },
925
+ isClosed() {
926
+ return closed;
927
+ },
928
+ isHidden() {
929
+ return hidden;
930
+ },
931
+ reveal() {
932
+ if (closed || !hidden) return;
933
+ hidden = false;
934
+ wrapper.removeAttribute("data-blink-embed-hidden");
935
+ warnIfContainerNotRendered(containerElement);
936
+ bridges.postViewportMetrics();
937
+ bridges.postReveal();
938
+ },
939
+ postReveal() {
940
+ bridges.postReveal();
941
+ },
942
+ downgradeToFixed() {
943
+ },
944
+ applyContentHeight(heightPx) {
945
+ if (closed) return;
946
+ const budget = readEmbedMaxHeightPx();
947
+ const capped = budget === void 0 ? heightPx : Math.min(heightPx, budget);
948
+ iframe.style.height = `${Math.max(0, Math.round(capped))}px`;
949
+ },
950
+ onClose(callback) {
951
+ closeCallback = callback;
952
+ },
953
+ destroy() {
954
+ closeCallback = null;
955
+ if (!closed) teardown();
956
+ }
957
+ };
958
+ }
959
+ function warnIfContainerNotRendered(containerElement) {
960
+ if (typeof containerElement.getBoundingClientRect !== "function") return;
961
+ const rect = containerElement.getBoundingClientRect();
962
+ if (rect.width > 0 || rect.height > 0) return;
963
+ const display = typeof getComputedStyle === "function" ? getComputedStyle(containerElement).display : "";
964
+ console.warn(
965
+ `[blink] The containerElement has no layout at the moment the flow is revealed${display === "none" ? " (display: none)" : ""}. A display:none subtree cannot be measured, so the flow may report no height and the panel stays blank at its budget height. Hide the slot with \`visibility: hidden\` (plus \`height: 0; overflow: hidden\`) instead of \`display: none\`, so it keeps its box while it warms up.`
966
+ );
967
+ }
968
+ function ensureEmbedStyles() {
969
+ const existingStyle = document.getElementById(EMBED_STYLE_ID);
970
+ if (existingStyle) {
971
+ if (existingStyle.textContent !== EMBED_STYLES) {
972
+ existingStyle.textContent = EMBED_STYLES;
973
+ }
974
+ return;
975
+ }
976
+ const style = document.createElement("style");
977
+ style.id = EMBED_STYLE_ID;
978
+ style.textContent = EMBED_STYLES;
979
+ document.head.appendChild(style);
980
+ }
981
+
790
982
  // src/signer.ts
791
983
  var DEFAULT_SIGNER_TIMEOUT_MS = 15e3;
792
984
  async function callSigner(signer, request, timeoutMs) {
@@ -917,6 +1109,13 @@ var SANDBOX_WEBVIEW_BASE_URL = "https://pay-sandbox.blink.cash";
917
1109
  var IFRAME_READY_TIMEOUT_MS = 2e3;
918
1110
  var WARM_IFRAME_READY_TIMEOUT_MS = 6e4;
919
1111
  var LAYOUT_FLUID_CAPABILITY = "layout-fluid";
1112
+ var DEFAULT_EMBED_MAX_HEIGHT_FRACTION = 0.9;
1113
+ function dismissedError() {
1114
+ return new DepositError(
1115
+ "DEPOSIT_DISMISSED",
1116
+ "The deposit was dismissed before the transfer completed."
1117
+ );
1118
+ }
920
1119
  var IFRAME_NOT_READY = { ready: false, fluidCapable: false };
921
1120
  function resolveWebviewBaseUrl(config) {
922
1121
  if (config.webviewBaseUrl) return config.webviewBaseUrl;
@@ -937,11 +1136,20 @@ var Deposit = class {
937
1136
  warmIframe = null;
938
1137
  warmIframeReady = null;
939
1138
  warmIframeReadyCancel = null;
1139
+ /** Resolved embedded-vs-overlay decision; see {@link Deposit.isEmbedded}. */
1140
+ embeddedResolved = null;
1141
+ /**
1142
+ * Settles the in-flight `requestDeposit()` as a dismissal. Non-null exactly
1143
+ * while a flow is in progress, so {@link Deposit.close} can reject the promise
1144
+ * the caller is awaiting instead of tearing the frame down under it.
1145
+ */
1146
+ dismissActiveFlow = null;
940
1147
  listeners = {
941
1148
  complete: /* @__PURE__ */ new Set(),
942
1149
  error: /* @__PURE__ */ new Set(),
943
1150
  close: /* @__PURE__ */ new Set(),
944
- "status-change": /* @__PURE__ */ new Set()
1151
+ "status-change": /* @__PURE__ */ new Set(),
1152
+ resize: /* @__PURE__ */ new Set()
945
1153
  };
946
1154
  /** Current phase of the deposit flow. */
947
1155
  get status() {
@@ -959,10 +1167,40 @@ var Deposit = class {
959
1167
  get isActive() {
960
1168
  return this._status === "signer-loading" || this._status === "iframe-active";
961
1169
  }
1170
+ /**
1171
+ * How this instance will actually present: `'embedded'` renders inline in
1172
+ * `containerElement`, `'overlay'` covers the page.
1173
+ *
1174
+ * Not simply an echo of the config — a host that asked for `'embedded'` gets
1175
+ * `'overlay'` on mobile, where a phone-sized method panel cannot hold the
1176
+ * flow (see {@link Deposit.isEmbedded}). Read it when laying out the panel:
1177
+ * on `'overlay'` no `resize` event ever fires, so a panel sized from those
1178
+ * reports would sit empty behind the overlay — collapse or skip it.
1179
+ *
1180
+ * Live until the SDK builds its first (warm-up) frame and fixed from then on,
1181
+ * so re-read it on resize rather than caching it at mount.
1182
+ */
1183
+ get presentation() {
1184
+ return this.isEmbedded() ? "embedded" : "overlay";
1185
+ }
962
1186
  constructor(config) {
963
1187
  if (!config.signer || typeof config.signer !== "string" && typeof config.signer !== "function") {
964
1188
  throw new DepositError("INVALID_REQUEST", "DepositConfig.signer is required (URL string or SignerFunction).");
965
1189
  }
1190
+ if (config.presentation === "embedded") {
1191
+ if (!config.containerElement) {
1192
+ throw new DepositError(
1193
+ "INVALID_REQUEST",
1194
+ 'DepositConfig.containerElement is required when presentation is "embedded".'
1195
+ );
1196
+ }
1197
+ if (config.layout === "fixed") {
1198
+ throw new DepositError(
1199
+ "INVALID_REQUEST",
1200
+ 'DepositConfig.layout "fixed" is an overlay-only escape hatch and cannot be combined with presentation "embedded".'
1201
+ );
1202
+ }
1203
+ }
966
1204
  this.config = config;
967
1205
  this.log("Deposit instance created", {
968
1206
  signer: typeof config.signer === "string" ? config.signer : "<function>"
@@ -988,10 +1226,7 @@ var Deposit = class {
988
1226
  const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
989
1227
  this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
990
1228
  const preloadUrl = this.buildPreloadUrl(webviewBaseUrl);
991
- const iframe = createIframe(preloadUrl, this.config.containerElement, {
992
- hidden: true,
993
- fluid: this.isFluidLayout()
994
- });
1229
+ const iframe = this.createPresentedIframe(preloadUrl, { hidden: true });
995
1230
  const ready = this.waitForIframeReady(iframe, WARM_IFRAME_READY_TIMEOUT_MS);
996
1231
  this.warmIframe = iframe;
997
1232
  this.warmIframeReady = ready.promise;
@@ -1020,6 +1255,83 @@ var Deposit = class {
1020
1255
  window.addEventListener("load", whenIdle, { once: true });
1021
1256
  }
1022
1257
  }
1258
+ /**
1259
+ * Whether the flow renders inline in the host's element rather than as an
1260
+ * overlay.
1261
+ *
1262
+ * `presentation: 'embedded'` is a request, not a guarantee: **on mobile the
1263
+ * flow presents as the normal overlay** even for an embedded host. An
1264
+ * aggregator's method panel is a narrow, short column on a phone, and the
1265
+ * flow inside it is a payment journey with a keypad, wallet lists and a QR
1266
+ * code — it needs the screen. The overlay is exactly what a phone user gets
1267
+ * from every other Blink integration, and the host's own dialog stays behind
1268
+ * it. Desktop keeps the inline card, where the panel has room.
1269
+ *
1270
+ * **The decision lives with the frame.** Read live until a frame is built,
1271
+ * then committed for good ({@link Deposit.createPresentedIframe}) — `preload()`
1272
+ * warms a frame from these same predicates and the flow that later adopts it
1273
+ * must agree, because a frame warmed at `layout=embed` handed to the overlay
1274
+ * presenter (or the reverse) is a blank modal. Committing at frame creation
1275
+ * rather than at construction matters: a host that constructs `Deposit` before
1276
+ * layout settles — a widget mounting inside a transition, a background or
1277
+ * prerendered tab — reports a zero-width viewport, and `(max-width: 640px)`
1278
+ * matches at zero, so caching then would lock a desktop user into the overlay
1279
+ * over a viewport that never existed. The warm-up already waits for `load`
1280
+ * plus an idle callback, so by the time it commits the measurement is real.
1281
+ */
1282
+ isEmbedded() {
1283
+ if (this.config.presentation !== "embedded") return false;
1284
+ if (this.embeddedResolved !== null) return this.embeddedResolved;
1285
+ if (typeof window === "undefined") return true;
1286
+ const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
1287
+ if (!(viewportWidth > 0)) return true;
1288
+ return !shouldUseMobileSheetLayout();
1289
+ }
1290
+ /**
1291
+ * The host's height budget for the inline iframe. Explicit config wins;
1292
+ * otherwise most of the host viewport, which keeps a tall flow from
1293
+ * outgrowing the page it is embedded in.
1294
+ */
1295
+ resolveEmbedMaxHeightPx() {
1296
+ if (this.config.embedMaxHeightPx !== void 0) return this.config.embedMaxHeightPx;
1297
+ if (typeof window === "undefined") return void 0;
1298
+ const derived = Math.round(window.innerHeight * DEFAULT_EMBED_MAX_HEIGHT_FRACTION);
1299
+ return derived > 0 ? derived : void 0;
1300
+ }
1301
+ /** Build the iframe for the configured presentation. */
1302
+ createPresentedIframe(url, options) {
1303
+ if (this.config.presentation === "embedded" && this.embeddedResolved === null) {
1304
+ this.embeddedResolved = this.isEmbedded();
1305
+ if (!this.embeddedResolved) {
1306
+ this.log("Mobile device: presenting the embedded flow as an overlay");
1307
+ }
1308
+ }
1309
+ if (this.isEmbedded()) {
1310
+ return createEmbeddedIframe(url, this.config.containerElement, {
1311
+ hidden: options.hidden,
1312
+ // A getter, not a value: the default is derived from the host
1313
+ // viewport, which changes on rotation and window resize.
1314
+ embedMaxHeightPx: () => this.resolveEmbedMaxHeightPx()
1315
+ });
1316
+ }
1317
+ return createIframe(url, this.overlayMountTarget(), {
1318
+ hidden: options.hidden,
1319
+ fluid: this.isFluidLayout()
1320
+ });
1321
+ }
1322
+ /**
1323
+ * Where the overlay mounts. `containerElement` means two different things by
1324
+ * presentation: an overlay mount point (`presentation: 'overlay'`) or the
1325
+ * aggregator's inline panel slot (`'embedded'`). When an embedded flow
1326
+ * presents as an overlay on mobile, that slot must NOT be the mount point —
1327
+ * `position: fixed` resolves against the nearest transformed/filtered/
1328
+ * contained ancestor, and an aggregator's animated dialog is exactly that, so
1329
+ * the "full-screen" overlay would end up positioned inside their panel.
1330
+ * Falls through to `document.body`.
1331
+ */
1332
+ overlayMountTarget() {
1333
+ return this.config.presentation === "embedded" ? void 0 : this.config.containerElement;
1334
+ }
1023
1335
  buildPreloadUrl(webviewBaseUrl) {
1024
1336
  const preloadUrl = new URL(webviewBaseUrl);
1025
1337
  preloadUrl.searchParams.set("preload", "true");
@@ -1085,6 +1397,7 @@ var Deposit = class {
1085
1397
  const settle = (fn) => {
1086
1398
  if (this.requestId !== currentRequestId || settled) return;
1087
1399
  settled = true;
1400
+ this.dismissActiveFlow = null;
1088
1401
  fn();
1089
1402
  };
1090
1403
  const onComplete = (result) => {
@@ -1110,6 +1423,12 @@ var Deposit = class {
1110
1423
  reject(error);
1111
1424
  });
1112
1425
  };
1426
+ this.dismissActiveFlow = () => {
1427
+ settle(() => {
1428
+ this.cleanup();
1429
+ reject(dismissedError());
1430
+ });
1431
+ };
1113
1432
  if (this.config.flowTimeoutMs != null && this.config.flowTimeoutMs > 0) {
1114
1433
  this.flowTimer = setTimeout(() => {
1115
1434
  onError(
@@ -1136,9 +1455,29 @@ var Deposit = class {
1136
1455
  /** No-op — retained for API compatibility with the popup-based SDK. */
1137
1456
  focus() {
1138
1457
  }
1139
- /** Close the deposit iframe without waiting for completion. */
1458
+ /**
1459
+ * Close the deposit iframe without waiting for completion — the host's own
1460
+ * back button or dialog chrome.
1461
+ *
1462
+ * This **settles a flow in progress** by rejecting its `requestDeposit()`
1463
+ * promise with `DEPOSIT_DISMISSED`, exactly as the flow's own close control
1464
+ * does. It has to: `cleanup()` destroys the frame through `destroy()`, which
1465
+ * drops the handle's close callback on purpose, so without this the caller's
1466
+ * `await` never returns. An aggregator whose back button awaited that promise
1467
+ * to restore its method list was left showing an empty panel — the flow gone,
1468
+ * its own list still not rendered, and no error to explain why.
1469
+ *
1470
+ * The rejection code is the same `DEPOSIT_DISMISSED` the in-flow control
1471
+ * produces, so a host writes one dismissal branch rather than one per
1472
+ * affordance. What it deliberately does not do is report an error: no `error`
1473
+ * event, and `status` lands on 'idle' as it always has, because the host
1474
+ * initiated this. With no flow in progress it is a plain teardown, unchanged.
1475
+ */
1140
1476
  close() {
1141
1477
  this.log("close() called");
1478
+ const dismiss = this.dismissActiveFlow;
1479
+ this.dismissActiveFlow = null;
1480
+ dismiss?.();
1142
1481
  this.cleanup();
1143
1482
  this.setStatus("idle");
1144
1483
  this.emit("close");
@@ -1173,10 +1512,72 @@ var Deposit = class {
1173
1512
  if (this.config.enableFullWidget === false) {
1174
1513
  url.searchParams.set("enableFullWidget", "false");
1175
1514
  }
1515
+ this.applyAppearanceParam(url);
1516
+ }
1517
+ /**
1518
+ * Appends `appearance=dark` (or `appearance=system`) to the hosted-flow URL.
1519
+ * An explicit `appearance: { theme }` config always wins — including
1520
+ * `theme: 'light'`, which pins light regardless of the merchant page. When
1521
+ * the merchant passes no theme, the default follows the merchant page's own
1522
+ * declared color scheme (see {@link detectMerchantColorScheme}) so the flow
1523
+ * doesn't render a light modal on a page that says it is dark. Absence of
1524
+ * the param means light. The hosted flow resolves `system` against the
1525
+ * user's `prefers-color-scheme`. Applied alongside the full-widget param on
1526
+ * every hosted/preload URL.
1527
+ */
1528
+ applyAppearanceParam(url) {
1529
+ const theme = this.config.appearance?.theme ?? detectMerchantColorScheme();
1530
+ if (theme === "dark" || theme === "system") {
1531
+ url.searchParams.set("appearance", theme);
1532
+ }
1533
+ this.applyBrandParam(url);
1176
1534
  }
1177
- /** Fluid is the default; `layout: 'fixed'` opts back into the legacy container. */
1535
+ /**
1536
+ * Appends the merchant's brand colors as `brand=p-0f62fe.bg-ffffff…`.
1537
+ *
1538
+ * On the URL rather than a message because the hosted flow's loading shell
1539
+ * paints from its entry chunk, hundreds of ms before React mounts: a palette
1540
+ * that arrived by `postMessage` would show Blink's own card color first and
1541
+ * then become the merchant's. Unreserved characters only, so the param costs
1542
+ * ~50 bytes of the URL budget rather than triple that in percent-encoding.
1543
+ *
1544
+ * Validated here purely so the merchant sees the complaint in their OWN
1545
+ * console — the hosted flow re-validates everything it decodes, since the URL
1546
+ * is host-controlled input that ends up in a stylesheet. Silence would be the
1547
+ * worst outcome: a dropped color that nobody is told about looks like the SDK
1548
+ * ignoring the config.
1549
+ */
1550
+ applyBrandParam(url) {
1551
+ const variables = this.config.appearance?.variables;
1552
+ if (!variables) return;
1553
+ const accepted = {};
1554
+ for (const [key, value] of Object.entries(variables)) {
1555
+ if (value === void 0 || value === null) continue;
1556
+ if (!BRAND_COLOR_PARAM_KEYS[key]) {
1557
+ console.error(
1558
+ `[blink] appearance.variables.${key} is not a supported brand color. Supported: ${Object.keys(BRAND_COLOR_PARAM_KEYS).join(", ")}.`
1559
+ );
1560
+ continue;
1561
+ }
1562
+ const hex = normalizeBrandHex(value);
1563
+ if (!hex) {
1564
+ console.error(
1565
+ `[blink] appearance.variables.${key} must be opaque hex (#rgb or #rrggbb); received ${JSON.stringify(value)}. Ignoring it.`
1566
+ );
1567
+ continue;
1568
+ }
1569
+ accepted[key] = hex;
1570
+ }
1571
+ const encoded = Object.keys(BRAND_COLOR_PARAM_KEYS).filter((key) => accepted[key] !== void 0).map((key) => `${BRAND_COLOR_PARAM_KEYS[key]}-${accepted[key].slice(1)}`).join(".");
1572
+ if (encoded) url.searchParams.set("brand", encoded);
1573
+ }
1574
+ /**
1575
+ * Fluid is the default; `layout: 'fixed'` opts back into the legacy
1576
+ * container. Embedded is a separate presentation and never fluid — the
1577
+ * constructor rejects the combination outright.
1578
+ */
1178
1579
  isFluidLayout() {
1179
- return this.config.layout !== "fixed";
1580
+ return !this.isEmbedded() && this.config.layout !== "fixed";
1180
1581
  }
1181
1582
  /**
1182
1583
  * Marks the hosted-flow URL as fluid-layout: the iframe spans the full
@@ -1186,6 +1587,10 @@ var Deposit = class {
1186
1587
  * assumed to understand fluid layout and gets the fixed container.
1187
1588
  */
1188
1589
  applyLayoutParam(url) {
1590
+ if (this.isEmbedded()) {
1591
+ url.searchParams.set("layout", "embed");
1592
+ return;
1593
+ }
1189
1594
  if (this.isFluidLayout()) {
1190
1595
  url.searchParams.set("layout", "fluid");
1191
1596
  }
@@ -1197,7 +1602,14 @@ var Deposit = class {
1197
1602
  * afterwards (rotation, window resize) — see iframe.ts.
1198
1603
  */
1199
1604
  applyViewportParams(url) {
1200
- if (!this.isFluidLayout()) return;
1605
+ const embedded = this.isEmbedded();
1606
+ if (!this.isFluidLayout() && !embedded) return;
1607
+ if (embedded) {
1608
+ const embedMaxHeightPx = this.resolveEmbedMaxHeightPx();
1609
+ if (embedMaxHeightPx !== void 0) {
1610
+ url.searchParams.set("embedMaxHeight", String(embedMaxHeightPx));
1611
+ }
1612
+ }
1201
1613
  const metrics = measureViewportMetrics();
1202
1614
  if (metrics.viewportLvh <= 0) return;
1203
1615
  url.searchParams.set("viewportLvh", String(metrics.viewportLvh));
@@ -1234,21 +1646,12 @@ var Deposit = class {
1234
1646
  });
1235
1647
  this.log("Reusing warm preload iframe");
1236
1648
  } else {
1237
- preloadIframe = createIframe(
1238
- this.buildPreloadUrl(webviewBaseUrl),
1239
- this.config.containerElement,
1240
- { fluid: this.isFluidLayout() }
1241
- );
1649
+ preloadIframe = this.createPresentedIframe(this.buildPreloadUrl(webviewBaseUrl), {});
1242
1650
  iframeReadyPromise = this.waitForIframeReady(preloadIframe).promise;
1243
1651
  }
1244
1652
  this.iframe = preloadIframe;
1245
1653
  preloadIframe.onClose(() => {
1246
- onError(
1247
- new DepositError(
1248
- "DEPOSIT_DISMISSED",
1249
- "The deposit was dismissed before the transfer completed."
1250
- )
1251
- );
1654
+ onError(dismissedError());
1252
1655
  this.cleanup();
1253
1656
  this.emit("close");
1254
1657
  });
@@ -1293,16 +1696,15 @@ var Deposit = class {
1293
1696
  hostedUrl.searchParams.set("payload", signerResponse.payload);
1294
1697
  hostedUrl.searchParams.set("signature", signerResponse.signature);
1295
1698
  this.applyFullWidgetParam(hostedUrl);
1699
+ if (this.isEmbedded()) {
1700
+ this.applyLayoutParam(hostedUrl);
1701
+ this.applyViewportParams(hostedUrl);
1702
+ }
1296
1703
  const targetUrl = hostedUrl.toString();
1297
- const iframeHandle = createIframe(targetUrl, this.config.containerElement);
1704
+ const iframeHandle = this.createPresentedIframe(targetUrl, {});
1298
1705
  this.iframe = iframeHandle;
1299
1706
  iframeHandle.onClose(() => {
1300
- onError(
1301
- new DepositError(
1302
- "DEPOSIT_DISMISSED",
1303
- "The deposit was dismissed before the transfer completed."
1304
- )
1305
- );
1707
+ onError(dismissedError());
1306
1708
  this.cleanup();
1307
1709
  this.emit("close");
1308
1710
  });
@@ -1378,6 +1780,16 @@ var Deposit = class {
1378
1780
  iframeHandle.close();
1379
1781
  return;
1380
1782
  }
1783
+ const contentHeight = parseContentHeight(event.data);
1784
+ if (contentHeight) {
1785
+ iframeHandle.applyContentHeight(contentHeight.heightPx);
1786
+ this.emit("resize", {
1787
+ heightPx: contentHeight.heightPx,
1788
+ ...contentHeight.preferredWidthPx === void 0 ? {} : { preferredWidthPx: contentHeight.preferredWidthPx },
1789
+ ...contentHeight.minWidthPx === void 0 ? {} : { minWidthPx: contentHeight.minWidthPx }
1790
+ });
1791
+ return;
1792
+ }
1381
1793
  const message = parseTransferComplete(event.data);
1382
1794
  if (!message) {
1383
1795
  return;
@@ -1435,8 +1847,25 @@ var Deposit = class {
1435
1847
  }
1436
1848
  }
1437
1849
  };
1850
+ function detectMerchantColorScheme() {
1851
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
1852
+ const root = document.documentElement;
1853
+ if (!root || typeof window.getComputedStyle !== "function") return null;
1854
+ let value;
1855
+ try {
1856
+ value = window.getComputedStyle(root).colorScheme ?? "";
1857
+ } catch {
1858
+ return null;
1859
+ }
1860
+ const tokens = value.toLowerCase().split(/\s+/);
1861
+ const dark = tokens.includes("dark");
1862
+ const light = tokens.includes("light");
1863
+ if (dark && light) return "system";
1864
+ if (dark) return "dark";
1865
+ return null;
1866
+ }
1438
1867
  var Checkout = Deposit;
1439
1868
 
1440
1869
  export { ALLOWED_RPC_METHODS, BRIDGE_PROTOCOL_VERSION, Checkout, CheckoutError, DEFAULT_WEBVIEW_BASE_URL, Deposit, DepositError, SANDBOX_WEBVIEW_BASE_URL, attachRpcHost, createWalletDiscoverer, getDisplayMessage, parseBridgeMessage };
1441
- //# sourceMappingURL=chunk-JD7GTL6D.js.map
1442
- //# sourceMappingURL=chunk-JD7GTL6D.js.map
1870
+ //# sourceMappingURL=chunk-HBW7IVRJ.js.map
1871
+ //# sourceMappingURL=chunk-HBW7IVRJ.js.map