@b3dotfun/sdk 0.0.49-alpha.0 → 0.0.49-alpha.2

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.
Files changed (78) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +2 -1
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +8 -4
  3. package/dist/cjs/anyspend/react/components/AnySpendCustom.js +12 -3
  4. package/dist/cjs/anyspend/react/components/AnyspendDepositHype.js +11 -4
  5. package/dist/cjs/anyspend/react/components/common/CryptoPaySection.d.ts +3 -1
  6. package/dist/cjs/anyspend/react/components/common/CryptoPaySection.js +2 -2
  7. package/dist/cjs/anyspend/react/components/common/CryptoReceiveSection.d.ts +3 -1
  8. package/dist/cjs/anyspend/react/components/common/CryptoReceiveSection.js +17 -11
  9. package/dist/cjs/anyspend/react/components/common/FeeBreakDown.d.ts +12 -0
  10. package/dist/cjs/anyspend/react/components/common/FeeBreakDown.js +19 -0
  11. package/dist/cjs/anyspend/react/components/common/FeeDetailPanel.d.ts +8 -0
  12. package/dist/cjs/anyspend/react/components/common/FeeDetailPanel.js +116 -0
  13. package/dist/cjs/anyspend/react/components/common/OrderDetails.d.ts +1 -0
  14. package/dist/cjs/anyspend/react/components/common/OrderDetails.js +6 -6
  15. package/dist/cjs/anyspend/react/components/common/OrderDetailsCollapsible.d.ts +1 -0
  16. package/dist/cjs/anyspend/react/components/common/OrderDetailsCollapsible.js +2 -2
  17. package/dist/cjs/anyspend/react/components/common/PanelOnramp.d.ts +2 -1
  18. package/dist/cjs/anyspend/react/components/common/PanelOnramp.js +36 -21
  19. package/dist/cjs/anyspend/react/components/common/PanelOnrampPayment.js +2 -1
  20. package/dist/cjs/anyspend/react/components/common/PointsDetailPanel.js +1 -2
  21. package/dist/cjs/anyspend/react/hooks/useAnyspendFlow.d.ts +3 -1
  22. package/dist/cjs/anyspend/react/hooks/useAnyspendFlow.js +1 -0
  23. package/dist/cjs/anyspend/react/hooks/useAnyspendOrderHistory.d.ts +24 -0
  24. package/dist/cjs/anyspend/types/api.d.ts +119 -176
  25. package/dist/cjs/global-account/react/components/ui/tooltip.js +1 -1
  26. package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -1
  27. package/dist/esm/anyspend/react/components/AnySpend.js +8 -4
  28. package/dist/esm/anyspend/react/components/AnySpendCustom.js +14 -5
  29. package/dist/esm/anyspend/react/components/AnyspendDepositHype.js +11 -4
  30. package/dist/esm/anyspend/react/components/common/CryptoPaySection.d.ts +3 -1
  31. package/dist/esm/anyspend/react/components/common/CryptoPaySection.js +4 -4
  32. package/dist/esm/anyspend/react/components/common/CryptoReceiveSection.d.ts +3 -1
  33. package/dist/esm/anyspend/react/components/common/CryptoReceiveSection.js +19 -13
  34. package/dist/esm/anyspend/react/components/common/FeeBreakDown.d.ts +12 -0
  35. package/dist/esm/anyspend/react/components/common/FeeBreakDown.js +16 -0
  36. package/dist/esm/anyspend/react/components/common/FeeDetailPanel.d.ts +8 -0
  37. package/dist/esm/anyspend/react/components/common/FeeDetailPanel.js +113 -0
  38. package/dist/esm/anyspend/react/components/common/OrderDetails.d.ts +1 -0
  39. package/dist/esm/anyspend/react/components/common/OrderDetails.js +6 -6
  40. package/dist/esm/anyspend/react/components/common/OrderDetailsCollapsible.d.ts +1 -0
  41. package/dist/esm/anyspend/react/components/common/OrderDetailsCollapsible.js +3 -3
  42. package/dist/esm/anyspend/react/components/common/PanelOnramp.d.ts +2 -1
  43. package/dist/esm/anyspend/react/components/common/PanelOnramp.js +38 -23
  44. package/dist/esm/anyspend/react/components/common/PanelOnrampPayment.js +2 -1
  45. package/dist/esm/anyspend/react/components/common/PointsDetailPanel.js +2 -3
  46. package/dist/esm/anyspend/react/hooks/useAnyspendFlow.d.ts +3 -1
  47. package/dist/esm/anyspend/react/hooks/useAnyspendFlow.js +1 -0
  48. package/dist/esm/anyspend/react/hooks/useAnyspendOrderHistory.d.ts +24 -0
  49. package/dist/esm/anyspend/types/api.d.ts +119 -176
  50. package/dist/esm/global-account/react/components/ui/tooltip.js +1 -1
  51. package/dist/styles/index.css +1 -1
  52. package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -1
  53. package/dist/types/anyspend/react/components/common/CryptoPaySection.d.ts +3 -1
  54. package/dist/types/anyspend/react/components/common/CryptoReceiveSection.d.ts +3 -1
  55. package/dist/types/anyspend/react/components/common/FeeBreakDown.d.ts +12 -0
  56. package/dist/types/anyspend/react/components/common/FeeDetailPanel.d.ts +8 -0
  57. package/dist/types/anyspend/react/components/common/OrderDetails.d.ts +1 -0
  58. package/dist/types/anyspend/react/components/common/OrderDetailsCollapsible.d.ts +1 -0
  59. package/dist/types/anyspend/react/components/common/PanelOnramp.d.ts +2 -1
  60. package/dist/types/anyspend/react/hooks/useAnyspendFlow.d.ts +3 -1
  61. package/dist/types/anyspend/react/hooks/useAnyspendOrderHistory.d.ts +24 -0
  62. package/dist/types/anyspend/types/api.d.ts +119 -176
  63. package/package.json +1 -1
  64. package/src/anyspend/react/components/AnySpend.tsx +20 -0
  65. package/src/anyspend/react/components/AnySpendCustom.tsx +69 -7
  66. package/src/anyspend/react/components/AnyspendDepositHype.tsx +24 -0
  67. package/src/anyspend/react/components/common/CryptoPaySection.tsx +14 -2
  68. package/src/anyspend/react/components/common/CryptoReceiveSection.tsx +31 -11
  69. package/src/anyspend/react/components/common/FeeBreakDown.tsx +105 -0
  70. package/src/anyspend/react/components/common/FeeDetailPanel.tsx +334 -0
  71. package/src/anyspend/react/components/common/OrderDetails.tsx +7 -0
  72. package/src/anyspend/react/components/common/OrderDetailsCollapsible.tsx +13 -0
  73. package/src/anyspend/react/components/common/PanelOnramp.tsx +58 -27
  74. package/src/anyspend/react/components/common/PanelOnrampPayment.tsx +18 -6
  75. package/src/anyspend/react/components/common/PointsDetailPanel.tsx +1 -13
  76. package/src/anyspend/react/hooks/useAnyspendFlow.ts +1 -0
  77. package/src/anyspend/types/api.ts +121 -176
  78. package/src/global-account/react/components/ui/tooltip.tsx +11 -9
@@ -22,6 +22,7 @@ import {
22
22
  TextShimmer,
23
23
  Tooltip,
24
24
  TooltipContent,
25
+ TooltipProvider,
25
26
  TooltipTrigger,
26
27
  TransitionPanel,
27
28
  useAccountWallet,
@@ -37,7 +38,7 @@ import { shortenAddress } from "@b3dotfun/sdk/shared/utils/formatAddress";
37
38
  import { formatTokenAmount, formatUnits } from "@b3dotfun/sdk/shared/utils/number";
38
39
  import { simpleHashChainToChainName } from "@b3dotfun/sdk/shared/utils/simplehash";
39
40
  import invariant from "invariant";
40
- import { ChevronRight, ChevronRightCircle, Loader2 } from "lucide-react";
41
+ import { ChevronRight, ChevronRightCircle, Info, Loader2 } from "lucide-react";
41
42
  import { motion } from "motion/react";
42
43
  import React, { useCallback, useEffect, useMemo, useState } from "react";
43
44
  import { toast } from "sonner";
@@ -45,6 +46,7 @@ import { base } from "viem/chains";
45
46
  import { useFeatureFlags } from "../contexts/FeatureFlagsContext";
46
47
  import { AnySpendFingerprintWrapper, getFingerprintConfig } from "./AnySpendFingerprintWrapper";
47
48
  import { CryptoPaymentMethod, CryptoPaymentMethodType } from "./common/CryptoPaymentMethod";
49
+ import { FeeBreakDown } from "./common/FeeBreakDown";
48
50
  import { FiatPaymentMethod, FiatPaymentMethodComponent } from "./common/FiatPaymentMethod";
49
51
  import { OrderDetails } from "./common/OrderDetails";
50
52
  import { OrderHistory } from "./common/OrderHistory";
@@ -77,6 +79,7 @@ function generateGetRelayQuoteRequest({
77
79
  contractType,
78
80
  encodedData,
79
81
  spenderAddress,
82
+ onrampVendor,
80
83
  }: {
81
84
  orderType: components["schemas"]["Order"]["type"];
82
85
  srcChainId: number;
@@ -90,6 +93,7 @@ function generateGetRelayQuoteRequest({
90
93
  contractType?: components["schemas"]["NftContract"]["type"];
91
94
  encodedData: string;
92
95
  spenderAddress?: string;
96
+ onrampVendor?: components["schemas"]["OnrampMetadata"]["vendor"];
93
97
  }): GetQuoteRequest {
94
98
  switch (orderType) {
95
99
  case "mint_nft": {
@@ -105,6 +109,7 @@ function generateGetRelayQuoteRequest({
105
109
  contractAddress: contractAddress,
106
110
  tokenId: tokenId,
107
111
  contractType: contractType,
112
+ onrampVendor,
108
113
  };
109
114
  }
110
115
  case "join_tournament": {
@@ -117,6 +122,7 @@ function generateGetRelayQuoteRequest({
117
122
  recipientAddress,
118
123
  price: dstAmount,
119
124
  contractAddress: contractAddress,
125
+ onrampVendor,
120
126
  };
121
127
  }
122
128
  case "fund_tournament": {
@@ -129,6 +135,7 @@ function generateGetRelayQuoteRequest({
129
135
  recipientAddress,
130
136
  fundAmount: dstAmount,
131
137
  contractAddress: contractAddress,
138
+ onrampVendor,
132
139
  };
133
140
  }
134
141
  case "custom": {
@@ -145,6 +152,7 @@ function generateGetRelayQuoteRequest({
145
152
  to: contractAddress,
146
153
  spenderAddress: spenderAddress,
147
154
  },
155
+ onrampVendor,
148
156
  };
149
157
  }
150
158
  default: {
@@ -336,6 +344,7 @@ function AnySpendCustomInner({
336
344
  contractType: orderType === "mint_nft" ? metadata?.nftContract?.type : undefined,
337
345
  encodedData: encodedData,
338
346
  spenderAddress: spenderAddress,
347
+ onrampVendor: selectedFiatPaymentMethod === FiatPaymentMethod.STRIPE ? "stripe-web2" : undefined,
339
348
  });
340
349
  }, [
341
350
  activeTab,
@@ -351,6 +360,7 @@ function AnySpendCustomInner({
351
360
  spenderAddress,
352
361
  srcChainId,
353
362
  srcToken,
363
+ selectedFiatPaymentMethod,
354
364
  ]);
355
365
  const { anyspendQuote, isLoadingAnyspendQuote } = useAnyspendQuote(getRelayQuoteRequest);
356
366
 
@@ -960,14 +970,40 @@ function AnySpendCustomInner({
960
970
  className="relative flex w-full items-center justify-between"
961
971
  >
962
972
  <div className="flex items-center gap-2">
963
- <span className="text-as-tertiarry text-sm">
973
+ <span className="text-as-tertiarry flex items-center gap-1.5 text-sm">
964
974
  Total <span className="text-as-tertiarry">(with fee)</span>
975
+ {anyspendQuote?.data?.fee && (
976
+ <TooltipProvider>
977
+ <Tooltip>
978
+ <TooltipTrigger asChild>
979
+ <button className="text-as-primary/40 hover:text-as-primary/60 transition-colors">
980
+ <Info className="h-4 w-4" />
981
+ </button>
982
+ </TooltipTrigger>
983
+ <TooltipContent side="top">
984
+ <FeeBreakDown fee={anyspendQuote.data.fee} />
985
+ </TooltipContent>
986
+ </Tooltip>
987
+ </TooltipProvider>
988
+ )}
965
989
  </span>
966
990
  {renderPointsBadge()}
967
991
  </div>
968
- <span className="text-as-primary font-semibold">
969
- {formattedSrcAmount || "--"} {srcToken.symbol}
970
- </span>
992
+ <div className="flex flex-col items-end gap-0.5">
993
+ <span className="text-as-primary font-semibold">
994
+ {formattedSrcAmount || "--"} {srcToken.symbol}
995
+ </span>
996
+ {anyspendQuote?.data?.fee?.type === "standard_fee" && anyspendQuote.data.currencyIn?.amountUsd && (
997
+ <span className="text-as-secondary text-xs">
998
+ incl. $
999
+ {(
1000
+ (Number(anyspendQuote.data.currencyIn.amountUsd) * anyspendQuote.data.fee.finalFeeBps) /
1001
+ 10000
1002
+ ).toFixed(2)}{" "}
1003
+ fee
1004
+ </span>
1005
+ )}
1006
+ </div>
971
1007
  </motion.div>
972
1008
  </div>
973
1009
  </div>
@@ -1086,12 +1122,38 @@ function AnySpendCustomInner({
1086
1122
  className="relative flex w-full items-center justify-between"
1087
1123
  >
1088
1124
  <div className="flex items-center gap-2">
1089
- <span className="text-as-tertiarry text-sm">
1125
+ <span className="text-as-tertiarry flex items-center gap-1.5 text-sm">
1090
1126
  Total <span className="text-as-tertiarry">(USD)</span>
1127
+ {anyspendQuote?.data?.fee && (
1128
+ <TooltipProvider>
1129
+ <Tooltip>
1130
+ <TooltipTrigger asChild>
1131
+ <button className="text-as-primary/40 hover:text-as-primary/60 transition-colors">
1132
+ <Info className="h-4 w-4" />
1133
+ </button>
1134
+ </TooltipTrigger>
1135
+ <TooltipContent side="top">
1136
+ <FeeBreakDown fee={anyspendQuote.data.fee} />
1137
+ </TooltipContent>
1138
+ </Tooltip>
1139
+ </TooltipProvider>
1140
+ )}
1091
1141
  </span>
1092
1142
  {renderPointsBadge()}
1093
1143
  </div>
1094
- <span className="text-as-primary text-xl font-semibold">${srcFiatAmount || "0.00"}</span>
1144
+ <div className="flex flex-col items-end gap-0.5">
1145
+ <span className="text-as-primary text-xl font-semibold">${srcFiatAmount || "0.00"}</span>
1146
+ {anyspendQuote?.data?.fee?.type === "stripeweb2_fee" && anyspendQuote.data.fee.originalAmount && (
1147
+ <span className="text-as-secondary text-xs">
1148
+ incl. $
1149
+ {(
1150
+ (Number(anyspendQuote.data.fee.originalAmount) - Number(anyspendQuote.data.fee.finalAmount)) /
1151
+ 1e6
1152
+ ).toFixed(2)}{" "}
1153
+ fee
1154
+ </span>
1155
+ )}
1156
+ </div>
1095
1157
  </motion.div>
1096
1158
  </div>
1097
1159
 
@@ -12,6 +12,7 @@ import { AnySpendFingerprintWrapper, getFingerprintConfig } from "./AnySpendFing
12
12
  import { CryptoPaySection } from "./common/CryptoPaySection";
13
13
  import { CryptoPaymentMethod, CryptoPaymentMethodType } from "./common/CryptoPaymentMethod";
14
14
  import { CryptoReceiveSection } from "./common/CryptoReceiveSection";
15
+ import { FeeDetailPanel } from "./common/FeeDetailPanel";
15
16
  import { FiatPaymentMethod, FiatPaymentMethodComponent } from "./common/FiatPaymentMethod";
16
17
  import { OrderDetails } from "./common/OrderDetails";
17
18
  import { PointsDetailPanel } from "./common/PointsDetailPanel";
@@ -80,6 +81,7 @@ function AnySpendDepositHypeInner({
80
81
  srcAmount,
81
82
  setSrcAmount,
82
83
  dstAmount,
84
+ isSrcInputDirty,
83
85
  setIsSrcInputDirty,
84
86
  selectedCryptoPaymentMethod,
85
87
  setSelectedCryptoPaymentMethod,
@@ -207,6 +209,7 @@ function AnySpendDepositHypeInner({
207
209
  setSelectedSrcToken={setSelectedSrcToken}
208
210
  srcAmount={srcAmount}
209
211
  setSrcAmount={setSrcAmount}
212
+ isSrcInputDirty={isSrcInputDirty}
210
213
  setIsSrcInputDirty={setIsSrcInputDirty}
211
214
  selectedCryptoPaymentMethod={selectedCryptoPaymentMethod}
212
215
  onSelectCryptoPaymentMethod={() => setActivePanel(PanelView.CRYPTO_PAYMENT_METHOD)}
@@ -236,6 +239,7 @@ function AnySpendDepositHypeInner({
236
239
  recipientSelectionPanelIndex={PanelView.RECIPIENT_SELECTION}
237
240
  anyspendQuote={anyspendQuote}
238
241
  onShowPointsDetail={() => setActivePanel(PanelView.POINTS_DETAIL)}
242
+ onShowFeeDetail={() => setActivePanel(PanelView.FEE_DETAIL)}
239
243
  customUsdInputValues={customUsdInputValues}
240
244
  />
241
245
  </motion.div>
@@ -272,12 +276,14 @@ function AnySpendDepositHypeInner({
272
276
  selectedDstChainId={base.id}
273
277
  setSelectedDstChainId={() => {}}
274
278
  setSelectedDstToken={() => {}}
279
+ isSrcInputDirty={isSrcInputDirty}
275
280
  onChangeDstAmount={value => {
276
281
  setIsSrcInputDirty(false);
277
282
  setSrcAmount(value);
278
283
  }}
279
284
  anyspendQuote={anyspendQuote}
280
285
  onShowPointsDetail={() => setActivePanel(PanelView.POINTS_DETAIL)}
286
+ onShowFeeDetail={() => setActivePanel(PanelView.FEE_DETAIL)}
281
287
  />
282
288
  )}
283
289
  </div>
@@ -410,6 +416,7 @@ function AnySpendDepositHypeInner({
410
416
  setActivePanel(PanelView.MAIN);
411
417
  }}
412
418
  disableUrlParamManagement
419
+ points={oat.data.points || undefined}
413
420
  />
414
421
  )}
415
422
  </div>
@@ -470,6 +477,20 @@ function AnySpendDepositHypeInner({
470
477
  />
471
478
  );
472
479
 
480
+ const feeDetailView = anyspendQuote?.data?.fee ? (
481
+ <FeeDetailPanel
482
+ fee={anyspendQuote.data.fee}
483
+ transactionAmountUsd={
484
+ paymentType === "fiat"
485
+ ? parseFloat(srcAmount)
486
+ : anyspendQuote.data.currencyIn?.amountUsd
487
+ ? Number(anyspendQuote.data.currencyIn.amountUsd)
488
+ : undefined
489
+ }
490
+ onBack={() => setActivePanel(PanelView.MAIN)}
491
+ />
492
+ ) : null;
493
+
473
494
  // If showing token selection, render with panel transitions
474
495
  return (
475
496
  <StyleRoot>
@@ -522,6 +543,9 @@ function AnySpendDepositHypeInner({
522
543
  <div key="points-detail-view" className={cn(mode === "page" && "p-6")}>
523
544
  {pointsDetailView}
524
545
  </div>,
546
+ <div key="fee-detail-view" className={cn(mode === "page" && "p-6")}>
547
+ {feeDetailView}
548
+ </div>,
525
549
  ]}
526
550
  </TransitionPanel>
527
551
  </div>
@@ -2,7 +2,7 @@ import { useAccountWallet, useProfile, useTokenData } from "@b3dotfun/sdk/global
2
2
  import { formatUsername } from "@b3dotfun/sdk/shared/utils";
3
3
  import { shortenAddress } from "@b3dotfun/sdk/shared/utils/formatAddress";
4
4
  import { formatDisplayNumber } from "@b3dotfun/sdk/shared/utils/number";
5
- import { ChevronRight } from "lucide-react";
5
+ import { ChevronRight, Info } from "lucide-react";
6
6
  import { motion } from "motion/react";
7
7
  import { useEffect, useRef } from "react";
8
8
  import { components } from "../../../types/api";
@@ -18,6 +18,7 @@ interface CryptoPaySectionProps {
18
18
  setSelectedSrcToken: (token: components["schemas"]["Token"]) => void;
19
19
  srcAmount: string;
20
20
  setSrcAmount: (amount: string) => void;
21
+ isSrcInputDirty: boolean;
21
22
  setIsSrcInputDirty: (dirty: boolean) => void;
22
23
  // Payment method state
23
24
  selectedCryptoPaymentMethod: CryptoPaymentMethodType;
@@ -26,6 +27,8 @@ interface CryptoPaySectionProps {
26
27
  anyspendQuote?: any;
27
28
  // Token selection callback
28
29
  onTokenSelect?: (token: components["schemas"]["Token"], event: { preventDefault: () => void }) => void;
30
+ // Fee detail callback
31
+ onShowFeeDetail?: () => void;
29
32
  }
30
33
 
31
34
  export function CryptoPaySection({
@@ -35,11 +38,13 @@ export function CryptoPaySection({
35
38
  setSelectedSrcToken,
36
39
  srcAmount,
37
40
  setSrcAmount,
41
+ isSrcInputDirty,
38
42
  setIsSrcInputDirty,
39
43
  selectedCryptoPaymentMethod,
40
44
  onSelectCryptoPaymentMethod,
41
45
  anyspendQuote,
42
46
  onTokenSelect,
47
+ onShowFeeDetail,
43
48
  }: CryptoPaySectionProps) {
44
49
  const { connectedSmartWallet, connectedEOAWallet } = useAccountWallet();
45
50
  const { data: srcTokenMetadata } = useTokenData(selectedSrcToken?.chainId, selectedSrcToken?.address);
@@ -89,7 +94,14 @@ export function CryptoPaySection({
89
94
  className="pay-section bg-as-surface-secondary border-as-border-secondary relative flex w-full flex-col gap-2 rounded-2xl border p-4 sm:p-6"
90
95
  >
91
96
  <div className="flex items-center justify-between">
92
- <div className="text-as-primary/50 flex h-7 items-center text-sm">Pay</div>
97
+ <div className="text-as-primary/50 flex h-7 items-center gap-1.5 text-sm">
98
+ Pay
99
+ {!isSrcInputDirty && anyspendQuote?.data?.fee && onShowFeeDetail && (
100
+ <button onClick={onShowFeeDetail} className="text-as-primary/40 hover:text-as-primary/60 transition-colors">
101
+ <Info className="h-4 w-4" />
102
+ </button>
103
+ )}
104
+ </div>
93
105
  <button
94
106
  className="text-as-tertiarry flex h-7 items-center gap-2 text-sm transition-colors focus:!outline-none"
95
107
  onClick={onSelectCryptoPaymentMethod}
@@ -2,7 +2,7 @@ import { formatUsername } from "@b3dotfun/sdk/shared/utils";
2
2
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
3
3
  import { shortenAddress } from "@b3dotfun/sdk/shared/utils/formatAddress";
4
4
  import { formatDisplayNumber } from "@b3dotfun/sdk/shared/utils/number";
5
- import { ChevronRight } from "lucide-react";
5
+ import { ChevronRight, Info } from "lucide-react";
6
6
  import { motion } from "motion/react";
7
7
  import { components } from "../../../types/api";
8
8
  import { useFeatureFlags } from "../../contexts/FeatureFlagsContext";
@@ -23,6 +23,7 @@ interface CryptoReceiveSectionProps {
23
23
  selectedDstChainId?: number;
24
24
  setSelectedDstChainId?: (chainId: number) => void;
25
25
  setSelectedDstToken?: (token: components["schemas"]["Token"]) => void;
26
+ isSrcInputDirty: boolean;
26
27
  onChangeDstAmount?: (value: string) => void;
27
28
  // Quote data
28
29
  anyspendQuote?: any;
@@ -31,6 +32,8 @@ interface CryptoReceiveSectionProps {
31
32
  dstTokenLogoURI?: string;
32
33
  // Points navigation
33
34
  onShowPointsDetail?: () => void;
35
+ // Fee detail navigation
36
+ onShowFeeDetail?: () => void;
34
37
  }
35
38
 
36
39
  export function CryptoReceiveSection({
@@ -44,11 +47,13 @@ export function CryptoReceiveSection({
44
47
  selectedDstChainId,
45
48
  setSelectedDstChainId,
46
49
  setSelectedDstToken,
50
+ isSrcInputDirty,
47
51
  onChangeDstAmount,
48
52
  anyspendQuote,
49
53
  dstTokenSymbol,
50
54
  dstTokenLogoURI,
51
55
  onShowPointsDetail,
56
+ onShowFeeDetail,
52
57
  }: CryptoReceiveSectionProps) {
53
58
  const featureFlags = useFeatureFlags();
54
59
 
@@ -60,7 +65,14 @@ export function CryptoReceiveSection({
60
65
  className="receive-section bg-as-surface-secondary border-as-border-secondary relative flex w-full flex-col gap-2 rounded-2xl border p-4 sm:p-6"
61
66
  >
62
67
  <div className="flex w-full items-center justify-between">
63
- <div className="text-as-primary/50 flex h-7 items-center text-sm">{isDepositMode ? "Deposit" : "Receive"}</div>
68
+ <div className="text-as-primary/50 flex h-7 items-center gap-1.5 text-sm">
69
+ {isDepositMode ? "Deposit" : "Receive"}
70
+ {isSrcInputDirty && anyspendQuote?.data?.fee && onShowFeeDetail && (
71
+ <button onClick={onShowFeeDetail} className="text-as-primary/40 hover:text-as-primary/60 transition-colors">
72
+ <Info className="h-4 w-4" />
73
+ </button>
74
+ )}
75
+ </div>
64
76
  {selectedRecipientAddress ? (
65
77
  <button
66
78
  className={cn("text-as-tertiarry flex h-7 items-center gap-2 rounded-lg")}
@@ -118,7 +130,7 @@ export function CryptoReceiveSection({
118
130
  (() => {
119
131
  const calculatePriceImpact = (inputUsd?: string | number, outputUsd?: string | number) => {
120
132
  if (!inputUsd || !outputUsd) {
121
- return { percentage: "0.00", isNegative: false };
133
+ return { percentage: "0.00", percentageNum: 0, isNegative: false };
122
134
  }
123
135
 
124
136
  const input = Number(inputUsd);
@@ -126,38 +138,46 @@ export function CryptoReceiveSection({
126
138
 
127
139
  // Handle edge cases
128
140
  if (input === 0 || isNaN(input) || isNaN(output) || input <= output) {
129
- return { percentage: "0.00", isNegative: false };
141
+ return { percentage: "0.00", percentageNum: 0, isNegative: false };
130
142
  }
131
143
 
132
144
  const percentageValue = ((output - input) / input) * 100;
133
145
 
134
146
  // Handle the -0.00% case
135
147
  if (percentageValue > -0.005 && percentageValue < 0) {
136
- return { percentage: "0.00", isNegative: false };
148
+ return { percentage: "0.00", percentageNum: 0, isNegative: false };
137
149
  }
138
150
 
139
151
  return {
140
152
  percentage: Math.abs(percentageValue).toFixed(2),
153
+ percentageNum: Math.abs(percentageValue),
141
154
  isNegative: percentageValue < 0,
142
155
  };
143
156
  };
144
157
 
145
- const { percentage, isNegative } = calculatePriceImpact(
158
+ const { percentage, percentageNum, isNegative } = calculatePriceImpact(
146
159
  anyspendQuote.data.currencyIn.amountUsd,
147
160
  anyspendQuote.data.currencyOut.amountUsd,
148
161
  );
149
162
 
150
- // Parse the percentage as a number for comparison
151
- const percentageNum = parseFloat(percentage);
163
+ // Get the fee percentage if available
164
+ const feePercent = anyspendQuote.data.fee?.finalFeeBps ? anyspendQuote.data.fee.finalFeeBps / 100 : 0;
165
+
166
+ // Calculate actual slippage (price impact minus fee)
167
+ const actualSlippage = percentageNum - feePercent;
168
+
169
+ // Show warning based on actual slippage, not total impact
170
+ const yellowThreshold = 1; // 1% actual slippage
171
+ const redThreshold = 2; // 2% actual slippage
152
172
 
153
- // Don't show if less than 1%
154
- if (percentageNum < 1) {
173
+ // Don't show if actual slippage is less than yellow threshold
174
+ if (actualSlippage < yellowThreshold) {
155
175
  return null;
156
176
  }
157
177
 
158
178
  // Using inline style to ensure color displays
159
179
  return (
160
- <span className="ml-2" style={{ color: percentageNum >= 10 ? "red" : "#FFD700" }}>
180
+ <span className="ml-2" style={{ color: actualSlippage >= redThreshold ? "red" : "#FFD700" }}>
161
181
  ({isNegative ? "-" : ""}
162
182
  {percentage}%)
163
183
  </span>
@@ -0,0 +1,105 @@
1
+ import { components } from "../../../types/api";
2
+
3
+ interface FeeBreakDownProps {
4
+ fee: components["schemas"]["Fee"];
5
+ /** Number of decimals for amount display (default: 6 for USDC) */
6
+ decimals?: number;
7
+ /** Show currency symbol for amounts (default: true) */
8
+ showCurrency?: boolean;
9
+ /** Custom className for the container */
10
+ className?: string;
11
+ }
12
+
13
+ export function FeeBreakDown({ fee, decimals = 6, showCurrency = true, className = "" }: FeeBreakDownProps) {
14
+ const isStripeFee = fee.type === "stripeweb2_fee";
15
+
16
+ // Convert basis points to percentage
17
+ const bpsToPercent = (bps: number) => (bps / 100).toFixed(2);
18
+
19
+ // Format amount with optional currency
20
+ const formatAmount = (amount: string) => {
21
+ const divisor = Math.pow(10, decimals);
22
+ const formatted = (Number(amount) / divisor).toFixed(2);
23
+ return showCurrency ? `$${formatted}` : formatted;
24
+ };
25
+
26
+ // Check if discount is active
27
+ const hasWhaleDiscount = fee.anyspendWhaleDiscountBps > 0;
28
+ const hasPartnerDiscount = fee.anyspendPartnerDiscountBps > 0;
29
+
30
+ return (
31
+ <div className={`min-w-[240px] ${className}`}>
32
+ {/* Fee Breakdown Section */}
33
+ <div className="mb-4">
34
+ <h3 className="text-as-primary mb-2 text-sm font-semibold">Fee Breakdown</h3>
35
+ <table className="w-full">
36
+ <tbody className="text-as-secondary text-xs">
37
+ {isStripeFee && (
38
+ <tr>
39
+ <td className="py-1">Stripe Fee</td>
40
+ <td className="py-1 text-right">
41
+ {bpsToPercent(fee.stripeFeeBps)}% + ${fee.stripeFeeUsd.toFixed(2)}
42
+ </td>
43
+ </tr>
44
+ )}
45
+ <tr>
46
+ <td className="py-1">AnySpend Fee</td>
47
+ <td className="py-1 text-right">
48
+ {bpsToPercent(fee.anyspendFeeBps)}%
49
+ {isStripeFee && fee.anyspendFeeUsd > 0 && ` + $${fee.anyspendFeeUsd.toFixed(2)}`}
50
+ </td>
51
+ </tr>
52
+ {hasWhaleDiscount && (
53
+ <tr className="text-green-600">
54
+ <td className="py-1">Whale Discount</td>
55
+ <td className="py-1 text-right">-{bpsToPercent(fee.anyspendWhaleDiscountBps)}%</td>
56
+ </tr>
57
+ )}
58
+ {hasPartnerDiscount && (
59
+ <tr className="text-green-600">
60
+ <td className="py-1">Partner Discount</td>
61
+ <td className="py-1 text-right">-{bpsToPercent(fee.anyspendPartnerDiscountBps)}%</td>
62
+ </tr>
63
+ )}
64
+ <tr className="border-as-border-secondary border-t">
65
+ <td className="text-as-primary py-1.5 pt-2 font-semibold">Total Fee</td>
66
+ <td className="text-as-primary py-1.5 pt-2 text-right font-semibold">
67
+ {bpsToPercent(fee.finalFeeBps)}%{isStripeFee && ` + $${fee.finalFeeUsd.toFixed(2)}`}
68
+ </td>
69
+ </tr>
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+
74
+ {/* Amount Calculation Section (Stripe only) */}
75
+ {isStripeFee && (
76
+ <>
77
+ <div className="border-as-border-secondary my-3 border-t"></div>
78
+ <div>
79
+ <h3 className="text-as-primary mb-2 text-sm font-semibold">Amount Calculation</h3>
80
+ <table className="w-full">
81
+ <tbody className="text-as-secondary text-xs">
82
+ <tr>
83
+ <td className="py-1">Original Amount</td>
84
+ <td className="py-1 text-right font-medium">{formatAmount(fee.originalAmount)}</td>
85
+ </tr>
86
+ <tr>
87
+ <td className="py-1">Total Fee</td>
88
+ <td className="py-1 text-right text-red-600">
89
+ -{formatAmount((Number(fee.originalAmount) - Number(fee.finalAmount)).toString())}
90
+ </td>
91
+ </tr>
92
+ <tr className="border-as-border-secondary border-t">
93
+ <td className="text-as-primary py-1.5 pt-2 font-semibold">You Receive</td>
94
+ <td className="text-as-primary py-1.5 pt-2 text-right font-semibold">
95
+ {formatAmount(fee.finalAmount)}
96
+ </td>
97
+ </tr>
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+ </>
102
+ )}
103
+ </div>
104
+ );
105
+ }