Skip to content

Conversation

@piyalbasu
Copy link
Contributor

@piyalbasu piyalbasu commented Dec 20, 2025

Closes #2329

This adds collectibles to Freighter's internal send flow. Users can send a collectible by either using the Send button on the Account view or they can use the Send button from the detail view. This makes sure to handle the UI for both collectibles that have metadata properly configured and those that do not.

I spent a good amount of time thinking through how we can add this flow to the app in a way that makes sense. My 2 main options were to either 1) make a new, separate view that implements its own standalone flow (similar to how Swap is separate from SendPayment) or 2) add a configuration in the existing SendPayment flow that toggles on/off some collectibles elements when needed.

I decided to do the latter for a few reasons. Ultimately, sending a collectible is so similar to sending a payment, there was going to be a lot of duplication making a separate flow. In both flows,

  • you're sending a "thing" to an external destination that needs to be validated. (The one difference is there is no amount for a collectible)
  • you need to configure fees (via tx simulation) and memo

In addition, because of CAP-67, memo validation is about to get a bit more complex. I'd like to be able to easily keep memo validation in sync between tokens/collectibles. Finally, sending collectibles doesn't really have its own standalone flow, begin to end. You can only send a collectible by starting a token payment flow. The UX flow is a branch off the existing token Send flow.

The downside is we have a few spots with isCollectible checks, but I tried to keep it as minimal and as clean as possible

VIDEO EXAMPLES:
Sending a collectible for a collectible with metadata

Screen.Recording.2025-12-19.at.5.42.06.PM.mov

Sending a collectible for a collectible without metadata

Screen.Recording.2025-12-19.at.8.05.23.PM.mov

An empty state for an account with no collectibles. This mirrors the empty state for assets:
Screenshot 2025-12-19 at 5 44 06 PM

return { assetsLists: response.assetsLists, error: response.error };
};

export const simulateSendCollectible = async (args: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this mirrors the code simulateTokenTransfer

await stubSimulateSendCollectible(page);
await stubScanTx(page);
await stubSubmitTx(page);
await stubFeeStats(page);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

these tests completely stub out interactions with Horizon and the network, so it should be fast and reliable 🎉 . This is a model we can start using for other tests that submit tx's, which are often flakey due to real world network conditions

} from "helpers/stellar";

import { SimulateTxData } from "popup/components/sendPayment/SendAmount/hooks/useSimulateTxData";
import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming SendPayment to Send as this flow can now send things that are not assets

</div>
);
})}
<TabButtons />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll reuse these in the Send flow

const handleSendCollectible = () => {
// prepopulate the collectible data in the redux state before navigating to the send flow
dispatch(saveIsCollectible(true));
dispatch(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is slightly different than how we do this with assets. With assets, we send over query params for the asset and destination. The view decodes them and sets them in Redux

I thought it was a bit cleaner to set all these params at once directly into redux

</div>
</div>
)}
<CollectibleInfo
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of these components will be reused in the Send flow

return name || `Token #${tokenId}`;
};

export const CollectibleInfoBlock = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

most of these components are ported over from the CollectibleDetail view

<div className="SendAmount__collectible-display">
<SelectedCollectible goToChooseDest={goToChooseDest} />
</div>
) : (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Either show the Collectible or the Amount selector

};
};

function useSimulateTxData({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this hook is similar to the simulateTx hook used by sending a token

return (
<div
className="CollectiblesList__collection"
key={title}
Copy link
Contributor

Choose a reason for hiding this comment

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

is this a unique key? I'd suggest ${collection.collectionAddress}-${collection.tokenId}

xdr={simulationState.data?.transactionXdr!}
xdr={
isCollectible
? collectibleSimulationState.data?.transactionXdr!
Copy link
Contributor

Choose a reason for hiding this comment

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

it shouldn't be necessary to assert these as non-null since the data hook gives us a way to narrow the type down to a success or error before the render happens. It would be safer to explicitly handle these states since TransactionConfirm expects that data in a non nullable way.

@piyalbasu piyalbasu force-pushed the feature/add-send-collectible branch from 63b28b2 to bba1d4c Compare December 23, 2025 22:14
@piyalbasu piyalbasu merged commit 9a3b1b7 into release/5.37.0 Dec 29, 2025
2 of 3 checks passed
@piyalbasu piyalbasu deleted the feature/add-send-collectible branch December 29, 2025 19:46
piyalbasu added a commit that referenced this pull request Dec 29, 2025
* Reapply "add collectibles to the Send flow"

This reverts commit be2a075.

* fix incorrect fee

* code cleanup

* pr comments

* check for found collectible

* add unit tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants