diff --git a/examples/react/src/components/Connected.tsx b/examples/react/src/components/Connected.tsx index b97bab9bd..a376f9cf8 100644 --- a/examples/react/src/components/Connected.tsx +++ b/examples/react/src/components/Connected.tsx @@ -4,9 +4,9 @@ import { useCheckoutModal, useERC1155SaleContractCheckout, useSelectPaymentModal, - useSwapModal + useSwapModal, + type SwapModalSettings } from '@0xsequence/checkout' -import type { SwapModalSettings } from '@0xsequence/checkout' import { getModalPositionCss, signEthAuthProof, @@ -23,16 +23,15 @@ import { useOpenWalletModal } from '@0xsequence/wallet-widget' import { CardButton, Header, WalletListItem } from 'example-shared-components' import { AnimatePresence } from 'motion/react' import React, { useEffect, type ComponentProps } from 'react' -import { encodeFunctionData, formatUnits, parseAbi, toHex, zeroAddress } from 'viem' +import { encodeFunctionData, formatUnits, parseAbi } from 'viem' import { createSiweMessage, generateSiweNonce } from 'viem/siwe' import { useAccount, useChainId, usePublicClient, useSendTransaction, useWalletClient, useWriteContract } from 'wagmi' import { sponsoredContractAddresses } from '../config' import { messageToSign } from '../constants' -import { ERC_1155_SALE_CONTRACT } from '../constants/erc1155-sale-contract' -// import { ERC_721_SALE_CONTRACT } from '../constants/erc721-sale-contract' import { abi } from '../constants/nft-abi' import { delay, getCheckoutSettings, getOrderbookCalldata } from '../utils' +import { checkoutPresets } from '../utils/checkout' import { CustomCheckout } from './CustomCheckout' import { Select } from './Select' @@ -42,6 +41,7 @@ const searchParams = new URLSearchParams(location.search) const isDebugMode = searchParams.has('debug') const checkoutProvider = searchParams.get('checkoutProvider') const onRampProvider = searchParams.get('onRampProvider') +const checkoutPreset = searchParams.get('checkoutPreset') || 'forte-payment-erc1155-sale-native-token-testnet' export const Connected = () => { const [isOpenCustomCheckout, setIsOpenCustomCheckout] = React.useState(false) @@ -413,78 +413,16 @@ export const Connected = () => { return } - // NATIVE token sale - const currencyAddress = zeroAddress - const salesContractAddress = '0xf0056139095224f4eec53c578ab4de1e227b9597' - const collectionAddress = '0x92473261f2c26f2264429c451f70b0192f858795' - const price = '200000000000000' - const contractId = '674eb55a3d739107bbd18ecb' - - // // ERC-20 contract - // const currencyAddress = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' - // const salesContractAddress = '0xe65b75eb7c58ffc0bf0e671d64d0e1c6cd0d3e5b' - // const collectionAddress = '0xdeb398f41ccd290ee5114df7e498cf04fac916cb' - // const price = '200000' - // const contractId = '674eb5613d739107bbd18ed2' - - const collectibles = [ - { - tokenId: '1', - quantity: '1' - } - ] - - const purchaseTransactionData = encodeFunctionData({ - abi: ERC_1155_SALE_CONTRACT, - functionName: 'mint', - // [to, tokenIds, amounts, data, expectedPaymentToken, maxTotal, proof] - args: [ - address, - collectibles.map(c => BigInt(c.tokenId)), - collectibles.map(c => BigInt(c.quantity)), - toHex(0), - currencyAddress, - price, - [toHex(0, { size: 32 })] - ] - }) - - // ERC-721 contract - // const currencyAddress = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' - // const salesContractAddress = '0xa0284905d29cbeb19f4be486f9091fac215b7a6a' - // const collectionAddress = '0xd705db0a96075b98758c4bdafe8161d8566a68f8' - // const price = '1' - // const contractId = '674eb5613d739107bbd18ed2' - - // const chainId = 137 - - // const collectibles = [ - // { - // quantity: '1' - // } - // ] - - // const purchaseTransactionData = encodeFunctionData({ - // abi: ERC_721_SALE_CONTRACT, - // functionName: 'mint', - // // [to, amount, expectedPaymentToken, maxTotal, proof] - // args: [address, BigInt(1), currencyAddress, price, [toHex(0, { size: 32 })]] - // }) + const creditCardProvider = checkoutProvider || 'forte' openSelectPaymentModal({ - collectibles, - chain: chainId, - price, - targetContractAddress: salesContractAddress, recipientAddress: address, - currencyAddress, - collectionAddress, - creditCardProviders: [checkoutProvider || 'transak'], + creditCardProviders: [creditCardProvider], onRampProvider: onRampProvider ? (onRampProvider as TransactionOnRampProvider) : TransactionOnRampProvider.transak, transakConfig: { - contractId + contractId: '674eb5613d739107bbd18ed2' }, - onSuccess: (txnHash: string) => { + onSuccess: (txnHash?: string) => { console.log('success!', txnHash) }, onError: (error: Error) => { @@ -493,7 +431,7 @@ export const Connected = () => { onClose: () => { console.log('modal closed!') }, - txData: purchaseTransactionData + ...checkoutPresets[checkoutPreset as keyof typeof checkoutPresets](address || '') }) } diff --git a/examples/react/src/components/CustomCheckout/index.tsx b/examples/react/src/components/CustomCheckout/index.tsx index 92ffcfb48..44eb6e32d 100644 --- a/examples/react/src/components/CustomCheckout/index.tsx +++ b/examples/react/src/components/CustomCheckout/index.tsx @@ -59,7 +59,7 @@ export const CustomCheckout = () => { contractId, apiKey: '5911d9ec-46b5-48fa-a755-d59a715ff0cf' }, - onSuccess: (txnHash: string) => { + onSuccess: (txnHash?: string) => { console.log('success!', txnHash) }, onError: (error: Error) => { diff --git a/examples/react/src/config.ts b/examples/react/src/config.ts index cd3cfa834..5e3ef4f3c 100644 --- a/examples/react/src/config.ts +++ b/examples/react/src/config.ts @@ -15,7 +15,7 @@ const walletType: WalletType = searchParams.get('type') === 'universal' ? 'unive const isDebugMode = searchParams.has('debug') // @ts-ignore const isDev = __SEQUENCE_WEB_SDK_IS_DEV__ -const projectAccessKey = isDev ? 'AQAAAAAAAAK2JvvZhWqZ51riasWBftkrVXE' : 'AQAAAAAAAEGvyZiWA9FMslYeG_yayXaHnSI' +const projectAccessKey = isDev ? 'AQAAAAAAAAbRfXdDS5e-ZD2pNeMcCtNnij4' : 'AQAAAAAAAEGvyZiWA9FMslYeG_yayXaHnSI' const walletConnectProjectId = 'c65a6cb1aa83c4e24500130f23a437d8' export const sponsoredContractAddresses: Record = { @@ -184,7 +184,8 @@ export const checkoutConfig: SequenceCheckoutConfig = { sardineCheckoutUrl: 'https://sardine-checkout-sandbox.sequence.info', sardineOnRampUrl: 'https://crypto.sandbox.sardine.ai/', transakApiUrl: 'https://global-stg.transak.com', - transakApiKey: 'c20f2a0e-fe6a-4133-8fa7-77e9f84edf98' + transakApiKey: 'c20f2a0e-fe6a-4133-8fa7-77e9f84edf98', + forteWidgetUrl: 'https://payments.sandbox.lemmax.com/forte-payments-widget.js' } : undefined } diff --git a/examples/react/src/utils/checkout.ts b/examples/react/src/utils/checkout.ts new file mode 100644 index 000000000..2118f82d9 --- /dev/null +++ b/examples/react/src/utils/checkout.ts @@ -0,0 +1,422 @@ +import { type ForteConfig } from '@0xsequence/checkout' +import { zeroAddress } from 'viem' +import { encodeFunctionData, toHex } from 'viem' + +import { ERC_1155_SALE_CONTRACT } from '../constants/erc1155-sale-contract' +import { ERC_721_SALE_CONTRACT } from '../constants/erc721-sale-contract' +import { orderbookAbi } from '../constants/orderbook-abi' + +interface PurchaseTransactionDataERC721Sale { + recipientAddress: string + currencyAddress: string + price: string +} + +const getPurchaseTransactionERC721Sale = ({ recipientAddress, currencyAddress, price }: PurchaseTransactionDataERC721Sale) => { + return encodeFunctionData({ + abi: ERC_721_SALE_CONTRACT, + functionName: 'mint', + // [to, amount, expectedPaymentToken, maxTotal, proof] + args: [recipientAddress, BigInt(1), currencyAddress, price, [toHex(0, { size: 32 })]] + }) as `0x${string}` +} + +interface PurchaseTransactionDataERC1155Sale { + recipientAddress: string + currencyAddress: string + price: string + collectibles: { + tokenId: string + quantity: string + }[] +} + +const getPurchaseTransactionERC1155Sale = ({ + recipientAddress, + currencyAddress, + price, + collectibles +}: PurchaseTransactionDataERC1155Sale) => { + return encodeFunctionData({ + abi: ERC_1155_SALE_CONTRACT, + functionName: 'mint', + // [to, tokenIds, amounts, data, expectedPaymentToken, maxTotal, proof] + args: [ + recipientAddress, + collectibles.map(c => BigInt(c.tokenId)), + collectibles.map(c => BigInt(c.quantity)), + toHex(0), + currencyAddress, + price, + [toHex(0, { size: 32 })] + ] + }) as `0x${string}` +} + +interface GetOrderbookTransactionDataArgs { + recipientAddress: string + requestId: string + quantity: string +} + +const getOrderbookTransactionData = ({ recipientAddress, requestId, quantity }: GetOrderbookTransactionDataArgs) => { + return encodeFunctionData({ + abi: orderbookAbi, + functionName: 'acceptRequest', + args: [requestId, quantity, recipientAddress, [], []] + }) as `0x${string}` +} + +interface CheckoutPreset { + chain: number | string + currencyAddress: string + targetContractAddress: string + collectionAddress: string + price: string + collectibles: { + tokenId?: string + quantity: string + }[] + txData: `0x${string}` + forteConfig?: ForteConfig +} + +export const checkoutPresets: Record CheckoutPreset> = { + 'erc1155-sale-native-token-polygon': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '200000000000000' + return { + chain: 137, + currencyAddress: zeroAddress, + targetContractAddress: '0xf0056139095224f4eec53c578ab4de1e227b9597', + collectionAddress: '0x92473261f2c26f2264429c451f70b0192f858795', + price, + collectibles, + txData: getPurchaseTransactionERC1155Sale({ + recipientAddress, + currencyAddress: zeroAddress, + price, + collectibles + }) + } + }, + 'erc1155-sale-erc20-token-polygon': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '20000' + return { + chain: 137, + currencyAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + targetContractAddress: '0xe65b75eb7c58ffc0bf0e671d64d0e1c6cd0d3e5b', + collectionAddress: '0xdeb398f41ccd290ee5114df7e498cf04fac916cb', + price, + collectibles, + txData: getPurchaseTransactionERC1155Sale({ + recipientAddress, + currencyAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + price, + collectibles + }) + } + }, + 'erc721-sale-erc20-token-polygon': (recipientAddress: string) => { + const collectibles = [ + { + quantity: '1' + } + ] + const price = '1' + return { + chain: 137, + currencyAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + targetContractAddress: '0xa0284905d29cbeb19f4be486f9091fac215b7a6a', + collectionAddress: '0xd705db0a96075b98758c4bdafe8161d8566a68f8', + price, + collectibles, + txData: getPurchaseTransactionERC721Sale({ + recipientAddress, + currencyAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + price + }) + } + }, + 'forte-payment-testnet-testing-opensea': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '1000000000000000' + const structuredCalldata = '' + return { + chain: 11155111, + currencyAddress: zeroAddress, + targetContractAddress: '0x1130e2e03f682f05f298fd702787d9bd0bf94316', + collectionAddress: '0xb496d64e1fe4f3465fb83f3fd8cb50d8e227101b', + price, + collectibles, + txData: getPurchaseTransactionERC1155Sale({ + recipientAddress, + currencyAddress: zeroAddress, + price, + collectibles + }), + forteConfig: { + protocol: 'custom_evm_call', + calldata: structuredCalldata, + sellerAddress: '0x184D4F89ad34bb0491563787ca28118273402986' + } + } + }, + 'forte-payment-erc1155-sale-native-token-testnet': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '1000000000000000' + + const structuredCalldata = { + functionName: 'mint', + arguments: [ + { + type: 'address', + value: '${receiver_address}' + }, + { + type: 'uint256[]', + value: ['1'] + }, + { + type: 'uint256[]', + value: ['1'] + }, + { + type: 'bytes', + value: toHex(0) + }, + { + type: 'address', + value: zeroAddress + }, + { + type: 'uint256', + value: price + }, + { + type: 'bytes32[]', + value: [toHex(0, { size: 32 })] + } + ] + } + + return { + chain: 11155111, + currencyAddress: zeroAddress, + targetContractAddress: '0x1130e2e03f682f05f298fd702787d9bd0bf94316', + collectionAddress: '0xb496d64e1fe4f3465fb83f3fd8cb50d8e227101b', + price, + collectibles, + txData: getPurchaseTransactionERC1155Sale({ + recipientAddress, + currencyAddress: zeroAddress, + price, + collectibles + }), + forteConfig: { + protocol: 'mint', + calldata: structuredCalldata, + sellerAddress: '0x184D4F89ad34bb0491563787ca28118273402986' + } + } + }, + 'forte-payment-custom-evm-call-native-token-testnet': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '1000000000000000' + const requestId = '40' + const txData = getOrderbookTransactionData({ + recipientAddress: recipientAddress, + requestId, + quantity: collectibles[0].quantity + }) + + const structuredCalldata = { + functionName: 'acceptRequest', + arguments: [ + { + type: 'uint256', + value: requestId + }, + { + type: 'uint256', + value: collectibles[0].quantity + }, + { + type: 'address', + value: '${receiver_address}' + }, + { + type: 'uint256[]', + value: [] + }, + { + type: 'address[]', + value: [] + } + ] + } + + return { + chain: 11155111, + currencyAddress: zeroAddress, + targetContractAddress: '0xfdb42A198a932C8D3B506Ffa5e855bC4b348a712', + collectionAddress: '0xb496d64e1fe4f3465fb83f3fd8cb50d8e227101b', + price, + collectibles, + txData, + forteConfig: { + protocol: 'custom_evm_call', + calldata: structuredCalldata, + sellerAddress: '0x184D4F89ad34bb0491563787ca28118273402986' + } + } + }, + 'forte-payment-erc1155-sale-erc20-token-testnet': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '1000000000000000' + const currencyAddress = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14' + + const structuredCalldata = { + functionName: 'mint', + arguments: [ + { + type: 'address', + value: '${receiver_address}' + }, + { + type: 'uint256[]', + value: ['1'] + }, + { + type: 'uint256[]', + value: ['1'] + }, + { + type: 'bytes', + value: toHex(0) + }, + { + type: 'address', + value: currencyAddress + }, + { + type: 'uint256', + value: price + }, + { + type: 'bytes32[]', + value: [toHex(0, { size: 32 })] + } + ] + } + + return { + chain: 11155111, + currencyAddress, + targetContractAddress: '0x0c29598a69aeda9f3fed0ba64a2d94c54f83e8c6', + approvedSpenderAddress: '0x0c29598a69aeda9f3fed0ba64a2d94c54f83e8c6', + collectionAddress: '0xb496d64e1fe4f3465fb83f3fd8cb50d8e227101b', + price, + collectibles, + txData: getPurchaseTransactionERC1155Sale({ + recipientAddress, + currencyAddress, + price, + collectibles + }), + forteConfig: { + protocol: 'mint', + calldata: structuredCalldata, + sellerAddress: '0x184D4F89ad34bb0491563787ca28118273402986' + } + } + }, + 'forte-payment-erc1155-orderbook-erc20-token-testnet': (recipientAddress: string) => { + const collectibles = [ + { + tokenId: '1', + quantity: '1' + } + ] + const price = '1000000000000000' + const currencyAddress = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14' + const requestId = '41' + const txData = getOrderbookTransactionData({ + recipientAddress: recipientAddress, + requestId, + quantity: collectibles[0].quantity + }) + + const structuredCalldata = { + functionName: 'acceptRequest', + arguments: [ + { + type: 'uint256', + value: requestId + }, + { + type: 'uint256', + value: collectibles[0].quantity + }, + { + type: 'address', + value: '${receiver_address}' + }, + { + type: 'uint256[]', + value: [] + }, + { + type: 'address[]', + value: [] + } + ] + } + + return { + chain: 11155111, + currencyAddress, + targetContractAddress: '0xfdb42A198a932C8D3B506Ffa5e855bC4b348a712', + collectionAddress: '0xb496d64e1fe4f3465fb83f3fd8cb50d8e227101b', + price, + collectibles, + txData, + forteConfig: { + protocol: 'custom_evm_call', + calldata: structuredCalldata, + sellerAddress: '0x184D4F89ad34bb0491563787ca28118273402986' + } + } + } +} diff --git a/packages/checkout/README.md b/packages/checkout/README.md index 603b1e853..6c78c2373 100644 --- a/packages/checkout/README.md +++ b/packages/checkout/README.md @@ -102,7 +102,7 @@ const MyComponent = () => { collectionAddress, creditCardProviders: ['sardine'], copyrightText: 'ⓒ2024 Sequence', - onSuccess: (txnHash: string) => { + onSuccess: (txnHash?: string) => { console.log('success!', txnHash) }, onError: (error: Error) => { @@ -156,7 +156,7 @@ const MyComponent = () => { quantity: "1", }, ], - onSuccess: (txnHash: string) => { + onSuccess: (txnHash?: string) => { console.log("success!", txnHash); }, onError: (error: Error) => { @@ -278,7 +278,7 @@ const CustomCheckoutUI = () => { contractId, apiKey: '5911d9ec-46b5-48fa-a755-d59a715ff0cf' }, - onSuccess: (txnHash: string) => { + onSuccess: (txnHash?: string) => { console.log('success!', txnHash) }, onError: (error: Error) => { diff --git a/packages/checkout/src/api/data.ts b/packages/checkout/src/api/data.ts index 75c44eb5c..fd0dfe2ea 100644 --- a/packages/checkout/src/api/data.ts +++ b/packages/checkout/src/api/data.ts @@ -1,8 +1,9 @@ import type { SequenceAPIClient } from '@0xsequence/api' import type { TokenMetadata } from '@0xsequence/metadata' -import { networks, type ChainId } from '@0xsequence/network' +import { findSupportedNetwork, networks, type ChainId } from '@0xsequence/network' +import { zeroAddress } from 'viem' -import type { CreditCardCheckout } from '../contexts/CheckoutModal.js' +import { type CreditCardCheckout, type ForteConfig, type StructuredCalldata } from '../contexts/index.js' export interface FetchSardineClientTokenReturn { token: string @@ -233,3 +234,268 @@ export const fetchSardineOnRampLink = async ({ return url.href } + +export interface FetchForteAccessTokenReturn { + accessToken: string + expiresIn: number + tokenType: string +} + +export const fetchForteAccessToken = async (forteApiUrl: string): Promise => { + const clientId = '' + const clientSecret = '' + + const url = `${forteApiUrl}/auth/v1/oauth2/token` + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + client_id: clientId, + client_secret: clientSecret + }) + }) + + const { data } = await res.json() + + return { + accessToken: data.access_token, + expiresIn: data.expires_in, + tokenType: data.token_type + } +} + +export interface CreateFortePaymentIntentArgs { + recipientAddress: string + chainId: string + signature?: string + nftAddress: string + currencyAddress: string + targetContractAddress: string + nftName: string + imageUrl: string + tokenId?: string + currencyQuantity: string + protocolConfig: ForteConfig + calldata: string | StructuredCalldata + approvedSpenderAddress?: string +} + +const forteCurrencyMap: { [chainId: string]: { [currencyAddress: string]: string } } = { + '1': { + [zeroAddress]: 'ETH', + ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'.toLowerCase()]: 'USDC_ETH' + }, + '137': { + [zeroAddress]: 'POL', + ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'.toLowerCase()]: 'USDC_POLYGON' + }, + '8453': { + [zeroAddress]: 'BASE_ETH' + }, + '11155111': { + [zeroAddress]: 'ETH', + ['0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'.toLowerCase()]: 'WETH' + } +} + +const getForteCurrency = (chainId: string, currencyAddress: string) => { + return forteCurrencyMap[chainId]?.[currencyAddress.toLowerCase()] || 'ETH' +} + +export const createFortePaymentIntent = async ( + sequenceApiUrl: string, + projectAccessKey: string, + args: CreateFortePaymentIntentArgs +): Promise => { + const { + recipientAddress, + chainId, + calldata, + targetContractAddress, + nftName, + nftAddress, + imageUrl, + tokenId, + protocolConfig, + currencyAddress, + currencyQuantity, + approvedSpenderAddress + } = args + + const network = findSupportedNetwork(chainId) + + if (!network) { + throw new Error('Invalid chainId') + } + + const url = `${sequenceApiUrl}/rpc/API/FortePayCreateIntent` + const forteBlockchainName = network.name.toLowerCase().replace('-', '_') + const idempotencyKey = `${recipientAddress}-${tokenId}-${targetContractAddress}-${nftName}-${new Date().getTime()}` + + let intent: { [key: string]: any } = { + blockchain: forteBlockchainName, + idempotencyKey: idempotencyKey, + buyer: { + id: recipientAddress, + wallet: { + address: recipientAddress, + blockchain: forteBlockchainName + } + } + } + + if (protocolConfig.protocol == 'mint') { + intent = { + ...intent, + transactionType: 'BUY_NFT_MINT', + currency: getForteCurrency(chainId, currencyAddress), + seller: { + wallet: { + address: protocolConfig.sellerAddress, + blockchain: forteBlockchainName + } + }, + items: [ + { + id: '1', + amount: currencyQuantity, + imageUrl: imageUrl, + title: nftName, + mintData: { + ...(approvedSpenderAddress ? { payToAddress: approvedSpenderAddress } : {}), + tokenContractAddress: nftAddress, + tokenIds: tokenId ? [tokenId] : [], + protocolAddress: targetContractAddress, + protocol: 'custom_evm_call', + ...(typeof calldata === 'string' + ? { + calldata: calldata + } + : { + structuredCalldata: { + function_name: calldata.functionName, + arguments: calldata.arguments + } + }) + } + } + ] + } + } else { + let listingData: { [key: string]: any } = {} + + if (protocolConfig.protocol == 'custom_evm_call') { + listingData = { + ...(approvedSpenderAddress ? { payToAddress: approvedSpenderAddress } : {}), + protocol: protocolConfig.protocol, + protocolAddress: targetContractAddress, + ...(typeof protocolConfig.calldata === 'string' + ? { calldata: protocolConfig.calldata } + : { + structuredCalldata: { + function_name: protocolConfig.calldata.functionName, + arguments: protocolConfig.calldata.arguments + } + }) + } + } + + intent = { + ...intent, + transactionType: 'BUY_NFT', + currency: getForteCurrency(chainId, currencyAddress), + items: [ + { + amount: currencyQuantity, + id: '1', + imageUrl: imageUrl, + listingData: listingData, + nftData: { + contractAddress: nftAddress, + tokenId: tokenId + }, + title: nftName + } + ], + seller: { + wallet: { + address: protocolConfig.sellerAddress || '', + blockchain: forteBlockchainName + } + } + } + } + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': projectAccessKey + }, + body: JSON.stringify({ intent }) + }) + + if (!res.ok) { + let errorMessage = `Failed to fetch widget data, with status: ${res.status}` + + try { + const data = await res.json() + + if (data.cause) { + errorMessage = `Failed to fetch widget data: ${data.cause}` + } else if (data.message) { + errorMessage = `Failed to fetch widget data: ${data.message}` + } else if (data.error) { + errorMessage = `Failed to fetch widget data: ${data.error}` + } + } catch (parseError) { + console.error('Could not parse error response as JSON:', parseError) + } + + throw new Error(errorMessage) + } + + const data = await res.json() + + return data.resp +} + +export interface FetchFortePaymentStatusArgs { + paymentIntentId: string +} + +export type FortePaymentStatus = 'Expired' | 'Created' | 'Declined' | 'Approved' + +export interface FetchFortePaymentStatusReturn { + status: FortePaymentStatus +} + +export const fetchFortePaymentStatus = async ( + forteApiUrl: string, + projectAccessKey: string, + args: FetchFortePaymentStatusArgs +): Promise => { + const { paymentIntentId } = args + + const url = `${forteApiUrl}/rpc/API/FortePayGetPaymentStatuses` + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': projectAccessKey + }, + body: JSON.stringify({ + paymentIntentIds: [paymentIntentId] + }) + }) + + const { statuses } = await res.json() + + return { + status: (statuses[0]?.status as FortePaymentStatus) || '' + } +} diff --git a/packages/checkout/src/components/SequenceCheckoutProvider/ForteController.tsx b/packages/checkout/src/components/SequenceCheckoutProvider/ForteController.tsx new file mode 100644 index 000000000..e58ea8b34 --- /dev/null +++ b/packages/checkout/src/components/SequenceCheckoutProvider/ForteController.tsx @@ -0,0 +1,136 @@ +import { useConfig } from '@0xsequence/hooks' +import { useEffect, useState } from 'react' + +import { fetchFortePaymentStatus } from '../../api/data.js' +import { FortePaymentControllerProvider, useEnvironmentContext, type FortePaymentData } from '../../contexts/index.js' + +const POLLING_TIME = 10 * 1000 + +export const ForteController = ({ children }: { children: React.ReactNode }) => { + const [fortePaymentData, setFortePaymentData] = useState() + const { forteWidgetUrl } = useEnvironmentContext() + const [isSuccess, setIsSuccess] = useState(false) + const [widgetInitialized, setWidgetInitialized] = useState(false) + const { env, projectAccessKey } = useConfig() + const apiUrl = env.apiUrl + + const initializeWidget = (fortePaymentData: FortePaymentData) => { + setIsSuccess(false) + setFortePaymentData(fortePaymentData) + } + + useEffect(() => { + const widgetInitializedListener: () => void = () => { + setWidgetInitialized(true) + } + + if (!widgetInitialized) { + window.addEventListener('FortePaymentsWidgetLoaded', widgetInitializedListener) + } + + return () => { + window.removeEventListener('FortePaymentsWidgetLoaded', widgetInitializedListener) + } + }, [widgetInitialized, fortePaymentData]) + + useEffect(() => { + if (widgetInitialized && fortePaymentData) { + const widgetData = fortePaymentData.widgetData + + // @ts-ignore-next-line + if (window?.initFortePaymentsWidget && widgetData) { + const data = { + notes: widgetData.notes, + widget_data: widgetData.widgetData, + payment_intent_id: widgetData.paymentIntentId, + error_code: widgetData.error_code ?? null, + flow: widgetData.flow + } + + // @ts-ignore-next-line + window.initFortePaymentsWidget({ + containerId: 'forte-payments-widget-container', + data + }) + } + } + }, [widgetInitialized, fortePaymentData]) + + useEffect(() => { + let interval: NodeJS.Timeout | undefined + let widgetClosedListener: () => void + + if (fortePaymentData && !isSuccess) { + interval = setInterval(() => { + checkFortePaymentStatus() + }, POLLING_TIME) + + widgetClosedListener = () => { + fortePaymentData.creditCardCheckout?.onClose?.() + } + + window.addEventListener('FortePaymentsWidgetClosed', widgetClosedListener) + } + + return () => { + clearInterval(interval) + window.removeEventListener('FortePaymentsWidgetClosed', widgetClosedListener) + } + }, [fortePaymentData, isSuccess, widgetInitialized]) + + useEffect(() => { + if (!fortePaymentData) { + return + } + if (document.getElementById('forte-widget-script')) { + return + } + + const container = document.createElement('div') + container.id = 'forte-payments-widget-container' + document.body.appendChild(container) + + const script = document.createElement('script') + script.id = 'forte-widget-script' + script.type = 'module' + script.async = true + script.src = forteWidgetUrl + + document.body.appendChild(script) + + // After loading the script, Forte will generate a FortePaymentsWidgetLoaded event when the widget is ready to be called + }, [fortePaymentData]) + + const checkFortePaymentStatus = async () => { + if (!fortePaymentData || isSuccess) { + return + } + + const { status } = await fetchFortePaymentStatus(apiUrl, projectAccessKey, { + paymentIntentId: fortePaymentData.paymentIntentId + }) + + if (status === 'Approved') { + fortePaymentData.creditCardCheckout?.onSuccess?.() + setIsSuccess(true) + } + + if (status === 'Declined' || status === 'Expired') { + fortePaymentData.creditCardCheckout?.onError?.( + new Error('A problem occurred while processing your payment'), + fortePaymentData.creditCardCheckout + ) + } + } + + return ( + + {children} + + ) +} diff --git a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx index 15832ae78..10d4c008f 100644 --- a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx +++ b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx @@ -42,6 +42,8 @@ import { } from '../../views/index.js' import { NavigationHeader } from '../NavigationHeader.js' +import { ForteController } from './ForteController.js' + export interface SequenceCheckoutConfig { env?: Partial } @@ -245,177 +247,180 @@ export const SequenceCheckoutProvider = ({ children, config }: SequenceCheckoutP sardineCheckoutUrl: config?.env?.sardineCheckoutUrl ?? 'https://sardine-checkout.sequence.info', sardineOnRampUrl: config?.env?.sardineOnRampUrl ?? 'https://crypto.sardine.ai/', transakApiUrl: config?.env?.transakApiUrl ?? 'https://global.transak.com', - transakApiKey: config?.env?.transakApiKey ?? '5911d9ec-46b5-48fa-a755-d59a715ff0cf' + transakApiKey: config?.env?.transakApiKey ?? '5911d9ec-46b5-48fa-a755-d59a715ff0cf', + forteWidgetUrl: config?.env?.forteWidgetUrl ?? 'https://payments.prod.lemmax.com/forte-payments-widget.js' }} > - - + - - - - - - - - - {openCheckoutModal && ( - setOpenCheckoutModal(false)} - > -
- {getCheckoutHeader()} - {getCheckoutContent()} -
-
- )} - {openAddFundsModal && ( - -
- {getAddFundsHeader()} - {getAddFundsContent()} -
-
- )} - {openPaymentSelectionModal && ( - setOpenPaymentSelectionModal(false)} - > -
- {getCheckoutFlowHeader()} - {getCheckoutFlowContent()} -
-
- )} - {openTransferFundsModal && ( - -
- - -
-
- )} - {openTransactionStatusModal && ( - -
- -
-
- )} - {isOpenSwapModal && ( - -
- - -
-
- )} -
-
- {children} -
-
-
-
-
-
-
-
+ + + + + + {openCheckoutModal && ( + setOpenCheckoutModal(false)} + > +
+ {getCheckoutHeader()} + {getCheckoutContent()} +
+
+ )} + {openAddFundsModal && ( + +
+ {getAddFundsHeader()} + {getAddFundsContent()} +
+
+ )} + {openPaymentSelectionModal && ( + setOpenPaymentSelectionModal(false)} + > +
+ {getCheckoutFlowHeader()} + {getCheckoutFlowContent()} +
+
+ )} + {openTransferFundsModal && ( + +
+ + +
+
+ )} + {openTransactionStatusModal && ( + +
+ +
+
+ )} + {isOpenSwapModal && ( + +
+ + +
+
+ )} +
+
+ {children} +
+
+
+ + + + + + ) } diff --git a/packages/checkout/src/contexts/CheckoutModal.ts b/packages/checkout/src/contexts/CheckoutModal.ts index 6d2d9d8ac..ad4eaf8b6 100644 --- a/packages/checkout/src/contexts/CheckoutModal.ts +++ b/packages/checkout/src/contexts/CheckoutModal.ts @@ -24,6 +24,27 @@ export interface TransakConfig { callDataOverride?: string } +export type ForteProtocolType = 'seaport' | 'mint' | 'custom_evm_call' + +export interface StructuredCalldata { + functionName: string + arguments: any[] +} + +export interface ForteMintConfig { + protocol: 'mint' + calldata: string | StructuredCalldata + sellerAddress: string +} + +export interface ForteCustomEvmCallConfig { + protocol: 'custom_evm_call' + calldata: string | StructuredCalldata + sellerAddress: string +} + +export type ForteConfig = ForteMintConfig | ForteCustomEvmCallConfig + export interface CreditCardCheckout { chainId: number contractAddress: string @@ -37,9 +58,10 @@ export interface CreditCardCheckout { nftQuantity: string nftDecimals?: string calldata: string - provider?: 'sardine' | 'transak' + provider?: 'sardine' | 'transak' | 'forte' transakConfig?: TransakConfig - onSuccess?: (transactionHash: string, settings: CreditCardCheckout) => void + forteConfig?: ForteConfig + onSuccess?: (transactionHash?: string, settings?: CreditCardCheckout) => void onError?: (error: Error, settings: CreditCardCheckout) => void onClose?: () => void approvedSpenderAddress?: string diff --git a/packages/checkout/src/contexts/Environment.ts b/packages/checkout/src/contexts/Environment.ts index 54b3fc7dc..94cf353af 100644 --- a/packages/checkout/src/contexts/Environment.ts +++ b/packages/checkout/src/contexts/Environment.ts @@ -8,6 +8,7 @@ export interface EnvironmentOverrides { transakApiKey: string sardineCheckoutUrl: string sardineOnRampUrl: string + forteWidgetUrl: string } const [useEnvironmentContext, EnvironmentContextProvider] = createGenericContext() diff --git a/packages/checkout/src/contexts/FortePayment.ts b/packages/checkout/src/contexts/FortePayment.ts new file mode 100644 index 000000000..8eec10a30 --- /dev/null +++ b/packages/checkout/src/contexts/FortePayment.ts @@ -0,0 +1,19 @@ +'use client' + +import { type CreditCardCheckout } from './CheckoutModal.js' +import { createGenericContext } from './genericContext.js' + +export interface FortePaymentData { + paymentIntentId: string + widgetData: any + creditCardCheckout: CreditCardCheckout +} + +export interface FortePaymentController { + data?: FortePaymentData + initializeWidget: (fortePaymentData: FortePaymentData) => void +} + +const [useFortePaymentController, FortePaymentControllerProvider] = createGenericContext() + +export { FortePaymentControllerProvider, useFortePaymentController } diff --git a/packages/checkout/src/contexts/SelectPaymentModal.ts b/packages/checkout/src/contexts/SelectPaymentModal.ts index a4d2ee601..138e6d602 100644 --- a/packages/checkout/src/contexts/SelectPaymentModal.ts +++ b/packages/checkout/src/contexts/SelectPaymentModal.ts @@ -2,11 +2,11 @@ import { TransactionOnRampProvider } from '@0xsequence/marketplace' import { type SequenceIndexer, type TransactionReceipt } from '@0xsequence/indexer' import type { Hex } from 'viem' -import type { TransakConfig } from '../contexts/CheckoutModal.js' +import type { ForteConfig, TransakConfig } from '../contexts/CheckoutModal.js' import { createGenericContext } from './genericContext.js' -export type CreditCardProviders = 'sardine' | 'transak' +export type CreditCardProviders = 'sardine' | 'transak' | 'forte' export interface Collectible { tokenId?: string @@ -39,13 +39,14 @@ export interface SelectPaymentSettings { recipientAddress: string | Hex approvedSpenderAddress?: string transactionConfirmations?: number - onSuccess?: (txHash: string) => void + onSuccess?: (txHash?: string) => void onError?: (error: Error) => void onClose?: () => void onRampProvider?: TransactionOnRampProvider creditCardProviders?: string[] transakConfig?: TransakConfig sardineConfig?: SardineConfig + forteConfig?: ForteConfig customProviderCallback?: (onSuccess: (txHash: string) => void, onError: (error: Error) => void, onClose: () => void) => void supplementaryAnalyticsInfo?: SupplementaryAnalyticsInfo skipNativeBalanceCheck?: boolean diff --git a/packages/checkout/src/contexts/index.ts b/packages/checkout/src/contexts/index.ts index 54086ba4d..fc38d9e36 100644 --- a/packages/checkout/src/contexts/index.ts +++ b/packages/checkout/src/contexts/index.ts @@ -7,3 +7,4 @@ export * from './SwapModal.js' export * from './TransferFundsModal.js' export * from './TransactionStatusModal.js' export * from './Environment.js' +export * from './FortePayment.js' diff --git a/packages/checkout/src/hooks/index.ts b/packages/checkout/src/hooks/index.ts index 892be5aa0..64778dda4 100644 --- a/packages/checkout/src/hooks/index.ts +++ b/packages/checkout/src/hooks/index.ts @@ -12,3 +12,5 @@ export * from './useCheckoutOptionsSalesContract.js' export * from './useERC1155SaleContractCheckout.js' export * from './useSkipOnCloseCallback.js' export * from './useSardineOnRampLink.js' +export * from './useFortePaymentIntent.js' +export * from './useAddFundsModal.js' diff --git a/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts b/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts index 3d1a5b207..601ef1331 100644 --- a/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts +++ b/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts @@ -1,7 +1,7 @@ import { useFindVersion } from '@0xsequence/hooks' import { type CheckoutOptionsSalesContractArgs } from '@0xsequence/marketplace' import { findSupportedNetwork } from '@0xsequence/network' -import { encodeFunctionData, keccak256, sha256, toHex, zeroAddress, type Abi, type Hex } from 'viem' +import { encodeFunctionData, sha256, toHex, zeroAddress, type Hex } from 'viem' import { useBytecode, useReadContract, useReadContracts } from 'wagmi' import { ERC_1155_SALE_CONTRACT } from '../constants/abi.js' diff --git a/packages/checkout/src/hooks/useFortePaymentIntent.ts b/packages/checkout/src/hooks/useFortePaymentIntent.ts new file mode 100644 index 000000000..0a472a40e --- /dev/null +++ b/packages/checkout/src/hooks/useFortePaymentIntent.ts @@ -0,0 +1,26 @@ +import { useConfig } from '@0xsequence/hooks' +import { useQuery } from '@tanstack/react-query' + +import { createFortePaymentIntent, type CreateFortePaymentIntentArgs } from '../api/data.js' + +interface UseFortePaymentIntentOptions { + disabled?: boolean +} + +export const useFortePaymentIntent = (args: CreateFortePaymentIntentArgs, options?: UseFortePaymentIntentOptions) => { + const { env, projectAccessKey } = useConfig() + const apiUrl = env.apiUrl + + return useQuery({ + queryKey: ['useFortePaymentIntent', args], + queryFn: async () => { + const res = await createFortePaymentIntent(apiUrl, projectAccessKey, args) + + return res + }, + retry: false, + staleTime: 60 * 1000, + refetchOnMount: 'always', + enabled: !options?.disabled + }) +} diff --git a/packages/checkout/src/index.ts b/packages/checkout/src/index.ts index d4a0ee817..de4ea8780 100644 --- a/packages/checkout/src/index.ts +++ b/packages/checkout/src/index.ts @@ -11,8 +11,10 @@ export { useSwapModal } from './hooks/useSwapModal.js' export { useERC1155SaleContractCheckout, useERC1155SaleContractPaymentModal } from './hooks/useERC1155SaleContractCheckout.js' export { useCheckoutUI } from './hooks/useCheckoutUI/index.js' -export { type CheckoutSettings } from './contexts/CheckoutModal.js' +export { type ForteConfig } from './contexts/CheckoutModal.js' export { type AddFundsSettings } from './contexts/AddFundsModal.js' +export { type CheckoutSettings } from './contexts/CheckoutModal.js' +export { type ForteProtocolType } from './contexts/CheckoutModal.js' export { type SelectPaymentSettings } from './contexts/SelectPaymentModal.js' export { type SwapModalSettings } from './contexts/SwapModal.js' export { type CreditCardProviders } from './contexts/SelectPaymentModal.js' diff --git a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCreditCard/index.tsx b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCreditCard/index.tsx index 33d370b5e..4393e656f 100644 --- a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCreditCard/index.tsx +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCreditCard/index.tsx @@ -27,8 +27,8 @@ export const PayWithCreditCardTab = ({ skipOnCloseCallback }: PayWithCreditCardT onError = () => {}, onClose = () => {}, creditCardProviders = [], - transakConfig, - supplementaryAnalyticsInfo = {} + approvedSpenderAddress, + ...rest } = selectPaymentSettings! const { address: userAddress } = useAccount() @@ -54,6 +54,7 @@ export const PayWithCreditCardTab = ({ skipOnCloseCallback }: PayWithCreditCardT return case 'sardine': case 'transak': + case 'forte': onPurchase() return default: @@ -77,14 +78,13 @@ export const PayWithCreditCardTab = ({ skipOnCloseCallback }: PayWithCreditCardT const checkoutSettings: CheckoutSettings = { creditCardCheckout: { - onSuccess: (txHash: string) => { + onSuccess: (txHash?: string) => { clearCachedBalances() onSuccess(txHash) }, onError, onClose, chainId, - recipientAddress: userAddress, contractAddress: targetContractAddress, currencyQuantity: price, currencySymbol: currencyInfoData.symbol, @@ -96,10 +96,9 @@ export const PayWithCreditCardTab = ({ skipOnCloseCallback }: PayWithCreditCardT nftDecimals: collectible.decimals === undefined ? undefined : String(collectible.decimals), provider: selectedPaymentProvider as BasePaymentProviderOptions, calldata: txData, - transakConfig, - approvedSpenderAddress: sardineConfig?.approvedSpenderAddress || targetContractAddress, - supplementaryAnalyticsInfo, - onSuccessChecker: selectPaymentSettings?.onSuccessChecker + onSuccessChecker: selectPaymentSettings?.onSuccessChecker, + approvedSpenderAddress: sardineConfig?.approvedSpenderAddress || approvedSpenderAddress, + ...rest } } diff --git a/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx b/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx new file mode 100644 index 000000000..bc268c8c3 --- /dev/null +++ b/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx @@ -0,0 +1,181 @@ +import { ArrowRightIcon, Card, PaymentsIcon, Spinner, Text } from '@0xsequence/design-system' +import { useClearCachedBalances, useGetContractInfo } from '@0xsequence/hooks' +import { findSupportedNetwork } from '@0xsequence/network' +import { useEffect, useState } from 'react' +import { useAccount } from 'wagmi' + +import type { CheckoutSettings } from '../../../contexts/CheckoutModal.js' +import type { SelectPaymentSettings } from '../../../contexts/SelectPaymentModal.js' +import { useCheckoutModal, useSelectPaymentModal } from '../../../hooks/index.js' + +interface PayWithCreditCardProps { + settings: SelectPaymentSettings + disableButtons: boolean + skipOnCloseCallback: () => void +} + +type BasePaymentProviderOptions = 'sardine' | 'transak' | 'forte' +type CustomPaymentProviderOptions = 'custom' +type PaymentProviderOptions = BasePaymentProviderOptions | CustomPaymentProviderOptions + +export const PayWithCreditCard = ({ settings, disableButtons, skipOnCloseCallback }: PayWithCreditCardProps) => { + const { + chain, + currencyAddress, + targetContractAddress, + price, + txData, + collectibles, + collectionAddress, + sardineConfig, + onSuccess = () => {}, + onError = () => {}, + onClose = () => {}, + creditCardProviders = [], + supplementaryAnalyticsInfo = {}, + transakConfig, + forteConfig + } = settings + + const { address: userAddress } = useAccount() + const { clearCachedBalances } = useClearCachedBalances() + const { closeSelectPaymentModal } = useSelectPaymentModal() + const { triggerCheckout } = useCheckoutModal() + const network = findSupportedNetwork(chain) + const chainId = network?.chainId || 137 + const { data: currencyInfoData, isLoading: isLoadingContractInfo } = useGetContractInfo({ + chainID: String(chainId), + contractAddress: currencyAddress + }) + const [selectedPaymentProvider, setSelectedPaymentProvider] = useState() + const isLoading = isLoadingContractInfo + + useEffect(() => { + if (selectedPaymentProvider) { + payWithSelectedProvider() + } + }, [selectedPaymentProvider]) + + const payWithSelectedProvider = () => { + switch (selectedPaymentProvider) { + case 'custom': + if (settings.customProviderCallback) { + onClickCustomProvider() + } + return + case 'sardine': + case 'transak': + case 'forte': + onPurchase() + return + default: + return + } + } + + const onClickCustomProvider = () => { + if (settings.customProviderCallback) { + closeSelectPaymentModal() + settings.customProviderCallback(onSuccess, onError, onClose) + } + } + + const onPurchase = () => { + if (!userAddress || !currencyInfoData) { + return + } + + const collectible = collectibles[0] + + const checkoutSettings: CheckoutSettings = { + creditCardCheckout: { + onSuccess: (txHash?: string) => { + clearCachedBalances() + onSuccess(txHash) + }, + onError, + onClose, + chainId, + recipientAddress: userAddress, + contractAddress: targetContractAddress, + currencyQuantity: price, + currencySymbol: currencyInfoData.symbol, + currencyAddress, + currencyDecimals: String(currencyInfoData?.decimals || 0), + nftId: collectible.tokenId ?? '', + nftAddress: collectionAddress, + nftQuantity: collectible.quantity, + nftDecimals: collectible.decimals === undefined ? undefined : String(collectible.decimals), + provider: selectedPaymentProvider as BasePaymentProviderOptions, + calldata: txData, + approvedSpenderAddress: sardineConfig?.approvedSpenderAddress || settings.approvedSpenderAddress, + supplementaryAnalyticsInfo, + transakConfig, + forteConfig + } + } + + skipOnCloseCallback() + closeSelectPaymentModal() + triggerCheckout(checkoutSettings) + } + + const Options = () => { + return ( +
+ {/* Only 1 option will be displayed, even if multiple providers are passed */} + {creditCardProviders + .slice(0, 1) + .filter(provider => { + // cannot display transak checkout if the settings aren't provided + if (provider === 'transak' && !settings.transakConfig) { + return false + } + return true + }) + .map(creditCardProvider => { + switch (creditCardProvider) { + case 'sardine': + case 'transak': + case 'forte': + case 'custom': + return ( + { + setSelectedPaymentProvider(creditCardProvider) + }} + disabled={disableButtons} + > +
+ + + Pay with credit or debit card + +
+
+ +
+
+ ) + default: + return null + } + })} +
+ ) + } + + return ( +
+ {isLoading ? ( +
+ +
+ ) : ( + + )} +
+ ) +} diff --git a/packages/checkout/src/views/PendingCreditCardTransaction.tsx b/packages/checkout/src/views/PendingCreditCardTransaction.tsx index c867db7af..83eda431e 100644 --- a/packages/checkout/src/views/PendingCreditCardTransaction.tsx +++ b/packages/checkout/src/views/PendingCreditCardTransaction.tsx @@ -8,9 +8,10 @@ import { formatUnits } from 'viem' import { fetchSardineOrderStatus } from '../api/data.js' import { EVENT_SOURCE } from '../constants/index.js' -import { useEnvironmentContext, type TransactionPendingNavigation } from '../contexts/index.js' +import { useEnvironmentContext, useFortePaymentController, type TransactionPendingNavigation } from '../contexts/index.js' import { useCheckoutModal, + useFortePaymentIntent, useNavigation, useSardineClientToken, useSkipOnCloseCallback, @@ -35,6 +36,8 @@ export const PendingCreditCardTransaction = () => { const { skipOnCloseCallback } = useSkipOnCloseCallback(onClose) switch (provider) { + case 'forte': + return case 'transak': return case 'sardine': @@ -440,3 +443,100 @@ export const PendingCreditCardTransactionSardine = ({ skipOnCloseCallback }: Pen ) } + +export const PendingCreditCardTransactionForte = ({ skipOnCloseCallback }: PendingCreditTransactionProps) => { + const { initializeWidget } = useFortePaymentController() + const nav = useNavigation() + const { + params: { creditCardCheckout } + } = nav.navigation as TransactionPendingNavigation + const { closeCheckout } = useCheckoutModal() + + const { + data: tokenMetadatas, + isLoading: isLoadingTokenMetadata, + isError: isErrorTokenMetadata + } = useGetTokenMetadata({ + chainID: String(creditCardCheckout.chainId), + contractAddress: creditCardCheckout.nftAddress, + tokenIDs: [creditCardCheckout.nftId] + }) + + const tokenMetadata = tokenMetadatas ? tokenMetadatas[0] : undefined + + const currencyQuantity = formatUnits( + BigInt(creditCardCheckout.currencyQuantity), + Number(creditCardCheckout.currencyDecimals || 18) + ) + + const { + data: paymentIntentData, + isError: isErrorPaymentIntent, + error: paymentIntentError + } = useFortePaymentIntent( + { + recipientAddress: creditCardCheckout.recipientAddress, + chainId: creditCardCheckout.chainId.toString(), + nftAddress: creditCardCheckout.nftAddress, + currencyAddress: creditCardCheckout.currencyAddress, + targetContractAddress: creditCardCheckout.contractAddress, + nftName: tokenMetadata?.name || '', + imageUrl: tokenMetadata?.image || '', + tokenId: creditCardCheckout.nftId, + protocolConfig: creditCardCheckout.forteConfig!, + currencyQuantity, + calldata: + creditCardCheckout.forteConfig!.protocol === 'mint' + ? creditCardCheckout.forteConfig!.calldata + : creditCardCheckout.calldata, + approvedSpenderAddress: creditCardCheckout.approvedSpenderAddress + }, + { + disabled: isLoadingTokenMetadata + } + ) + + const isPriceTooLow = paymentIntentError?.message?.includes('price too low') + // A more unique error message in the case of a high price is pending from forte + const isPriceTooHigh = paymentIntentError?.message?.includes('failed with status code 500') + + const getErrorMessage = () => { + if (isPriceTooLow) { + return 'The price for the item is too low for credit card paymetns' + } + if (isPriceTooHigh) { + return 'The price for the item is too high for credit card payments' + } + return 'An error has occurred' + } + + useEffect(() => { + if (!paymentIntentData) { + return + } + + initializeWidget({ + paymentIntentId: paymentIntentData.paymentIntentId, + widgetData: paymentIntentData, + creditCardCheckout + }) + skipOnCloseCallback() + closeCheckout() + }, [paymentIntentData]) + + const isError = isErrorTokenMetadata || isErrorPaymentIntent + + if (isError) { + return ( +
+ {getErrorMessage()} +
+ ) + } + + return ( +
+ +
+ ) +} diff --git a/packages/checkout/src/views/TransactionStatus/index.tsx b/packages/checkout/src/views/TransactionStatus/index.tsx index f8c076ab0..23be6dca5 100644 --- a/packages/checkout/src/views/TransactionStatus/index.tsx +++ b/packages/checkout/src/views/TransactionStatus/index.tsx @@ -225,9 +225,12 @@ export const TransactionStatus = () => { return (
- +
- + Transaction complete
@@ -238,7 +241,7 @@ export const TransactionStatus = () => {
- + Transaction failed @@ -247,8 +250,8 @@ export const TransactionStatus = () => { default: return (
- - + + Processing transaction
@@ -353,7 +356,7 @@ export const TransactionStatus = () => { ) : ( <>
- + {getInformationText()}
@@ -365,7 +368,12 @@ export const TransactionStatus = () => { )}
- + {truncateAddress(txHash, 4, 4)} diff --git a/packages/connect/src/connectors/epic/EpicLogo.tsx b/packages/connect/src/connectors/epic/EpicLogo.tsx index ba93be912..dd60e01bb 100644 --- a/packages/connect/src/connectors/epic/EpicLogo.tsx +++ b/packages/connect/src/connectors/epic/EpicLogo.tsx @@ -3,7 +3,7 @@ import type { FunctionComponent } from 'react' import type { LogoProps } from '../../types.js' export const EpicLogo: FunctionComponent = (props: LogoProps) => ( - + ) @@ -17,7 +17,7 @@ export const getMonochromeEpicLogo = ({ isDarkMode }: GetEpicMonochromeLogo) => const EpicMonochromeLogo: FunctionComponent = (props: LogoProps) => { return ( - + ) diff --git a/packages/connect/src/styles.ts b/packages/connect/src/styles.ts index 4d855452a..2700224e8 100644 --- a/packages/connect/src/styles.ts +++ b/packages/connect/src/styles.ts @@ -8,9 +8,11 @@ export const styles = String.raw` "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --color-red-500: oklch(63.7% 0.237 25.331); --color-violet-600: oklch(54.1% 0.281 293.009); + --color-gray-500: oklch(55.1% 0.027 264.364); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; + --container-md: 28rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; @@ -313,6 +315,9 @@ export const styles = String.raw` .my-4 { margin-block: calc(var(--spacing) * 4); } + .mt-0 { + margin-top: calc(var(--spacing) * 0); + } .mt-1 { margin-top: calc(var(--spacing) * 1); } @@ -349,6 +354,9 @@ export const styles = String.raw` .mb-2 { margin-bottom: calc(var(--spacing) * 2); } + .mb-3 { + margin-bottom: calc(var(--spacing) * 3); + } .mb-4 { margin-bottom: calc(var(--spacing) * 4); } @@ -595,6 +603,9 @@ export const styles = String.raw` .max-w-full { max-width: 100%; } + .max-w-md { + max-width: var(--container-md); + } .min-w-0 { min-width: calc(var(--spacing) * 0); } @@ -767,6 +778,9 @@ export const styles = String.raw` text-overflow: ellipsis; white-space: nowrap; } + .overflow-auto { + overflow: auto; + } .overflow-hidden { overflow: hidden; } @@ -822,10 +836,22 @@ export const styles = String.raw` border-top-left-radius: var(--radius-2xl); border-top-right-radius: var(--radius-2xl); } + .rounded-t-none { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .rounded-t-xl { + border-top-left-radius: var(--radius-xl); + border-top-right-radius: var(--radius-xl); + } .rounded-b-none { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } + .rounded-b-xl { + border-bottom-right-radius: var(--radius-xl); + border-bottom-left-radius: var(--radius-xl); + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -870,6 +896,12 @@ export const styles = String.raw` .border-border-normal { border-color: var(--seq-color-border-normal); } + .border-primary { + border-color: var(--seq-color-primary); + } + .border-red-500 { + border-color: var(--color-red-500); + } .border-transparent { border-color: transparent; } @@ -936,6 +968,10 @@ export const styles = String.raw` .bg-white { background-color: var(--color-white); } + .bg-gradient-to-r { + --tw-gradient-position: to right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } .bg-gradient-primary { background-image: var(--seq-color-gradient-primary); } @@ -1020,6 +1056,9 @@ export const styles = String.raw` .py-4 { padding-block: calc(var(--spacing) * 4); } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } .py-6 { padding-block: calc(var(--spacing) * 6); } @@ -1225,6 +1264,9 @@ export const styles = String.raw` .text-black { color: var(--color-black); } + .text-gray-500 { + color: var(--color-gray-500); + } .text-info { color: var(--seq-color-info); } @@ -2458,6 +2500,4 @@ export const styles = String.raw` --tw-gradient-to-position: 100%; } } -} - -` +}` diff --git a/packages/wallet-widget/src/views/CollectibleDetails/index.tsx b/packages/wallet-widget/src/views/CollectibleDetails/index.tsx index bcd30d834..a19fd7223 100644 --- a/packages/wallet-widget/src/views/CollectibleDetails/index.tsx +++ b/packages/wallet-widget/src/views/CollectibleDetails/index.tsx @@ -35,7 +35,7 @@ export const CollectibleDetails = ({ contractAddress, chainId, tokenId, accountA const [triggerWidth, setTriggerWidth] = useState(0) const [isExternalPopoverOpen, setIsExternalPopoverOpen] = useState(false) - const [foundMarketplaceURL, setFoundMarketplaceURL] = useState(null) + const [foundMarketplaceURL] = useState(null) useEffect(() => { if (triggerRef.current) {