@swype-org/deposit 0.3.20 → 0.3.23

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
@@ -98,7 +98,19 @@ function parseIframeReady(data) {
98
98
  if (msg.type !== "blink:iframe-ready") {
99
99
  return null;
100
100
  }
101
- return { type: "blink:iframe-ready" };
101
+ return { type: "blink:iframe-ready", capabilities: parseCapabilities(msg.capabilities) };
102
+ }
103
+ function parseCapabilities(value) {
104
+ if (!Array.isArray(value)) {
105
+ return [];
106
+ }
107
+ return value.filter((entry) => typeof entry === "string");
108
+ }
109
+ function buildViewportMessage(viewportLvh, safeAreaBottom) {
110
+ return { type: "blink:viewport", viewportLvh, safeAreaBottom };
111
+ }
112
+ function buildRevealMessage() {
113
+ return { type: "blink:reveal" };
102
114
  }
103
115
  function buildSignedPayloadMessage(merchantId, payload, signature) {
104
116
  return {
@@ -322,13 +334,20 @@ function attachRpcHost(options) {
322
334
  });
323
335
  }
324
336
  }
325
- function advertiseWallets(snapshot) {
337
+ let lastAdvertisedKey = null;
338
+ function advertiseWallets(snapshot, opts) {
326
339
  const wallets = snapshot.map((entry) => ({
327
340
  uuid: entry.info.uuid,
328
341
  rdns: entry.info.rdns,
329
342
  name: entry.info.name,
330
343
  icon: entry.info.icon
331
344
  }));
345
+ const key = JSON.stringify(wallets.map((w) => [w.uuid, w.rdns, w.name, w.icon]));
346
+ if (!opts?.force && key === lastAdvertisedKey) {
347
+ log("skipping identical re-advertisement", { count: wallets.length });
348
+ return;
349
+ }
350
+ lastAdvertisedKey = key;
332
351
  console.info("[blink-bridge:parent] advertising wallets", wallets.map((w) => ({
333
352
  rdns: w.rdns,
334
353
  name: w.name
@@ -409,7 +428,7 @@ function attachRpcHost(options) {
409
428
  if (!message) return;
410
429
  switch (message.type) {
411
430
  case "blink:bridge-hello": {
412
- advertiseWallets(discoverer.list());
431
+ advertiseWallets(discoverer.list(), { force: true });
413
432
  discoverer.requestProviders();
414
433
  return;
415
434
  }
@@ -505,6 +524,34 @@ function normalizeProviderError(err) {
505
524
  return { code: -32603, message: typeof err === "string" ? err : "Provider error" };
506
525
  }
507
526
 
527
+ // src/viewportMetrics.ts
528
+ function measureViewportMetrics() {
529
+ const unavailable = { viewportLvh: 0, safeAreaBottom: 0 };
530
+ if (typeof document === "undefined" || typeof window === "undefined" || typeof window.getComputedStyle !== "function" || !document.body) {
531
+ return unavailable;
532
+ }
533
+ try {
534
+ const probe = document.createElement("div");
535
+ probe.style.cssText = "position:fixed;top:0;left:0;width:0;visibility:hidden;pointer-events:none;height:100lvh;padding-bottom:env(safe-area-inset-bottom,0px)";
536
+ document.body.appendChild(probe);
537
+ let viewportLvh = 0;
538
+ let safeAreaBottom = 0;
539
+ try {
540
+ const computed = window.getComputedStyle(probe);
541
+ viewportLvh = Math.round(Number.parseFloat(computed.height) || 0);
542
+ safeAreaBottom = Math.round(Number.parseFloat(computed.paddingBottom) || 0);
543
+ } finally {
544
+ probe.remove();
545
+ }
546
+ if (viewportLvh <= 0) {
547
+ viewportLvh = Math.round(window.innerHeight || 0);
548
+ }
549
+ return { viewportLvh, safeAreaBottom };
550
+ } catch {
551
+ return unavailable;
552
+ }
553
+ }
554
+
508
555
  // src/iframe.ts
509
556
  var STYLE_ID = "blink-deposit-styles";
510
557
  var CLOSE_DURATION_MS = 280;
@@ -525,6 +572,8 @@ var STYLES = `
525
572
  [data-blink-overlay][data-blink-mobile-sheet] [data-blink-container]{width:100%;max-width:100%;height:79vh;border-radius:24px 24px 0 0;box-shadow:0 -8px 40px rgba(0,0,0,.25);animation:blink-slide-up-full .35s cubic-bezier(.32,.72,0,1);padding-bottom:env(safe-area-inset-bottom,0px)}
526
573
  [data-blink-overlay][data-blink-mobile-sheet][data-blink-closing] [data-blink-container]{opacity:1;animation:blink-slide-down-full ${CLOSE_DURATION_MS}ms ease-in forwards}
527
574
  @media(max-width:${MOBILE_SHEET_MAX_VIEWPORT_PX}px){[data-blink-overlay]{align-items:flex-end;touch-action:none;overscroll-behavior:contain}[data-blink-container]{width:100%;max-width:100%;height:79vh;border-radius:24px 24px 0 0;box-shadow:0 -8px 40px rgba(0,0,0,.25);animation:blink-slide-up-full .35s cubic-bezier(.32,.72,0,1);padding-bottom:env(safe-area-inset-bottom,0px)}[data-blink-overlay][data-blink-closing] [data-blink-container]{opacity:1;animation:blink-slide-down-full ${CLOSE_DURATION_MS}ms ease-in forwards}}
575
+ [data-blink-overlay][data-blink-fluid] [data-blink-container],[data-blink-overlay][data-blink-fluid][data-blink-mobile-sheet] [data-blink-container]{width:100%;max-width:100%;height:100%;max-height:100%;border-radius:0;box-shadow:none;animation:none;background:transparent;padding-bottom:0}
576
+ [data-blink-overlay][data-blink-fluid][data-blink-closing] [data-blink-container]{animation:none;transition:none;opacity:1}
528
577
  `;
529
578
  function createIframe(url, containerElement, options) {
530
579
  ensureStyles();
@@ -536,6 +585,9 @@ function createIframe(url, containerElement, options) {
536
585
  if (shouldUseMobileSheetLayout()) {
537
586
  overlay.setAttribute("data-blink-mobile-sheet", "");
538
587
  }
588
+ if (options?.fluid) {
589
+ overlay.setAttribute("data-blink-fluid", "");
590
+ }
539
591
  if (hidden) {
540
592
  overlay.style.display = "none";
541
593
  }
@@ -564,6 +616,37 @@ function createIframe(url, containerElement, options) {
564
616
  };
565
617
  attachBridge();
566
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);
567
650
  let savedOverflow = "";
568
651
  let scrollLocked = false;
569
652
  const onBackdropClick = (event) => {
@@ -594,6 +677,8 @@ function createIframe(url, containerElement, options) {
594
677
  overlay.removeEventListener("click", onBackdropClick);
595
678
  overlay.removeEventListener("touchmove", onTouchMove);
596
679
  document.removeEventListener("keydown", onKeyDown);
680
+ window.removeEventListener("resize", onViewportChange);
681
+ window.removeEventListener("orientationchange", onViewportChange);
597
682
  }
598
683
  function unlockScroll() {
599
684
  if (!scrollLocked) return;
@@ -619,6 +704,7 @@ function createIframe(url, containerElement, options) {
619
704
  }
620
705
  function detachBridge() {
621
706
  iframe.removeEventListener("load", attachBridge);
707
+ iframe.removeEventListener("load", onLoadReveal);
622
708
  if (rpcHostHandle) {
623
709
  rpcHostHandle.detach();
624
710
  rpcHostHandle = null;
@@ -649,6 +735,14 @@ function createIframe(url, containerElement, options) {
649
735
  overlay.style.display = "";
650
736
  lockScroll();
651
737
  attachDismissalListeners();
738
+ postViewportMetrics();
739
+ postReveal();
740
+ },
741
+ postReveal() {
742
+ postReveal();
743
+ },
744
+ downgradeToFixed() {
745
+ overlay.removeAttribute("data-blink-fluid");
652
746
  },
653
747
  onClose(callback) {
654
748
  closeCallback = callback;
@@ -824,6 +918,8 @@ var DEFAULT_WEBVIEW_BASE_URL = "https://pay.blink.cash";
824
918
  var SANDBOX_WEBVIEW_BASE_URL = "https://pay-sandbox.blink.cash";
825
919
  var IFRAME_READY_TIMEOUT_MS = 2e3;
826
920
  var WARM_IFRAME_READY_TIMEOUT_MS = 6e4;
921
+ var LAYOUT_FLUID_CAPABILITY = "layout-fluid";
922
+ var IFRAME_NOT_READY = { ready: false, fluidCapable: false };
827
923
  function resolveWebviewBaseUrl(config) {
828
924
  if (config.webviewBaseUrl) return config.webviewBaseUrl;
829
925
  return config.environment === "sandbox" ? SANDBOX_WEBVIEW_BASE_URL : DEFAULT_WEBVIEW_BASE_URL;
@@ -894,7 +990,10 @@ var Deposit = class {
894
990
  const webviewBaseUrl = resolveWebviewBaseUrl(this.config);
895
991
  this.hostedOrigin = this.config.hostedFlowOrigin ?? new URL(webviewBaseUrl).origin;
896
992
  const preloadUrl = this.buildPreloadUrl(webviewBaseUrl);
897
- const iframe = createIframe(preloadUrl, this.config.containerElement, { hidden: true });
993
+ const iframe = createIframe(preloadUrl, this.config.containerElement, {
994
+ hidden: true,
995
+ fluid: this.isFluidLayout()
996
+ });
898
997
  const ready = this.waitForIframeReady(iframe, WARM_IFRAME_READY_TIMEOUT_MS);
899
998
  this.warmIframe = iframe;
900
999
  this.warmIframeReady = ready.promise;
@@ -930,6 +1029,8 @@ var Deposit = class {
930
1029
  preloadUrl.searchParams.set("merchantId", this.config.merchantId);
931
1030
  }
932
1031
  this.applyFullWidgetParam(preloadUrl);
1032
+ this.applyLayoutParam(preloadUrl);
1033
+ this.applyViewportParams(preloadUrl);
933
1034
  return preloadUrl.toString();
934
1035
  }
935
1036
  /**
@@ -1075,6 +1176,35 @@ var Deposit = class {
1075
1176
  url.searchParams.set("enableFullWidget", "false");
1076
1177
  }
1077
1178
  }
1179
+ /** Fluid is the default; `layout: 'fixed'` opts back into the legacy container. */
1180
+ isFluidLayout() {
1181
+ return this.config.layout !== "fixed";
1182
+ }
1183
+ /**
1184
+ * Marks the hosted-flow URL as fluid-layout: the iframe spans the full
1185
+ * viewport and the hosted flow renders the card/sheet chrome itself.
1186
+ * Only applied to preload URLs — the URL-param fallback path exists
1187
+ * precisely because the webview never signalled ready, so it cannot be
1188
+ * assumed to understand fluid layout and gets the fixed container.
1189
+ */
1190
+ applyLayoutParam(url) {
1191
+ if (this.isFluidLayout()) {
1192
+ url.searchParams.set("layout", "fluid");
1193
+ }
1194
+ }
1195
+ /**
1196
+ * Seeds the merchant page's large-viewport height and safe-area bottom
1197
+ * inset onto the fluid preload URL so the hosted flow can size the sheet
1198
+ * before its JS boots. `blink:viewport` messages keep the values fresh
1199
+ * afterwards (rotation, window resize) — see iframe.ts.
1200
+ */
1201
+ applyViewportParams(url) {
1202
+ if (!this.isFluidLayout()) return;
1203
+ const metrics = measureViewportMetrics();
1204
+ if (metrics.viewportLvh <= 0) return;
1205
+ url.searchParams.set("viewportLvh", String(metrics.viewportLvh));
1206
+ url.searchParams.set("safeAreaBottom", String(metrics.safeAreaBottom));
1207
+ }
1078
1208
  setStatus(status) {
1079
1209
  if (this._status === status) return;
1080
1210
  this.log(`Status: ${this._status} \u2192 ${status}`);
@@ -1098,7 +1228,7 @@ var Deposit = class {
1098
1228
  iframeReadyPromise = Promise.race([
1099
1229
  warm.ready,
1100
1230
  new Promise(
1101
- (resolve) => setTimeout(() => resolve(false), IFRAME_READY_TIMEOUT_MS)
1231
+ (resolve) => setTimeout(() => resolve(IFRAME_NOT_READY), IFRAME_READY_TIMEOUT_MS)
1102
1232
  )
1103
1233
  ]).then((ready) => {
1104
1234
  warm.cancelReady();
@@ -1108,7 +1238,8 @@ var Deposit = class {
1108
1238
  } else {
1109
1239
  preloadIframe = createIframe(
1110
1240
  this.buildPreloadUrl(webviewBaseUrl),
1111
- this.config.containerElement
1241
+ this.config.containerElement,
1242
+ { fluid: this.isFluidLayout() }
1112
1243
  );
1113
1244
  iframeReadyPromise = this.waitForIframeReady(preloadIframe).promise;
1114
1245
  }
@@ -1138,7 +1269,11 @@ var Deposit = class {
1138
1269
  this.log("Request superseded or iframe dismissed during signer call");
1139
1270
  return;
1140
1271
  }
1141
- if (iframeReady) {
1272
+ if (iframeReady.ready) {
1273
+ if (this.isFluidLayout() && !iframeReady.fluidCapable) {
1274
+ preloadIframe.downgradeToFixed();
1275
+ this.log("Webview lacks layout-fluid capability \u2014 downgraded to fixed container");
1276
+ }
1142
1277
  const contentWindow = preloadIframe.contentWindow;
1143
1278
  if (contentWindow) {
1144
1279
  contentWindow.postMessage(
@@ -1193,8 +1328,10 @@ var Deposit = class {
1193
1328
  }
1194
1329
  }
1195
1330
  /**
1196
- * Resolves true when the iframe posts `blink:iframe-ready`, false on
1197
- * timeout. `cancel()` settles the promise as false immediately and releases
1331
+ * Resolves `{ ready: true, ... }` when the iframe posts
1332
+ * `blink:iframe-ready` (with `fluidCapable` reflecting its advertised
1333
+ * capabilities), or `{ ready: false, fluidCapable: false }` on timeout.
1334
+ * `cancel()` settles the promise as not-ready immediately and releases
1198
1335
  * the listener + timer — used to reap the generous warm-up wait once it no
1199
1336
  * longer matters (warm iframe consumed or discarded).
1200
1337
  */
@@ -1205,25 +1342,29 @@ var Deposit = class {
1205
1342
  const handler = (event) => {
1206
1343
  if (event.origin !== this.hostedOrigin) return;
1207
1344
  if (event.source !== iframeHandle.contentWindow) return;
1208
- if (parseIframeReady(event.data)) {
1209
- finish(true);
1345
+ const readyMessage = parseIframeReady(event.data);
1346
+ if (readyMessage) {
1347
+ finish({
1348
+ ready: true,
1349
+ fluidCapable: readyMessage.capabilities.includes(LAYOUT_FLUID_CAPABILITY)
1350
+ });
1210
1351
  }
1211
1352
  };
1212
1353
  window.addEventListener("message", handler);
1213
1354
  const timeout = setTimeout(() => {
1214
1355
  this.log("Iframe ready timeout \u2014 falling back to URL params");
1215
- finish(false);
1356
+ finish(IFRAME_NOT_READY);
1216
1357
  }, timeoutMs);
1217
1358
  let settled = false;
1218
- finish = (ready) => {
1359
+ finish = (result) => {
1219
1360
  if (settled) return;
1220
1361
  settled = true;
1221
1362
  window.removeEventListener("message", handler);
1222
1363
  clearTimeout(timeout);
1223
- resolve(ready);
1364
+ resolve(result);
1224
1365
  };
1225
1366
  });
1226
- return { promise, cancel: () => finish(false) };
1367
+ return { promise, cancel: () => finish(IFRAME_NOT_READY) };
1227
1368
  }
1228
1369
  startMessageListener(iframeHandle, onComplete) {
1229
1370
  const handler = (event) => {