@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/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 {
|
|
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 =
|
|
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
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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;
|