Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8831c9e
feat: send collectibles wrapper
paolampilla Dec 4, 2025
fe604f5
Merge branch 'main' into 574-send---nft-wrapper
paolampilla Dec 4, 2025
30488e4
feat: add send nft wrapper
paolampilla Dec 4, 2025
16f07b8
feat: add translations
Dec 4, 2025
b944d84
chore: add review details strings
paolampilla Dec 5, 2025
5233390
Merge branch '574-send---nft-wrapper' of https://github.com/vechain/v…
paolampilla Dec 5, 2025
327461e
Merge branch 'main' into 574-send---nft-wrapper
paolampilla Dec 5, 2025
7f1dcd7
feat: initial refactor
HiiiiD Dec 5, 2025
ab1448f
fix: small refactor of numpad
HiiiiD Dec 5, 2025
dad759f
fix: mini refactor of summary
HiiiiD Dec 5, 2025
91ad1b9
fix: another step of the refactor
HiiiiD Dec 5, 2025
507126b
fix: final fixes for tx summary
HiiiiD Dec 5, 2025
0148ae9
fix: remove animation on btns
HiiiiD Dec 5, 2025
31df23f
fix: fix tests
HiiiiD Dec 5, 2025
beb9343
fix: cleanup provider
HiiiiD Dec 5, 2025
55efd0c
fix: apply coderabbit suggestion
HiiiiD Dec 5, 2025
28531c0
refactor: replace Pressable with TouchableOpacity in SendNumPad and a…
Dec 7, 2025
2a8569c
fix: fix receiver screen test
HiiiiD Dec 7, 2025
91670c9
fix: make tests work
HiiiiD Dec 7, 2025
ed1dad7
fix: add more format error
HiiiiD Dec 7, 2025
367a2b8
Merge branch 'main' into 574-send---nft-wrapper
paolampilla Dec 8, 2025
684b164
feat: add translations
Dec 8, 2025
7c1eb0d
Merge branch 'refactor-send-screen' into 574-send---nft-wrapper
paolampilla Dec 8, 2025
9680a36
chore: add nft to send flow
paolampilla Dec 8, 2025
db804c1
chore: refactor send nft wrapper
paolampilla Dec 9, 2025
a8978c1
Merge branch '574-send---nft-wrapper' of https://github.com/vechain/v…
paolampilla Dec 9, 2025
870e019
Merge branch 'main' into 574-send---nft-wrapper
paolampilla Dec 9, 2025
d8dede5
chore: merge main
paolampilla Dec 9, 2025
69b0de0
fix: fix unit tests
paolampilla Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/Components/Collectibles/CollectiblesSendActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useCallback } from "react"
import { useI18nContext } from "~i18n"
import { Routes } from "~Navigation"
import { CollectiblesActionButton } from "./CollectiblesActionButton"
import { useFeatureFlags } from "~Components"

type Props = {
address: string
Expand All @@ -13,14 +14,22 @@ export const CollectiblesSendActionButton = ({ address, tokenId, onClose }: Prop
const { LL } = useI18nContext()

const nav = useNavigation()
const { betterWorldFeature } = useFeatureFlags()

const onPress = useCallback(async () => {
onClose()
if (betterWorldFeature.balanceScreen?.send?.enabled) {
nav.navigate(Routes.SEND_NFT, {
contractAddress: address,
tokenId,
})
return
}
nav.navigate(Routes.INSERT_ADDRESS_SEND, {
contractAddress: address,
tokenId,
})
}, [address, nav, onClose, tokenId])
}, [address, nav, onClose, tokenId, betterWorldFeature.balanceScreen?.send?.enabled])

return (
<CollectiblesActionButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { useNonVechainTokensBalance } from "~Hooks/useNonVechainTokensBalance"
import { useSendableTokensWithBalance } from "~Hooks/useSendableTokensWithBalance"
import { useTokenWithCompleteInfo } from "~Hooks/useTokenWithCompleteInfo"
import { BalanceUtils, BigNutils } from "~Utils"
import { useSendContext } from "../../Provider"
import { useTokenSendContext } from "../../Provider"

export const useDefaultToken = () => {
const { flowState } = useSendContext()
const { flowState } = useTokenSendContext()
const availableTokens = useSendableTokensWithBalance()

const vetInfo = useTokenWithCompleteInfo(VET)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FungibleTokenWithBalance } from "~Model"
import { TestWrapper } from "~Test"

import { useExchangeRate } from "~Api/Coingecko"
import { useSendContext } from "../Provider"
import { useSendContext, useTokenSendContext } from "../Provider"
import { useDefaultToken } from "./Hooks"

import { SelectAmountSendComponent } from "./SelectAmountSendComponent"
Expand All @@ -15,6 +15,7 @@ const mockSetFlowState = jest.fn()
jest.mock("../Provider", () => ({
...jest.requireActual("../Provider"),
useSendContext: jest.fn(),
useTokenSendContext: jest.fn(),
}))

jest.mock("~Api/Coingecko", () => ({
Expand Down Expand Up @@ -72,11 +73,17 @@ const mockVTHOToken: FungibleTokenWithBalance = {
const goToNext = jest.fn()

const setupMockContext = (token: FungibleTokenWithBalance = mockVETToken) => {
;(useSendContext as jest.Mock).mockReturnValue({
flowState: { token, amount: "0", fiatAmount: "", address: "", amountInFiat: false },
const mockContextValue = {
flowState: { type: "token", token, amount: "0", fiatAmount: "", address: "", amountInFiat: false },
setFlowState: mockSetFlowState,
goToNext,
})
step: "selectAmount" as const,
goToPrevious: jest.fn(),
EnteringAnimation: jest.fn(),
ExitingAnimation: jest.fn(),
}
;(useTokenSendContext as jest.Mock).mockReturnValue(mockContextValue)
;(useSendContext as jest.Mock).mockReturnValue(mockContextValue)
jest.mocked(useDefaultToken).mockReturnValue(token)
}

Expand Down Expand Up @@ -217,6 +224,7 @@ describe("SelectAmountSendComponent", () => {
// setFlowState is called with a function, so we need to call it to get the result
if (typeof lastCall === "function") {
const result = lastCall({
type: "token",
token: mockVETToken,
amount: "0",
fiatAmount: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { selectCurrency, selectCurrencyFormat, useAppSelector } from "~Storage/R
import { BigNutils } from "~Utils"
import { getDecimalSeparator } from "~Utils/BigNumberUtils/BigNumberUtils"
import { formatFullPrecision } from "~Utils/StandardizedFormatting"
import { useSendContext } from "../Provider"
import { useTokenSendContext } from "../Provider"
import { SendContent } from "../Shared"
import { SelectAmountConversionToggle } from "./Components/SelectAmountConversionToggle"
import { SelectAmountInput } from "./Components/SelectAmountInput"
Expand All @@ -26,7 +26,7 @@ const MAX_FIAT_DECIMALS = 2
const MAX_TOKEN_DECIMALS = 5

export const SelectAmountSendComponent = () => {
const { setFlowState, goToNext, flowState } = useSendContext()
const { setFlowState, goToNext, flowState } = useTokenSendContext()
const { formatLocale } = useFormatFiat()

const bottomSheetRef = useRef<BottomSheetModalMethods>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export const ReceiverScreen = () => {

const disabled = useMemo(() => !selectedAddress || !AddressUtils.isValid(selectedAddress), [selectedAddress])

const isTokenFlow = flowState.type === "token"

return (
<SendContent>
<SendContent.Header />
Expand All @@ -84,7 +86,7 @@ export const ReceiverScreen = () => {
/>
</SendContent.Container>
<SendContent.Footer>
<SendContent.Footer.Back />
{isTokenFlow && <SendContent.Footer.Back />}
<SendContent.Footer.Next action={goToNext} disabled={disabled} />
</SendContent.Footer>
</SendContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Animated } from "react-native"
import { useFormatFiat } from "~Hooks/useFormatFiat"
import { BigNutils } from "~Utils"
import { formatFullPrecision } from "~Utils/StandardizedFormatting"
import { useSendContext } from "../../Provider"
import { useTokenSendContext } from "../../Provider"
import { useCurrentExchangeRate } from "../Hooks"
import { DetailsContainer } from "./DetailsContainer"

export const TokenReceiverCard = () => {
const { flowState } = useSendContext()
const { flowState } = useTokenSendContext()
const { formatLocale } = useFormatFiat()

const { data: exchangeRate } = useCurrentExchangeRate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react-native"
import React, { ComponentProps, useEffect } from "react"
import { FungibleTokenWithBalance } from "~Model"
import { TestWrapper } from "~Test"
import { SendContextProvider, useSendContext } from "../../Provider/SendContextProvider"
import { SendContextProvider, useTokenSendContext } from "../../Provider/SendContextProvider"
import { useCurrentExchangeRate } from "../Hooks"
import { TransactionAlert } from "./TransactionAlert"

Expand Down Expand Up @@ -39,10 +39,11 @@ const InitializeSendFlow: React.FC<{ children: React.ReactNode; token: FungibleT
children,
token,
}) => {
const { setFlowState } = useSendContext()
const { setFlowState } = useTokenSendContext()

useEffect(() => {
setFlowState({
type: "token",
token,
amount: "1.234",
address: "0xreceiver",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"
import { AlertInline, BaseView } from "~Components"
import { AlertStatus } from "~Components/Reusable/Alert/utils/AlertConfigs"
import { useI18nContext } from "~i18n"
import { useSendContext } from "../../Provider"
import { useTokenSendContext } from "../../Provider"
import { useCurrentExchangeRate } from "../Hooks"

type Props = {
Expand All @@ -12,7 +12,7 @@ type Props = {

export const TransactionAlert = ({ txError, hasGasAdjustment }: Props) => {
const { LL } = useI18nContext()
const { flowState } = useSendContext()
const { flowState } = useTokenSendContext()
const [priceUpdated, setPriceUpdated] = useState(false)
const { data: exchangeRate } = useCurrentExchangeRate()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useMemo } from "react"
import { getCoinGeckoIdBySymbol, useExchangeRate } from "~Api/Coingecko"
import { useSendContext } from "../../Provider"
import { useTokenSendContext } from "../../Provider"
import { selectCurrency, useAppSelector } from "~Storage/Redux"

export const useCurrentExchangeRate = () => {
const currency = useAppSelector(selectCurrency)
const { flowState } = useSendContext()
const { flowState } = useTokenSendContext()

const exchangeRateId = useMemo(
() => (flowState.token ? getCoinGeckoIdBySymbol[flowState.token.symbol] : undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { StyleSheet } from "react-native"
import Animated from "react-native-reanimated"
import { BaseView } from "~Components"
import { useSendContext } from "~Components/Reusable/Send"
import { useTokenSendContext } from "~Components/Reusable/Send"
import { VTHO } from "~Constants"
import { useThemedStyles, useTransactionScreen } from "~Hooks"
import { useI18nContext } from "~i18n"
Expand All @@ -17,7 +17,7 @@ import { useTransactionCallbacks } from "./Hooks"
export const SummaryScreen = () => {
const { LL } = useI18nContext()
const { styles } = useThemedStyles(baseStyles)
const { flowState, setFlowState } = useSendContext()
const { flowState, setFlowState } = useTokenSendContext()
const [txError, setTxError] = useState(false)
const originalAmount = useRef(flowState.amount)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("SendContextProvider", () => {
})

expect(result.current.flowState).toMatchObject({
type: "token",
token: undefined,
amount: "0",
fiatAmount: "",
Expand All @@ -34,15 +35,18 @@ describe("SendContextProvider", () => {
})

result.current.setFlowState({
type: "token",
token: VETWithBalance,
amount: "1",
address: "0x1234567890123456789012345678901234567890",
})

expect(result.current.flowState.token).not.toBeUndefined()
expect(result.current.flowState.token?.symbol).toEqual(VETWithBalance.symbol)
expect(result.current.flowState.amount).toBe("1")
expect(result.current.flowState.address).toBe("0x1234567890123456789012345678901234567890")
if (result.current.flowState.type === "token") {
expect(result.current.flowState.token).not.toBeUndefined()
expect(result.current.flowState.token?.symbol).toEqual(VETWithBalance.symbol)
expect(result.current.flowState.amount).toBe("1")
expect(result.current.flowState.address).toBe("0x1234567890123456789012345678901234567890")
}
Comment on lines +44 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit type assertion to prevent false positives.

The conditional wrapper allows the test to pass silently if flowState.type !== "token", creating a false positive where important assertions are skipped.

Apply this diff to add an explicit type assertion:

+        expect(result.current.flowState.type).toBe("token")
         if (result.current.flowState.type === "token") {
             expect(result.current.flowState.token).not.toBeUndefined()
             expect(result.current.flowState.token?.symbol).toEqual(VETWithBalance.symbol)
             expect(result.current.flowState.amount).toBe("1")
             expect(result.current.flowState.address).toBe("0x1234567890123456789012345678901234567890")
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result.current.flowState.type === "token") {
expect(result.current.flowState.token).not.toBeUndefined()
expect(result.current.flowState.token?.symbol).toEqual(VETWithBalance.symbol)
expect(result.current.flowState.amount).toBe("1")
expect(result.current.flowState.address).toBe("0x1234567890123456789012345678901234567890")
}
expect(result.current.flowState.type).toBe("token")
if (result.current.flowState.type === "token") {
expect(result.current.flowState.token).not.toBeUndefined()
expect(result.current.flowState.token?.symbol).toEqual(VETWithBalance.symbol)
expect(result.current.flowState.amount).toBe("1")
expect(result.current.flowState.address).toBe("0x1234567890123456789012345678901234567890")
}
🤖 Prompt for AI Agents
In src/Components/Reusable/Send/Provider/SendContextProvider.spec.tsx around
lines 44 to 49, the test currently wraps assertions in an if-check which can
silently skip them if flowState.type !== "token"; replace the conditional with
an explicit assertion that flowState.type === "token" (e.g.
expect(result.current.flowState.type).toBe("token")) and then either cast
flowState to the token-specific type or confidently access token/amount/address
after that assertion so the test fails loudly if the type is wrong.

})

it("should update the step", () => {
Expand Down
32 changes: 24 additions & 8 deletions src/Components/Reusable/Send/Provider/SendContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { PropsWithChildren, useCallback, useContext, useMemo, useState } from "react"
import { EntryAnimationsValues, ExitAnimationsValues, LayoutAnimation, useSharedValue } from "react-native-reanimated"
import { FungibleTokenWithBalance } from "~Model"
import { FungibleTokenWithBalance, NonFungibleToken } from "~Model"
import {
EnteringFromLeftAnimation,
EnteringFromRightAnimation,
Expand All @@ -9,7 +9,8 @@ import { ExitingToLeftAnimation, ExitingToRightAnimation } from "~Screens/Flows/

export type SendFlowStep = "insertAddress" | "selectAmount" | "summary"

type SendFlowState = {
export type SendFlowState = {
type: "token"
token?: FungibleTokenWithBalance
amount?: string
fiatAmount?: string
Expand All @@ -22,17 +23,23 @@ type SendFlowState = {
amountInFiat?: boolean
}

type SendContextType = {
flowState: SendFlowState
setFlowState: React.Dispatch<React.SetStateAction<SendFlowState>>
export type SendNFTFlowState = {
type: "nft"
nft: NonFungibleToken
address?: string
}

export type SendContextType<T = SendFlowState | SendNFTFlowState> = {
flowState: T
setFlowState: React.Dispatch<React.SetStateAction<T>>
step: SendFlowStep
goToNext: () => void
goToPrevious: () => void
EnteringAnimation: (values: EntryAnimationsValues) => LayoutAnimation
ExitingAnimation: (values: ExitAnimationsValues) => LayoutAnimation
}

const SendContext = React.createContext<SendContextType | undefined>(undefined)
export const SendContext = React.createContext<SendContextType<any> | undefined>(undefined)

type SendContextProviderProps = PropsWithChildren<{
initialToken?: FungibleTokenWithBalance
Expand All @@ -43,6 +50,7 @@ const ORDER: SendFlowStep[] = ["selectAmount", "insertAddress", "summary"]
export const SendContextProvider = ({ children, initialToken }: SendContextProviderProps) => {
const [step, setStep] = useState<SendFlowStep>("selectAmount")
const [flowState, setFlowState] = useState<SendFlowState>({
type: "token",
token: initialToken,
amount: "0",
fiatAmount: "",
Expand Down Expand Up @@ -111,7 +119,7 @@ export const SendContextProvider = ({ children, initialToken }: SendContextProvi
[nextStep.value, previousStep.value],
)

const contextValue: SendContextType = useMemo(
const contextValue: SendContextType<SendFlowState> = useMemo(
() => ({
flowState,
setFlowState,
Expand All @@ -132,5 +140,13 @@ export const useSendContext = () => {
if (!context) {
throw new Error("useSendContext must be used within a SendContextProvider")
}
return context
return context as SendContextType<SendFlowState | SendNFTFlowState>
}

export const useTokenSendContext = () => {
return useSendContext() as SendContextType<SendFlowState>
}

export const useNFTSendContext = () => {
return useSendContext() as SendContextType<SendNFTFlowState>
}
Comment on lines +146 to 152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe type assertions in specialized hooks may cause runtime errors.

useTokenSendContext and useNFTSendContext perform type casts without runtime validation. If used incorrectly (e.g., calling useTokenSendContext inside an NFT flow), the consumer will get the wrong type with no error.

Consider adding runtime checks:

 export const useTokenSendContext = () => {
-    return useSendContext() as SendContextType<SendFlowState>
+    const context = useSendContext()
+    if (context.flowState.type !== "token") {
+        throw new Error("useTokenSendContext must be used within a token Send flow")
+    }
+    return context as SendContextType<SendFlowState>
 }

 export const useNFTSendContext = () => {
-    return useSendContext() as SendContextType<SendNFTFlowState>
+    const context = useSendContext()
+    if (context.flowState.type !== "nft") {
+        throw new Error("useNFTSendContext must be used within an NFT Send flow")
+    }
+    return context as SendContextType<SendNFTFlowState>
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useTokenSendContext = () => {
return useSendContext() as SendContextType<SendFlowState>
}
export const useNFTSendContext = () => {
return useSendContext() as SendContextType<SendNFTFlowState>
}
export const useTokenSendContext = () => {
const context = useSendContext()
if (context.flowState.type !== "token") {
throw new Error("useTokenSendContext must be used within a token Send flow")
}
return context as SendContextType<SendFlowState>
}
export const useNFTSendContext = () => {
const context = useSendContext()
if (context.flowState.type !== "nft") {
throw new Error("useNFTSendContext must be used within an NFT Send flow")
}
return context as SendContextType<SendNFTFlowState>
}
🤖 Prompt for AI Agents
In src/Components/Reusable/Send/Provider/SendContextProvider.tsx around lines
147-153, the specialized hooks blindly cast the generic send context to
token/NFT types which can lead to silent runtime errors; change each hook to
call useSendContext(), inspect a discriminant on the returned context/state
(e.g., a flowType/kind property or a runtime shape check) to ensure it matches
the expected SendFlowState or SendNFTFlowState, and if it does not, throw a
clear Error (or return null) explaining the wrong hook usage; update
callers/tests if needed to account for the thrown error.

Loading
Loading