diff --git a/packages/keychain/src/components/purchase/CostBreakdown.tsx b/packages/keychain/src/components/purchase/CostBreakdown.tsx index 0d71f2c1d..ea3c8e771 100644 --- a/packages/keychain/src/components/purchase/CostBreakdown.tsx +++ b/packages/keychain/src/components/purchase/CostBreakdown.tsx @@ -15,6 +15,7 @@ import { Separator, Thumbnail, } from "@cartridge/ui"; +import { TOKEN_ICONS } from "@/constants"; import { PricingDetails } from "."; import { ExternalWalletType } from "@cartridge/controller"; import { FeesTooltip } from "./FeesTooltip"; @@ -135,13 +136,7 @@ const PaymentType = ({ unit }: { unit?: PaymentUnit }) => { - ) - } + icon={unit === "usdc" ? TOKEN_ICONS.USDC : } variant="light" rounded /> diff --git a/packages/keychain/src/components/purchase/FeesTooltip.tsx b/packages/keychain/src/components/purchase/FeesTooltip.tsx index c0ccc0c69..3a05b9cf3 100644 --- a/packages/keychain/src/components/purchase/FeesTooltip.tsx +++ b/packages/keychain/src/components/purchase/FeesTooltip.tsx @@ -15,6 +15,8 @@ export const FeesTooltip = ({ defaultOpen?: boolean; isStripe: boolean; }) => { + const clientFeePercentage = isStripe ? "8.9%" : "2.5%"; // Combined Stripe + Cartridge or just Cartridge + return ( @@ -26,13 +28,17 @@ export const FeesTooltip = ({ >
Processing Fees:
- {isStripe && ( -
- Stripe Processing Fee:
3.9%
-
- )}
- Cartridge Processing Fee:
{isStripe ? "5%" : "2.5%"}
+ Marketplace Fee: + 0.00% +
+
+ Creator Royalties: + 0.00% +
+
+ Client Fee: + {clientFeePercentage}
diff --git a/packages/keychain/src/components/purchasenew/review/cost.stories.tsx b/packages/keychain/src/components/purchasenew/review/cost.stories.tsx index 0dfb7b259..42f3dbfb6 100644 --- a/packages/keychain/src/components/purchasenew/review/cost.stories.tsx +++ b/packages/keychain/src/components/purchasenew/review/cost.stories.tsx @@ -1,28 +1,89 @@ import type { Meta, StoryObj } from "@storybook/react"; - import { CostBreakdown } from "./cost"; -import { PurchaseProvider } from "@/context"; -import { WalletType } from "@cartridge/ui"; +import { PurchaseContext } from "@/context/purchase"; +import { ItemType, type Item } from "@/context"; +import type { PurchaseContextType } from "@/context"; +import { TOKEN_ICONS } from "@/constants"; + +// Mock context with purchase items +const MockPurchaseProvider = ({ + children, + purchaseItems = [], + layerswapFees, +}: { + children: React.ReactNode; + purchaseItems?: Item[]; + layerswapFees?: string; +}) => { + const mockContext: PurchaseContextType = { + usdAmount: 100, + purchaseItems, + claimItems: [], + layerswapFees, + isFetchingFees: false, + selectedPlatform: "ethereum", + stripePromise: Promise.resolve(null), + isStripeLoading: false, + isDepositLoading: false, + isStarterpackLoading: false, + clearError: () => {}, + clearSelectedWallet: () => {}, + availableTokens: [], + convertedPrice: null, + swapQuote: null, + isFetchingConversion: false, + conversionError: null, + setUsdAmount: () => {}, + setDepositAmount: () => {}, + setStarterpackId: () => {}, + setClaimItems: () => {}, + setTransactionHash: () => {}, + setSelectedToken: () => {}, + onCreditCardPurchase: async () => {}, + onBackendCryptoPurchase: async () => {}, + onOnchainPurchase: async () => {}, + onExternalConnect: async () => undefined, + waitForDeposit: async () => true, + fetchFees: async () => {}, + }; + + return ( + + {children} + + ); +}; + +// Extend the component args to include mock data +type StoryArgs = React.ComponentProps & { + mockPurchaseItems?: Item[]; + mockLayerswapFees?: string; +}; -const meta: Meta = { +const meta: Meta = { component: CostBreakdown, parameters: { layout: "centered", }, decorators: [ - (Story) => ( - -
- -
-
- ), + (Story, { args }) => { + return ( + +
+ +
+
+ ); + }, ], }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const WithoutFee: Story = { args: { @@ -32,6 +93,22 @@ export const WithoutFee: Story = { totalInCents: 1000, }, rails: "stripe", + mockPurchaseItems: [ + { + title: "Adventurer #8", + subtitle: "Adventurers", + icon: TOKEN_ICONS.LORDS, + value: 5.0, + type: ItemType.NFT, + }, + { + title: "Adventurer #12", + subtitle: "Adventurers", + icon: TOKEN_ICONS.LORDS, + value: 5.0, + type: ItemType.NFT, + }, + ], }, }; @@ -43,7 +120,16 @@ export const WithCartridgeFee: Story = { totalInCents: 1025, }, rails: "crypto", - walletType: WalletType.Controller, + walletType: "metamask", + mockPurchaseItems: [ + { + title: "1000 Credits", + subtitle: "Game Credits", + icon: TOKEN_ICONS.CREDITS, + value: 10.0, + type: ItemType.CREDIT, + }, + ], }, }; @@ -55,5 +141,78 @@ export const WithStripeFee: Story = { totalInCents: 1089, }, rails: "stripe", + mockPurchaseItems: [ + { + title: "Starter Pack #1", + subtitle: "Loot Survivor", + icon: TOKEN_ICONS.LORDS, + value: 8.0, + type: ItemType.NFT, + }, + { + title: "500 Credits", + subtitle: "Game Credits", + icon: TOKEN_ICONS.CREDITS, + value: 2.0, + type: ItemType.CREDIT, + }, + ], + }, +}; + +export const WithLayerswapFee: Story = { + args: { + costDetails: { + baseCostInCents: 2500, + processingFeeInCents: 62, + totalInCents: 2562, + }, + rails: "crypto", + walletType: "metamask", + mockPurchaseItems: [ + { + title: "Premium Pack", + subtitle: "Realms World", + icon: TOKEN_ICONS.LORDS, + value: 15.0, + type: ItemType.NFT, + }, + { + title: "Building Materials", + subtitle: "Resources", + icon: TOKEN_ICONS.STRK, + value: 10.0, + type: ItemType.ERC20, + }, + ], + mockLayerswapFees: "2500000", // $2.50 in wei + }, +}; + +export const WithTooltipOpen: Story = { + args: { + costDetails: { + baseCostInCents: 1500, + processingFeeInCents: 134, + totalInCents: 1634, + }, + rails: "stripe", + openFeesTooltip: true, + mockPurchaseItems: [ + { + title: "Epic Sword", + subtitle: "Weapons", + icon: TOKEN_ICONS.ETH, + value: 12.0, + type: ItemType.NFT, + }, + { + title: "Magic Potion x3", + subtitle: "Consumables", + icon: TOKEN_ICONS.STRK, + value: 3.0, + type: ItemType.ERC20, + }, + ], }, }; diff --git a/packages/keychain/src/components/purchasenew/review/cost.tsx b/packages/keychain/src/components/purchasenew/review/cost.tsx index 0bd328483..afa9a4321 100644 --- a/packages/keychain/src/components/purchasenew/review/cost.tsx +++ b/packages/keychain/src/components/purchasenew/review/cost.tsx @@ -1,32 +1,22 @@ import { - ArbitrumIcon, - BaseIcon, - Card, CardContent, CreditIcon, - EthereumIcon, InfoIcon, - OptimismIcon, Select, SelectContent, SelectItem, - SolanaIcon, - StarknetIcon, Thumbnail, TokenSelectHeader, } from "@cartridge/ui"; import { CostDetails } from "../types"; -import { - ExternalPlatform, - ExternalWalletType, - humanizeString, -} from "@cartridge/controller"; +import { ExternalWalletType } from "@cartridge/controller"; import { FeesTooltip } from "./tooltip"; import { OnchainFeesTooltip } from "./onchain-tooltip"; import type { Quote } from "@/types/starterpack-types"; import { useCallback, useMemo, useEffect } from "react"; import { usePurchaseContext } from "@/context"; import { num } from "starknet"; +import { TOKEN_ICONS } from "@/constants"; type PaymentRails = "stripe" | "crypto"; type PaymentUnit = "usdc" | "credits"; @@ -35,61 +25,52 @@ export const convertCentsToDollars = (cents: number): string => { return `$${(cents / 100).toFixed(2)}`; }; +/** + * Fiat/Stripe Cost Breakdown - displays total with fees tooltip + */ export function CostBreakdown({ rails, costDetails, walletType, - platform, paymentUnit, openFeesTooltip = false, }: { rails: PaymentRails; costDetails?: CostDetails; walletType?: ExternalWalletType; - platform?: ExternalPlatform; paymentUnit?: PaymentUnit; openFeesTooltip?: boolean; }) { if (rails === "crypto" && !walletType) { - return; + return null; } if (!costDetails) { - return <>; + return null; } return ( - - {rails === "crypto" && platform && ( - -
- Purchase on -
-
- )} - -
- -
-
- Total - {costDetails.processingFeeInCents > 0 && ( - } - isStripe={rails === "stripe"} - defaultOpen={openFeesTooltip} - costDetails={costDetails} - /> - )} -
- - {convertCentsToDollars(costDetails.totalInCents)} - +
+ +
+
+ Total + {costDetails.processingFeeInCents > 0 && ( + } + isStripe={rails === "stripe"} + defaultOpen={openFeesTooltip} + costDetails={costDetails} + /> + )}
- - -
- + + {convertCentsToDollars(costDetails.totalInCents)} + +
+ + +
); } @@ -98,12 +79,10 @@ export function CostBreakdown({ */ export function OnchainCostBreakdown({ quote, - platform, openFeesTooltip = false, showTokenSelector = false, }: { quote: Quote; - platform?: ExternalPlatform; openFeesTooltip?: boolean; showTokenSelector?: boolean; }) { @@ -114,6 +93,11 @@ export function OnchainCostBreakdown({ convertedPrice, isFetchingConversion, } = usePurchaseContext(); + + // Get token icon from available tokens + const paymentTokenIcon = availableTokens.find( + (token) => token.address.toLowerCase() === quote.paymentToken.toLowerCase(), + )?.icon; const { symbol, decimals } = quote.paymentTokenMetadata; // Get default token (USDC if available) for fallback @@ -179,114 +163,145 @@ export function OnchainCostBreakdown({ ); return ( - - {platform && ( - -
- Purchase on +
+ +
+
+ Total + } + defaultOpen={openFeesTooltip} + quote={quote} + />
- - )} - -
- -
-
- Total - } - defaultOpen={openFeesTooltip} - quote={quote} - /> -
-
- {showTokenSelector ? ( - <> - - {formatAmount(paymentAmount)} {symbol} +
+ {showTokenSelector ? ( + <> + {!isPaymentTokenSameAsSelected && + convertedEquivalent !== null && + displayToken && + !isFetchingConversion && ( + + ${formatAmount(convertedEquivalent)} + + )} + + {formatAmount( + !isPaymentTokenSameAsSelected && + convertedEquivalent !== null + ? convertedEquivalent + : paymentAmount, + )}{" "} + {!isPaymentTokenSameAsSelected && convertedEquivalent !== null + ? convertedPrice?.tokenMetadata.symbol + : symbol} + +
+ {((!isPaymentTokenSameAsSelected && + convertedPrice?.tokenMetadata.symbol && + availableTokens.find( + (t) => t.symbol === convertedPrice?.tokenMetadata.symbol, + )?.icon) || + paymentTokenIcon) && ( + + t.symbol === convertedPrice?.tokenMetadata.symbol, + )?.icon) || + paymentTokenIcon + } + rounded + size="xs" + variant="light" + /> + )} + + {!isPaymentTokenSameAsSelected && + convertedEquivalent !== null + ? convertedPrice?.tokenMetadata.symbol + : symbol} - {!isPaymentTokenSameAsSelected && - convertedEquivalent !== null && - displayToken && - !isFetchingConversion && ( - - {formatAmount(convertedEquivalent)}{" "} - {convertedPrice?.tokenMetadata.symbol} - - )} - - ) : ( - <> +
+ + ) : ( + <> + {convertedEquivalent !== null && !isFetchingConversion && ( - {formatAmount(paymentAmount)} {symbol} + ${formatAmount(convertedEquivalent)} - {convertedEquivalent !== null && !isFetchingConversion && ( - - ${formatAmount(convertedEquivalent)} - + )} + + {formatAmount(paymentAmount)} {symbol} + +
+ {paymentTokenIcon && ( + )} - - )} -
+ {symbol} +
+ + )}
- - {showTokenSelector && availableTokens.length > 0 && ( - - )} -
- +
+ + {showTokenSelector && availableTokens.length > 0 && ( + + )} +
); } const PaymentType = ({ unit }: { unit?: PaymentUnit }) => { if (!unit) { - return <>; + return null; } return ( - ) - } + icon={unit === "usdc" ? TOKEN_ICONS.USDC : } variant="light" rounded /> @@ -294,37 +309,3 @@ const PaymentType = ({ unit }: { unit?: PaymentUnit }) => { ); }; - -const Network = ({ platform }: { platform: ExternalPlatform }) => { - const getNetworkIconComponent = (platform: ExternalPlatform) => { - switch (platform) { - case "starknet": - return StarknetIcon; - case "ethereum": - return EthereumIcon; - case "solana": - return SolanaIcon; - case "base": - return BaseIcon; - case "arbitrum": - return ArbitrumIcon; - case "optimism": - return OptimismIcon; - default: - return null; - } - }; - - const NetworkIcon = getNetworkIconComponent(platform); - - if (!NetworkIcon) { - return {humanizeString(platform)}; - } - - return ( - <> - - {humanizeString(platform)} - - ); -}; diff --git a/packages/keychain/src/components/purchasenew/review/onchain-cost.stories.tsx b/packages/keychain/src/components/purchasenew/review/onchain-cost.stories.tsx index 160a40eed..72d55f5d0 100644 --- a/packages/keychain/src/components/purchasenew/review/onchain-cost.stories.tsx +++ b/packages/keychain/src/components/purchasenew/review/onchain-cost.stories.tsx @@ -1,37 +1,167 @@ import type { Meta, StoryObj } from "@storybook/react"; import { OnchainCostBreakdown } from "./cost"; +import { PurchaseContext } from "@/context/purchase"; +import { ItemType, type Item, type PurchaseContextType } from "@/context"; +import type { TokenOption } from "@/context"; +import { TOKEN_ICONS } from "@/constants"; // USDC address with leading zeros (tests normalization) const USDC_ADDRESS = "0x053C91253BC9682c04929cA02ED00b3E423f6710D2ee7e0D5EBB06F3eCF368A8"; +const MockPurchaseProvider = ({ + children, + purchaseItems = [], + availableTokens = [], + convertedPrice = null, +}: { + children: React.ReactNode; + purchaseItems?: Item[]; + availableTokens?: Omit[]; + convertedPrice?: { + amount: bigint; + tokenMetadata: { + symbol: string; + decimals: number; + }; + } | null; +}) => { + const mockContext: PurchaseContextType = { + usdAmount: 100, + purchaseItems, + claimItems: [], + layerswapFees: undefined, + isFetchingFees: false, + selectedPlatform: "ethereum", + stripePromise: Promise.resolve(null), + isStripeLoading: false, + isDepositLoading: false, + isStarterpackLoading: false, + clearError: () => {}, + clearSelectedWallet: () => {}, + availableTokens: availableTokens as TokenOption[], + convertedPrice, + swapQuote: null, + isFetchingConversion: false, + conversionError: null, + setUsdAmount: () => {}, + setDepositAmount: () => {}, + setStarterpackId: () => {}, + setClaimItems: () => {}, + setTransactionHash: () => {}, + setSelectedToken: () => {}, + onCreditCardPurchase: async () => {}, + onBackendCryptoPurchase: async () => {}, + onOnchainPurchase: async () => {}, + onExternalConnect: async () => undefined, + waitForDeposit: async () => true, + fetchFees: async () => {}, + }; + + return ( + + {children} + + ); +}; + +// Extend the component args to include mock data +type StoryArgs = React.ComponentProps & { + mockPurchaseItems?: Item[]; + mockAvailableTokens?: Omit[]; + mockConvertedPrice?: { + amount: string; // String to avoid BigInt serialization + tokenMetadata: { + symbol: string; + decimals: number; + }; + }; +}; + const meta = { + parameters: { + layout: "centered", + }, component: OnchainCostBreakdown, argTypes: { quote: { control: false, // Disable controls for BigInt serialization }, }, -} satisfies Meta; + decorators: [ + (Story, { args }) => { + // Convert mockConvertedPrice with string amount to BigInt + const convertedPrice = args.mockConvertedPrice + ? { + amount: BigInt(args.mockConvertedPrice.amount), + tokenMetadata: args.mockConvertedPrice.tokenMetadata, + } + : null; + + return ( + +
+ +
+
+ ); + }, + ], +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; // USDC payment example with referral export const USDCPayment: Story = { args: { quote: { - basePrice: 100000000n, // $100 USDC (6 decimals) - protocolFee: 2500000n, // $2.50 protocol fee - referralFee: 5000000n, // $5 USDC referral fee - totalCost: 107500000n, // $107.50 total + basePrice: BigInt(100000000), // $100 USDC (6 decimals) + protocolFee: BigInt(2500000), // $2.50 protocol fee + referralFee: BigInt(5000000), // $5 USDC referral fee + totalCost: BigInt(107500000), // $107.50 total paymentToken: USDC_ADDRESS, paymentTokenMetadata: { symbol: "USDC", decimals: 6, }, }, - platform: "starknet", + mockPurchaseItems: [ + { + title: "Adventurer #8", + subtitle: "Adventurers", + icon: TOKEN_ICONS.LORDS, + value: 50.0, + type: ItemType.NFT, + }, + { + title: "Adventurer #12", + subtitle: "Adventurers", + icon: TOKEN_ICONS.LORDS, + value: 50.0, + type: ItemType.NFT, + }, + ], + mockAvailableTokens: [ + { + name: "USD Coin", + address: USDC_ADDRESS, + symbol: "USDC", + decimals: 6, + icon: TOKEN_ICONS.USDC, + }, + ], + mockConvertedPrice: { + amount: "107500000", // $107.50 equivalent (string to avoid BigInt serialization) + tokenMetadata: { + symbol: "USDC", + decimals: 6, + }, + }, }, }; @@ -39,10 +169,10 @@ export const USDCPayment: Story = { export const ETHPayment: Story = { args: { quote: { - basePrice: 50000000000000000n, // 0.05 ETH - protocolFee: 1250000000000000n, // 0.00125 ETH - referralFee: 2500000000000000n, // 0.0025 ETH - totalCost: 53750000000000000n, // 0.05375 ETH + basePrice: BigInt(50000000000000000), // 0.05 ETH + protocolFee: BigInt(1250000000000000), // 0.00125 ETH + referralFee: BigInt(2500000000000000), // 0.0025 ETH + totalCost: BigInt(53750000000000000), // 0.05375 ETH paymentToken: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", // ETH on Starknet paymentTokenMetadata: { @@ -50,7 +180,32 @@ export const ETHPayment: Story = { decimals: 18, }, }, - platform: "starknet", + mockPurchaseItems: [ + { + title: "Realms World Pack", + subtitle: "Premium Bundle", + icon: TOKEN_ICONS.LORDS, + value: 100.0, + type: ItemType.NFT, + }, + ], + mockAvailableTokens: [ + { + name: "Ethereum", + address: + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + symbol: "ETH", + decimals: 18, + icon: TOKEN_ICONS.ETH, + }, + ], + mockConvertedPrice: { + amount: "134375000000000000", // $134.375 equivalent in wei (string to avoid BigInt serialization) + tokenMetadata: { + symbol: "USDC", + decimals: 6, + }, + }, }, }; @@ -58,17 +213,34 @@ export const ETHPayment: Story = { export const NoReferral: Story = { args: { quote: { - basePrice: 50000000n, // $50 USDC - protocolFee: 1250000n, // $1.25 protocol fee - referralFee: 0n, // No referral - totalCost: 51250000n, // $51.25 total + basePrice: BigInt(50000000), // $50 USDC + protocolFee: BigInt(1250000), // $1.25 protocol fee + referralFee: BigInt(0), // No referral + totalCost: BigInt(51250000), // $51.25 total paymentToken: USDC_ADDRESS, paymentTokenMetadata: { symbol: "USDC", decimals: 6, }, }, - platform: "base", + mockPurchaseItems: [ + { + title: "Starter Credits", + subtitle: "Game Currency", + icon: TOKEN_ICONS.CREDITS, + value: 50.0, + type: ItemType.CREDIT, + }, + ], + mockAvailableTokens: [ + { + name: "USD Coin", + address: USDC_ADDRESS, + symbol: "USDC", + decimals: 6, + icon: TOKEN_ICONS.USDC, + }, + ], }, }; @@ -76,17 +248,48 @@ export const NoReferral: Story = { export const WithTooltipOpen: Story = { args: { quote: { - basePrice: 25000000n, // $25 USDC - protocolFee: 625000n, // $0.625 protocol fee - referralFee: 1250000n, // $1.25 referral - totalCost: 26875000n, // $26.875 total + basePrice: BigInt(25000000), // $25 USDC + protocolFee: BigInt(625000), // $0.625 protocol fee + referralFee: BigInt(1250000), // $1.25 referral + totalCost: BigInt(26875000), // $26.875 total paymentToken: USDC_ADDRESS, paymentTokenMetadata: { symbol: "USDC", decimals: 6, }, }, - platform: "ethereum", openFeesTooltip: true, + mockPurchaseItems: [ + { + title: "Epic Sword", + subtitle: "Legendary Weapon", + icon: TOKEN_ICONS.ETH, + value: 15.0, + type: ItemType.NFT, + }, + { + title: "Health Potion x5", + subtitle: "Consumables", + icon: TOKEN_ICONS.STRK, + value: 10.0, + type: ItemType.ERC20, + }, + ], + mockAvailableTokens: [ + { + name: "USD Coin", + address: USDC_ADDRESS, + symbol: "USDC", + decimals: 6, + icon: TOKEN_ICONS.USDC, + }, + ], + mockConvertedPrice: { + amount: "26875000", // $26.875 equivalent (string to avoid BigInt serialization) + tokenMetadata: { + symbol: "USDC", + decimals: 6, + }, + }, }, }; diff --git a/packages/keychain/src/components/purchasenew/review/onchain-tooltip.tsx b/packages/keychain/src/components/purchasenew/review/onchain-tooltip.tsx index 5f259e3c3..94a09b1df 100644 --- a/packages/keychain/src/components/purchasenew/review/onchain-tooltip.tsx +++ b/packages/keychain/src/components/purchasenew/review/onchain-tooltip.tsx @@ -6,6 +6,7 @@ import { TooltipTrigger, } from "@cartridge/ui"; import type { Quote } from "@/types/starterpack-types"; +import { usePurchaseContext } from "@/context"; /** * Format bigint token amount with symbol @@ -29,6 +30,22 @@ export const OnchainFeesTooltip = ({ quote: Quote; }) => { const { decimals, symbol } = quote.paymentTokenMetadata; + const { purchaseItems } = usePurchaseContext(); + + // Calculate individual item prices (distribute base price across items) + const itemPrices = purchaseItems.map((item) => { + // For now, distribute evenly. In the future, this could be based on actual item prices + const itemPrice = quote.basePrice / BigInt(purchaseItems.length); + return { + ...item, + tokenPrice: itemPrice, + }; + }); + + // Calculate fees + const marketplaceFee = 0n; // Marketplace fee is 0 as mentioned + const creatorRoyalties = 0n; // Creator royalties is 0 as mentioned + const clientFee = quote.protocolFee + quote.referralFee; // Merge protocol and referral as client fee return ( @@ -37,31 +54,48 @@ export const OnchainFeesTooltip = ({ + {/* Purchase Items */} + {itemPrices.map((item, index) => ( +
+ {item.title} + + {formatTokenAmount(item.tokenPrice, decimals, symbol)} + +
+ ))} + + + + {/* Fees */}
- Base Price: - {formatTokenAmount(quote.basePrice, decimals, symbol)} + Marketplace Fee: + + {formatTokenAmount(marketplaceFee, decimals, symbol)} (0.00%) +
- +
- Protocol Fee: + Creator Royalties: - {formatTokenAmount(quote.protocolFee, decimals, symbol)} + {formatTokenAmount(creatorRoyalties, decimals, symbol)} (0.00%)
- {quote.referralFee > 0n && ( - <> -
- Referral Fee: - - {formatTokenAmount(quote.referralFee, decimals, symbol)} - -
- - )} + +
+ Client Fee: + + {formatTokenAmount(clientFee, decimals, symbol)} (2.50%) + +
+ -
+ +
Total: {formatTokenAmount(quote.totalCost, decimals, symbol)}
diff --git a/packages/keychain/src/components/purchasenew/review/tooltip.tsx b/packages/keychain/src/components/purchasenew/review/tooltip.tsx index 578c606e9..585a4776c 100644 --- a/packages/keychain/src/components/purchasenew/review/tooltip.tsx +++ b/packages/keychain/src/components/purchasenew/review/tooltip.tsx @@ -22,22 +22,38 @@ export const FeesTooltip = ({ isStripe: boolean; costDetails: CostDetails; }) => { - const { layerswapFees } = usePurchaseContext(); + const { layerswapFees, purchaseItems } = usePurchaseContext(); - const cartridgeFeeInCents = useMemo(() => { - const percent = isStripe ? 0.05 : 0.025; - // round to nearest cent - return Math.round(costDetails.baseCostInCents * percent); - }, [costDetails.baseCostInCents, isStripe]); + // Calculate individual item prices (distribute base cost across items) + const itemPrices = useMemo(() => { + if (purchaseItems.length === 0) return []; + + return purchaseItems.map((item) => { + // For now, distribute evenly. In the future, this could be based on actual item prices + const itemPrice = costDetails.baseCostInCents / purchaseItems.length; + return { + ...item, + priceInCents: Math.round(itemPrice), + }; + }); + }, [purchaseItems, costDetails.baseCostInCents]); + + const clientFeeInCents = useMemo(() => { + const cartridgePercent = isStripe ? 0.05 : 0.025; + const cartridgeFee = Math.round( + costDetails.baseCostInCents * cartridgePercent, + ); - const stripeFeeInCents = useMemo(() => { - if (!isStripe) return 0; - const percentFee = Math.round(costDetails.baseCostInCents * 0.039); // 3.9% percent part - return percentFee + STRIPE_FIXED_FEE_CENTS; // include fixed fee + if (!isStripe) return cartridgeFee; + + // Add Stripe fee to Cartridge fee for total client fee + const stripePercentFee = Math.round(costDetails.baseCostInCents * 0.039); // 3.9% percent part + const stripeTotalFee = stripePercentFee + STRIPE_FIXED_FEE_CENTS; // include fixed fee + + return cartridgeFee + stripeTotalFee; }, [costDetails.baseCostInCents, isStripe]); - const cartridgeFee = convertCentsToDollars(cartridgeFeeInCents); - const stripeFee = convertCentsToDollars(stripeFeeInCents); + const clientFeePercentage = isStripe ? "8.9%" : "2.5%"; // Approximate combined percentage for Stripe + Cartridge return ( @@ -46,29 +62,52 @@ export const FeesTooltip = ({ + {/* Purchase Items */} + {itemPrices.map((item, index) => ( +
+ {item.title} + {convertCentsToDollars(item.priceInCents)} +
+ ))} + + + + {/* Fees */}
- Credits: - {convertCentsToDollars(costDetails.baseCostInCents)} + Marketplace Fee: + $0.00 (0.00%)
- - {isStripe && ( -
- Stripe Fee: - {stripeFee} -
- )} + +
+ Creator Royalties: + $0.00 (0.00%) +
+
- Cartridge Fee: - {cartridgeFee} + Client Fee: + + {convertCentsToDollars(clientFeeInCents)} ({clientFeePercentage}) +
+ {layerswapFees && (
- Layerswap Bridging Fee:{" "} -
${(Number(layerswapFees) / 1e6).toFixed(2)}
+ Layerswap Bridging Fee: + ${(Number(layerswapFees) / 1e6).toFixed(2)}
)} + + + +
+ Total: + {convertCentsToDollars(costDetails.totalInCents)} +
diff --git a/packages/keychain/src/constants.ts b/packages/keychain/src/constants.ts index a8f6fc68b..a6a7bc483 100644 --- a/packages/keychain/src/constants.ts +++ b/packages/keychain/src/constants.ts @@ -12,3 +12,16 @@ export const CLIENT_FEE_NUMERATOR = 250; export const CLIENT_FEE_DENOMINATOR = 10_000; export const CLIENT_FEE_RECEIVER = "0x03F7F4E5a23A712787F0C100f02934c4A88606B7F0C880c2FD43e817E6275d83"; + +// Token icon URLs + +export const TOKEN_ICON_BASE_URL = + "https://imagedelivery.net/0xPAQaDtnQhBs8IzYRIlNg"; + +export const TOKEN_ICONS = { + USDC: `${TOKEN_ICON_BASE_URL}/e5aaa970-a998-47e8-bd43-4a3b56b87200/logo`, + STRK: `${TOKEN_ICON_BASE_URL}/1b126320-367c-48ed-cf5a-ba7580e49600/logo`, + ETH: `${TOKEN_ICON_BASE_URL}/e07829b7-0382-4e03-7ecd-a478c5aa9f00/logo`, + LORDS: `${TOKEN_ICON_BASE_URL}/a3bfe959-50c4-4f89-0aef-b19207d82a00/logo`, + CREDITS: `${TOKEN_ICON_BASE_URL}/e5aaa970-a998-47e8-bd43-4a3b56b87200/logo`, // Using USDC icon for credits +} as const;