@daimo/pay 1.9.2 → 1.9.5

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/build/index.js CHANGED
@@ -2,7 +2,7 @@ import { http, useConnectors as useConnectors$1, useAccount, useSwitchChain, use
2
2
  import { mainnet, base as base$1, polygon, optimism, arbitrum, linea, bsc, sepolia, baseSepolia, worldchain, mantle } from 'wagmi/chains';
3
3
  import { safe, injected, coinbaseWallet, walletConnect } from '@wagmi/connectors';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { DaimoPayIntentStatus, assert, DaimoPayOrderMode, readDaimoPayOrderID, getOrderDestChainId, assertNotNull, getChainName, arbitrum as arbitrum$1, base as base$2, bsc as bsc$1, ethereum, linea as linea$1, mantle as mantle$1, optimism as optimism$1, polygon as polygon$1, worldchain as worldchain$1, solana, getAddressContraction, supportedChains, getChainExplorerTxUrl, ExternalPaymentOptions, DepositAddressPaymentOptions, ethereumUSDC, polygonUSDC, baseUSDC, arbitrumUSDC, optimismUSDC, isCCTPV1Chain, debugJson, writeDaimoPayOrderID, getOrderSourceChainId, DaimoPayEventType, getDaimoPayOrderView } from '@daimo/pay-common';
5
+ import { DaimoPayIntentStatus, assert, DaimoPayOrderMode, isHydrated, readDaimoPayOrderID, getOrderDestChainId, assertNotNull, getChainName, arbitrum as arbitrum$1, base as base$2, bsc as bsc$1, ethereum, linea as linea$1, mantle as mantle$1, optimism as optimism$1, polygon as polygon$1, worldchain as worldchain$1, solana, getAddressContraction, supportedChains, getChainExplorerTxUrl, ExternalPaymentOptions, DepositAddressPaymentOptions, ethereumUSDC, polygonUSDC, baseUSDC, arbitrumUSDC, optimismUSDC, isCCTPV1Chain, debugJson, writeDaimoPayOrderID, DaimoPayOrderStatusSource, getOrderSourceChainId, DaimoPayEventType, getDaimoPayOrderView } from '@daimo/pay-common';
6
6
  import { Buffer } from 'buffer';
7
7
  import React, { createContext, useRef, useState, useEffect, useLayoutEffect, useMemo, useContext, useSyncExternalStore, useCallback, createElement } from 'react';
8
8
  import styled$1, { css, keyframes, ThemeProvider } from 'styled-components';
@@ -22,7 +22,7 @@ import { VersionedTransaction } from '@solana/web3.js';
22
22
  import { normalize } from 'viem/ens';
23
23
 
24
24
  var name = "@daimo/pay";
25
- var version = "1.9.2";
25
+ var version = "1.9.5";
26
26
  var author = "Daimo";
27
27
  var homepage = "https://pay.daimo.com";
28
28
  var license = "BSD-2-Clause license";
@@ -61,7 +61,7 @@ var keywords = [
61
61
  "crypto"
62
62
  ];
63
63
  var dependencies = {
64
- "@daimo/pay-common": "1.9.2",
64
+ "@daimo/pay-common": "1.9.5",
65
65
  "@rollup/plugin-typescript": "^12.1.2",
66
66
  "@solana/wallet-adapter-base": "^0.9.23",
67
67
  "@solana/wallet-adapter-react": "^0.15.35",
@@ -2163,6 +2163,7 @@ function useLockBodyScroll(initialLocked) {
2163
2163
  return [locked, setLocked];
2164
2164
  }
2165
2165
 
2166
+ const WarningIcon = () => (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "size-6", width: "16", height: "16", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" }) }));
2166
2167
  const ExternalLinkIcon = ({ ...props }) => (jsxs("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: {
2167
2168
  left: 0,
2168
2169
  top: 0,
@@ -2391,8 +2392,8 @@ function reduceUnhydrated(state, event) {
2391
2392
  }
2392
2393
  function reducePaymentUnpaid(state, event) {
2393
2394
  switch (event.type) {
2394
- case "payment_started":
2395
- return { type: "payment_started", order: state.order };
2395
+ case "order_refreshed":
2396
+ return reduceOrderRefreshed(state, event.order);
2396
2397
  case "error":
2397
2398
  return {
2398
2399
  type: "error",
@@ -2407,14 +2408,8 @@ function reducePaymentUnpaid(state, event) {
2407
2408
  }
2408
2409
  function reducePaymentStarted(state, event) {
2409
2410
  switch (event.type) {
2410
- case "order_refreshed": {
2411
- assert(event.order.mode === DaimoPayOrderMode.HYDRATED, `[PAYMENT_REDUCER] order ${event.order.id} is ${event.order.intentStatus} but not hydrated`);
2412
- return { type: "payment_started", order: event.order };
2413
- }
2414
- case "dest_processed":
2415
- return event.order.intentStatus === DaimoPayIntentStatus.COMPLETED
2416
- ? { type: "payment_completed", order: event.order }
2417
- : { type: "payment_bounced", order: event.order };
2411
+ case "order_refreshed":
2412
+ return reduceOrderRefreshed(state, event.order);
2418
2413
  case "error":
2419
2414
  return {
2420
2415
  type: "error",
@@ -2427,6 +2422,21 @@ function reducePaymentStarted(state, event) {
2427
2422
  return state;
2428
2423
  }
2429
2424
  }
2425
+ function reduceOrderRefreshed(state, order) {
2426
+ assert(isHydrated(order), `[PAYMENT_REDUCER] unhydrated`);
2427
+ switch (order.intentStatus) {
2428
+ case DaimoPayIntentStatus.UNPAID:
2429
+ return { type: "payment_unpaid", order };
2430
+ case DaimoPayIntentStatus.STARTED:
2431
+ return { type: "payment_started", order };
2432
+ case DaimoPayIntentStatus.COMPLETED:
2433
+ return { type: "payment_completed", order };
2434
+ case DaimoPayIntentStatus.BOUNCED:
2435
+ return { type: "payment_bounced", order };
2436
+ default:
2437
+ return state;
2438
+ }
2439
+ }
2430
2440
  function reduceTerminal(state, event) {
2431
2441
  switch (event.type) {
2432
2442
  case "reset":
@@ -2460,10 +2470,9 @@ function waitForPaymentState(store, predicate) {
2460
2470
  * Will poll the given function at the specified interval. Stops when the
2461
2471
  * returned handle is invoked.
2462
2472
  */
2463
- function startPolling({ key, intervalMs, pollFn, onResult, onError, maxErrors = 5, log = console.log, }) {
2473
+ function startPolling({ key, intervalMs, pollFn, onResult, onError, log = console.log, }) {
2464
2474
  let active = true;
2465
2475
  let timer;
2466
- let errorCount = 0;
2467
2476
  const stop = () => {
2468
2477
  active = false;
2469
2478
  clearTimeout(timer);
@@ -2475,7 +2484,6 @@ function startPolling({ key, intervalMs, pollFn, onResult, onError, maxErrors =
2475
2484
  const res = await pollFn();
2476
2485
  if (!active)
2477
2486
  return;
2478
- errorCount = 0;
2479
2487
  log(`[POLL] ${key} success`);
2480
2488
  onResult(res);
2481
2489
  }
@@ -2484,11 +2492,6 @@ function startPolling({ key, intervalMs, pollFn, onResult, onError, maxErrors =
2484
2492
  return;
2485
2493
  log(`[POLL] ${key} error: ${e}`);
2486
2494
  onError(e);
2487
- errorCount++;
2488
- if (errorCount >= maxErrors) {
2489
- log(`[POLL] ${key} reached max errors (${maxErrors}), giving up`);
2490
- return stop();
2491
- }
2492
2495
  }
2493
2496
  timer = setTimeout(tick, intervalMs);
2494
2497
  };
@@ -2526,7 +2529,7 @@ function attachPaymentEffectHandlers(store, trpc, log) {
2526
2529
  if (prev.type !== next.type) {
2527
2530
  // Start watching for source payment
2528
2531
  if (next.type === "payment_unpaid") {
2529
- pollFindSourcePayment(store, trpc, next.order.id);
2532
+ pollFindPayments(store, trpc, next.order.id);
2530
2533
  }
2531
2534
  // Refresh the order to watch for destination processing
2532
2535
  if (next.type === "payment_started") {
@@ -2590,22 +2593,19 @@ function attachPaymentEffectHandlers(store, trpc, log) {
2590
2593
  };
2591
2594
  return cleanup;
2592
2595
  }
2593
- async function pollFindSourcePayment(store, trpc, orderId) {
2596
+ async function pollFindPayments(store, trpc, orderId) {
2594
2597
  const key = `${PollerType.FIND_SOURCE_PAYMENT}:${orderId}`;
2595
2598
  const stopPolling = startPolling({
2596
2599
  key,
2597
2600
  intervalMs: 1_000,
2598
- pollFn: () => trpc.findSourcePayment.query({ orderId: orderId.toString() }),
2599
- onResult: (found) => {
2601
+ pollFn: () => trpc.findOrderPayments.query({ orderId: orderId.toString() }),
2602
+ onResult: (order) => {
2600
2603
  const state = store.getState();
2601
- // Check that we're still in the payment_unpaid state
2602
2604
  if (state.type !== "payment_unpaid") {
2603
2605
  stopPolling();
2606
+ return;
2604
2607
  }
2605
- else if (found) {
2606
- stopPolling();
2607
- store.dispatch({ type: "payment_started", order: state.order });
2608
- }
2608
+ store.dispatch({ type: "order_refreshed", order });
2609
2609
  },
2610
2610
  onError: () => { },
2611
2611
  });
@@ -2620,16 +2620,12 @@ async function pollRefreshOrder(store, trpc, orderId) {
2620
2620
  onResult: (res) => {
2621
2621
  const state = store.getState();
2622
2622
  // Check that we're still in the payment_started state
2623
- if (state.type !== "payment_started")
2623
+ if (state.type !== "payment_started") {
2624
+ stopPolling();
2624
2625
  return;
2626
+ }
2625
2627
  const order = res.order;
2626
2628
  store.dispatch({ type: "order_refreshed", order });
2627
- if (order.intentStatus === "payment_completed" ||
2628
- order.intentStatus === "payment_bounced") {
2629
- assert(order.mode === DaimoPayOrderMode.HYDRATED, `[PAYMENT_EFFECTS] order ${order.id} is ${order.intentStatus} but not hydrated`);
2630
- store.dispatch({ type: "dest_processed", order });
2631
- stopPolling();
2632
- }
2633
2629
  },
2634
2630
  onError: () => { },
2635
2631
  });
@@ -2738,7 +2734,7 @@ async function runHydratePayIdEffects(store, trpc, prev, event) {
2738
2734
  async function runPayEthereumSourceEffects(store, trpc, prev, event) {
2739
2735
  const orderId = prev.order.id;
2740
2736
  try {
2741
- await trpc.processSourcePayment.mutate({
2737
+ const order = await trpc.processSourcePayment.mutate({
2742
2738
  orderId: orderId.toString(),
2743
2739
  sourceInitiateTxHash: event.paymentTxHash,
2744
2740
  sourceChainId: event.sourceChainId,
@@ -2746,8 +2742,7 @@ async function runPayEthereumSourceEffects(store, trpc, prev, event) {
2746
2742
  sourceToken: event.sourceToken,
2747
2743
  sourceAmount: event.sourceAmount.toString(),
2748
2744
  });
2749
- // TODO: Update order state with updated txHash
2750
- store.dispatch({ type: "payment_started", order: prev.order });
2745
+ store.dispatch({ type: "order_refreshed", order });
2751
2746
  }
2752
2747
  catch (e) {
2753
2748
  store.dispatch({ type: "error", order: prev.order, message: e.message });
@@ -2756,13 +2751,12 @@ async function runPayEthereumSourceEffects(store, trpc, prev, event) {
2756
2751
  async function runPaySolanaSourceEffects(store, trpc, prev, event) {
2757
2752
  const orderId = prev.order.id;
2758
2753
  try {
2759
- await trpc.processSolanaSourcePayment.mutate({
2754
+ const order = await trpc.processSolanaSourcePayment.mutate({
2760
2755
  orderId: orderId.toString(),
2761
2756
  startIntentTxHash: event.paymentTxHash,
2762
2757
  token: event.sourceToken,
2763
2758
  });
2764
- // TODO: Update order state with updated txHash
2765
- store.dispatch({ type: "payment_started", order: prev.order });
2759
+ store.dispatch({ type: "order_refreshed", order });
2766
2760
  }
2767
2761
  catch (e) {
2768
2762
  store.dispatch({ type: "error", order: prev.order, message: e.message });
@@ -7696,7 +7690,7 @@ const QRPlaceholder = styled(motion.div) `
7696
7690
  content: "";
7697
7691
  position: absolute;
7698
7692
  inset: 0;
7699
- transform: scale(1.5) rotate(45deg);
7693
+ transform: scale(1.7) rotate(45deg);
7700
7694
  background-image: linear-gradient(
7701
7695
  90deg,
7702
7696
  rgba(255, 255, 255, 0) 50%,
@@ -7739,16 +7733,6 @@ const LogoIcon = styled(motion.div) `
7739
7733
  : css `
7740
7734
  width: 28%;
7741
7735
  height: 28%;
7742
- border-radius: 17px;
7743
- &:before {
7744
- pointer-events: none;
7745
- z-index: 2;
7746
- content: "";
7747
- position: absolute;
7748
- inset: 0;
7749
- border-radius: inherit;
7750
- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.02);
7751
- }
7752
7736
  `}
7753
7737
  `;
7754
7738
 
@@ -8033,6 +8017,7 @@ const WalletItem = styled.button `
8033
8017
  text-align: center;
8034
8018
  transition: opacity 100ms ease;
8035
8019
  opacity: ${(props) => (props.$waiting ? 0.4 : 1)};
8020
+ background: transparent;
8036
8021
  `;
8037
8022
  const WalletIcon = styled.div `
8038
8023
  z-index: 9;
@@ -10270,9 +10255,10 @@ const SelectDepositAddressAmount = () => {
10270
10255
  const { paymentState, setRoute, triggerResize } = usePayContext();
10271
10256
  const { selectedDepositAddressOption } = paymentState;
10272
10257
  const maxUsdLimit = paymentState.getOrderUsdLimit();
10273
- // const minimumMessage = `Minimum ${formatUsd(MIN_USD_VALUE, "up")}`;
10258
+ const minUsd = selectedDepositAddressOption?.minimumUsd ?? 0;
10259
+ const minimumMessage = `Minimum ${formatUsd(minUsd, "up")}`;
10274
10260
  const [usdInput, setUsdInput] = useState("");
10275
- const [message, setMessage] = useState("");
10261
+ const [message, setMessage] = useState(minimumMessage);
10276
10262
  const [continueDisabled, setContinueDisabled] = useState(true);
10277
10263
  useEffect(() => {
10278
10264
  triggerResize();
@@ -10289,7 +10275,7 @@ const SelectDepositAddressAmount = () => {
10289
10275
  setMessage(`Maximum ${formatUsd(maxUsdLimit)}`);
10290
10276
  }
10291
10277
  else {
10292
- setMessage("");
10278
+ setMessage(minimumMessage);
10293
10279
  }
10294
10280
  const usd = Number(sanitizeNumber(value));
10295
10281
  setContinueDisabled(usd <= 0 || usd > maxUsdLimit);
@@ -10612,6 +10598,8 @@ function SelectAnotherMethodButton() {
10612
10598
 
10613
10599
  const SelectDepositAddressChain = () => {
10614
10600
  const { setRoute, paymentState } = usePayContext();
10601
+ const pay = useDaimoPay();
10602
+ const { order } = pay;
10615
10603
  const { isDepositFlow, setSelectedDepositAddressOption, depositAddressOptions, } = paymentState;
10616
10604
  return (jsxs(PageContent, { children: [jsx(OrderHeader, { minified: true }), !depositAddressOptions.loading &&
10617
10605
  depositAddressOptions.options?.length === 0 && (jsxs(ModalContent, { style: {
@@ -10625,6 +10613,9 @@ const SelectDepositAddressChain = () => {
10625
10613
  id: option.id,
10626
10614
  title: option.id,
10627
10615
  icons: [option.logoURI],
10616
+ disabled: option.minimumUsd > 0 &&
10617
+ order?.mode === DaimoPayOrderMode.HYDRATED &&
10618
+ order.usdValue < option.minimumUsd,
10628
10619
  onClick: () => {
10629
10620
  setSelectedDepositAddressOption(option);
10630
10621
  const meta = { event: "click-option", option: option.id };
@@ -10891,26 +10882,52 @@ function getDepositAddressOption(setRoute) {
10891
10882
  };
10892
10883
  }
10893
10884
 
10894
- const TokenChainLogo = ({ token, size = 32, offset, }) => {
10895
- const s1 = useMemo(() => ({ width: size, height: size }), [size]);
10896
- const s2 = useMemo(() => ({ width: size / 2, height: size / 2, right: offset }), [size, offset]);
10897
- return (jsxs(TokenChainContainer, { style: s1, children: [jsx("img", { src: token.logoURI, alt: token.symbol, style: { borderRadius: 9999 } }), jsx(ChainContainer$1, { style: s2, children: chainToLogo[token.chainId] })] }));
10885
+ const TokenChainLogo = ({ token, size = 32, offset = 0, }) => {
10886
+ const chainLogoSize = Math.round((size * 30) / 64);
10887
+ return (jsxs(TokenChainContainer, { "$size": size, children: [jsx(TokenImage, { src: token.logoURI, alt: token.symbol, "$size": size }), jsx(ChainContainer$1, { "$size": chainLogoSize, "$offset": offset, children: chainToLogo[token.chainId] })] }));
10898
10888
  };
10899
10889
  const TokenChainContainer = styled(motion.div) `
10900
- width: 100%;
10901
- height: 100%;
10890
+ position: relative;
10891
+ width: ${(props) => props.$size}px;
10892
+ height: ${(props) => props.$size}px;
10893
+ display: flex;
10894
+ align-items: center;
10895
+ justify-content: center;
10896
+ `;
10897
+ const TokenImage = styled.img `
10898
+ width: ${(props) => props.$size}px;
10899
+ height: ${(props) => props.$size}px;
10900
+ border-radius: 50%;
10901
+ object-fit: cover;
10902
+ transition: transform 0.2s ease;
10903
+
10904
+ ${(props) => props.$showBorder &&
10905
+ `
10906
+ border: 2px solid var(--ck-body-background, #fff);
10907
+ `}
10902
10908
  `;
10903
10909
  const ChainContainer$1 = styled(motion.div) `
10904
10910
  position: absolute;
10905
- border-radius: 9999px;
10906
- overflow: hidden;
10911
+ width: ${(props) => props.$size}px;
10912
+ height: ${(props) => props.$size}px;
10913
+ min-width: ${(props) => props.$size}px;
10914
+ min-height: ${(props) => props.$size}px;
10907
10915
  bottom: 0px;
10908
- right: 0px;
10916
+ right: ${(props) => props.$offset}px;
10917
+ border-radius: 50%;
10918
+ aspect-ratio: 1 / 1;
10919
+ overflow: hidden;
10920
+ background: ${(props) => props.$showBorder ? "var(--ck-body-background, #fff)" : "transparent"};
10921
+ display: flex;
10922
+ align-items: center;
10923
+ justify-content: center;
10924
+ flex-shrink: 0;
10909
10925
 
10910
10926
  svg {
10911
- position: absolute;
10912
10927
  width: 100%;
10913
10928
  height: 100%;
10929
+ border-radius: 50%;
10930
+ flex-shrink: 0;
10914
10931
  }
10915
10932
  `;
10916
10933
 
@@ -11309,7 +11326,8 @@ const CircleTimer = ({ total, size = 24, stroke = 3, currentTime, onTimeChange,
11309
11326
  return () => clearInterval(id);
11310
11327
  }, [target, onTimeChange]);
11311
11328
  const ratio = Math.round((left * 100) / total); // 0-100
11312
- const radius = Math.round((size - stroke) / 2); // integer radius
11329
+ // Ensure stroke stays within viewBox: use floor to be conservative
11330
+ const radius = Math.floor((size - stroke) / 2);
11313
11331
  const circumference = Math.round((2 * 314 * radius) / 100); // 2πr, π≈3.14
11314
11332
  const dashoffset = Math.round((circumference * (100 - ratio)) / 100);
11315
11333
  // colour transition: green → orange → red
@@ -11332,49 +11350,91 @@ function WaitingDepositAddress() {
11332
11350
  const context = usePayContext();
11333
11351
  const { triggerResize, paymentState } = context;
11334
11352
  const { payWithDepositAddress, selectedDepositAddressOption } = paymentState;
11335
- const [details, setDetails] = useState();
11353
+ const { order } = useDaimoPay();
11354
+ const [depAddr, setDepAddr] = useState();
11336
11355
  const [failed, setFailed] = useState(false);
11356
+ // If we selected a deposit address option, generate the address...
11337
11357
  const generateDepositAddress = () => {
11338
- if (!selectedDepositAddressOption)
11339
- return;
11340
- payWithDepositAddress(selectedDepositAddressOption.id).then((details) => {
11341
- if (!details)
11342
- setFailed(true);
11343
- else
11344
- setDetails(details);
11345
- });
11358
+ if (selectedDepositAddressOption == null) {
11359
+ if (order == null || !isHydrated(order))
11360
+ return;
11361
+ if (order.sourceTokenAmount == null)
11362
+ return;
11363
+ // Pay underpaid order
11364
+ const taPaid = order.sourceTokenAmount;
11365
+ const usdPaid = taPaid.usd; // TODO: get usdPaid directly from the order
11366
+ const usdToPay = Math.max(order.usdValue - usdPaid, 0.01);
11367
+ const dispDecimals = taPaid.token.displayDecimals;
11368
+ const unitsToPay = (usdToPay / taPaid.token.usd).toFixed(dispDecimals);
11369
+ const unitsPaid = (Number(taPaid.amount) /
11370
+ 10 ** taPaid.token.decimals).toFixed(dispDecimals);
11371
+ // Hack to always show a <= 60 minute countdown
11372
+ let expirationS = (order.createdAt ?? 0) + 59.5 * 60;
11373
+ if (order.expirationTs != null &&
11374
+ Number(order.expirationTs) < expirationS) {
11375
+ expirationS = Number(order.expirationTs);
11376
+ }
11377
+ setDepAddr({
11378
+ address: order.intentAddr,
11379
+ amount: unitsToPay,
11380
+ underpayment: { unitsPaid, coin: taPaid.token.symbol },
11381
+ coins: `${taPaid.token.symbol} on ${getChainName(taPaid.token.chainId)}`,
11382
+ expirationS: expirationS,
11383
+ uri: order.intentAddr,
11384
+ displayToken: taPaid.token,
11385
+ logoURI: "", // Not needed for underpaid orders
11386
+ });
11387
+ }
11388
+ else {
11389
+ payWithDepositAddress(selectedDepositAddressOption.id).then((details) => {
11390
+ if (details) {
11391
+ setDepAddr({
11392
+ address: details.address,
11393
+ amount: details.amount,
11394
+ coins: details.suffix,
11395
+ expirationS: details.expirationS,
11396
+ uri: details.uri,
11397
+ displayToken: getDisplayToken(selectedDepositAddressOption),
11398
+ logoURI: selectedDepositAddressOption.logoURI,
11399
+ });
11400
+ }
11401
+ else {
11402
+ setFailed(true);
11403
+ }
11404
+ });
11405
+ }
11346
11406
  };
11347
- // TODO: load payment status, show underpayment
11348
11407
  // eslint-disable-next-line react-hooks/exhaustive-deps
11349
11408
  useEffect(generateDepositAddress, [selectedDepositAddressOption]);
11350
11409
  // eslint-disable-next-line react-hooks/exhaustive-deps
11351
- useEffect(triggerResize, [details, failed]);
11352
- return (jsx(PageContent, { children: selectedDepositAddressOption == null ? null : failed ? (jsx(DepositFailed, { meta: selectedDepositAddressOption })) : (jsx(DepositAddressInfo, { meta: selectedDepositAddressOption, details: details, refresh: generateDepositAddress, triggerResize: triggerResize })) }));
11410
+ useEffect(triggerResize, [depAddr, failed]);
11411
+ return (jsx(PageContent, { children: failed ? (selectedDepositAddressOption && (jsx(DepositFailed, { name: selectedDepositAddressOption.id }))) : (jsx(DepositAddressInfo, { depAddr: depAddr, refresh: generateDepositAddress, triggerResize: triggerResize })) }));
11353
11412
  }
11354
- function DepositAddressInfo({ meta, details, refresh, triggerResize, }) {
11413
+ function DepositAddressInfo({ depAddr, refresh, triggerResize, }) {
11355
11414
  const { isMobile } = useIsMobile();
11356
- const [remainingS, totalS] = useCountdown(details?.expirationS);
11357
- const isExpired = details?.expirationS != null && remainingS === 0;
11415
+ const [remainingS, totalS] = useCountdown(depAddr?.expirationS);
11416
+ const isExpired = depAddr?.expirationS != null && remainingS === 0;
11358
11417
  // eslint-disable-next-line react-hooks/exhaustive-deps
11359
11418
  useEffect(triggerResize, [isExpired]);
11360
- const displayToken = (() => {
11361
- switch (meta.id) {
11362
- case DepositAddressPaymentOptions.OP_MAINNET:
11363
- return optimismUSDC;
11364
- case DepositAddressPaymentOptions.ARBITRUM:
11365
- return arbitrumUSDC;
11366
- case DepositAddressPaymentOptions.BASE:
11367
- return baseUSDC;
11368
- case DepositAddressPaymentOptions.POLYGON:
11369
- return polygonUSDC;
11370
- case DepositAddressPaymentOptions.ETH_L1:
11371
- return ethereumUSDC;
11372
- default:
11373
- return null;
11374
- }
11375
- })();
11376
- const logoElement = displayToken ? (jsx(TokenChainLogo, { token: displayToken, size: 64, offset: -4 })) : (jsx("img", { src: meta.logoURI, width: "64px", height: "64px" }));
11377
- return (jsxs(ModalContent, { children: [isExpired ? (jsx(LogoRow, { children: jsx(Button, { onClick: refresh, style: { width: 128 }, children: "Refresh" }) })) : isMobile ? (jsx(LogoRow, { children: jsx(LogoWrap, { children: logoElement }) })) : (jsx(QRWrap, { children: jsx(CustomQRCode, { value: details?.uri, contentPadding: 24, size: 200, image: logoElement }) })), jsx(CopyableInfo, { details: details, remainingS: remainingS, totalS: totalS })] }));
11419
+ const logoOffset = isMobile ? 4 : 0;
11420
+ const logoElement = depAddr?.displayToken ? (jsx(TokenChainLogo, { token: depAddr.displayToken, size: 64, offset: logoOffset })) : (jsx("img", { src: depAddr?.logoURI, width: "64px", height: "64px" }));
11421
+ return (jsxs(ModalContent, { children: [isExpired ? (jsx(LogoRow, { children: jsx(Button, { onClick: refresh, style: { width: 128 }, children: "Refresh" }) })) : isMobile ? (jsx(LogoRow, { children: jsx(LogoWrap, { children: logoElement }) })) : (jsx(QRWrap, { children: jsx(CustomQRCode, { value: depAddr?.uri, contentPadding: 24, size: 200, image: logoElement }) })), jsx(CopyableInfo, { depAddr: depAddr, remainingS: remainingS, totalS: totalS })] }));
11422
+ }
11423
+ function getDisplayToken(meta) {
11424
+ switch (meta.id) {
11425
+ case DepositAddressPaymentOptions.OP_MAINNET:
11426
+ return optimismUSDC;
11427
+ case DepositAddressPaymentOptions.ARBITRUM:
11428
+ return arbitrumUSDC;
11429
+ case DepositAddressPaymentOptions.BASE:
11430
+ return baseUSDC;
11431
+ case DepositAddressPaymentOptions.POLYGON:
11432
+ return polygonUSDC;
11433
+ case DepositAddressPaymentOptions.ETH_L1:
11434
+ return ethereumUSDC;
11435
+ default:
11436
+ return null;
11437
+ }
11378
11438
  }
11379
11439
  const LogoWrap = styled.div `
11380
11440
  position: relative;
@@ -11393,11 +11453,29 @@ const QRWrap = styled.div `
11393
11453
  margin: 0 auto;
11394
11454
  width: 280px;
11395
11455
  `;
11396
- function CopyableInfo({ details, remainingS, totalS, }) {
11397
- const currencies = details?.suffix;
11398
- const isExpired = details?.expirationS != null && remainingS === 0;
11399
- return (jsxs(CopyableInfoWrapper, { children: [jsx(CopyRowOrThrobber, { title: "Send Exactly", value: details?.amount, smallText: currencies + " only", disabled: isExpired }), jsx(CopyRowOrThrobber, { title: "Receiving Address", value: details?.address, valueText: details && getAddressContraction(details.address), disabled: isExpired }), jsx(CountdownWrap, { children: jsx(CountdownTimer, { remainingS: remainingS, totalS: totalS }) })] }));
11456
+ function CopyableInfo({ depAddr, remainingS, totalS, }) {
11457
+ const underpayment = depAddr?.underpayment;
11458
+ const isExpired = depAddr?.expirationS != null && remainingS === 0;
11459
+ return (jsxs(CopyableInfoWrapper, { children: [underpayment && jsx(UnderpaymentInfo, { underpayment: underpayment }), jsx(CopyRowOrThrobber, { title: "Send Exactly", value: depAddr?.amount, smallText: depAddr?.coins, disabled: isExpired }), jsx(CopyRowOrThrobber, { title: "Receiving Address", value: depAddr?.address, valueText: depAddr?.address && getAddressContraction(depAddr.address), disabled: isExpired }), jsx(CountdownWrap, { children: jsx(CountdownTimer, { remainingS: remainingS, totalS: totalS }) })] }));
11400
11460
  }
11461
+ function UnderpaymentInfo({ underpayment }) {
11462
+ return (jsxs(UnderpaymentWrapper, { children: [jsxs(UnderpaymentHeader, { children: [jsx(WarningIcon, {}), jsxs("span", { children: ["Received ", underpayment.unitsPaid, " ", underpayment.coin] })] }), jsx(SmallText, { children: "Finish by sending the extra amount below." })] }));
11463
+ }
11464
+ const UnderpaymentWrapper = styled.div `
11465
+ background: var(--ck-body-background-tertiary);
11466
+ border-radius: 8px;
11467
+ padding: 16px;
11468
+ margin: 0 4px 16px 4px;
11469
+ margin-bottom: 16px;
11470
+ `;
11471
+ const UnderpaymentHeader = styled.div `
11472
+ font-weight: 500;
11473
+ display: flex;
11474
+ justify-content: center;
11475
+ align-items: flex-end;
11476
+ gap: 8px;
11477
+ margin-bottom: 8px;
11478
+ `;
11401
11479
  const CopyableInfoWrapper = styled.div `
11402
11480
  display: flex;
11403
11481
  flex-direction: column;
@@ -11441,8 +11519,8 @@ const formatTime = (sec) => {
11441
11519
  const s = `${sec % 60}`.padStart(2, "0");
11442
11520
  return `${m}:${s}`;
11443
11521
  };
11444
- function DepositFailed({ meta, }) {
11445
- return (jsxs(ModalContent, { style: { marginLeft: 24, marginRight: 24 }, children: [jsxs(ModalH1, { children: [meta.id, " unavailable"] }), jsxs(ModalBody, { children: ["We're unable to process ", meta.id, " payments at this time. Please select another payment method."] }), jsx(SelectAnotherMethodButton, {})] }));
11522
+ function DepositFailed({ name }) {
11523
+ return (jsxs(ModalContent, { style: { marginLeft: 24, marginRight: 24 }, children: [jsxs(ModalH1, { children: [name, " unavailable"] }), jsxs(ModalBody, { children: ["We're unable to process ", name, " payments at this time. Please select another payment method."] }), jsx(SelectAnotherMethodButton, {})] }));
11446
11524
  }
11447
11525
  const CopyRow = styled.button `
11448
11526
  display: block;
@@ -11451,6 +11529,7 @@ const CopyRow = styled.button `
11451
11529
  padding: 8px 16px;
11452
11530
 
11453
11531
  cursor: pointer;
11532
+ background-color: var(--ck-body-background);
11454
11533
 
11455
11534
  display: flex;
11456
11535
  align-items: center;
@@ -11489,6 +11568,16 @@ const ValueContainer = styled.div `
11489
11568
  `;
11490
11569
  const SmallText = styled.span `
11491
11570
  font-size: 14px;
11571
+ color: var(--ck-primary-button-color);
11572
+ `;
11573
+ const ValueText = styled.span `
11574
+ font-size: 14px;
11575
+ font-weight: 600;
11576
+ color: var(--ck-primary-button-color);
11577
+ `;
11578
+ const LabelText = styled(ModalBody) `
11579
+ margin: 0;
11580
+ text-align: left;
11492
11581
  `;
11493
11582
  const pulse = keyframes `
11494
11583
  0% {
@@ -11523,10 +11612,10 @@ function CopyRowOrThrobber({ title, value, valueText, smallText, disabled, }) {
11523
11612
  setTimeout(() => setCopied(false), 1000);
11524
11613
  };
11525
11614
  if (!value) {
11526
- return (jsxs(CopyRow, { children: [jsx(LabelRow, { children: jsx(ModalBody, { style: { margin: 0, textAlign: "left" }, children: title }) }), jsx(MainRow, { children: jsx(Skeleton, {}) })] }));
11615
+ return (jsxs(CopyRow, { children: [jsx(LabelRow, { children: jsx(LabelText, { children: title }) }), jsx(MainRow, { children: jsx(Skeleton, {}) })] }));
11527
11616
  }
11528
11617
  const displayValue = valueText || value;
11529
- return (jsxs(CopyRow, { as: "button", onClick: handleCopy, disabled: disabled, children: [jsxs("div", { children: [jsx(LabelRow, { children: jsx(ModalBody, { style: { margin: 0, textAlign: "left" }, children: title }) }), jsx(MainRow, { children: jsxs(ValueContainer, { children: [jsx("span", { style: { fontWeight: 600 }, children: displayValue }), smallText && jsx(SmallText, { children: smallText })] }) })] }), jsx(CopyIconWrap, { children: jsx(CopyToClipboardIcon, { copied: copied, dark: true }) })] }));
11618
+ return (jsxs(CopyRow, { as: "button", onClick: handleCopy, disabled: disabled, children: [jsxs("div", { children: [jsx(LabelRow, { children: jsx(LabelText, { children: title }) }), jsx(MainRow, { children: jsxs(ValueContainer, { children: [jsx(ValueText, { children: displayValue }), smallText && jsx(SmallText, { children: smallText })] }) })] }), jsx(CopyIconWrap, { children: jsx(CopyToClipboardIcon, { copied: copied, dark: true }) })] }));
11530
11619
  }
11531
11620
  const CopyIconWrap = styled.div `
11532
11621
  --color: var(--ck-copytoclipboard-stroke);
@@ -11983,7 +12072,12 @@ function useExternalPaymentOptions({ trpc, filterIds, platform, usdRequired, mod
11983
12072
  if (usdRequired != null && mode != null) {
11984
12073
  refreshExternalPaymentOptions(usdRequired, mode);
11985
12074
  }
11986
- }, [usdRequired, filterIds, platform, mode, trpc]);
12075
+ // TODO: this is an ugly way to handle polling/refresh
12076
+ // Notice the load-bearing JSON.stringify() to prevent a visible infinite
12077
+ // refresh glitch on the SelectMethod screen. Replace this useEffect().
12078
+ //
12079
+ // eslint-disable-next-line react-hooks/exhaustive-deps
12080
+ }, [usdRequired, JSON.stringify(filterIds), platform, mode, trpc]);
11987
12081
  return { options, loading };
11988
12082
  }
11989
12083
 
@@ -12709,13 +12803,22 @@ const DaimoPayUIProvider = ({ children, theme = "auto", mode = "auto", customThe
12709
12803
  }
12710
12804
  };
12711
12805
  // Watch when the order gets paid and navigate to confirmation
12806
+ // ...if underpaid, go to the deposit addr screen, let the user finish paying.
12807
+ const isUnderpaid = pay.order?.mode === DaimoPayOrderMode.HYDRATED &&
12808
+ pay.order.sourceStatus === DaimoPayOrderStatusSource.WAITING_PAYMENT &&
12809
+ pay.order.sourceTokenAmount != null;
12712
12810
  useEffect(() => {
12713
12811
  if (pay.paymentState === "payment_started" ||
12714
12812
  pay.paymentState === "payment_completed" ||
12715
12813
  pay.paymentState === "payment_bounced") {
12716
12814
  setRoute(ROUTES.CONFIRMATION, { event: "payment-started" });
12717
12815
  }
12718
- }, [pay.paymentState, setRoute]);
12816
+ else if (isUnderpaid) {
12817
+ paymentState.setSelectedDepositAddressOption(undefined);
12818
+ setRoute(ROUTES.WAITING_DEPOSIT_ADDRESS);
12819
+ }
12820
+ // eslint-disable-next-line react-hooks/exhaustive-deps
12821
+ }, [pay.paymentState, setRoute, isUnderpaid]);
12719
12822
  const value = {
12720
12823
  theme: ckTheme,
12721
12824
  setTheme,