diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..956457a0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @0xsequence/core diff --git a/README.md b/README.md index 5f12627a..a031f90b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![npm version](https://badge.fury.io/js/@0xsequence%2Fkit.svg)](https://badge.fury.io/js/@0xsequence%2Fkit) -Sequence Kit 🧰 is a library enabling developers to easily integrate web3 wallets in their app. It is based on [wagmi](https://wagmi.sh/) and supports all wagmi features. +Easily integrate web3 wallets in your app with Sequence Kit 🧰. Based on [wagmi](https://wagmi.sh/), and supporting all wagmi features. - Connect via social logins eg: facebook, google, discord, etc...! 🔐🪪 - Connect to popular web3 wallets eg: walletConnect, metamask ! 🦊 ⛓️ diff --git a/examples/components/package.json b/examples/components/package.json index 3261b576..d04b8ff9 100644 --- a/examples/components/package.json +++ b/examples/components/package.json @@ -8,8 +8,8 @@ "typescript": "latest" }, "peerDependencies": { - "@0xsequence/design-system": ">= 1.7.8", - "@0xsequence/network": ">=2.0.12", + "@0xsequence/design-system": "*", + "@0xsequence/network": "*", "wagmi": "*" }, "private": true diff --git a/examples/next/package.json b/examples/next/package.json index ed7284b1..45a1bac7 100644 --- a/examples/next/package.json +++ b/examples/next/package.json @@ -15,13 +15,14 @@ "@0xsequence/kit-checkout": "workspace:*", "@0xsequence/kit-wallet": "workspace:*", "@0xsequence/kit-example-shared-components": "workspace:*", - "@0xsequence/network": "2.0.12", + "@0xsequence/network": "2.1.4", + "@0xsequence/waas": "2.1.4", "@tanstack/react-query": "^5.37.1", "next": "14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", "viem": "^2.12.0", - "wagmi": "^2.9.5" + "wagmi": "^2.13.3" }, "devDependencies": { "@types/node": "^20.12.12", diff --git a/examples/next/src/config.ts b/examples/next/src/config.ts index 5951cdf6..8e668ca4 100644 --- a/examples/next/src/config.ts +++ b/examples/next/src/config.ts @@ -50,7 +50,13 @@ export const kitConfig: KitConfig = { export const config = createConfig('waas', { ...kitConfig, appName: 'Kit Demo', - chainIds: [ChainId.ARBITRUM_NOVA, ChainId.ARBITRUM_SEPOLIA, ChainId.POLYGON], + chainIds: [ + ChainId.ARBITRUM_NOVA, + ChainId.ARBITRUM_SEPOLIA, + ChainId.POLYGON, + ChainId.IMMUTABLE_ZKEVM, + ChainId.IMMUTABLE_ZKEVM_TESTNET + ], defaultChainId: ChainId.ARBITRUM_NOVA, // Waas specific config options diff --git a/examples/react/package.json b/examples/react/package.json index 9cf866fa..4fc4be42 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -17,13 +17,14 @@ "@0xsequence/kit-wallet": "workspace:*", "@0xsequence/kit-example-shared-components": "workspace:*", "@tanstack/react-query": "^5.37.1", - "@0xsequence/network": "2.0.12", + "@0xsequence/network": "2.1.4", + "@0xsequence/waas": "2.1.4", "framer-motion": "^8.5.2", "react": "^18.3.1", "react-dom": "^18.3.1", "typescript": "^5.4.5", "viem": "^2.12.0", - "wagmi": "^2.9.5" + "wagmi": "^2.13.3" }, "devDependencies": { "@types/node": "^20.12.12", diff --git a/examples/react/src/components/Connected.tsx b/examples/react/src/components/Connected.tsx index 249b6426..f8b7c1e7 100644 --- a/examples/react/src/components/Connected.tsx +++ b/examples/react/src/components/Connected.tsx @@ -26,7 +26,7 @@ import { useWriteContract } from 'wagmi' -import { sponsoredContractAddresses } from '../config' +import { sponsoredContractAddresses, getErc1155SaleContractConfig } from '../config' import { messageToSign } from '../constants' import { abi } from '../constants/nft-abi' import { delay, getCheckoutSettings, getOrderbookCalldata } from '../utils' @@ -336,12 +336,14 @@ export const Connected = () => { // 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 = '20000' + const contractId = '674eb5613d739107bbd18ed2' const chainId = 137 @@ -358,8 +360,12 @@ export const Connected = () => { recipientAddress: address, currencyAddress, collectionAddress, - creditCardProviders: ['sardine'], - isDev: true, + creditCardProviders: ['sardine', 'transak'], + transakConfig: { + contractId, + apiKey: '5911d9ec-46b5-48fa-a755-d59a715ff0cf' + }, + isDev: false, copyrightText: 'ⓒ2024 Sequence', onSuccess: (txnHash: string) => { console.log('success!', txnHash) diff --git a/examples/react/src/config.ts b/examples/react/src/config.ts index 571e70b8..9349f0c2 100644 --- a/examples/react/src/config.ts +++ b/examples/react/src/config.ts @@ -58,7 +58,13 @@ export const config = ? createConfig('waas', { ...kitConfig, appName: 'Kit Demo', - chainIds: [ChainId.ARBITRUM_NOVA, ChainId.ARBITRUM_SEPOLIA, ChainId.POLYGON], + chainIds: [ + ChainId.ARBITRUM_NOVA, + ChainId.ARBITRUM_SEPOLIA, + ChainId.POLYGON, + ChainId.IMMUTABLE_ZKEVM, + ChainId.IMMUTABLE_ZKEVM_TESTNET + ], defaultChainId: ChainId.ARBITRUM_NOVA, waasConfigKey: isDebugMode ? 'eyJwcm9qZWN0SWQiOjY5NCwicnBjU2VydmVyIjoiaHR0cHM6Ly9kZXYtd2Fhcy5zZXF1ZW5jZS5hcHAiLCJlbWFpbFJlZ2lvbiI6ImNhLWNlbnRyYWwtMSIsImVtYWlsQ2xpZW50SWQiOiI1NGF0bjV1cGk2M3FjNTlhMWVtM3ZiaHJzbiJ9' @@ -81,10 +87,33 @@ export const config = : createConfig('universal', { ...kitConfig, appName: 'Kit Demo', - chainIds: [ChainId.ARBITRUM_NOVA, ChainId.ARBITRUM_SEPOLIA, ChainId.POLYGON], + chainIds: [ + ChainId.ARBITRUM_NOVA, + ChainId.ARBITRUM_SEPOLIA, + ChainId.POLYGON, + ChainId.IMMUTABLE_ZKEVM, + ChainId.IMMUTABLE_ZKEVM_TESTNET + ], defaultChainId: ChainId.ARBITRUM_NOVA, walletConnect: { projectId: walletConnectProjectId } }) + +export const getErc1155SaleContractConfig = (walletAddress: string) => ({ + chain: 137, + // ERC20 token sale + contractAddress: '0xe65b75eb7c58ffc0bf0e671d64d0e1c6cd0d3e5b', + collectionAddress: '0xdeb398f41ccd290ee5114df7e498cf04fac916cb', + // Native token sale + // contractAddress: '0xf0056139095224f4eec53c578ab4de1e227b9597', + // collectionAddress: '0x92473261f2c26f2264429c451f70b0192f858795', + wallet: walletAddress, + items: [{ + tokenId: '1', + quantity: '1' + }], + onSuccess: () => { console.log('success') }, + isDev: isDebugMode +}) diff --git a/package.json b/package.json index ba2fe48d..4897b073 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "rimraf": "^5.0.7", "turbo": "2.0.1", "typescript": "~5.4.5", - "wagmi": "^2.9.5" + "wagmi": "^2.13.3" }, "resolutions": {}, "packageManager": "pnpm@9.0.6" diff --git a/packages/checkout/CHANGELOG.md b/packages/checkout/CHANGELOG.md index 9be04484..d4e1fcf0 100644 --- a/packages/checkout/CHANGELOG.md +++ b/packages/checkout/CHANGELOG.md @@ -1,5 +1,52 @@ # @0xsequence/kit-connectors +## 4.5.2 + +### Patch Changes + +- Fixing walletconnect default chainId + +- Updated dependencies []: + - @0xsequence/kit@4.5.2 + +## 4.5.1 + +### Patch Changes + +- Updating onEmailConflict listeners to handle multiple providers + +- Updated dependencies []: + - @0xsequence/kit@4.5.1 + +## 4.5.0 + +### Minor Changes + +- Adding metamask connector + +### Patch Changes + +- Updated dependencies []: + - @0xsequence/kit@4.5.0 + +## 4.4.6 + +### Patch Changes + +- Updating sequence to include fix for projectAccessKey sending to universal wallet + +- Updated dependencies []: + - @0xsequence/kit@4.4.6 + +## 4.4.5 + +### Patch Changes + +- Fixing waas time drift + +- Updated dependencies []: + - @0xsequence/kit@4.4.5 + ## 4.4.4 ### Patch Changes diff --git a/packages/checkout/README.md b/packages/checkout/README.md index 5b6f587f..6dd9f82f 100644 --- a/packages/checkout/README.md +++ b/packages/checkout/README.md @@ -1,10 +1,12 @@ # Sequence Kit Checkout -
- -
+Sequence Checkout provides a seamless and flexible payment experience for interacting with NFTs, cryptocurrencies, and fiat currencies. It supports multiple payment options, including cryptocurrency transfers, currency swaps, and even credit card payments for whitelisted contracts. -Checkout modal for Sequence Kit. Displays a list a summary of collectibles to be purchased +## Key Features + +- **NFT Checkout**: Buy NFTs using either the main currency (e.g., ETH), a swapped currency, or a credit card. +- **Currency Swap**: Swap one token for another before completing the transaction. +- **Fiat Onramp**: Onboard users with fiat currency to interact with the blockchain. # Installing the module @@ -34,115 +36,221 @@ const App = () => { } ``` -## Open the checkout modal +# NFT Checkout (Sequence Pay) + +
+ +
+ +Sequence Pay Checkout allows users to purchase NFTs using various payment methods. Users can pay with the main currency (e.g., ETH), swap tokens for payment, or use a credit card provided the smart contract is whitelisted (contact a member of the Sequence team to whitelist your contract for credit card payments). -The `useCheckoutModal` hook must be used to open the modal. -Furthermore, it is necessary to pass a settings object. +## Basic Usage + +To enable this functionality in your app, use the `useSelectPaymentModal` hook from the `@0xsequence/kit-checkout` package. The following code demonstrates how to set up the checkout modal and trigger it on a button click: ```js - import { useCheckoutModal } from '@0xsequence/kit-checkout' +import { useSelectPaymentModal, type SelectPaymentSettings } from '@0xsequence/kit-checkout' +const MyComponent = () => { + const { openSelectPaymentModal } = useSelectPaymentModal() - const MyComponent = () => { - const { triggerCheckout } = useCheckoutModal() + const onClick = () => { + const erc1155SalesContractAbi = [ + { + type: 'function', + name: 'mint', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'tokenIds', type: 'uint256[]', internalType: 'uint256[]' }, + { name: 'amounts', type: 'uint256[]', internalType: 'uint256[]' }, + { name: 'data', type: 'bytes', internalType: 'bytes' }, + { name: 'expectedPaymentToken', type: 'address', internalType: 'address' }, + { name: 'maxTotal', type: 'uint256', internalType: 'uint256' }, + { name: 'proof', type: 'bytes32[]', internalType: 'bytes32[]' } + ], + outputs: [], + stateMutability: 'payable' + } + ] + + const purchaseTransactionData = encodeFunctionData({ + abi: erc1155SalesContractAbi, + functionName: 'mint', + args: [ + recipientAddress, + collectibles.map(c => BigInt(c.tokenId)), + collectibles.map(c => BigInt(c.quantity)), + toHex(0), + currencyAddress, + price, + [toHex(0, { size: 32 })] + ] + }) - const onClick = () => { - const checkoutSettings = {...} - triggerCheckout(checkoutSettings) + const settings: SelectPaymentSettings = { + collectibles: [ + { + tokenId: '1', + quantity: '1' + } + ], + chain: chainId, + price, + targetContractAddress: salesContractAddress, + recipientAddress: address, + currencyAddress, + collectionAddress, + creditCardProviders: ['sardine'], + copyrightText: 'ⓒ2024 Sequence', + onSuccess: (txnHash: string) => { + console.log('success!', txnHash) + }, + onError: (error: Error) => { + console.error(error) + }, + txData: purchaseTransactionData, } - return ( - - ) + openSelectPaymentModal(settings) } + return +} ``` -## Configuration +## Parameters + +- **collectibles**: List of NFT collectibles, including their token IDs and quantities. +- **chain**: The blockchain network ID. +- **price**: Total price for the transaction in the selected currency. This value should not contain decimals. +- **currencyAddress**: The address of the currency used for executing the transaction on the target contract. +- **targetContractAddress**: The address of the smart contract handling the minting function. + creditCardProviders: Providers like sardine for credit card payments. +- **collectionAddress**: The contract address of the collectible such as an ERC-1155 or ERC-721 +- **creditCardProviders**: The list of credit card providers to execute a payment with. It is up to the developer to make sure that the region, currency and network is compatible. +- **txData**: Encoded transaction data to interact with the mint function. +- **copyrightText**: The copyright text shown at the bottom of the modal. +- **onSuccess**: Callback function triggered once the transaction has been confirmed on the blockchain +- **onError**: Callback function triggered if an error has occurred before or after sending the transaction. -The react example has an example configuration for setting up the checkout. +## Utility functions -Example [settings](../../examples/react/src/utils/settings.ts) +The `@0xsequence/kit-checkout` library indeed simplifies the integration of Web3 payment solutions by providing utility functions. One such function, `useERC1155SaleContractPaymentModal`, is tailored for use cases involving the minting of ERC-1155 tokens. This function works in conjunction with Sequence's wallet ecosystem and its deployable smart contract infrastructure, such as the ERC-1155 sale contract available through the [Sequence Builder](https://sequence.build). ```js -const checkoutSettings = { - creditCardCheckout: {...}, - cryptoCheckout: {...}, - orderSummaryItems: {...} -} -``` +import { useERC1155SaleContractPaymentModal } from '@0xsequence/kit-checkout' -### cryptoCheckout +const MyComponent = () => { + const { openERC1155SaleContractPaymentModal } = useERC1155SaleContractPaymentModal() -The `cryptoCheckout` specifies settings regarding checking out with crypto. -An example usecase might be interacting with a minting contract. -The actual cryptoTransaction must be passed down to `triggerCheckout`. + const onClick = () => { + if (!address) { + return + } + // // ERC-20 contract + const currencyAddress = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' + const salesContractAddress = '0xe65b75eb7c58ffc0bf0e671d64d0e1c6cd0d3e5b' + const collectionAddress = '0xdeb398f41ccd290ee5114df7e498cf04fac916cb' + const price = '20000' + + const chainId = 137 + + openERC1155SaleContractPaymentModal({ + collectibles: [ + { + tokenId: '1', + quantity: '1' + } + ], + chain: chainId, + price, + targetContractAddress: salesContractAddress, + recipientAddress: address, + currencyAddress, + collectionAddress, + creditCardProviders: ['sardine'], + isDev: true, + copyrightText: 'ⓒ2024 Sequence', + onSuccess: (txnHash: string) => { + console.log('success!', txnHash) + }, + onError: (error: Error) => { + console.error(error) + } + }) + } -```js -const checkoutConfig = { - {...}, - cryptoCheckout: { - chainId: 137, - triggerTransaction: async () => { console.log('triggered transaction') }, - coinQuantity: { - contractAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', - amountRequiredRaw: '10000000000' - }, - }, + return } ``` -### creditCardCheckout +# Swap + +
+ +
+ +The **Swap Modal** allows users to swap one currency for another (e.g., ETH to USDC) before completing a transaction. This feature is useful when users need to convert their tokens into the correct currency for payment. -The `creditCardCheckout` field specifies settings regarding checking out with credit card. +## Basic Usage -`triggerCheckout` must be called in order to open the checkout modal. +Here’s an example of how to use the Swap Modal with the `useSwapModal` hook: ```js -const creditCardCheckout = { - {...}, - cryptoCheckout: { - defaultPaymentMethodType: 'us_debit', - onSuccess: (hash) => { console.log('credit card checkout success', hash) }, - onError: (e) => { console.log('credit card checkout error', e) }, +import { useSwapModal, type SwapModalSettings } from '@0xsequence/kit-checkout' + +const MyComponent = () => { + const { openSwapModal } = useSwapModal() + + const onClick = () => { + const chainId = 137 + const currencyAddress = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' + const currencyAmount = '20000' + + const contractAbiInterface = new ethers.Interface(['function demo()']) + + const data = contractAbiInterface.encodeFunctionData('demo', []) as `0x${string}` + + const swapModalSettings: SwapModalSettings = { + onSuccess: () => { + console.log('swap successful!') + }, chainId, - contractAddress: orderbookAddress, - recipientAddress, - currencyQuantity: '100000', - currencySymbol: 'USDC', - currencyAddress: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', - currencyDecimals: '6', - nftId: checkoutTokenId, - nftAddress: checkoutTokenContractAddress, - nftQuantity, - isDev: true, - calldata: getOrderbookCalldata({ - orderId: checkoutOrderId, - quantity: nftQuantity, - recipient: recipientAddress - }) - }, + currencyAddress, + currencyAmount, + postSwapTransactions: [ + { + to: '0x37470dac8a0255141745906c972e414b1409b470', + data + } + ], + title: 'Swap and Pay', + description: 'Select a token in your wallet to swap to 0.2 USDC.' + } + + openSwapModal(swapModalSettings) + } + + return } ``` -### orderSummaryItems +## Key Parameters -This field specific the list of collectibles that will show up in the order summary. +- **currencyAddress**: The address of the token to swap from (e.g., USDC). +- **currencyAmount**: The amount to swap. +- **postSwapTransactions**: An optional array of transactions to be executed after the swap, using the swapped tokens. +- **title**: The modal’s title. +- **description**: A description of the swap and payment process. -```js -orderSummaryItems: [ - { - contractAddress: '0x631998e91476da5b870d741192fc5cbc55f5a52e', - tokenId: '66597', - quantityRaw: '100' - } -] -``` +# Fiat Onramp -## Open the Add Funds by Credit Card Modal +
+ +
-Kit allows users to buy cryptocurrencies using credit card. Calling the `triggerAddFunds` function will cause a modal to appear. +The Fiat Onramp feature allows users to convert traditional fiat currencies (e.g., USD) into cryptocurrencies. This feature makes it easier for non-crypto users to interact with decentralized applications (dApps) by onboarding them directly through fiat payments. ```js import { useAddFundsModal } from '@0xsequence/kit-checkout' diff --git a/packages/checkout/package.json b/packages/checkout/package.json index 498f8ec2..a6cfd0f4 100644 --- a/packages/checkout/package.json +++ b/packages/checkout/package.json @@ -1,6 +1,6 @@ { "name": "@0xsequence/kit-checkout", - "version": "4.4.4", + "version": "4.5.2", "description": "Checkout UI for Sequence Kit", "repository": "https://github.com/0xsequence/kit/tree/master/packages/checkout", "author": "Horizon Blockchain Games", @@ -31,36 +31,39 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "pako": "^2.1.0", + "@0xsequence/marketplace": "^2.1.3", "qrcode.react": "^4.0.1", "react-copy-to-clipboard": "^5.1.0", "timeago-react": "^3.0.6" }, "peerDependencies": { - "0xsequence": ">= 2.0.20", - "@0xsequence/api": ">= 2.0.20", + "0xsequence": ">= 2.1.4", + "@0xsequence/api": ">= 2.1.4", "@0xsequence/design-system": ">= 1.7.8", - "@0xsequence/indexer": ">= 2.0.20", - "@0xsequence/metadata": ">= 2.0.20", - "@0xsequence/network": ">= 2.0.20", + "@0xsequence/indexer": ">= 2.1.4", + "@0xsequence/metadata": ">= 2.1.4", + "@0xsequence/network": ">= 2.1.4", "@0xsequence/kit": "workspace:*", - "@0xsequence/waas": ">= 2.0.20", + "@0xsequence/waas": ">= 2.1.4", "@tanstack/react-query": ">= 5.0.0", "ethers": ">= 6.13.0", "framer-motion": ">= 8.5.2", "react": ">= 17", "react-dom": ">= 17", "viem": ">= 2.0.0", - "wagmi": ">= 2.0.0" + "wagmi": "^2.13.3" }, "devDependencies": { "@0xsequence/design-system": "^1.7.8", "@0xsequence/kit": "workspace:*", + "@types/pako": "^2.0.3", "@types/react-copy-to-clipboard": "^5.0.7", "ethers": "^6.13.0", "framer-motion": "^8.5.2", "react": "^18.3.1", "react-dom": "^18.3.1", "vite": "^5.2.11", - "wagmi": "^2.9.5" + "wagmi": "^2.13.3" } } diff --git a/packages/checkout/src/constants/abi.ts b/packages/checkout/src/constants/abi.ts index d72bfb03..b593e3de 100644 --- a/packages/checkout/src/constants/abi.ts +++ b/packages/checkout/src/constants/abi.ts @@ -20,3 +20,166 @@ export const ERC_20_CONTRACT_ABI = [ stateMutability: 'nonpayable' } ] + +export const ERC_1155_SALE_CONTRACT = [ + { + "type": "function", + "name": "globalSaleDetails", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IERC1155SaleFunctions.SaleDetails", + "components": [ + { + "name": "cost", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "supplyCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "startTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "endTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "merkleRoot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "amounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "expectedPaymentToken", + "type": "address", + "internalType": "address" + }, + { + "name": "maxTotal", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proof", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "paymentToken", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "tokenSaleDetails", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IERC1155SaleFunctions.SaleDetails", + "components": [ + { + "name": "cost", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "supplyCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "startTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "endTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "merkleRoot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ], + "stateMutability": "view" + } +] diff --git a/packages/checkout/src/contexts/AddFundsModal.ts b/packages/checkout/src/contexts/AddFundsModal.ts index eeeda95a..f1766034 100644 --- a/packages/checkout/src/contexts/AddFundsModal.ts +++ b/packages/checkout/src/contexts/AddFundsModal.ts @@ -4,9 +4,11 @@ import { createGenericContext } from './genericContext' export interface AddFundsSettings { walletAddress: string | Hex + fiatAmount?: string fiatCurrency?: string defaultFiatAmount?: string defaultCryptoCurrency?: string + cryptoCurrencyList?: string networks?: string onClose?: () => void } diff --git a/packages/checkout/src/contexts/CheckoutModal.ts b/packages/checkout/src/contexts/CheckoutModal.ts index 1956bc94..5d2e359c 100644 --- a/packages/checkout/src/contexts/CheckoutModal.ts +++ b/packages/checkout/src/contexts/CheckoutModal.ts @@ -16,6 +16,12 @@ interface OrderSummaryItem { tokenId: string } +export interface TransakConfig { + apiKey: string + contractId: string + callDataOverride?: string +} + export interface CreditCardCheckout { chainId: number contractAddress: string @@ -29,6 +35,8 @@ export interface CreditCardCheckout { nftQuantity: string nftDecimals?: string calldata: string + provider?: 'sardine' | 'transak' + transakConfig?: TransakConfig onSuccess?: (transactionHash: string, settings: CreditCardCheckout) => void onError?: (error: Error, settings: CreditCardCheckout) => void isDev?: boolean diff --git a/packages/checkout/src/contexts/SelectPaymentModal.ts b/packages/checkout/src/contexts/SelectPaymentModal.ts index 9f477637..1a857699 100644 --- a/packages/checkout/src/contexts/SelectPaymentModal.ts +++ b/packages/checkout/src/contexts/SelectPaymentModal.ts @@ -1,6 +1,7 @@ import { Hex } from 'viem' import { createGenericContext } from './genericContext' +import type { TransakConfig } from '../contexts/CheckoutModal' export type CreditCardProviders = 'sardine' | 'transak' @@ -30,6 +31,7 @@ export interface SelectPaymentSettings { enableTransferFunds?: boolean creditCardProviders?: string[] copyrightText?: string + transakConfig?: TransakConfig } type SelectPaymentModalContext = { diff --git a/packages/checkout/src/hooks/index.ts b/packages/checkout/src/hooks/index.ts index d6d751cd..7cb76639 100644 --- a/packages/checkout/src/hooks/index.ts +++ b/packages/checkout/src/hooks/index.ts @@ -9,3 +9,5 @@ export * from './useClearCachedBalances' export * from './useTransferFundsModal' export * from './useTransactionStatusModal' export * from './useSwapModal' +export * from './useCheckoutOptionsSalesContract' +export * from './useERC1155SaleContractCheckout' \ No newline at end of file diff --git a/packages/checkout/src/hooks/useCheckoutOptionsSalesContract.ts b/packages/checkout/src/hooks/useCheckoutOptionsSalesContract.ts new file mode 100644 index 00000000..f6e4bae1 --- /dev/null +++ b/packages/checkout/src/hooks/useCheckoutOptionsSalesContract.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query' +import { CheckoutOptionsSalesContractArgs } from '@0xsequence/marketplace' +import { useMarketplaceClient } from './useMarketplaceClient' + +export interface UseGenerateBuyTransactionOptions { + disabled?: boolean, + isDev?: boolean +} + +export const useCheckoutOptionsSalesContract = (chain: number | string, args: CheckoutOptionsSalesContractArgs, options?: UseGenerateBuyTransactionOptions) => { + const marketplaceClient = useMarketplaceClient({ chain, isDev: options?.isDev }) + + return useQuery({ + queryKey: ['useCheckoutOptionsSalesContract', args], + queryFn: async () => { + const res = await marketplaceClient.checkoutOptionsSalesContract(args) + + return res + }, + retry: false, + staleTime: 360 * 1000, + enabled: !options?.disabled && !!args.wallet, + }) +} diff --git a/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts b/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts new file mode 100644 index 00000000..811a1aec --- /dev/null +++ b/packages/checkout/src/hooks/useERC1155SaleContractCheckout.ts @@ -0,0 +1,187 @@ +import { CheckoutOptionsSalesContractArgs } from '@0xsequence/marketplace' +import { findSupportedNetwork } from '@0xsequence/network' +import { TransactionSwapProvider, TransactionNFTCheckoutProvider } from '@0xsequence/marketplace' + +import { useERC1155SaleContractPaymentModal } from './useSelectPaymentModal' +import { useCheckoutOptionsSalesContract } from "./useCheckoutOptionsSalesContract" +import { ERC_1155_SALE_CONTRACT } from '../constants/abi' +import { SelectPaymentSettings } from '../contexts/SelectPaymentModal' + +import { Abi, Hex, padBytes } from 'viem' +import { useReadContract, useReadContracts } from 'wagmi' + +type BasePaymentModalSettings = Pick + +interface UseERC1155SaleContractCheckoutArgs extends CheckoutOptionsSalesContractArgs { + chain: number | string, +} + +interface UseERC1155SaleContractCheckoutReturn { + openCheckoutModal: () => void, + isLoading: boolean, + isError: boolean +} + +export const useERC1155SaleContractCheckout = ({ + chain, + contractAddress, + wallet, + collectionAddress, + items, + ...restArgs +}: UseERC1155SaleContractCheckoutArgs & BasePaymentModalSettings): UseERC1155SaleContractCheckoutReturn => { + const { openERC1155SaleContractPaymentModal } = useERC1155SaleContractPaymentModal() + const { data: checkoutOptions, isLoading: isLoadingCheckoutOptions, isError: isErrorCheckoutOptions } = useCheckoutOptionsSalesContract(chain, { + contractAddress, + wallet, + collectionAddress, + items + }, { + isDev: restArgs.isDev + }) + const network = findSupportedNetwork(chain) + const chainId = network?.chainId || 137 + + const { data: saleConfigData, isLoading: isLoadingSaleConfig, isError: isErrorSaleConfig } = useSaleContractConfig({ chainId, contractAddress, tokenIds: items.map(i => i.tokenId) }) + + const isLoading = isLoadingCheckoutOptions || isLoadingSaleConfig + const error = isErrorCheckoutOptions || isErrorSaleConfig + + const openCheckoutModal = () => { + if (isLoading || error) { + console.error('Error loading checkout options or sale config', { isLoading, error }) + return + } + + openERC1155SaleContractPaymentModal({ + collectibles: items.map(item => ({ + tokenId: item.tokenId, + quantity: item.quantity + })), + chain: chainId, + price: items.reduce((acc, item) => { + const price = BigInt(saleConfigData?.saleConfigs.find(sale => sale.tokenId === item.tokenId)?.price || 0) + console.log('price...', price) + + return acc + BigInt(item.quantity) * price + }, BigInt(0)).toString(), + currencyAddress: saleConfigData?.currencyAddress || '', + recipientAddress: wallet, + collectionAddress, + targetContractAddress: contractAddress, + enableMainCurrencyPayment: true, + enableSwapPayments: checkoutOptions?.options?.swap?.includes(TransactionSwapProvider.zerox) || false, + creditCardProviders: checkoutOptions?.options.nftCheckout || [], + ...restArgs + }) + } + + return ({ + openCheckoutModal, + isLoading, + isError: error + }) +} + +interface UseSaleContractConfigArgs { + chainId: number + contractAddress: string + tokenIds: string[] +} + +interface SaleConfig { + tokenId: string + price: string +} + +interface UseSaleContractConfigData { + currencyAddress: string + saleConfigs: SaleConfig[] +} + +interface UseSaleContractConfigReturn { + data?: UseSaleContractConfigData, + isLoading: boolean, + isError: boolean +} + +export const useSaleContractConfig = ({ chainId, contractAddress, tokenIds, }: UseSaleContractConfigArgs): UseSaleContractConfigReturn => { + const { data: paymentTokenERC1155, isLoading: isLoadingPaymentTokenERC1155, isError: isErrorPaymentTokenERC1155 } = useReadContract({ + chainId, + abi: ERC_1155_SALE_CONTRACT, + address: contractAddress as Hex, + functionName: 'paymentToken', + }) + + interface SaleDetailsERC1155 { + cost: bigint, + startTime: bigint, + endTime: bigint, + supplyCap: bigint, + merkleRoot: string + } + + const { data: globalSaleDetailsERC1155, isLoading: isLoadingGlobalSaleDetailsERC1155, isError: isErrorGlobalSaleDetailsERC1155 } = useReadContract({ + chainId, + abi: ERC_1155_SALE_CONTRACT, + address: contractAddress as Hex, + functionName: 'globalSaleDetails', + }) + + const baseTokenSaleContract = { + chainId, + abi: ERC_1155_SALE_CONTRACT as Abi, + address: contractAddress as Hex, + functionName: 'tokenSaleDetails', + } + + const tokenSaleContracts = tokenIds.map(tokenId => ({ + ...baseTokenSaleContract, + args: [BigInt(tokenId)] + })) + + const { data: tokenSaleDetailsERC1155, isLoading: isLoadingTokenSaleDetailsERC1155, isError: isErrorTokenSaleDetailsERC1155 } = useReadContracts({ + contracts: tokenSaleContracts, + }) + + const isLoadingERC1155 = isLoadingPaymentTokenERC1155 || isLoadingGlobalSaleDetailsERC1155 || isLoadingTokenSaleDetailsERC1155 + const isErrorERC1155 = isErrorPaymentTokenERC1155 || isErrorGlobalSaleDetailsERC1155 || isErrorTokenSaleDetailsERC1155 + + if (isLoadingERC1155 || isErrorERC1155) { + return ({ + data: undefined, + isLoading: isLoadingERC1155, + isError: isErrorERC1155 + }) + } + + const getSaleConfigs = (): SaleConfig[] => { + let saleInfos: SaleConfig[] = [] + + if (isLoadingERC1155 || isErrorERC1155 ) return saleInfos + + // In the sale contract, the global sale has priority over the token sale + // So we need to check if the global sale is set, and if it is, use that + // Otherwise, we use the token sale + const { cost: globalCost, startTime, endTime } = globalSaleDetailsERC1155 as SaleDetailsERC1155 + const isGlobalSaleInvalid = endTime === BigInt(0) || (BigInt(Math.floor(Date.now() / 1000)) <= startTime || BigInt(Math.floor(Date.now() / 1000)) >= endTime) + saleInfos = tokenIds.map((tokenId, index) => { + const tokenPrice = (tokenSaleDetailsERC1155?.[index].result as SaleDetailsERC1155)['cost'] || BigInt(0) + return ({ + tokenId, + price: (!isGlobalSaleInvalid ? globalCost : tokenPrice).toString() + }) + }) + + return saleInfos + } + + return ({ + data: { + currencyAddress: paymentTokenERC1155 as string, + saleConfigs: getSaleConfigs() + }, + isLoading: isLoadingERC1155, + isError: isErrorERC1155 + }) +} \ No newline at end of file diff --git a/packages/checkout/src/hooks/useMarketplaceClient.ts b/packages/checkout/src/hooks/useMarketplaceClient.ts new file mode 100644 index 00000000..04ac9cdf --- /dev/null +++ b/packages/checkout/src/hooks/useMarketplaceClient.ts @@ -0,0 +1,54 @@ +import { useProjectAccessKey } from '@0xsequence/kit' +import { MarketplaceIndexer } from '@0xsequence/marketplace' +import { networks, stringTemplate } from '@0xsequence/network'; +import { useMemo } from 'react' + + +export interface UseMarketplaceClientArgs { + chain: ChainNameOrId, + isDev?: boolean, +} + +export const useMarketplaceClient = ({ chain, isDev = false }: UseMarketplaceClientArgs) => { + const projectAccessKey = useProjectAccessKey() + + const marketplaceClient = useMemo(() => { + const env = isDev ? 'development' : 'production' + const clientUrl = marketplaceApiURL(chain, env) + return new MarketplaceIndexer(clientUrl, projectAccessKey) + }, [projectAccessKey]) + + return marketplaceClient +} + +type ChainNameOrId = string | number; + +const getNetwork = (nameOrId: ChainNameOrId) => { + for (const network of Object.values(networks)) { + if ( + network.name === String(nameOrId).toLowerCase() || + network.chainId === Number(nameOrId) + ) { + return network; + } + } + throw new Error(`Unsopported chain; ${nameOrId}`); +}; + +export type Env = 'development' | 'production'; + +const getPrefix = (env: Env) => { + switch (env) { + case 'development': + return 'dev-'; + case 'production': + return ''; + } +}; + +const marketplaceApiURL = (chain: ChainNameOrId, env: Env = 'production') => { + const prefix = getPrefix(env); + const network = getNetwork(chain).name; + const apiBaseUrl = 'https://${prefix}marketplace-api.sequence.app/${network}' + return stringTemplate(apiBaseUrl, { network: network, prefix }); +}; \ No newline at end of file diff --git a/packages/checkout/src/hooks/useSelectPaymentModal.ts b/packages/checkout/src/hooks/useSelectPaymentModal.ts index 0da8a8e0..b0c92481 100644 --- a/packages/checkout/src/hooks/useSelectPaymentModal.ts +++ b/packages/checkout/src/hooks/useSelectPaymentModal.ts @@ -1,6 +1,7 @@ import { ethers } from 'ethers' import { encodeFunctionData, toHex } from 'viem' +import { ERC_1155_SALE_CONTRACT } from '../constants/abi' import { SelectPaymentSettings } from '../contexts' import { useSelectPaymentContext } from '../contexts/SelectPaymentModal' @@ -10,7 +11,7 @@ export const useSelectPaymentModal = () => { return { openSelectPaymentModal, closeSelectPaymentModal, selectPaymentSettings } } -type ERC1155SaleContractSettings = Omit +type SaleContractSettings = Omit export const getERC1155SaleContractConfig = ({ chain, @@ -21,28 +22,11 @@ export const getERC1155SaleContractConfig = ({ collectionAddress, isDev = false, ...restProps -}: ERC1155SaleContractSettings): SelectPaymentSettings => { - const erc1155SalesContractAbi = [ - { - type: 'function', - name: 'mint', - inputs: [ - { name: 'to', type: 'address', internalType: 'address' }, - { name: 'tokenIds', type: 'uint256[]', internalType: 'uint256[]' }, - { name: 'amounts', type: 'uint256[]', internalType: 'uint256[]' }, - { name: 'data', type: 'bytes', internalType: 'bytes' }, - { name: 'expectedPaymentToken', type: 'address', internalType: 'address' }, - { name: 'maxTotal', type: 'uint256', internalType: 'uint256' }, - { name: 'proof', type: 'bytes32[]', internalType: 'bytes32[]' } - ], - outputs: [], - stateMutability: 'payable' - } - ] - +}: SaleContractSettings): SelectPaymentSettings => { const purchaseTransactionData = encodeFunctionData({ - abi: erc1155SalesContractAbi, + 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 })]] }) @@ -61,7 +45,7 @@ export const getERC1155SaleContractConfig = ({ export const useERC1155SaleContractPaymentModal = () => { const { openSelectPaymentModal, closeSelectPaymentModal, selectPaymentSettings } = useSelectPaymentModal() - const openERC1155SaleContractPaymentModal = (saleContractSettings: ERC1155SaleContractSettings) => { + const openERC1155SaleContractPaymentModal = (saleContractSettings: SaleContractSettings) => { openSelectPaymentModal(getERC1155SaleContractConfig(saleContractSettings)) } diff --git a/packages/checkout/src/index.ts b/packages/checkout/src/index.ts index 8c27b62a..f32d294c 100644 --- a/packages/checkout/src/index.ts +++ b/packages/checkout/src/index.ts @@ -8,6 +8,7 @@ export { useSelectPaymentModal, useERC1155SaleContractPaymentModal } from './hoo export { useTransferFundsModal } from './hooks/useTransferFundsModal' export { useCheckoutWhitelistStatus } from './hooks/useCheckoutWhitelistStatus' export { useSwapModal } from './hooks/useSwapModal' +export { useERC1155SaleContractCheckout } from './hooks/useERC1155SaleContractCheckout' export { type CheckoutSettings } from './contexts/CheckoutModal' export { type AddFundsSettings } from './contexts/AddFundsModal' diff --git a/packages/checkout/src/shared/components/KitCheckoutProvider.tsx b/packages/checkout/src/shared/components/KitCheckoutProvider.tsx index b5a6b8ef..ee15a66e 100644 --- a/packages/checkout/src/shared/components/KitCheckoutProvider.tsx +++ b/packages/checkout/src/shared/components/KitCheckoutProvider.tsx @@ -25,7 +25,7 @@ import { } from '../../contexts' import { NavigationHeader } from '../../shared/components/NavigationHeader' import { - PendingTransaction, + PendingCreditCardTransaction, TransactionError, TransactionSuccess, CheckoutSelection, @@ -153,7 +153,7 @@ export const KitCheckoutContent = ({ children }: KitCheckoutProvider) => { case 'select-method-checkout': return case 'transaction-pending': - return + return case 'transaction-success': return case 'transaction-error': diff --git a/packages/checkout/src/shared/components/NavigationHeader.tsx b/packages/checkout/src/shared/components/NavigationHeader.tsx index d39e6d47..0766dcb3 100644 --- a/packages/checkout/src/shared/components/NavigationHeader.tsx +++ b/packages/checkout/src/shared/components/NavigationHeader.tsx @@ -21,7 +21,6 @@ export const NavigationHeader = ({ secondaryText, primaryText, disableBack = fal { - const defaultNetworks = 'ethereum,mainnet,arbitrum,optimism,polygon,polygonzkevm,zksync,base,bnb,oasys,astar,avaxcchain' + const defaultNetworks = 'ethereum,mainnet,arbitrum,optimism,polygon,polygonzkevm,zksync,base,bnb,oasys,astar,avaxcchain,immutablezkevm' interface Options { [index: string]: string | undefined @@ -13,10 +14,12 @@ export const getTransakLink = (addFundsSettings: AddFundsSettings) => { apiKey: TRANSAK_API_KEY, referrerDomain: window.location.origin, walletAddress: addFundsSettings.walletAddress, + fiatAmount: addFundsSettings?.fiatAmount, fiatCurrency: addFundsSettings?.fiatCurrency, disableWalletAddressForm: 'true', defaultFiatAmount: addFundsSettings?.defaultFiatAmount || '50', defaultCryptoCurrency: addFundsSettings?.defaultCryptoCurrency || 'USDC', + cryptoCurrencyList: addFundsSettings?.cryptoCurrencyList, networks: addFundsSettings?.networks || defaultNetworks } diff --git a/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx b/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx index ceb7a4b3..2e8e2080 100644 --- a/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx +++ b/packages/checkout/src/views/PaymentSelection/PayWithCreditCard/index.tsx @@ -24,7 +24,7 @@ interface PayWithCreditCardProps { disableButtons: boolean } -type PaymentProviderOptions = 'sardine' +type PaymentProviderOptions = 'sardine' | 'transak' export const PayWithCreditCard = ({ settings, disableButtons }: PayWithCreditCardProps) => { const { @@ -39,7 +39,8 @@ export const PayWithCreditCard = ({ settings, disableButtons }: PayWithCreditCar isDev = false, onSuccess = () => {}, onError = () => {}, - creditCardProviders = [] + creditCardProviders = [], + transakConfig } = settings const { address: userAddress } = useAccount() @@ -61,14 +62,15 @@ export const PayWithCreditCard = ({ settings, disableButtons }: PayWithCreditCar const payWithSelectedProvider = () => { switch (selectedPaymentProvider) { case 'sardine': - onPurchaseSardine() + case 'transak': + onPurchase() return default: return } } - const onPurchaseSardine = () => { + const onPurchase = () => { if (!userAddress || !currencyInfoData) { return } @@ -94,7 +96,9 @@ export const PayWithCreditCard = ({ settings, disableButtons }: PayWithCreditCar nftQuantity: collectible.quantity, nftDecimals: collectible.decimals === undefined ? undefined : String(collectible.decimals), isDev, + provider: selectedPaymentProvider, calldata: txData, + transakConfig, approvedSpenderAddress: approvedSpenderAddress || targetContractAddress } } @@ -106,40 +110,51 @@ export const PayWithCreditCard = ({ settings, disableButtons }: PayWithCreditCar const Options = () => { return ( - {creditCardProviders.map(creditCardProvider => { - switch (creditCardProvider) { - case 'sardine': - return ( - { - setSelectedPaymentProvider('sardine') - }} - opacity={{ - hover: '80', - base: '100' - }} - cursor="pointer" - disabled={disableButtons} - > - - - - Pay with credit or debit card - - - - - - - ) - default: - return null - } - })} + {/* 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': + return ( + { + setSelectedPaymentProvider(creditCardProvider) + }} + opacity={{ + hover: '80', + base: '100' + }} + cursor="pointer" + disabled={disableButtons} + > + + + + Pay with credit or debit card + + + + + + + ) + default: + return null + } + })} ) } diff --git a/packages/checkout/src/views/PendingCreditCardTransaction.tsx b/packages/checkout/src/views/PendingCreditCardTransaction.tsx new file mode 100644 index 00000000..d6ffb2ae --- /dev/null +++ b/packages/checkout/src/views/PendingCreditCardTransaction.tsx @@ -0,0 +1,378 @@ +import { Box, Spinner, Text } from '@0xsequence/design-system' +import { useProjectAccessKey, useContractInfo, useTokenMetadata } from '@0xsequence/kit' +import { findSupportedNetwork } from '@0xsequence/network' +import pako from 'pako' +import { useEffect } from 'react' +import { formatUnits } from 'viem' + +import { fetchSardineOrderStatus } from '../api' +import { TransactionPendingNavigation } from '../contexts' +import { useNavigation, useCheckoutModal, useSardineClientToken, useTransactionStatusModal } from '../hooks' +import { TRANSAK_PROXY_ADDRESS } from '../utils/transak' +const POLLING_TIME = 10 * 1000 + +export const PendingCreditCardTransaction = () => { + const nav = useNavigation() + const { + params: { + creditCardCheckout: { provider } + } + } = nav.navigation as TransactionPendingNavigation + + switch (provider) { + case 'transak': + return + case 'sardine': + default: + return + } +} + +export const PendingCreditCardTransactionTransak = () => { + const { openTransactionStatusModal } = useTransactionStatusModal() + const nav = useNavigation() + const { settings, closeCheckout } = useCheckoutModal() + + const { + params: { creditCardCheckout } + } = nav.navigation as TransactionPendingNavigation + + const { setNavigation } = nav + + const { + data: tokensMetadata, + isLoading: isLoadingTokenMetadata, + isError: isErrorTokenMetadata + } = useTokenMetadata(creditCardCheckout.chainId, creditCardCheckout.nftAddress, [creditCardCheckout.nftId]) + const { + data: collectionInfo, + isLoading: isLoadingCollectionInfo, + isError: isErrorCollectionInfo + } = useContractInfo(creditCardCheckout.chainId, creditCardCheckout.nftAddress) + + const network = findSupportedNetwork(creditCardCheckout.chainId) + + const tokenMetadata = tokensMetadata ? tokensMetadata[0] : undefined + + const transakConfig = settings?.creditCardCheckout?.transakConfig + + const baseUrl = creditCardCheckout.isDev ? 'https://global-stg.transak.com' : 'https://global.transak.com' + + // Transak requires the recipient address to be the proxy address + // so we need to replace the recipient address with the proxy address in the calldata + // this is a weird hack so that credit card integrations are as simple as possible and should work 99% of the time + // If an issue arises, the user can override the calldata in the transak settings + + const calldataWithProxy = + transakConfig?.callDataOverride ?? + creditCardCheckout.calldata.replace( + creditCardCheckout.recipientAddress.toLowerCase().substring(2), + TRANSAK_PROXY_ADDRESS.toLowerCase().substring(2) + ) + + const pakoData = Array.from(pako.deflate(calldataWithProxy)) + + const transakCallData = encodeURIComponent(btoa(String.fromCharCode.apply(null, pakoData))) + + const price = Number(formatUnits(BigInt(creditCardCheckout.currencyQuantity), Number(creditCardCheckout.currencyDecimals))) + + const transakNftDataJson = JSON.stringify([ + { + imageURL: tokenMetadata?.image || '', + nftName: tokenMetadata?.name || 'collectible', + collectionAddress: creditCardCheckout.nftAddress, + tokenID: [creditCardCheckout.nftId], + price: [price], + quantity: Number(creditCardCheckout.nftQuantity), + nftType: collectionInfo?.type || 'ERC721' + } + ]) + + console.log('transakNftDataJson', JSON.parse(transakNftDataJson)) + const transakNftData = encodeURIComponent(btoa(transakNftDataJson)) + + const estimatedGasLimit = '500000' + + const partnerOrderId = `${creditCardCheckout.recipientAddress}-${new Date().getTime()}` + + // Note: the network name might not always line up with Transak. A conversion function might be necessary + const networkName = network?.name.toLowerCase() + + const transakLink = `${baseUrl}?apiKey=${transakConfig?.apiKey}&isNFT=true&calldata=${transakCallData}&contractId=${transakConfig?.contractId}&cryptoCurrencyCode=${creditCardCheckout.currencySymbol}&estimatedGasLimit=${estimatedGasLimit}&nftData=${transakNftData}&walletAddress=${creditCardCheckout.recipientAddress}&disableWalletAddressForm=true&partnerOrderId=${partnerOrderId}&network=${networkName}` + + const isLoading = isLoadingTokenMetadata || isLoadingCollectionInfo + const isError = isErrorTokenMetadata || isErrorCollectionInfo + + useEffect(() => { + const transakIframeElement = document.getElementById('transakIframe') as HTMLIFrameElement + const transakIframe = transakIframeElement.contentWindow + + const readMessage = (message: any) => { + if (message.source !== transakIframe) return + + if (message?.data?.event_id === 'TRANSAK_ORDER_SUCCESSFUL' && message?.data?.data?.status === 'COMPLETED') { + console.log('Order Data: ', message?.data?.data) + const txHash = message?.data?.data?.transactionHash || '' + + closeCheckout() + openTransactionStatusModal({ + chainId: creditCardCheckout.chainId, + currencyAddress: creditCardCheckout.currencyAddress, + collectionAddress: creditCardCheckout.nftAddress, + txHash: txHash, + items: [ + { + tokenId: creditCardCheckout.nftId, + quantity: creditCardCheckout.nftQuantity, + decimals: creditCardCheckout.nftDecimals === undefined ? undefined : Number(creditCardCheckout.nftDecimals), + price: creditCardCheckout.currencyQuantity + } + ], + onSuccess: () => { + if (creditCardCheckout.onSuccess) { + creditCardCheckout.onSuccess(txHash, creditCardCheckout) + } + } + }) + return + } + + if (message?.data?.event_id === 'TRANSAK_ORDER_FAILED') { + setNavigation({ + location: 'transaction-error', + params: { + error: new Error('Transak transaction failed') + } + }) + } + } + + window.addEventListener('message', readMessage) + + return () => window.removeEventListener('message', readMessage) + }, [isLoading]) + + if (isError || !transakConfig) { + return ( + + + {!transakConfig ? ( + Error: No Transak configuration found + ) : ( + An error has occurred + )} + + + ) + } + + if (isLoading) { + return ( + + + + + + ) + } + + return ( + +