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