@swype-org/deposit 0.3.26 → 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.
package/dist/index.cjs CHANGED
@@ -106,8 +106,43 @@ function parseCapabilities(value) {
106
106
  }
107
107
  return value.filter((entry) => typeof entry === "string");
108
108
  }
109
- function buildViewportMessage(viewportLvh, safeAreaBottom) {
110
- return { type: "blink:viewport", viewportLvh, safeAreaBottom };
109
+ function buildViewportMessage(viewportLvh, safeAreaBottom, embedMaxHeightPx) {
110
+ return {
111
+ type: "blink:viewport",
112
+ viewportLvh,
113
+ safeAreaBottom,
114
+ ...embedMaxHeightPx === void 0 ? {} : { embedMaxHeightPx }
115
+ };
116
+ }
117
+ var MAX_CONTENT_HEIGHT_PX = 2e4;
118
+ function parseContentHeight(data) {
119
+ if (!data || typeof data !== "object") {
120
+ return null;
121
+ }
122
+ const msg = data;
123
+ if (msg.type !== "blink:content-height") {
124
+ return null;
125
+ }
126
+ const raw = msg.heightPx;
127
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
128
+ return null;
129
+ }
130
+ const heightPx = Math.round(raw);
131
+ if (heightPx <= 0 || heightPx > MAX_CONTENT_HEIGHT_PX) {
132
+ return null;
133
+ }
134
+ return {
135
+ type: "blink:content-height",
136
+ heightPx,
137
+ ...parseWidthHint("preferredWidthPx", msg.preferredWidthPx),
138
+ ...parseWidthHint("minWidthPx", msg.minWidthPx)
139
+ };
140
+ }
141
+ function parseWidthHint(key, value) {
142
+ if (typeof value !== "number" || !Number.isFinite(value)) return {};
143
+ const px = Math.round(value);
144
+ if (px <= 0 || px > MAX_CONTENT_HEIGHT_PX) return {};
145
+ return { [key]: px };
111
146
  }
112
147
  function buildRevealMessage() {
113
148
  return { type: "blink:reveal" };
@@ -121,6 +156,23 @@ function buildSignedPayloadMessage(merchantId, payload, signature) {
121
156
  };
122
157
  }
123
158
 
159
+ // src/brandParam.ts
160
+ var BRAND_COLOR_PARAM_KEYS = {
161
+ colorPrimary: "p",
162
+ colorBackground: "bg",
163
+ colorText: "t",
164
+ colorDanger: "d",
165
+ colorBorder: "b"
166
+ };
167
+ function normalizeBrandHex(value) {
168
+ if (typeof value !== "string") return null;
169
+ const hex = value.trim().toLowerCase();
170
+ if (!/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/.test(hex)) return null;
171
+ const digits = hex.slice(1);
172
+ if (digits.length === 6) return `#${digits}`;
173
+ return `#${digits[0]}${digits[0]}${digits[1]}${digits[1]}${digits[2]}${digits[2]}`;
174
+ }
175
+
124
176
  // src/walletBridge/discover.ts
125
177
  function createWalletDiscoverer() {
126
178
  if (typeof window === "undefined") {
@@ -552,6 +604,84 @@ function measureViewportMetrics() {
552
604
  }
553
605
  }
554
606
 
607
+ // src/iframeCore.ts
608
+ function createFrameElement(url) {
609
+ const iframe = document.createElement("iframe");
610
+ iframe.src = url;
611
+ const iframeOrigin = new URL(url).origin;
612
+ iframe.allow = `publickey-credentials-get ${iframeOrigin}; publickey-credentials-create ${iframeOrigin}; clipboard-write ${iframeOrigin}; web-share ${iframeOrigin}`;
613
+ return { iframe, iframeOrigin };
614
+ }
615
+ function attachFrameBridges(options) {
616
+ const { iframe, iframeOrigin, postViewport, getEmbedMaxHeightPx, shouldRevealOnLoad } = options;
617
+ let detached = false;
618
+ const discoverer = createWalletDiscoverer();
619
+ let rpcHostHandle = null;
620
+ const attachBridge = () => {
621
+ if (rpcHostHandle) return;
622
+ if (!iframe.contentWindow) return;
623
+ rpcHostHandle = attachRpcHost({
624
+ iframeWindow: iframe.contentWindow,
625
+ iframeOrigin,
626
+ discoverer
627
+ });
628
+ };
629
+ attachBridge();
630
+ iframe.addEventListener("load", attachBridge);
631
+ function postReveal() {
632
+ if (detached) return;
633
+ iframe.contentWindow?.postMessage(buildRevealMessage(), iframeOrigin);
634
+ }
635
+ function postViewportMetrics() {
636
+ if (detached || !postViewport) return;
637
+ const metrics = measureViewportMetrics();
638
+ if (metrics.viewportLvh <= 0) return;
639
+ iframe.contentWindow?.postMessage(
640
+ buildViewportMessage(
641
+ metrics.viewportLvh,
642
+ metrics.safeAreaBottom,
643
+ getEmbedMaxHeightPx?.()
644
+ ),
645
+ iframeOrigin
646
+ );
647
+ }
648
+ let viewportPostScheduled = false;
649
+ const onViewportChange = () => {
650
+ if (viewportPostScheduled) return;
651
+ viewportPostScheduled = true;
652
+ requestAnimationFrame(() => {
653
+ viewportPostScheduled = false;
654
+ postViewportMetrics();
655
+ });
656
+ };
657
+ if (postViewport) {
658
+ window.addEventListener("resize", onViewportChange);
659
+ window.addEventListener("orientationchange", onViewportChange);
660
+ }
661
+ const onLoadReveal = () => {
662
+ postViewportMetrics();
663
+ if (shouldRevealOnLoad()) postReveal();
664
+ };
665
+ iframe.addEventListener("load", onLoadReveal);
666
+ return {
667
+ postReveal,
668
+ postViewportMetrics,
669
+ detach() {
670
+ if (detached) return;
671
+ detached = true;
672
+ iframe.removeEventListener("load", attachBridge);
673
+ iframe.removeEventListener("load", onLoadReveal);
674
+ window.removeEventListener("resize", onViewportChange);
675
+ window.removeEventListener("orientationchange", onViewportChange);
676
+ if (rpcHostHandle) {
677
+ rpcHostHandle.detach();
678
+ rpcHostHandle = null;
679
+ }
680
+ discoverer.destroy();
681
+ }
682
+ };
683
+ }
684
+
555
685
  // src/iframe.ts
556
686
  var STYLE_ID = "blink-deposit-styles";
557
687
  var CLOSE_DURATION_MS = 280;
@@ -593,60 +723,20 @@ function createIframe(url, containerElement, options) {
593
723
  }
594
724
  const container = document.createElement("div");
595
725
  container.setAttribute("data-blink-container", "");
596
- const iframe = document.createElement("iframe");
597
- iframe.src = url;
598
- const iframeOrigin = new URL(url).origin;
599
- iframe.allow = `publickey-credentials-get ${iframeOrigin}; publickey-credentials-create ${iframeOrigin}; clipboard-write ${iframeOrigin}`;
726
+ const { iframe, iframeOrigin } = createFrameElement(url);
600
727
  const handle = document.createElement("div");
601
728
  container.appendChild(handle);
602
729
  container.appendChild(iframe);
603
730
  overlay.appendChild(container);
604
731
  const mountTarget = containerElement ?? document.body;
605
732
  mountTarget.appendChild(overlay);
606
- const discoverer = createWalletDiscoverer();
607
- let rpcHostHandle = null;
608
- const attachBridge = () => {
609
- if (rpcHostHandle) return;
610
- if (!iframe.contentWindow) return;
611
- rpcHostHandle = attachRpcHost({
612
- iframeWindow: iframe.contentWindow,
613
- iframeOrigin,
614
- discoverer
615
- });
616
- };
617
- attachBridge();
618
- iframe.addEventListener("load", attachBridge);
619
- function postReveal() {
620
- if (closed) return;
621
- iframe.contentWindow?.postMessage(buildRevealMessage(), iframeOrigin);
622
- }
623
- function postViewportMetrics() {
624
- if (closed || !options?.fluid) return;
625
- const metrics = measureViewportMetrics();
626
- if (metrics.viewportLvh <= 0) return;
627
- iframe.contentWindow?.postMessage(
628
- buildViewportMessage(metrics.viewportLvh, metrics.safeAreaBottom),
629
- iframeOrigin
630
- );
631
- }
632
- let viewportPostScheduled = false;
633
- const onViewportChange = () => {
634
- if (viewportPostScheduled) return;
635
- viewportPostScheduled = true;
636
- requestAnimationFrame(() => {
637
- viewportPostScheduled = false;
638
- postViewportMetrics();
639
- });
640
- };
641
- if (options?.fluid) {
642
- window.addEventListener("resize", onViewportChange);
643
- window.addEventListener("orientationchange", onViewportChange);
644
- }
645
- const onLoadReveal = () => {
646
- postViewportMetrics();
647
- if (!hidden) postReveal();
648
- };
649
- iframe.addEventListener("load", onLoadReveal);
733
+ const bridges = attachFrameBridges({
734
+ iframe,
735
+ iframeOrigin,
736
+ // Only fluid needs them: the legacy fixed overlay sizes the iframe itself.
737
+ postViewport: options?.fluid === true,
738
+ shouldRevealOnLoad: () => !hidden
739
+ });
650
740
  let savedOverflow = "";
651
741
  let scrollLocked = false;
652
742
  const onBackdropClick = (event) => {
@@ -677,8 +767,6 @@ function createIframe(url, containerElement, options) {
677
767
  overlay.removeEventListener("click", onBackdropClick);
678
768
  overlay.removeEventListener("touchmove", onTouchMove);
679
769
  document.removeEventListener("keydown", onKeyDown);
680
- window.removeEventListener("resize", onViewportChange);
681
- window.removeEventListener("orientationchange", onViewportChange);
682
770
  }
683
771
  function unlockScroll() {
684
772
  if (!scrollLocked) return;
@@ -689,7 +777,7 @@ function createIframe(url, containerElement, options) {
689
777
  if (closed) return;
690
778
  closed = true;
691
779
  removeListeners();
692
- detachBridge();
780
+ bridges.detach();
693
781
  overlay.setAttribute("data-blink-closing", "");
694
782
  let removed = false;
695
783
  const removeOverlay = () => {
@@ -702,15 +790,6 @@ function createIframe(url, containerElement, options) {
702
790
  container.addEventListener("animationend", removeOverlay, { once: true });
703
791
  setTimeout(removeOverlay, CLOSE_DURATION_MS + 50);
704
792
  }
705
- function detachBridge() {
706
- iframe.removeEventListener("load", attachBridge);
707
- iframe.removeEventListener("load", onLoadReveal);
708
- if (rpcHostHandle) {
709
- rpcHostHandle.detach();
710
- rpcHostHandle = null;
711
- }
712
- discoverer.destroy();
713
- }
714
793
  return {
715
794
  get contentWindow() {
716
795
  return iframe.contentWindow;
@@ -735,15 +814,17 @@ function createIframe(url, containerElement, options) {
735
814
  overlay.style.display = "";
736
815
  lockScroll();
737
816
  attachDismissalListeners();
738
- postViewportMetrics();
739
- postReveal();
817
+ bridges.postViewportMetrics();
818
+ bridges.postReveal();
740
819
  },
741
820
  postReveal() {
742
- postReveal();
821
+ bridges.postReveal();
743
822
  },
744
823
  downgradeToFixed() {
745
824
  overlay.removeAttribute("data-blink-fluid");
746
825
  },
826
+ applyContentHeight() {
827
+ },
747
828
  onClose(callback) {
748
829
  closeCallback = callback;
749
830
  },
@@ -752,7 +833,7 @@ function createIframe(url, containerElement, options) {
752
833
  if (!closed) {
753
834
  closed = true;
754
835
  removeListeners();
755
- detachBridge();
836
+ bridges.detach();
756
837
  overlay.remove();
757
838
  unlockScroll();
758
839
  }
@@ -789,6 +870,117 @@ function shouldUseMobileSheetLayout() {
789
870
  return shortestScreenSide > 0 && shortestScreenSide <= MOBILE_SHEET_MAX_SCREEN_PX;
790
871
  }
791
872
 
873
+ // src/embeddedIframe.ts
874
+ var EMBED_STYLE_ID = "blink-deposit-embed-styles";
875
+ var EMBED_STYLES = `
876
+ [data-blink-embed]{display:block;width:100%;position:relative}
877
+ [data-blink-embed][data-blink-embed-hidden]{height:0;overflow:hidden;visibility:hidden;pointer-events:none}
878
+ [data-blink-embed] iframe{display:block;width:100%;height:0;border:none;background:transparent;color-scheme:light}
879
+ `;
880
+ function createEmbeddedIframe(url, containerElement, options) {
881
+ ensureEmbedStyles();
882
+ if (!containerElement.isConnected) {
883
+ console.warn(
884
+ "[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."
885
+ );
886
+ }
887
+ let closed = false;
888
+ let hidden = options?.hidden === true;
889
+ let closeCallback = null;
890
+ const readEmbedMaxHeightPx = () => typeof options?.embedMaxHeightPx === "function" ? options.embedMaxHeightPx() : options?.embedMaxHeightPx;
891
+ const wrapper = document.createElement("div");
892
+ wrapper.setAttribute("data-blink-embed", "");
893
+ if (hidden) {
894
+ wrapper.setAttribute("data-blink-embed-hidden", "");
895
+ }
896
+ const { iframe, iframeOrigin } = createFrameElement(url);
897
+ iframe.setAttribute("scrolling", "no");
898
+ const initialBudget = readEmbedMaxHeightPx();
899
+ if (initialBudget !== void 0) {
900
+ iframe.style.height = `${initialBudget}px`;
901
+ }
902
+ wrapper.appendChild(iframe);
903
+ containerElement.appendChild(wrapper);
904
+ const bridges = attachFrameBridges({
905
+ iframe,
906
+ iframeOrigin,
907
+ // Embedded always needs them: the flow has no usable viewport of its own,
908
+ // so the basis it sizes against can only come from here.
909
+ postViewport: true,
910
+ getEmbedMaxHeightPx: readEmbedMaxHeightPx,
911
+ shouldRevealOnLoad: () => !hidden
912
+ });
913
+ function teardown() {
914
+ closed = true;
915
+ bridges.detach();
916
+ wrapper.remove();
917
+ }
918
+ return {
919
+ get contentWindow() {
920
+ return iframe.contentWindow;
921
+ },
922
+ close() {
923
+ if (closed) return;
924
+ teardown();
925
+ closeCallback?.();
926
+ },
927
+ isClosed() {
928
+ return closed;
929
+ },
930
+ isHidden() {
931
+ return hidden;
932
+ },
933
+ reveal() {
934
+ if (closed || !hidden) return;
935
+ hidden = false;
936
+ wrapper.removeAttribute("data-blink-embed-hidden");
937
+ warnIfContainerNotRendered(containerElement);
938
+ bridges.postViewportMetrics();
939
+ bridges.postReveal();
940
+ },
941
+ postReveal() {
942
+ bridges.postReveal();
943
+ },
944
+ downgradeToFixed() {
945
+ },
946
+ applyContentHeight(heightPx) {
947
+ if (closed) return;
948
+ const budget = readEmbedMaxHeightPx();
949
+ const capped = budget === void 0 ? heightPx : Math.min(heightPx, budget);
950
+ iframe.style.height = `${Math.max(0, Math.round(capped))}px`;
951
+ },
952
+ onClose(callback) {
953
+ closeCallback = callback;
954
+ },
955
+ destroy() {
956
+ closeCallback = null;
957
+ if (!closed) teardown();
958
+ }
959
+ };
960
+ }
961
+ function warnIfContainerNotRendered(containerElement) {
962
+ if (typeof containerElement.getBoundingClientRect !== "function") return;
963
+ const rect = containerElement.getBoundingClientRect();
964
+ if (rect.width > 0 || rect.height > 0) return;
965
+ const display = typeof getComputedStyle === "function" ? getComputedStyle(containerElement).display : "";
966
+ console.warn(
967
+ `[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.`
968
+ );
969
+ }
970
+ function ensureEmbedStyles() {
971
+ const existingStyle = document.getElementById(EMBED_STYLE_ID);
972
+ if (existingStyle) {
973
+ if (existingStyle.textContent !== EMBED_STYLES) {
974
+ existingStyle.textContent = EMBED_STYLES;
975
+ }
976
+ return;
977
+ }
978
+ const style = document.createElement("style");
979
+ style.id = EMBED_STYLE_ID;
980
+ style.textContent = EMBED_STYLES;
981
+ document.head.appendChild(style);
982
+ }
983
+
792
984
  // src/signer.ts
793
985
  var DEFAULT_SIGNER_TIMEOUT_MS = 15e3;
794
986
  async function callSigner(signer, request, timeoutMs) {
@@ -919,6 +1111,13 @@ var SANDBOX_WEBVIEW_BASE_URL = "https://pay-sandbox.blink.cash";
919
1111
  var IFRAME_READY_TIMEOUT_MS = 2e3;
920
1112
  var WARM_IFRAME_READY_TIMEOUT_MS = 6e4;
921
1113
  var LAYOUT_FLUID_CAPABILITY = "layout-fluid";
1114
+ var DEFAULT_EMBED_MAX_HEIGHT_FRACTION = 0.9;
1115
+ function dismissedError() {
1116
+ return new DepositError(
1117
+ "DEPOSIT_DISMISSED",
1118
+ "The deposit was dismissed before the transfer completed."
1119
+ );
1120
+ }
922
1121
  var IFRAME_NOT_READY = { ready: false, fluidCapable: false };
923
1122
  function resolveWebviewBaseUrl(config) {
924
1123
  if (config.webviewBaseUrl) return config.webviewBaseUrl;
@@ -939,11 +1138,20 @@ var Deposit = class {
939
1138
  warmIframe = null;
940
1139
  warmIframeReady = null;
941
1140
  warmIframeReadyCancel = null;
1141
+ /** Resolved embedded-vs-overlay decision; see {@link Deposit.isEmbedded}. */
1142
+ embeddedResolved = null;
1143
+ /**
1144
+ * Settles the in-flight `requestDeposit()` as a dismissal. Non-null exactly
1145
+ * while a flow is in progress, so {@link Deposit.close} can reject the promise
1146
+ * the caller is awaiting instead of tearing the frame down under it.
1147
+ */
1148
+ dismissActiveFlow = null;
942
1149
  listeners = {
943
1150
  complete: /* @__PURE__ */ new Set(),
944
1151
  error: /* @__PURE__ */ new Set(),
945
1152
  close: /* @__PURE__ */ new Set(),
946
- "status-change": /* @__PURE__ */ new Set()
1153
+ "status-change": /* @__PURE__ */ new Set(),
1154
+ resize: /* @__PURE__ */ new Set()
947
1155
  };
948
1156
  /** Current phase of the deposit flow. */
949
1157
  get status() {
@@ -961,10 +1169,40 @@ var Deposit = class {
961
1169
  get isActive() {
962
1170
  return this._status === "signer-loading" || this._status === "iframe-active";
963
1171
  }
1172
+ /**
1173
+ * How this instance will actually present: `'embedded'` renders inline in
1174
+ * `containerElement`, `'overlay'` covers the page.
1175
+ *
1176
+ * Not simply an echo of the config — a host that asked for `'embedded'` gets
1177
+ * `'overlay'` on mobile, where a phone-sized method panel cannot hold the
1178
+ * flow (see {@link Deposit.isEmbedded}). Read it when laying out the panel:
1179
+ * on `'overlay'` no `resize` event ever fires, so a panel sized from those
1180
+ * reports would sit empty behind the overlay — collapse or skip it.
1181
+ *
1182
+ * Live until the SDK builds its first (warm-up) frame and fixed from then on,
1183
+ * so re-read it on resize rather than caching it at mount.
1184
+ */
1185
+ get presentation() {
1186
+ return this.isEmbedded() ? "embedded" : "overlay";
1187
+ }
964
1188
  constructor(config) {
965
1189
  if (!config.signer || typeof config.signer !== "string" && typeof config.signer !== "function") {
966
1190
  throw new DepositError("INVALID_REQUEST", "DepositConfig.signer is required (URL string or SignerFunction).");
967
1191
  }
1192
+ if (config.presentation === "embedded") {
1193
+ if (!config.containerElement) {
1194
+ throw new DepositError(
1195
+ "INVALID_REQUEST",
1196
+ 'DepositConfig.containerElement is required when presentation is "embedded".'
1197
+ );
1198
+ }
1199
+ if (config.layout === "fixed") {
1200
+ throw new DepositError(
1201
+ "INVALID_REQUEST",
1202
+ 'DepositConfig.layout "fixed" is an overlay-only escape hatch and cannot be combined with presentation "embedded".'
1203
+ );
1204
+ }
1205
+ }
968
1206
  this.config = config;
969
1207
  this.log("Deposit instance created", {
970
1208
  signer: typeof config.signer === "string" ? config.signer : "<function>"
@@ -990,10 +1228,7 @@ var Deposit = class {
990
1228
  const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
991
1229
  this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
992
1230
  const preloadUrl = this.buildPreloadUrl(webviewBaseUrl);
993
- const iframe = createIframe(preloadUrl, this.config.containerElement, {
994
- hidden: true,
995
- fluid: this.isFluidLayout()
996
- });
1231
+ const iframe = this.createPresentedIframe(preloadUrl, { hidden: true });
997
1232
  const ready = this.waitForIframeReady(iframe, WARM_IFRAME_READY_TIMEOUT_MS);
998
1233
  this.warmIframe = iframe;
999
1234
  this.warmIframeReady = ready.promise;
@@ -1022,6 +1257,83 @@ var Deposit = class {
1022
1257
  window.addEventListener("load", whenIdle, { once: true });
1023
1258
  }
1024
1259
  }
1260
+ /**
1261
+ * Whether the flow renders inline in the host's element rather than as an
1262
+ * overlay.
1263
+ *
1264
+ * `presentation: 'embedded'` is a request, not a guarantee: **on mobile the
1265
+ * flow presents as the normal overlay** even for an embedded host. An
1266
+ * aggregator's method panel is a narrow, short column on a phone, and the
1267
+ * flow inside it is a payment journey with a keypad, wallet lists and a QR
1268
+ * code — it needs the screen. The overlay is exactly what a phone user gets
1269
+ * from every other Blink integration, and the host's own dialog stays behind
1270
+ * it. Desktop keeps the inline card, where the panel has room.
1271
+ *
1272
+ * **The decision lives with the frame.** Read live until a frame is built,
1273
+ * then committed for good ({@link Deposit.createPresentedIframe}) — `preload()`
1274
+ * warms a frame from these same predicates and the flow that later adopts it
1275
+ * must agree, because a frame warmed at `layout=embed` handed to the overlay
1276
+ * presenter (or the reverse) is a blank modal. Committing at frame creation
1277
+ * rather than at construction matters: a host that constructs `Deposit` before
1278
+ * layout settles — a widget mounting inside a transition, a background or
1279
+ * prerendered tab — reports a zero-width viewport, and `(max-width: 640px)`
1280
+ * matches at zero, so caching then would lock a desktop user into the overlay
1281
+ * over a viewport that never existed. The warm-up already waits for `load`
1282
+ * plus an idle callback, so by the time it commits the measurement is real.
1283
+ */
1284
+ isEmbedded() {
1285
+ if (this.config.presentation !== "embedded") return false;
1286
+ if (this.embeddedResolved !== null) return this.embeddedResolved;
1287
+ if (typeof window === "undefined") return true;
1288
+ const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
1289
+ if (!(viewportWidth > 0)) return true;
1290
+ return !shouldUseMobileSheetLayout();
1291
+ }
1292
+ /**
1293
+ * The host's height budget for the inline iframe. Explicit config wins;
1294
+ * otherwise most of the host viewport, which keeps a tall flow from
1295
+ * outgrowing the page it is embedded in.
1296
+ */
1297
+ resolveEmbedMaxHeightPx() {
1298
+ if (this.config.embedMaxHeightPx !== void 0) return this.config.embedMaxHeightPx;
1299
+ if (typeof window === "undefined") return void 0;
1300
+ const derived = Math.round(window.innerHeight * DEFAULT_EMBED_MAX_HEIGHT_FRACTION);
1301
+ return derived > 0 ? derived : void 0;
1302
+ }
1303
+ /** Build the iframe for the configured presentation. */
1304
+ createPresentedIframe(url, options) {
1305
+ if (this.config.presentation === "embedded" && this.embeddedResolved === null) {
1306
+ this.embeddedResolved = this.isEmbedded();
1307
+ if (!this.embeddedResolved) {
1308
+ this.log("Mobile device: presenting the embedded flow as an overlay");
1309
+ }
1310
+ }
1311
+ if (this.isEmbedded()) {
1312
+ return createEmbeddedIframe(url, this.config.containerElement, {
1313
+ hidden: options.hidden,
1314
+ // A getter, not a value: the default is derived from the host
1315
+ // viewport, which changes on rotation and window resize.
1316
+ embedMaxHeightPx: () => this.resolveEmbedMaxHeightPx()
1317
+ });
1318
+ }
1319
+ return createIframe(url, this.overlayMountTarget(), {
1320
+ hidden: options.hidden,
1321
+ fluid: this.isFluidLayout()
1322
+ });
1323
+ }
1324
+ /**
1325
+ * Where the overlay mounts. `containerElement` means two different things by
1326
+ * presentation: an overlay mount point (`presentation: 'overlay'`) or the
1327
+ * aggregator's inline panel slot (`'embedded'`). When an embedded flow
1328
+ * presents as an overlay on mobile, that slot must NOT be the mount point —
1329
+ * `position: fixed` resolves against the nearest transformed/filtered/
1330
+ * contained ancestor, and an aggregator's animated dialog is exactly that, so
1331
+ * the "full-screen" overlay would end up positioned inside their panel.
1332
+ * Falls through to `document.body`.
1333
+ */
1334
+ overlayMountTarget() {
1335
+ return this.config.presentation === "embedded" ? void 0 : this.config.containerElement;
1336
+ }
1025
1337
  buildPreloadUrl(webviewBaseUrl) {
1026
1338
  const preloadUrl = new URL(webviewBaseUrl);
1027
1339
  preloadUrl.searchParams.set("preload", "true");
@@ -1087,6 +1399,7 @@ var Deposit = class {
1087
1399
  const settle = (fn) => {
1088
1400
  if (this.requestId !== currentRequestId || settled) return;
1089
1401
  settled = true;
1402
+ this.dismissActiveFlow = null;
1090
1403
  fn();
1091
1404
  };
1092
1405
  const onComplete = (result) => {
@@ -1112,6 +1425,12 @@ var Deposit = class {
1112
1425
  reject(error);
1113
1426
  });
1114
1427
  };
1428
+ this.dismissActiveFlow = () => {
1429
+ settle(() => {
1430
+ this.cleanup();
1431
+ reject(dismissedError());
1432
+ });
1433
+ };
1115
1434
  if (this.config.flowTimeoutMs != null && this.config.flowTimeoutMs > 0) {
1116
1435
  this.flowTimer = setTimeout(() => {
1117
1436
  onError(
@@ -1138,9 +1457,29 @@ var Deposit = class {
1138
1457
  /** No-op — retained for API compatibility with the popup-based SDK. */
1139
1458
  focus() {
1140
1459
  }
1141
- /** Close the deposit iframe without waiting for completion. */
1460
+ /**
1461
+ * Close the deposit iframe without waiting for completion — the host's own
1462
+ * back button or dialog chrome.
1463
+ *
1464
+ * This **settles a flow in progress** by rejecting its `requestDeposit()`
1465
+ * promise with `DEPOSIT_DISMISSED`, exactly as the flow's own close control
1466
+ * does. It has to: `cleanup()` destroys the frame through `destroy()`, which
1467
+ * drops the handle's close callback on purpose, so without this the caller's
1468
+ * `await` never returns. An aggregator whose back button awaited that promise
1469
+ * to restore its method list was left showing an empty panel — the flow gone,
1470
+ * its own list still not rendered, and no error to explain why.
1471
+ *
1472
+ * The rejection code is the same `DEPOSIT_DISMISSED` the in-flow control
1473
+ * produces, so a host writes one dismissal branch rather than one per
1474
+ * affordance. What it deliberately does not do is report an error: no `error`
1475
+ * event, and `status` lands on 'idle' as it always has, because the host
1476
+ * initiated this. With no flow in progress it is a plain teardown, unchanged.
1477
+ */
1142
1478
  close() {
1143
1479
  this.log("close() called");
1480
+ const dismiss = this.dismissActiveFlow;
1481
+ this.dismissActiveFlow = null;
1482
+ dismiss?.();
1144
1483
  this.cleanup();
1145
1484
  this.setStatus("idle");
1146
1485
  this.emit("close");
@@ -1193,10 +1532,54 @@ var Deposit = class {
1193
1532
  if (theme === "dark" || theme === "system") {
1194
1533
  url.searchParams.set("appearance", theme);
1195
1534
  }
1535
+ this.applyBrandParam(url);
1536
+ }
1537
+ /**
1538
+ * Appends the merchant's brand colors as `brand=p-0f62fe.bg-ffffff…`.
1539
+ *
1540
+ * On the URL rather than a message because the hosted flow's loading shell
1541
+ * paints from its entry chunk, hundreds of ms before React mounts: a palette
1542
+ * that arrived by `postMessage` would show Blink's own card color first and
1543
+ * then become the merchant's. Unreserved characters only, so the param costs
1544
+ * ~50 bytes of the URL budget rather than triple that in percent-encoding.
1545
+ *
1546
+ * Validated here purely so the merchant sees the complaint in their OWN
1547
+ * console — the hosted flow re-validates everything it decodes, since the URL
1548
+ * is host-controlled input that ends up in a stylesheet. Silence would be the
1549
+ * worst outcome: a dropped color that nobody is told about looks like the SDK
1550
+ * ignoring the config.
1551
+ */
1552
+ applyBrandParam(url) {
1553
+ const variables = this.config.appearance?.variables;
1554
+ if (!variables) return;
1555
+ const accepted = {};
1556
+ for (const [key, value] of Object.entries(variables)) {
1557
+ if (value === void 0 || value === null) continue;
1558
+ if (!BRAND_COLOR_PARAM_KEYS[key]) {
1559
+ console.error(
1560
+ `[blink] appearance.variables.${key} is not a supported brand color. Supported: ${Object.keys(BRAND_COLOR_PARAM_KEYS).join(", ")}.`
1561
+ );
1562
+ continue;
1563
+ }
1564
+ const hex = normalizeBrandHex(value);
1565
+ if (!hex) {
1566
+ console.error(
1567
+ `[blink] appearance.variables.${key} must be opaque hex (#rgb or #rrggbb); received ${JSON.stringify(value)}. Ignoring it.`
1568
+ );
1569
+ continue;
1570
+ }
1571
+ accepted[key] = hex;
1572
+ }
1573
+ 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(".");
1574
+ if (encoded) url.searchParams.set("brand", encoded);
1196
1575
  }
1197
- /** Fluid is the default; `layout: 'fixed'` opts back into the legacy container. */
1576
+ /**
1577
+ * Fluid is the default; `layout: 'fixed'` opts back into the legacy
1578
+ * container. Embedded is a separate presentation and never fluid — the
1579
+ * constructor rejects the combination outright.
1580
+ */
1198
1581
  isFluidLayout() {
1199
- return this.config.layout !== "fixed";
1582
+ return !this.isEmbedded() && this.config.layout !== "fixed";
1200
1583
  }
1201
1584
  /**
1202
1585
  * Marks the hosted-flow URL as fluid-layout: the iframe spans the full
@@ -1206,6 +1589,10 @@ var Deposit = class {
1206
1589
  * assumed to understand fluid layout and gets the fixed container.
1207
1590
  */
1208
1591
  applyLayoutParam(url) {
1592
+ if (this.isEmbedded()) {
1593
+ url.searchParams.set("layout", "embed");
1594
+ return;
1595
+ }
1209
1596
  if (this.isFluidLayout()) {
1210
1597
  url.searchParams.set("layout", "fluid");
1211
1598
  }
@@ -1217,7 +1604,14 @@ var Deposit = class {
1217
1604
  * afterwards (rotation, window resize) — see iframe.ts.
1218
1605
  */
1219
1606
  applyViewportParams(url) {
1220
- if (!this.isFluidLayout()) return;
1607
+ const embedded = this.isEmbedded();
1608
+ if (!this.isFluidLayout() && !embedded) return;
1609
+ if (embedded) {
1610
+ const embedMaxHeightPx = this.resolveEmbedMaxHeightPx();
1611
+ if (embedMaxHeightPx !== void 0) {
1612
+ url.searchParams.set("embedMaxHeight", String(embedMaxHeightPx));
1613
+ }
1614
+ }
1221
1615
  const metrics = measureViewportMetrics();
1222
1616
  if (metrics.viewportLvh <= 0) return;
1223
1617
  url.searchParams.set("viewportLvh", String(metrics.viewportLvh));
@@ -1254,21 +1648,12 @@ var Deposit = class {
1254
1648
  });
1255
1649
  this.log("Reusing warm preload iframe");
1256
1650
  } else {
1257
- preloadIframe = createIframe(
1258
- this.buildPreloadUrl(webviewBaseUrl),
1259
- this.config.containerElement,
1260
- { fluid: this.isFluidLayout() }
1261
- );
1651
+ preloadIframe = this.createPresentedIframe(this.buildPreloadUrl(webviewBaseUrl), {});
1262
1652
  iframeReadyPromise = this.waitForIframeReady(preloadIframe).promise;
1263
1653
  }
1264
1654
  this.iframe = preloadIframe;
1265
1655
  preloadIframe.onClose(() => {
1266
- onError(
1267
- new DepositError(
1268
- "DEPOSIT_DISMISSED",
1269
- "The deposit was dismissed before the transfer completed."
1270
- )
1271
- );
1656
+ onError(dismissedError());
1272
1657
  this.cleanup();
1273
1658
  this.emit("close");
1274
1659
  });
@@ -1313,16 +1698,15 @@ var Deposit = class {
1313
1698
  hostedUrl.searchParams.set("payload", signerResponse.payload);
1314
1699
  hostedUrl.searchParams.set("signature", signerResponse.signature);
1315
1700
  this.applyFullWidgetParam(hostedUrl);
1701
+ if (this.isEmbedded()) {
1702
+ this.applyLayoutParam(hostedUrl);
1703
+ this.applyViewportParams(hostedUrl);
1704
+ }
1316
1705
  const targetUrl = hostedUrl.toString();
1317
- const iframeHandle = createIframe(targetUrl, this.config.containerElement);
1706
+ const iframeHandle = this.createPresentedIframe(targetUrl, {});
1318
1707
  this.iframe = iframeHandle;
1319
1708
  iframeHandle.onClose(() => {
1320
- onError(
1321
- new DepositError(
1322
- "DEPOSIT_DISMISSED",
1323
- "The deposit was dismissed before the transfer completed."
1324
- )
1325
- );
1709
+ onError(dismissedError());
1326
1710
  this.cleanup();
1327
1711
  this.emit("close");
1328
1712
  });
@@ -1398,6 +1782,16 @@ var Deposit = class {
1398
1782
  iframeHandle.close();
1399
1783
  return;
1400
1784
  }
1785
+ const contentHeight = parseContentHeight(event.data);
1786
+ if (contentHeight) {
1787
+ iframeHandle.applyContentHeight(contentHeight.heightPx);
1788
+ this.emit("resize", {
1789
+ heightPx: contentHeight.heightPx,
1790
+ ...contentHeight.preferredWidthPx === void 0 ? {} : { preferredWidthPx: contentHeight.preferredWidthPx },
1791
+ ...contentHeight.minWidthPx === void 0 ? {} : { minWidthPx: contentHeight.minWidthPx }
1792
+ });
1793
+ return;
1794
+ }
1401
1795
  const message = parseTransferComplete(event.data);
1402
1796
  if (!message) {
1403
1797
  return;
@@ -1475,7 +1869,7 @@ function detectMerchantColorScheme() {
1475
1869
  var Checkout = Deposit;
1476
1870
 
1477
1871
  // src/index.ts
1478
- var VERSION = "0.3.0";
1872
+ var VERSION = "0.3.27";
1479
1873
 
1480
1874
  exports.ALLOWED_RPC_METHODS = ALLOWED_RPC_METHODS;
1481
1875
  exports.BRIDGE_PROTOCOL_VERSION = BRIDGE_PROTOCOL_VERSION;