From 6c25205e3ebb32f4d9c6dbfc16b3376e600a7496 Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 5 Feb 2026 18:54:00 +0000 Subject: [PATCH 1/3] feat: add NFT transfer support to wallet send flow - Add nft state and setNft action to wallet Redux store - Add transferNft saga action and handleTransferNft saga - Update handleNext saga to skip Amount stage for NFT transfers - Update handlePrevious saga to handle NFT flow navigation - Add Tokens/NFTs tabs to asset selection screen - Update review transfer screen to show NFT details - Update success screen to show NFT transfer confirmation - Add tests for NFT transfer flow --- .../wallet-review-transfer.module.scss | 54 +++++++++ .../wallet-review-transfer.tsx | 60 +++++++--- .../wallet-transfer-success.module.scss | 58 ++++++++++ .../send/success/wallet-transfer-success.tsx | 41 +++++-- .../wallet-token-select.module.scss | 105 ++++++++++++++++++ .../send/token-select/wallet-token-select.tsx | 104 +++++++++++++++-- src/store/wallet/index.ts | 15 ++- src/store/wallet/saga.test.ts | 81 +++++++++++++- src/store/wallet/saga.ts | 69 +++++++++++- src/store/wallet/selectors.ts | 1 + 10 files changed, 537 insertions(+), 51 deletions(-) diff --git a/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss b/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss index 8aaf28130..a6e1b2905 100644 --- a/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss +++ b/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss @@ -106,3 +106,57 @@ text-align: center; color: var(--text-secondary); } + +.nftTransferDetails { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px var(--l-spacing); + border: 1px solid var(--border-primary); + border-radius: 12px; + gap: 16px; +} + +.nftPreview { + width: 120px; + height: 120px; +} + +.nftImage { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.nftImagePlaceholder { + width: 100%; + height: 100%; + background: var(--bg-secondary); + border-radius: 8px; +} + +.nftInfo { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + text-align: center; +} + +.nftName { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.nftCollection { + font-size: 14px; + color: var(--text-secondary); +} + +.nftTokenId { + font-size: 12px; + color: var(--text-tertiary); + font-family: monospace; +} diff --git a/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx b/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx index 942c7e67a..55cc820fb 100644 --- a/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx +++ b/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx @@ -1,8 +1,8 @@ import { useDispatch, useSelector } from 'react-redux'; -import { previousStage, transferToken } from '../../../../store/wallet'; +import { previousStage, transferToken, transferNft } from '../../../../store/wallet'; import styles from './wallet-review-transfer.module.scss'; import { SendHeader } from '../components/send-header'; -import { amountSelector, recipientSelector, tokenSelector } from '../../../../store/wallet/selectors'; +import { amountSelector, recipientSelector, tokenSelector, nftSelector } from '../../../../store/wallet/selectors'; import { MatrixAvatar } from '../../../../components/matrix-avatar'; import { TokenIcon } from '../../components/token-icon/token-icon'; import { FormattedNumber } from '../../components/formatted-number/formatted-number'; @@ -14,13 +14,20 @@ export const WalletReviewTransfer = () => { const recipient = useSelector(recipientSelector); const token = useSelector(tokenSelector); const amount = useSelector(amountSelector); + const nft = useSelector(nftSelector); + + const isNftTransfer = nft !== null; const handleBack = () => { dispatch(previousStage()); }; const handleConfirm = () => { - dispatch(transferToken()); + if (isNftTransfer) { + dispatch(transferNft()); + } else { + dispatch(transferToken()); + } }; return ( @@ -35,27 +42,44 @@ export const WalletReviewTransfer = () => {
{recipient?.publicAddress}
-
-
- -
{token.name}
-
- + {isNftTransfer ? ( +
+
+ {nft.imageUrl ? ( + {nft.metadata?.name + ) : ( +
+ )} +
+
+
{nft.metadata?.name || 'Unnamed NFT'}
+
{nft.collectionName}
+
Token ID: {nft.id}
+ ) : ( +
+
+ +
{token?.name}
+
+ +
+
-
- -
+
+ +
-
- -
{token.name}
-
- +
+ +
{token?.name}
+
+ +
-
+ )}
diff --git a/src/apps/wallet/send/success/wallet-transfer-success.module.scss b/src/apps/wallet/send/success/wallet-transfer-success.module.scss index da4cfe31b..75ae3c85e 100644 --- a/src/apps/wallet/send/success/wallet-transfer-success.module.scss +++ b/src/apps/wallet/send/success/wallet-transfer-success.module.scss @@ -158,3 +158,61 @@ font-weight: 400; color: var(--text-secondary); } + +.nftHero { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 30vh; +} + +.nftSuccessImageContainer { + display: grid; + place-items: center; + margin-bottom: 16px; + + & > * { + grid-column: 1; + grid-row: 1; + } +} + +.nftSuccessGlow { + background-color: rgb(172, 253, 90, 0.15); + filter: blur(20px); + border-radius: 12px; + width: 140px; + height: 140px; +} + +.nftSuccessImage { + width: 120px; + height: 120px; + object-fit: cover; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.nftSuccessImagePlaceholder { + width: 120px; + height: 120px; + background: var(--bg-secondary); + border-radius: 8px; +} + +.nftSuccessName { + color: #fff; + filter: drop-shadow(0px 0px 4px rgba(255, 255, 255, 0.4)); + font-family: IBM Plex Sans; + font-weight: 600; + font-size: 24px; + text-align: center; +} + +.nftSuccessCollection { + font-size: 16px; + color: var(--text-secondary); + font-weight: 400; + margin-top: 4px; +} diff --git a/src/apps/wallet/send/success/wallet-transfer-success.tsx b/src/apps/wallet/send/success/wallet-transfer-success.tsx index 6eb68dbee..23a57035a 100644 --- a/src/apps/wallet/send/success/wallet-transfer-success.tsx +++ b/src/apps/wallet/send/success/wallet-transfer-success.tsx @@ -11,6 +11,7 @@ import { recipientSelector, selectedWalletSelector, tokenSelector, + nftSelector, txReceiptSelector, } from '../../../../store/wallet/selectors'; import { FormattedNumber } from '../../components/formatted-number/formatted-number'; @@ -31,6 +32,9 @@ export const WalletTransferSuccess = () => { const currentUser = useSelector(currentUserSelector); const selectedWallet = useSelector(selectedWalletSelector); const txReceipt = useSelector(txReceiptSelector); + const nft = useSelector(nftSelector); + + const isNftTransfer = nft !== null; const handleClose = () => { dispatch(reset()); @@ -42,25 +46,40 @@ export const WalletTransferSuccess = () => { }; const dollarAmount = useMemo(() => { - if (!token.price) return '--'; + if (!token?.price) return '--'; return formatDollars(Number(amount) * Number(token.price)); - }, [token.price, amount]); + }, [token?.price, amount]); return (
} />
-
-
-
-
- + {isNftTransfer ? ( +
+
+
+ {nft.imageUrl ? ( + {nft.metadata?.name + ) : ( +
+ )} +
+
{nft.metadata?.name || 'NFT'}
+
{nft.collectionName}
-
{dollarAmount}
-
- {token.symbol} + ) : ( +
+
+
+
+ +
+
{dollarAmount}
+
+ {token?.symbol} +
-
+ )}
diff --git a/src/apps/wallet/send/token-select/wallet-token-select.module.scss b/src/apps/wallet/send/token-select/wallet-token-select.module.scss index 7a8159d44..c2ee671cb 100644 --- a/src/apps/wallet/send/token-select/wallet-token-select.module.scss +++ b/src/apps/wallet/send/token-select/wallet-token-select.module.scss @@ -9,6 +9,37 @@ flex-direction: column; gap: var(--l-spacing); flex: 1; + overflow: hidden; +} + +.tabSwitcher { + display: flex; + gap: 8px; + padding: 0 var(--l-spacing); +} + +.tab { + flex: 1; + padding: 10px 16px; + border: 1px solid var(--border-primary); + background: transparent; + color: var(--text-secondary); + cursor: pointer; + border-radius: 8px; + transition: all 0.2s; + font-family: IBM Plex Sans; + font-weight: 500; + font-size: 14px; + + &:hover { + background: var(--bg-hover); + } +} + +.tabActive { + background: var(--bg-accent-dark-blue); + color: var(--text-primary); + border-color: var(--bg-accent-dark-blue); } .inputContainer { @@ -17,6 +48,11 @@ padding: 0 var(--l-spacing); } +.resultsContainer { + flex: 1; + overflow-y: auto; +} + .resultsHeaderLabel { padding: 0 var(--l-spacing); margin-bottom: 8px; @@ -72,3 +108,72 @@ color: var(--text-secondary); } + +.nftGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + padding: 0 var(--l-spacing); +} + +.nftSelectItem { + display: flex; + flex-direction: column; + padding: 8px; + border: 1px solid var(--border-primary); + border-radius: 8px; + background: transparent; + cursor: pointer; + text-align: left; + transition: all 0.2s; + + &:hover { + background: var(--bg-hover); + border-color: var(--border-hover); + } +} + +.nftImage { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 4px; +} + +.nftImagePlaceholder { + width: 100%; + aspect-ratio: 1; + background: var(--bg-secondary); + border-radius: 4px; +} + +.nftInfo { + margin-top: 8px; +} + +.nftName { + font-family: IBM Plex Sans; + font-weight: 500; + font-size: 14px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nftCollection { + font-family: IBM Plex Sans; + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.emptyState { + padding: 24px var(--l-spacing); + text-align: center; + color: var(--text-secondary); + font-family: IBM Plex Sans; + font-size: 14px; +} diff --git a/src/apps/wallet/send/token-select/wallet-token-select.tsx b/src/apps/wallet/send/token-select/wallet-token-select.tsx index ec3abb332..1b268cb3f 100644 --- a/src/apps/wallet/send/token-select/wallet-token-select.tsx +++ b/src/apps/wallet/send/token-select/wallet-token-select.tsx @@ -1,34 +1,53 @@ import { useDispatch, useSelector } from 'react-redux'; -import { nextStage, previousStage, setToken } from '../../../../store/wallet'; +import { nextStage, previousStage, setToken, setNft } from '../../../../store/wallet'; import { SendHeader } from '../components/send-header'; import { Input } from '@zero-tech/zui/components'; import { IconSearchMd } from '@zero-tech/zui/icons'; import { useState } from 'react'; import { useBalancesQuery } from '../../queries/useBalancesQuery'; +import { useNFTsQuery } from '../../queries/useNFTsQuery'; import { Token } from '../../tokens/token'; -import { TokenBalance } from '../../types'; +import { NFT, TokenBalance } from '../../types'; import { recipientSelector, selectedWalletSelector } from '../../../../store/wallet/selectors'; import { truncateAddress } from '../../utils/address'; +import cn from 'classnames'; import styles from './wallet-token-select.module.scss'; +type AssetTab = 'tokens' | 'nfts'; + export const WalletTokenSelect = () => { const dispatch = useDispatch(); const [assetQuery, setAssetQuery] = useState(''); + const [activeTab, setActiveTab] = useState('tokens'); const selectedWallet = useSelector(selectedWalletSelector); const recipient = useSelector(recipientSelector); const hasZid = recipient?.primaryZid !== null; const hasName = recipient?.name !== null; const { data } = useBalancesQuery(selectedWallet.address); - // TODO: Add search functionality to token balances endpoint - const assets = data?.tokens?.filter((asset) => asset.name.toLowerCase().includes(assetQuery.toLowerCase())); + const { nfts, isLoading: isLoadingNfts } = useNFTsQuery(selectedWallet.address); + + const filteredTokens = data?.tokens?.filter((asset) => asset.name.toLowerCase().includes(assetQuery.toLowerCase())); + + const filteredNfts = nfts?.filter( + (nft) => + nft.metadata?.name?.toLowerCase().includes(assetQuery.toLowerCase()) || + nft.collectionName?.toLowerCase().includes(assetQuery.toLowerCase()) + ); const handleTokenClick = (token: TokenBalance) => { + dispatch(setNft(null)); dispatch(setToken(token)); dispatch(nextStage()); }; + const handleNftClick = (nft: NFT) => { + dispatch(setToken(null)); + dispatch(setNft(nft)); + dispatch(nextStage()); + }; + const handleBack = () => { dispatch(previousStage()); }; @@ -38,6 +57,21 @@ export const WalletTokenSelect = () => {
+
+ + +
+
{
-
-
Results
-
- {assets?.map((asset) => ( - handleTokenClick(asset)} /> - ))} + {activeTab === 'tokens' && ( +
+
Tokens
+
+ {filteredTokens?.map((asset) => ( + handleTokenClick(asset)} + /> + ))} + {filteredTokens?.length === 0 &&
No tokens found
} +
-
+ )} + + {activeTab === 'nfts' && ( +
+
NFTs
+ {isLoadingNfts ? ( +
Loading NFTs...
+ ) : ( +
+ {filteredNfts?.map((nft) => ( + handleNftClick(nft)} + /> + ))} +
+ )} + {!isLoadingNfts && filteredNfts?.length === 0 &&
No NFTs found
} +
+ )}
@@ -78,3 +139,24 @@ export const WalletTokenSelect = () => {
); }; + +interface NFTSelectItemProps { + nft: NFT; + onClick: () => void; +} + +const NFTSelectItem = ({ nft, onClick }: NFTSelectItemProps) => { + return ( + + ); +}; diff --git a/src/store/wallet/index.ts b/src/store/wallet/index.ts index af1ae7509..9549c0bd9 100644 --- a/src/store/wallet/index.ts +++ b/src/store/wallet/index.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction, createAction } from '@reduxjs/toolkit'; -import { Recipient, TokenBalance } from '../../apps/wallet/types'; +import { NFT, Recipient, TokenBalance } from '../../apps/wallet/types'; import { TxReceiptResponse } from '../../apps/wallet/queries/txReceiptQueryOptions'; export enum SendStage { @@ -17,6 +17,7 @@ export enum SagaActionTypes { NextStage = 'wallet/saga/nextStage', PreviousStage = 'wallet/saga/previousStage', TransferToken = 'wallet/saga/transferToken', + TransferNft = 'wallet/saga/transferNft', } export type WalletState = { @@ -27,6 +28,7 @@ export type WalletState = { recipient: Recipient | null; sendStage: SendStage; token: TokenBalance | null; + nft: NFT | null; amount: string | null; txReceipt: TxReceiptResponse | null; error: boolean; @@ -41,6 +43,7 @@ const initialState: WalletState = { recipient: null, sendStage: SendStage.Search, token: null, + nft: null, amount: null, txReceipt: null, error: false, @@ -57,10 +60,13 @@ const slice = createSlice({ setRecipient: (state, action: PayloadAction) => { state.recipient = action.payload; }, - setToken: (state, action: PayloadAction) => { + setToken: (state, action: PayloadAction) => { state.token = action.payload; }, - setAmount: (state, action: PayloadAction) => { + setNft: (state, action: PayloadAction) => { + state.nft = action.payload; + }, + setAmount: (state, action: PayloadAction) => { state.amount = action.payload; }, setSendStage: (state, action: PayloadAction) => { @@ -79,6 +85,7 @@ const slice = createSlice({ state.recipient = initialState.recipient; state.sendStage = initialState.sendStage; state.token = initialState.token; + state.nft = initialState.nft; state.amount = initialState.amount; state.txReceipt = initialState.txReceipt; state.error = initialState.error; @@ -89,11 +96,13 @@ const slice = createSlice({ export const nextStage = createAction(SagaActionTypes.NextStage); export const previousStage = createAction(SagaActionTypes.PreviousStage); export const transferToken = createAction(SagaActionTypes.TransferToken); +export const transferNft = createAction(SagaActionTypes.TransferNft); export const { setSelectedWallet, setRecipient, setToken, + setNft, setAmount, setSendStage, setTxReceipt, diff --git a/src/store/wallet/saga.test.ts b/src/store/wallet/saga.test.ts index a2dd4eaa3..4fded1816 100644 --- a/src/store/wallet/saga.test.ts +++ b/src/store/wallet/saga.test.ts @@ -2,12 +2,13 @@ import { expectSaga } from 'redux-saga-test-plan'; import { select } from 'redux-saga/effects'; import { saga } from './saga'; -import { nextStage, previousStage, SendStage, setSendStage, setRecipient, setToken, setAmount } from '.'; +import { nextStage, previousStage, SendStage, setSendStage, setRecipient, setToken, setNft, setAmount } from '.'; import { recipientSelector, sendStageSelector, amountSelector, tokenSelector, + nftSelector, walletSelector, selectedWalletAddressSelector, } from './selectors'; @@ -19,6 +20,7 @@ describe('wallet saga', () => { .provide([ [select(sendStageSelector), SendStage.Search], [select(recipientSelector), '0x1234567890'], + [select(nftSelector), null], [ select(walletSelector), { @@ -40,6 +42,7 @@ describe('wallet saga', () => { .provide([ [select(sendStageSelector), SendStage.Search], [select(recipientSelector), null], + [select(nftSelector), null], [ select(walletSelector), { @@ -56,10 +59,11 @@ describe('wallet saga', () => { .run(); }); - it('moves from token to amount if token is set', () => { + it('moves from token to amount if token is set (no NFT)', () => { return expectSaga(saga) .provide([ [select(sendStageSelector), SendStage.Token], + [select(nftSelector), null], [ select(tokenSelector), { @@ -86,11 +90,43 @@ describe('wallet saga', () => { .run(); }); + it('moves from token to confirm if NFT is set (skips amount)', () => { + return expectSaga(saga) + .provide([ + [select(sendStageSelector), SendStage.Token], + [ + select(nftSelector), + { + id: '123', + collectionAddress: '0xNFT1234', + collectionName: 'Test Collection', + imageUrl: 'https://example.com/nft.png', + metadata: { name: 'Test NFT', description: '', attributes: [] }, + }, + ], + [select(tokenSelector), null], + [ + select(walletSelector), + { + selectedWallet: { + address: '0x1234567890', + label: null, + }, + }, + ], + [select(selectedWalletAddressSelector), '0x1234567890'], + ]) + .put(setSendStage(SendStage.Confirm)) + .dispatch(nextStage()) + .run(); + }); + it('moves from amount to confirm if amount is set', () => { return expectSaga(saga) .provide([ [select(sendStageSelector), SendStage.Amount], [select(amountSelector), '123'], + [select(nftSelector), null], [ select(walletSelector), { @@ -109,10 +145,11 @@ describe('wallet saga', () => { }); describe('previous', () => { - it('moves from confirm to amount and clears amount', () => { + it('moves from confirm to amount and clears amount (token flow)', () => { return expectSaga(saga) .provide([ [select(sendStageSelector), SendStage.Confirm], + [select(nftSelector), null], [ select(walletSelector), { @@ -130,10 +167,42 @@ describe('wallet saga', () => { .run(); }); + it('moves from confirm to token and clears NFT (NFT flow)', () => { + return expectSaga(saga) + .provide([ + [select(sendStageSelector), SendStage.Confirm], + [ + select(nftSelector), + { + id: '123', + collectionAddress: '0xNFT1234', + collectionName: 'Test Collection', + imageUrl: 'https://example.com/nft.png', + metadata: { name: 'Test NFT', description: '', attributes: [] }, + }, + ], + [ + select(walletSelector), + { + selectedWallet: { + address: '0x1234567890', + label: null, + }, + }, + ], + [select(selectedWalletAddressSelector), '0x1234567890'], + ]) + .put(setNft(null)) + .put(setSendStage(SendStage.Token)) + .dispatch(previousStage()) + .run(); + }); + it('moves from amount to token and clears token info', () => { return expectSaga(saga) .provide([ [select(sendStageSelector), SendStage.Amount], + [select(nftSelector), null], [ select(walletSelector), { @@ -151,10 +220,11 @@ describe('wallet saga', () => { .run(); }); - it('moves from token to search and clears recipient', () => { + it('moves from token to search and clears recipient, nft, and token', () => { return expectSaga(saga) .provide([ [select(sendStageSelector), SendStage.Token], + [select(nftSelector), null], [ select(walletSelector), { @@ -167,6 +237,8 @@ describe('wallet saga', () => { [select(selectedWalletAddressSelector), '0x1234567890'], ]) .put(setRecipient(null)) + .put(setNft(null)) + .put(setToken(null)) .put(setSendStage(SendStage.Search)) .dispatch(previousStage()) .run(); @@ -176,6 +248,7 @@ describe('wallet saga', () => { return expectSaga(saga) .provide([ [select(sendStageSelector), SendStage.Search], + [select(nftSelector), null], [ select(walletSelector), { diff --git a/src/store/wallet/saga.ts b/src/store/wallet/saga.ts index 2894302af..e85d2020d 100644 --- a/src/store/wallet/saga.ts +++ b/src/store/wallet/saga.ts @@ -7,8 +7,10 @@ import { setRecipient, setAmount, setToken, + setNft, setTxReceipt, transferToken, + transferNft, setSelectedWallet, setError, reset, @@ -19,13 +21,15 @@ import { sendStageSelector, amountSelector, tokenSelector, + nftSelector, selectedWalletAddressSelector, } from './selectors'; import { queryClient } from '../../lib/web3/rainbowkit/provider'; import { transferTokenRequest, TransferTokenResponse } from '../../apps/wallet/queries/transferTokenRequest'; import { txReceiptQueryOptions, TxReceiptResponse } from '../../apps/wallet/queries/txReceiptQueryOptions'; import { setUser } from '../authentication'; -import { Recipient, TokenBalance } from '../../apps/wallet/types'; +import { NFT, Recipient, TokenBalance } from '../../apps/wallet/types'; +import { transferNFTRequest, TransferNFTResponse } from '../../apps/wallet/queries/transferNFTRequest'; import { transferNativeAssetRequest, TransferNativeAssetResponse, @@ -103,6 +107,7 @@ function* handleTransferToken() { function* handleNext() { const stage: SendStage = yield select(sendStageSelector); + const nft: NFT | null = yield select(nftSelector); switch (stage) { case SendStage.Search: { @@ -114,7 +119,11 @@ function* handleNext() { } case SendStage.Token: { const token = yield select(tokenSelector); - if (token) { + if (nft) { + // NFT selected - skip Amount, go straight to Confirm + yield put(setSendStage(SendStage.Confirm)); + } else if (token) { + // Token selected - go to Amount yield put(setSendStage(SendStage.Amount)); } break; @@ -131,11 +140,19 @@ function* handleNext() { function* handlePrevious() { const stage: SendStage = yield select(sendStageSelector); + const nft: NFT | null = yield select(nftSelector); switch (stage) { case SendStage.Confirm: - yield put(setAmount(null)); - yield put(setSendStage(SendStage.Amount)); + if (nft) { + // NFT flow - go back to Token selection (skip Amount) + yield put(setNft(null)); + yield put(setSendStage(SendStage.Token)); + } else { + // Token flow - go back to Amount + yield put(setAmount(null)); + yield put(setSendStage(SendStage.Amount)); + } break; case SendStage.Amount: yield put(setToken(null)); @@ -143,6 +160,8 @@ function* handlePrevious() { break; case SendStage.Token: yield put(setRecipient(null)); + yield put(setNft(null)); + yield put(setToken(null)); yield put(setSendStage(SendStage.Search)); break; case SendStage.Search: @@ -151,6 +170,47 @@ function* handlePrevious() { } } +function* handleTransferNft() { + const stage: SendStage = yield select(sendStageSelector); + + try { + if (stage === SendStage.Confirm) { + const recipient: Recipient = yield select(recipientSelector); + const selectedWallet: string | undefined = yield select(selectedWalletAddressSelector); + const nft: NFT = yield select(nftSelector); + + if (recipient && nft && selectedWallet) { + yield put(setSendStage(SendStage.Processing)); + + const result: TransferNFTResponse = yield call(() => + transferNFTRequest(selectedWallet, recipient.publicAddress, nft.id, nft.collectionAddress) + ); + + if (result.transactionHash) { + yield put(setSendStage(SendStage.Broadcasting)); + const receipt: TxReceiptResponse = yield call(() => + queryClient.fetchQuery(txReceiptQueryOptions(result.transactionHash)) + ); + yield put(setTxReceipt(receipt)); + if (receipt.status === 'confirmed') { + yield put(setSendStage(SendStage.Success)); + } else { + yield put(setSendStage(SendStage.Error)); + } + } else { + yield put(setSendStage(SendStage.Error)); + } + } + } + } catch (e) { + if (isWalletAPIError(e)) { + yield put(setErrorCode(e.response.body.code)); + } + yield put(setError(true)); + yield put(setSendStage(SendStage.Error)); + } +} + export function* clearWallet() { yield put(setSelectedWallet({ address: '', label: null })); yield put(reset()); @@ -161,4 +221,5 @@ export function* saga() { yield takeLatest(nextStage.type, handleNext); yield takeLatest(previousStage.type, handlePrevious); yield takeLatest(transferToken.type, handleTransferToken); + yield takeLatest(transferNft.type, handleTransferNft); } diff --git a/src/store/wallet/selectors.ts b/src/store/wallet/selectors.ts index a36d07424..70ebcae74 100644 --- a/src/store/wallet/selectors.ts +++ b/src/store/wallet/selectors.ts @@ -4,6 +4,7 @@ export const walletSelector = (state: RootState) => state.wallet; export const recipientSelector = (state: RootState) => state.wallet.recipient; export const sendStageSelector = (state: RootState) => state.wallet.sendStage; export const tokenSelector = (state: RootState) => state.wallet.token; +export const nftSelector = (state: RootState) => state.wallet.nft; export const amountSelector = (state: RootState) => state.wallet.amount; export const selectedWalletSelector = (state: RootState) => state.wallet.selectedWallet; export const txReceiptSelector = (state: RootState) => state.wallet.txReceipt; From 2cd37e6244dcead04048fdc52abae270408369b5 Mon Sep 17 00:00:00 2001 From: dominic Date: Wed, 11 Feb 2026 11:03:37 +0000 Subject: [PATCH 2/3] fix: polish NFT transfer flow - chainId, placeholders, pagination, and UI improvements - Fix receipt polling to use correct Z chain ID instead of ETH chain - Reuse NFTTile component in send flow for consistent NFT display - Add IconPackageMinus placeholder for NFTs without images on review and success screens - Add infinite scroll pagination for NFT selection - Truncate recipient address and long token IDs on review screen - Use WalletEmptyState component for empty NFT list --- src/apps/wallet/nfts/nft/nft-tile.tsx | 9 ++- .../wallet-review-transfer.module.scss | 7 ++ .../wallet-review-transfer.tsx | 11 +++- .../wallet-transfer-success.module.scss | 3 + .../send/success/wallet-transfer-success.tsx | 6 +- .../wallet-token-select.module.scss | 53 +-------------- .../send/token-select/wallet-token-select.tsx | 65 ++++++++----------- src/store/wallet/saga.ts | 3 +- 8 files changed, 60 insertions(+), 97 deletions(-) diff --git a/src/apps/wallet/nfts/nft/nft-tile.tsx b/src/apps/wallet/nfts/nft/nft-tile.tsx index 87fbfecdd..517342345 100644 --- a/src/apps/wallet/nfts/nft/nft-tile.tsx +++ b/src/apps/wallet/nfts/nft/nft-tile.tsx @@ -9,16 +9,21 @@ import styles from './nft-tile.module.scss'; interface NFTTileProps { nft: NFT; + onClick?: (nft: NFT) => void; } -export const NFTTile = ({ nft }: NFTTileProps) => { +export const NFTTile = ({ nft, onClick }: NFTTileProps) => { const history = useHistory(); const [isCopied, setIsCopied] = useState(false); const nftId = nft.id; const nftName = nft.metadata?.name ?? 'Unnamed Token'; const handleTileClick = () => { - history.push(`/wallet/nfts/${encodeURIComponent(nft.collectionAddress)}/${encodeURIComponent(nftId)}`); + if (onClick) { + onClick(nft); + } else { + history.push(`/wallet/nfts/${encodeURIComponent(nft.collectionAddress)}/${encodeURIComponent(nftId)}`); + } }; const handleCopyId = (e: React.MouseEvent) => { diff --git a/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss b/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss index a6e1b2905..f73f8adcc 100644 --- a/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss +++ b/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss @@ -134,6 +134,9 @@ height: 100%; background: var(--bg-secondary); border-radius: 8px; + display: grid; + place-items: center; + color: var(--text-secondary); } .nftInfo { @@ -159,4 +162,8 @@ font-size: 12px; color: var(--text-tertiary); font-family: monospace; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx b/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx index 55cc820fb..38d9367a1 100644 --- a/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx +++ b/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx @@ -7,7 +7,8 @@ import { MatrixAvatar } from '../../../../components/matrix-avatar'; import { TokenIcon } from '../../components/token-icon/token-icon'; import { FormattedNumber } from '../../components/formatted-number/formatted-number'; import { Button } from '../../components/button/button'; -import { IconChevronRightDouble } from '@zero-tech/zui/icons'; +import { IconChevronRightDouble, IconPackageMinus } from '@zero-tech/zui/icons'; +import { truncateAddress } from '../../utils/address'; export const WalletReviewTransfer = () => { const dispatch = useDispatch(); @@ -39,7 +40,9 @@ export const WalletReviewTransfer = () => {
Confirm transaction with
{recipient?.primaryZid || recipient?.name}
-
{recipient?.publicAddress}
+
+ {recipient?.publicAddress ? truncateAddress(recipient.publicAddress) : ''} +
{isNftTransfer ? ( @@ -48,7 +51,9 @@ export const WalletReviewTransfer = () => { {nft.imageUrl ? ( {nft.metadata?.name ) : ( -
+
+ +
)}
diff --git a/src/apps/wallet/send/success/wallet-transfer-success.module.scss b/src/apps/wallet/send/success/wallet-transfer-success.module.scss index 75ae3c85e..de2fa2ec5 100644 --- a/src/apps/wallet/send/success/wallet-transfer-success.module.scss +++ b/src/apps/wallet/send/success/wallet-transfer-success.module.scss @@ -199,6 +199,9 @@ height: 120px; background: var(--bg-secondary); border-radius: 8px; + display: grid; + place-items: center; + color: var(--text-secondary); } .nftSuccessName { diff --git a/src/apps/wallet/send/success/wallet-transfer-success.tsx b/src/apps/wallet/send/success/wallet-transfer-success.tsx index 23a57035a..4a5fa05ad 100644 --- a/src/apps/wallet/send/success/wallet-transfer-success.tsx +++ b/src/apps/wallet/send/success/wallet-transfer-success.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@zero-tech/zui/components'; import { SendHeader } from '../components/send-header'; import styles from './wallet-transfer-success.module.scss'; -import { IconCheck, IconChevronRightDouble, IconXClose } from '@zero-tech/zui/icons'; +import { IconCheck, IconChevronRightDouble, IconXClose, IconPackageMinus } from '@zero-tech/zui/icons'; import { useDispatch, useSelector } from 'react-redux'; import { reset } from '../../../../store/wallet'; import { getHistory } from '../../../../lib/browser'; @@ -61,7 +61,9 @@ export const WalletTransferSuccess = () => { {nft.imageUrl ? ( {nft.metadata?.name ) : ( -
+
+ +
)}
{nft.metadata?.name || 'NFT'}
diff --git a/src/apps/wallet/send/token-select/wallet-token-select.module.scss b/src/apps/wallet/send/token-select/wallet-token-select.module.scss index c2ee671cb..2078d9a4e 100644 --- a/src/apps/wallet/send/token-select/wallet-token-select.module.scss +++ b/src/apps/wallet/send/token-select/wallet-token-select.module.scss @@ -116,58 +116,9 @@ padding: 0 var(--l-spacing); } -.nftSelectItem { - display: flex; - flex-direction: column; - padding: 8px; - border: 1px solid var(--border-primary); - border-radius: 8px; - background: transparent; - cursor: pointer; - text-align: left; - transition: all 0.2s; - - &:hover { - background: var(--bg-hover); - border-color: var(--border-hover); - } -} - -.nftImage { - width: 100%; - aspect-ratio: 1; - object-fit: cover; - border-radius: 4px; -} - -.nftImagePlaceholder { - width: 100%; +.nftSkeleton { aspect-ratio: 1; - background: var(--bg-secondary); - border-radius: 4px; -} - -.nftInfo { - margin-top: 8px; -} - -.nftName { - font-family: IBM Plex Sans; - font-weight: 500; - font-size: 14px; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.nftCollection { - font-family: IBM Plex Sans; - font-size: 12px; - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + border-radius: 8px; } .emptyState { diff --git a/src/apps/wallet/send/token-select/wallet-token-select.tsx b/src/apps/wallet/send/token-select/wallet-token-select.tsx index 1b268cb3f..93fc63730 100644 --- a/src/apps/wallet/send/token-select/wallet-token-select.tsx +++ b/src/apps/wallet/send/token-select/wallet-token-select.tsx @@ -1,15 +1,18 @@ import { useDispatch, useSelector } from 'react-redux'; import { nextStage, previousStage, setToken, setNft } from '../../../../store/wallet'; import { SendHeader } from '../components/send-header'; -import { Input } from '@zero-tech/zui/components'; +import { Input, Skeleton } from '@zero-tech/zui/components'; import { IconSearchMd } from '@zero-tech/zui/icons'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useBalancesQuery } from '../../queries/useBalancesQuery'; import { useNFTsQuery } from '../../queries/useNFTsQuery'; import { Token } from '../../tokens/token'; +import { NFTTile } from '../../nfts/nft/nft-tile'; import { NFT, TokenBalance } from '../../types'; import { recipientSelector, selectedWalletSelector } from '../../../../store/wallet/selectors'; import { truncateAddress } from '../../utils/address'; +import { WalletEmptyState } from '../../components/empty-state/wallet-empty-state'; +import { Waypoint } from '../../../leaderboard/components/waypoint'; import cn from 'classnames'; import styles from './wallet-token-select.module.scss'; @@ -26,7 +29,13 @@ export const WalletTokenSelect = () => { const hasName = recipient?.name !== null; const { data } = useBalancesQuery(selectedWallet.address); - const { nfts, isLoading: isLoadingNfts } = useNFTsQuery(selectedWallet.address); + const { + nfts, + isLoading: isLoadingNfts, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useNFTsQuery(selectedWallet.address); const filteredTokens = data?.tokens?.filter((asset) => asset.name.toLowerCase().includes(assetQuery.toLowerCase())); @@ -36,6 +45,12 @@ export const WalletTokenSelect = () => { nft.collectionName?.toLowerCase().includes(assetQuery.toLowerCase()) ); + const handleLoadMore = useCallback(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + const handleTokenClick = (token: TokenBalance) => { dispatch(setNft(null)); dispatch(setToken(token)); @@ -102,20 +117,15 @@ export const WalletTokenSelect = () => { {activeTab === 'nfts' && (
NFTs
- {isLoadingNfts ? ( -
Loading NFTs...
- ) : ( -
- {filteredNfts?.map((nft) => ( - handleNftClick(nft)} - /> - ))} -
- )} - {!isLoadingNfts && filteredNfts?.length === 0 &&
No NFTs found
} +
+ {filteredNfts?.map((nft) => ( + + ))} + {(isLoadingNfts || isFetchingNextPage) && + Array.from({ length: 4 }).map((_, i) => )} +
+ {hasNextPage && !isFetchingNextPage && } + {!isLoadingNfts && filteredNfts?.length === 0 && }
)}
@@ -139,24 +149,3 @@ export const WalletTokenSelect = () => {
); }; - -interface NFTSelectItemProps { - nft: NFT; - onClick: () => void; -} - -const NFTSelectItem = ({ nft, onClick }: NFTSelectItemProps) => { - return ( - - ); -}; diff --git a/src/store/wallet/saga.ts b/src/store/wallet/saga.ts index e85d2020d..eaceb13a4 100644 --- a/src/store/wallet/saga.ts +++ b/src/store/wallet/saga.ts @@ -35,6 +35,7 @@ import { TransferNativeAssetResponse, } from '../../apps/wallet/queries/transferNativeAssetRequest'; import { isWalletAPIError } from './utils'; +import { config } from '../../config'; /** * Loads the user's ThirdWeb wallet address into the store once the user has been fetched from the API @@ -189,7 +190,7 @@ function* handleTransferNft() { if (result.transactionHash) { yield put(setSendStage(SendStage.Broadcasting)); const receipt: TxReceiptResponse = yield call(() => - queryClient.fetchQuery(txReceiptQueryOptions(result.transactionHash)) + queryClient.fetchQuery(txReceiptQueryOptions(result.transactionHash, Number(config.zChainId))) ); yield put(setTxReceipt(receipt)); if (receipt.status === 'confirmed') { From cbeb656aa16e67ca2b9a908e82dc7ab0f7893bcd Mon Sep 17 00:00:00 2001 From: dominic Date: Wed, 11 Feb 2026 11:44:06 +0000 Subject: [PATCH 3/3] feat: add ERC-1155 quantity transfer support to wallet send flow --- src/apps/wallet/queries/transferNFTRequest.ts | 18 ++-- .../wallet-nft-quantity.module.scss | 89 +++++++++++++++++++ .../send/nft-quantity/wallet-nft-quantity.tsx | 85 ++++++++++++++++++ .../wallet-review-transfer.module.scss | 7 ++ .../wallet-review-transfer.tsx | 1 + .../wallet-transfer-success.module.scss | 7 ++ .../send/success/wallet-transfer-success.tsx | 1 + src/apps/wallet/send/wallet-send.tsx | 6 +- src/store/wallet/saga.ts | 30 +++++-- 9 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 src/apps/wallet/send/nft-quantity/wallet-nft-quantity.module.scss create mode 100644 src/apps/wallet/send/nft-quantity/wallet-nft-quantity.tsx diff --git a/src/apps/wallet/queries/transferNFTRequest.ts b/src/apps/wallet/queries/transferNFTRequest.ts index bfeec3281..e593db36f 100644 --- a/src/apps/wallet/queries/transferNFTRequest.ts +++ b/src/apps/wallet/queries/transferNFTRequest.ts @@ -8,13 +8,19 @@ export const transferNFTRequest = async ( address: string, to: string, tokenId: string, - nftAddress: string + nftAddress: string, + amount?: string | null, + tokenType?: string ): Promise => { - const response = await post(`/api/wallet/${address}/transactions/transfer-nft`).send({ - to, - tokenId, - nftAddress, - }); + const body: Record = { to, tokenId, nftAddress }; + if (amount) { + body.amount = amount; + } + if (tokenType) { + body.tokenType = tokenType; + } + + const response = await post(`/api/wallet/${address}/transactions/transfer-nft`).send(body); return response.body as TransferNFTResponse; }; diff --git a/src/apps/wallet/send/nft-quantity/wallet-nft-quantity.module.scss b/src/apps/wallet/send/nft-quantity/wallet-nft-quantity.module.scss new file mode 100644 index 000000000..c47f53fc0 --- /dev/null +++ b/src/apps/wallet/send/nft-quantity/wallet-nft-quantity.module.scss @@ -0,0 +1,89 @@ +.container { + display: flex; + flex-direction: column; + height: 100%; +} + +.content { + display: flex; + flex-direction: column; + gap: var(--l-spacing); + flex: 1; + padding: 0 var(--l-spacing); +} + +.nftPreview { + display: flex; + align-items: center; + gap: var(--l-spacing); + padding: var(--l-spacing); + border: 1px solid var(--border-primary); + border-radius: 12px; +} + +.nftImage { + width: 64px; + height: 64px; + object-fit: cover; + border-radius: 8px; + flex-shrink: 0; +} + +.nftImagePlaceholder { + width: 64px; + height: 64px; + background: var(--bg-secondary); + border-radius: 8px; + display: grid; + place-items: center; + color: var(--text-secondary); + flex-shrink: 0; +} + +.nftDetails { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.nftName { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nftCollection { + font-size: 14px; + color: var(--text-secondary); +} + +.quantityInput { + display: flex; + flex-direction: column; + gap: 8px; +} + +.quantityLabel { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: var(--text-secondary); +} + +.maxButton { + background: none; + border: none; + color: var(--zero-green); + cursor: pointer; + font-size: 14px; + padding: 0; + + &:hover { + text-decoration: underline; + } +} diff --git a/src/apps/wallet/send/nft-quantity/wallet-nft-quantity.tsx b/src/apps/wallet/send/nft-quantity/wallet-nft-quantity.tsx new file mode 100644 index 000000000..7139707b2 --- /dev/null +++ b/src/apps/wallet/send/nft-quantity/wallet-nft-quantity.tsx @@ -0,0 +1,85 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { nextStage, previousStage, setAmount } from '../../../../store/wallet'; +import { SendHeader } from '../components/send-header'; +import { Button } from '../../components/button/button'; +import { Input } from '@zero-tech/zui/components'; +import { IconPackageMinus } from '@zero-tech/zui/icons'; +import { amountSelector, nftSelector } from '../../../../store/wallet/selectors'; +import { useMemo } from 'react'; + +import styles from './wallet-nft-quantity.module.scss'; + +export const WalletNftQuantity = () => { + const dispatch = useDispatch(); + const nft = useSelector(nftSelector); + const amount = useSelector(amountSelector); + + const maxQuantity = nft?.quantity ?? 1; + + const disabled = useMemo(() => { + if (!amount) return true; + const num = Number(amount); + return !Number.isInteger(num) || num < 1 || num > maxQuantity; + }, [amount, maxQuantity]); + + const handleAmountChange = (value: string) => { + const sanitized = value.replace(/[^0-9]/g, ''); + dispatch(setAmount(sanitized || null)); + }; + + const handleMax = () => { + dispatch(setAmount(String(maxQuantity))); + }; + + const handleBack = () => { + dispatch(previousStage()); + }; + + const handleContinue = () => { + dispatch(nextStage()); + }; + + if (!nft) return null; + + return ( +
+ + +
+
+ {nft.imageUrl ? ( + {nft.metadata?.name + ) : ( +
+ +
+ )} +
+
{nft.metadata?.name || 'Unnamed NFT'}
+
{nft.collectionName}
+
+
+ +
+
+ Quantity + +
+ +
+ + +
+
+ ); +}; diff --git a/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss b/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss index f73f8adcc..87ed22e46 100644 --- a/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss +++ b/src/apps/wallet/send/review-transfer/wallet-review-transfer.module.scss @@ -167,3 +167,10 @@ text-overflow: ellipsis; white-space: nowrap; } + +.nftQuantity { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-top: 4px; +} diff --git a/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx b/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx index 38d9367a1..951491895 100644 --- a/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx +++ b/src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx @@ -60,6 +60,7 @@ export const WalletReviewTransfer = () => {
{nft.metadata?.name || 'Unnamed NFT'}
{nft.collectionName}
Token ID: {nft.id}
+ {amount &&
Quantity: {amount}
}
) : ( diff --git a/src/apps/wallet/send/success/wallet-transfer-success.module.scss b/src/apps/wallet/send/success/wallet-transfer-success.module.scss index de2fa2ec5..6d753e52d 100644 --- a/src/apps/wallet/send/success/wallet-transfer-success.module.scss +++ b/src/apps/wallet/send/success/wallet-transfer-success.module.scss @@ -219,3 +219,10 @@ font-weight: 400; margin-top: 4px; } + +.nftSuccessQuantity { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-top: 8px; +} diff --git a/src/apps/wallet/send/success/wallet-transfer-success.tsx b/src/apps/wallet/send/success/wallet-transfer-success.tsx index 4a5fa05ad..d24c4391f 100644 --- a/src/apps/wallet/send/success/wallet-transfer-success.tsx +++ b/src/apps/wallet/send/success/wallet-transfer-success.tsx @@ -68,6 +68,7 @@ export const WalletTransferSuccess = () => {
{nft.metadata?.name || 'NFT'}
{nft.collectionName}
+ {amount &&
Quantity: {amount}
}
) : (
diff --git a/src/apps/wallet/send/wallet-send.tsx b/src/apps/wallet/send/wallet-send.tsx index adc5a2a37..127d8b5ce 100644 --- a/src/apps/wallet/send/wallet-send.tsx +++ b/src/apps/wallet/send/wallet-send.tsx @@ -1,10 +1,11 @@ import { PanelBody } from '../../../components/layout/panel'; import { WalletUserSearch } from './search/wallet-user-search'; import { useSelector } from 'react-redux'; -import { sendStageSelector } from '../../../store/wallet/selectors'; +import { nftSelector, sendStageSelector } from '../../../store/wallet/selectors'; import { SendStage } from '../../../store/wallet'; import { WalletTokenSelect } from './token-select/wallet-token-select'; import { WalletTransferAmount } from './transfer-amount/wallet-transfer-amount'; +import { WalletNftQuantity } from './nft-quantity/wallet-nft-quantity'; import { WalletReviewTransfer } from './review-transfer/wallet-review-transfer'; import { WalletProcessingTransaction } from './processing/wallet-processing-transaction'; import { WalletTransferSuccess } from './success/wallet-transfer-success'; @@ -14,11 +15,12 @@ import styles from './wallet-send.module.scss'; export const WalletSend = () => { const stage = useSelector(sendStageSelector); + const nft = useSelector(nftSelector); return ( {stage === SendStage.Search && } {stage === SendStage.Token && } - {stage === SendStage.Amount && } + {stage === SendStage.Amount && (nft ? : )} {stage === SendStage.Confirm && } {(stage === SendStage.Processing || stage === SendStage.Broadcasting) && } {stage === SendStage.Success && } diff --git a/src/store/wallet/saga.ts b/src/store/wallet/saga.ts index eaceb13a4..25b1ff898 100644 --- a/src/store/wallet/saga.ts +++ b/src/store/wallet/saga.ts @@ -120,8 +120,11 @@ function* handleNext() { } case SendStage.Token: { const token = yield select(tokenSelector); - if (nft) { - // NFT selected - skip Amount, go straight to Confirm + if (nft && nft.tokenType === 'ERC-1155') { + // ERC-1155 NFT - go to Amount for quantity input + yield put(setSendStage(SendStage.Amount)); + } else if (nft) { + // ERC-721 NFT - skip Amount, go straight to Confirm yield put(setSendStage(SendStage.Confirm)); } else if (token) { // Token selected - go to Amount @@ -145,8 +148,12 @@ function* handlePrevious() { switch (stage) { case SendStage.Confirm: - if (nft) { - // NFT flow - go back to Token selection (skip Amount) + if (nft && nft.tokenType === 'ERC-1155') { + // ERC-1155 NFT flow - go back to Amount (quantity input) + yield put(setAmount(null)); + yield put(setSendStage(SendStage.Amount)); + } else if (nft) { + // ERC-721 NFT flow - go back to Token selection (skip Amount) yield put(setNft(null)); yield put(setSendStage(SendStage.Token)); } else { @@ -156,6 +163,11 @@ function* handlePrevious() { } break; case SendStage.Amount: + if (nft) { + // ERC-1155 NFT flow - go back to Token selection + yield put(setNft(null)); + yield put(setAmount(null)); + } yield put(setToken(null)); yield put(setSendStage(SendStage.Token)); break; @@ -179,12 +191,20 @@ function* handleTransferNft() { const recipient: Recipient = yield select(recipientSelector); const selectedWallet: string | undefined = yield select(selectedWalletAddressSelector); const nft: NFT = yield select(nftSelector); + const amount: string | null = yield select(amountSelector); if (recipient && nft && selectedWallet) { yield put(setSendStage(SendStage.Processing)); const result: TransferNFTResponse = yield call(() => - transferNFTRequest(selectedWallet, recipient.publicAddress, nft.id, nft.collectionAddress) + transferNFTRequest( + selectedWallet, + recipient.publicAddress, + nft.id, + nft.collectionAddress, + amount, + nft.tokenType + ) ); if (result.transactionHash) {