Skip to content

Conversation

aristidesstaffieri
Copy link
Contributor

@aristidesstaffieri aristidesstaffieri commented Oct 16, 2025

Closes #453 #454 #455

What

Adds the ability to send a collectible.
Adds a type prop(token or collectible) to several steps of the send flow(transaction details, send review, transaction processing).
Adds collectible tab in the token selection step, updates nav to follow designed nav structure.
Updates all review screens(send/swap/generic tx) to use updated summary style with icons on the left.
Adds send button in collectible detail view.
Adds logic to distinguish between token transfers and collectible transfers in helpers(getArgsForTokenInvocation).
Adds analytics events and translations for collectible related additions.
Adds new service functions and duck methods to get and store collectibles details in send flow.

Why

Roadmap feature.

Known limitations

N/A

Checklist

PR structure

  • This PR does not mix refactoring changes with feature changes (break it down into smaller PRs if not).
  • This PR has reasonably narrow scope (break it down into smaller PRs if not).
  • This PR includes relevant before and after screenshots/videos highlighting these changes.
  • I took the time to review my own PR.

Testing

  • These changes have been tested and confirmed to work as intended on Android.
  • These changes have been tested and confirmed to work as intended on iOS.
  • These changes have been tested and confirmed to work as intended on small iOS screens.
  • These changes have been tested and confirmed to work as intended on small Android screens.
  • I have tried to break these changes while extensively testing them.
  • This PR adds tests for the new functionality or fixes.

Release

  • This is not a breaking change.
  • This PR updates existing JSDocs when applicable.
  • This PR adds JSDocs to new functionalities.
  • I've checked with the product team if we should add metrics to these changes.
  • I've shared relevant before and after screenshots/videos highlighting these changes with the design team and they've approved the changes.

History:
simulator_screenshot_60CDEE02-1588-46A8-A871-D72A5EF0BD8F

simulator_screenshot_08A6C347-1E4E-4168-A678-D71D42474506

Collectible Details:
simulator_screenshot_00502EAD-0318-4D59-8710-3358F9B4F5AF

Send Flow:
simulator_screenshot_A7170AC1-0816-4947-ABB8-38F64C22FFA5

simulator_screenshot_C94F2EF3-A054-41E5-8022-E599A0F0E61A simulator_screenshot_8BB958BF-536A-4E7C-A6E1-843F6CA20F08 simulator_screenshot_A55C0A42-7246-4231-9BF7-0F36E4B2D268 simulator_screenshot_454A0749-0239-43BB-ACA6-CDC08A5A7A22 simulator_screenshot_06984373-B8C3-44B8-99A7-AFCC915489C9

@aristidesstaffieri aristidesstaffieri self-assigned this Oct 16, 2025
);
});

it("should handle simulation without optional fee", async () => {
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 test is actually wrong and not needed, the transaction is built before submitting.

"/submit-transaction",
mockSubmitParams,
);
expect(mockPost).toHaveBeenCalledWith("/submit-tx", mockSubmitParams);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the route names were incorrect from the previous functional PR.

@aristidesstaffieri aristidesstaffieri requested review from a team October 16, 2025 18:11
// we can guess that the contract is either a token or a collectible
// by the type of the 3rd argument.
// Token transfer - (from: Address, to: Address, amount: i128)
// Collectible transfer - (from: Address, to: Address, tokenId: u32)
Copy link
Contributor

Choose a reason for hiding this comment

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

nice one

from: sorobanAttributes.from,
to: sorobanAttributes.to,
tokenId: sorobanAttributes.tokenId,
collectibleName: transferedCollectible.name || "",
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought transferedCollectible.name would be always present here, is lint complaining?

Copy link
Contributor Author

@aristidesstaffieri aristidesstaffieri Oct 20, 2025

Choose a reason for hiding this comment

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

it's because the Collectible type defines this and a few of the other metadata fields as string | undefined which I think is correct since these fields are not guaranteed to be set(probably in practice will often be set for real ones).

I think it makes sense also use the same fallback here, Unknown Collectible. wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good to me!

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 done in ad64b10

@CassioMG
Copy link
Contributor

CassioMG commented Oct 17, 2025

what is displayed when user taps on "View transaction" on this screen?
Screenshot 2025-10-17 at 11 35 15

@aristidesstaffieri
Copy link
Contributor Author

what is displayed when user taps on "View transaction" on this screen? Screenshot 2025-10-17 at 11 35 15

Simulator Screenshot - iPhone 16 Pro - 2025-10-20 at 10 40 38

{type === "token" && selectedBalance && (
<TokenIcon token={selectedBalance} size="lg" />
)}
{type === "collectible" && selectedCollectible && (
Copy link
Contributor

Choose a reason for hiding this comment

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

could you please review all places that could make use of the new enum types this PR is adding? thanks!

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 are all added in 4361451

@aristidesstaffieri
Copy link
Contributor Author

@aristidesstaffieri would you mind posting a video with all the possible paths the user could take to send a collectible? E.g. starting from Home screen, starting from Collectible details screen, playing with changing the destination address, changing the selected collectible, etc - thanks!

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-10-20.at.10.40.29.mp4

@CassioMG a couple of notes here -
Ignore the dev errors for some collections, its just because my proxy is using dev which still has hardcoded collections for mainnet and I have pointed it at testnet to dev against.
Also, notice the failure in the process whenI try to add a memo. This happens because memos are not supported on soroban transactions. This is an existing bug that needs to be fixed for transaction generally so imo we should add a task this sprint to handle this in this release cycle. wdyt?

@CassioMG
Copy link
Contributor

@aristidesstaffieri would you mind posting a video with all the possible paths the user could take to send a collectible? E.g. starting from Home screen, starting from Collectible details screen, playing with changing the destination address, changing the selected collectible, etc - thanks!

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-10-20.at.10.40.29.mp4
@CassioMG a couple of notes here - Ignore the dev errors for some collections, its just because my proxy is using dev which still has hardcoded collections for mainnet and I have pointed it at testnet to dev against. Also, notice the failure in the process whenI try to add a memo. This happens because memos are not supported on soroban transactions. This is an existing bug that needs to be fixed for transaction generally so imo we should add a task this sprint to handle this in this release cycle. wdyt?

@aristidesstaffieri thanks for the video, looks great. I noticed one thing: after you have a collectible selected and you change its destination address the app is stacking new screens (of the same functionality) on top of each other, which suggests the code is "navigating" to a "new" screen instead of "popping" (or "going back") to an existing screen. I think ideally we should try to avoid that as all the screens on the stack would react to changes in state which could lead to weird unexpected behaviors. Wdyt?

This happens because memos are not supported on soroban transactions. This is an existing bug that needs to be fixed for transaction generally so imo we should add a task this sprint to handle this in this release cycle. wdyt?

That's good to know. Yeah agree this should be a separate task. Would you mind creating it and setting as P1 so we try tackling it soon? Thanks

@aristidesstaffieri
Copy link
Contributor Author

@aristidesstaffieri FYI, I've linked #454 and #455 to this PR as it appears this is implementing the whole package, could you confirm?
The only thing that appears to be missing apparently is the "Memo" and "XDR" fields on the Transaction history details component?
Screenshot 2025-10-17 at 11 51 44 Screenshot 2025-10-17 at 11 52 02
Which I suspect are actually missing for other transaction types too 🤔

oh thanks yeah I think just those fields need to be added to all transaction types so I can take a look adding those here.

added in 2f2f3ec

…appropriate store state based on nav targets
@aristidesstaffieri
Copy link
Contributor Author

aristidesstaffieri commented Oct 21, 2025

@aristidesstaffieri thanks for the video, looks great. I noticed one thing: after you have a collectible selected and you change its destination address the app is stacking new screens (of the same functionality) on top of each other, which suggests the code is "navigating" to a "new" screen instead of "popping" (or "going back") to an existing screen. I think ideally we should try to avoid that as all the screens on the stack would react to changes in state which could lead to weird unexpected behaviors. Wdyt?

@CassioMG I've refactored the nav flows to avoid this, check out 514d118

Here's a summary of the nav flow for send-

## Entry Points

1. **Home Page → TransactionTokenScreen** (token/collectible flow)
2. **CollectibleDetail** (collectible flow)
3. **Token Detail** (token flow)

---

## Token Send Flows

### Flow 1: Home → TransactionTokenScreen → Token Selection

[Home]
  ↓ (navigate)
[TransactionTokenScreen]
  ↓ (select token, goBack)
[TransactionAmountScreen]
  ↓ (tap recipient / no recipient, navigate)
[SearchContacts]
  ↓ (select contact, goBack - no collectible details)
[TransactionAmountScreen]
  ↓ (enter amount, review)
[ReviewBottomSheet]
  ↓ (confirm)
[TransactionProcessing]
  ↓ (complete, navigation.reset)
[History Tab]

Stack at each step:
1. [Home]
2. [Home, TransactionTokenScreen]
3. [Home, TransactionAmountScreen]
4. [Home, TransactionAmountScreen, SearchContacts]
5. [Home, TransactionAmountScreen] (goBack)
6. Modal on TransactionAmountScreen
7. Full screen TransactionProcessing
8. Reset to [History]


### Flow 2: Token Detail → Send

[Token Detail Screen]
  ↓ (navigate to send flow)
[TransactionAmountScreen]
  ↓ (navigate)
[SearchContacts]
  ↓ (select contact, goBack - no collectible details)
[TransactionAmountScreen]
  ↓ (continue to review)
[ReviewBottomSheet]
  ↓ (confirm)
[TransactionProcessing]
  ↓ (complete)
[History Tab]

Stack at each step:
1. [TokenDetail]
2. [TokenDetail, TransactionAmountScreen]
3. [TokenDetail, TransactionAmountScreen, SearchContacts]
4. [TokenDetail, TransactionAmountScreen] (goBack)


---

## Collectible Send Flows

### Flow 3: CollectibleDetail → Send (First Time, No Changes)

[CollectibleDetail]
  ↓ (navigate, sets selectedCollectibleDetails)
[SearchContacts]
  ↓ (select contact, navigate - first time, no Review in stack)
[CollectibleReview]
  ↓ (confirm)
[TransactionProcessing]
  ↓ (complete, clears selectedCollectibleDetails)
[History Tab]

Stack at each step:
1. [CollectibleDetail]
2. [CollectibleDetail, SearchContacts]
3. [CollectibleDetail, SearchContacts, CollectibleReview] (navigate adds Review)
4. Full screen TransactionProcessing
5. Reset to [History]

Navigation methods:
- CollectibleDetail → SearchContacts: navigate()
- SearchContacts → CollectibleReview: navigate() (first time)


### Flow 4: CollectibleDetail → Send → Change Contact (Circular Navigation)

[CollectibleDetail]
  ↓ (navigate)
[SearchContacts]
  ↓ (select contact, navigate - first time)
[CollectibleReview]
  ↓ (tap contact to change, replace)
[SearchContacts]
  ↓ (select new contact, replace - circular nav detected)
[CollectibleReview]
  ↓ (Back button)
[SearchContacts]
  ↓ (Back button)
[CollectibleDetail]

Stack at each step:
1. [CollectibleDetail]
2. [CollectibleDetail, SearchContacts]
3. [CollectibleDetail, SearchContacts, CollectibleReview] (navigate)
4. [CollectibleDetail, SearchContacts] (replace removes Review)
5. [CollectibleDetail, SearchContacts, CollectibleReview] (replace, SearchContacts count > 1)
6. [CollectibleDetail, SearchContacts] (back)
7. [CollectibleDetail] (back)

Navigation methods:
- SearchContacts → Review (first): navigate() (searchContactCount = 1)
- Review → SearchContacts: replace() (always)
- SearchContacts → Review (circular): replace() (searchContactCount > 1 detected)


### Flow 5: TransactionTokenScreen → Collectible Selection (with recipient already set)

[Home]
  ↓ (navigate)
[TransactionTokenScreen]
  ↓ (tap contact row, navigate)
[SearchContacts]
  ↓ (select contact, goBack)
[TransactionTokenScreen]
  ↓ (select collectible, navigate - has recipientAddress)
[CollectibleReview]
  ↓ (tap contact to change, replace)
[SearchContacts]
  ↓ (select new contact, navigate - first time for this session)
[CollectibleReview]
  ↓ (confirm)
[TransactionProcessing]
  ↓ (complete)
[History Tab]

Stack at each step:
1. [Home]
2. [Home, TransactionTokenScreen]
3. [Home, TransactionTokenScreen, SearchContacts]
4. [Home, TransactionTokenScreen] (goBack)
5. [Home, TransactionTokenScreen, CollectibleReview] (navigate)
6. [Home, TransactionTokenScreen, SearchContacts] (replace)
7. [Home, TransactionTokenScreen, SearchContacts, CollectibleReview] (navigate - first time)
8. Full screen TransactionProcessing
9. Reset to [History]


### Flow 6: TransactionTokenScreen → Collectible Selection (no recipient)

[Home]
  ↓ (navigate)
[TransactionTokenScreen]
  ↓ (select collectible, navigate - no recipientAddress)
[SearchContacts]
  ↓ (select contact, navigate - first time)
[CollectibleReview]
  ↓ (complete or back)
[History Tab] or [SearchContacts]

Stack at each step:
1. [Home]
2. [Home, TransactionTokenScreen]
3. [Home, TransactionTokenScreen, SearchContacts]
4. [Home, TransactionTokenScreen, SearchContacts, CollectibleReview] (navigate)

---

## Navigation Methods Summary

### From SearchContacts (handleContactPress)
```javascript
if (selectedCollectibleDetails.tokenId) {
  // COLLECTIBLE FLOW
  const searchContactCount = count SearchContacts in stack

  if (searchContactCount > 1) {
    // Circular navigation detected
    navigation.replace(COLLECTIBLE_REVIEW)  // Prevents duplicate SearchContacts
  } else {
    // First time selecting contact
    navigation.navigate(COLLECTIBLE_REVIEW)  // Adds Review to stack
  }
} else {
  // TOKEN FLOW
  navigation.goBack()  // Returns to TransactionAmountScreen
}

From CollectibleReview (navigateToSelectContactScreen)

// Always replace to avoid duplicating SearchContacts
navigation.replace(SEND_SEARCH_CONTACTS_SCREEN)

From TransactionTokenScreen (handleCollectiblePress)

saveSelectedCollectibleDetails(collectibleDetails)

if (recipientAddress) {
  // Already have recipient, go directly to review
  navigation.navigate(COLLECTIBLE_REVIEW)
} else {
  // Need to select recipient first
  navigation.navigate(SEND_SEARCH_CONTACTS_SCREEN)
}

State Management (useTransactionSettingsStore)

When to Set State

  • selectedCollectibleDetails: Set when entering collectible flow (CollectibleDetail or TransactionTokenScreen)
  • recipientAddress: Set when contact is selected in SearchContacts
  • selectedTokenId: Set when token is selected

When to Clear State

  • SearchContacts unmount: Clears selectedCollectibleDetails (prevents token flow using stale data)
  • CollectibleReview completion: Clears all state (selectedCollectibleDetails, recipientAddress, settings)
  • TransactionAmountScreen unmount: Clears selectedTokenId, settings

State Persistence During Navigation

  • CollectibleReview unmount: Does NOT clear state (allows circular nav SearchContacts ↔ Review)
  • SearchContacts to Review: selectedCollectibleDetails persist (but cleared on unmount)
  • Review to SearchContacts: CollectibleReview re-sets selectedCollectibleDetails from route params on mount

I don't love how complex this flow is, SearchContact has to be aware of nav history and store state in order to make the right nav choices. Im curious if you see any easier ways to construct this flow or if you have any thoughts on how to simplify the send flow UX?

As a side note to this, the send flow navigation is pretty complex at this point but one thing that makes this flow more error prone is the use of remote state in the store for tx details. Every screen in the flow needs to be aware and responsible for setting/clearing the correct data for the next or previous screens to work correctly. This is a smell imo, and creates a view managed data store. We can move this state away from the store and into hooks that are consumed at common parent entry points in order to let the react lifecycle clear any stale data naturally. IMO the prop drilling this creates is a good trade off to avoid the stale data problems that come with complex view flows managing remote state outside of the react lifecycle.

Comment on lines 131 to 141
if (
searchContactCount > 1 ||
currentRouteName !== SEND_PAYMENT_ROUTES.SEND_SEARCH_CONTACTS_SCREEN
) {
// We have duplicate SearchContacts or we're not at the current screen (shouldn't happen)
// Use replace() to swap
navigation.replace(
SEND_PAYMENT_ROUTES.SEND_COLLECTIBLE_REVIEW,
selectedCollectibleDetails,
);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the navigation.replace() command replaces the current screen with an existing one in stack if any?
Otherwise by reading the code I believe the Flow 4 would look like the following:

Flow 4: CollectibleDetail → Send → Change Contact (Circular Navigation)

[CollectibleDetail]
↓ (navigate)
[SearchContacts]
↓ (select contact, navigate - first time)
[CollectibleReview]
↓ (tap contact to change, replace CollectibleReview with another SearchContacts)
[SearchContacts]
↓ (select new contact, replace SearchContacts with CollectibleReview - circular nav detected)
[CollectibleReview]
↓ (Back button)
[SearchContacts]
↓ (Back button)
[CollectibleDetail]

Stack at each step:

  1. [CollectibleDetail]
  2. [CollectibleDetail, SearchContacts]
  3. [CollectibleDetail, SearchContacts, CollectibleReview] (navigate)
  4. [CollectibleDetail, SearchContacts, SearchContacts] (edit contact action replaces CollectibleReview with another SearchContacts, we now momentarily have 2 SearchContacts in stack)
  5. [CollectibleDetail, SearchContacts, CollectibleReview] (selecting contact replaces the duplicate SearchContacts with CollectibleReview since SearchContacts count > 1)
  6. [CollectibleDetail, SearchContacts] (back)
  7. [CollectibleDetail] (back)

Which works but we'd momentarily have 2 SearchContacts screens on stack.

Have we tried using popTo() instead to handle the duplicate screen issues? It appears to have the behavior we want:
Screenshot 2025-10-21 at 13 52 35

So whenever users tap on edit contact we could do something like this: navigation.popTo(SEND_PAYMENT_ROUTES.SEND_SEARCH_CONTACTS_SCREEN) and hopefully it should work out of the box.

I'm actually surprised (and happy) with the current popTo() description as formerly I believe it used to just throw an error when the screen you were trying to pop to was not in stack.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the navigation.replace() command replaces the current screen with an existing one in stack if any?

hey yeah so I believe the conditional avoids this. Here is a walk through.

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-10-22.at.09.12.33.mp4

@CassioMG
Copy link
Contributor

CassioMG commented Oct 21, 2025

Here's a summary of the nav flow for send-

@aristidesstaffieri thanks for all the navigation flows/graphs they are really helpful to understand what the PR is proposing.

I don't love how complex this flow is, SearchContact has to be aware of nav history and store state in order to make the right nav choices. Im curious if you see any easier ways to construct this flow or if you have any thoughts on how to simplify the send flow UX?

I've just posted a suggestion on using popTo() instead of replace() which hopefully should simplify things.

As a side note to this, the send flow navigation is pretty complex at this point but one thing that makes this flow more error prone is the use of remote state in the store for tx details. Every screen in the flow needs to be aware and responsible for setting/clearing the correct data for the next or previous screens to work correctly. This is a smell imo, and creates a view managed data store. We can move this state away from the store and into hooks that are consumed at common parent entry points in order to let the react lifecycle clear any stale data naturally. IMO the prop drilling this creates is a good trade off to avoid the stale data problems that come with complex view flows managing remote state outside of the react lifecycle.

Thanks for raising this. I agree with you that using navigation parameters and/or hooks to handle those flows would be better instead of using a shared data store managed by the views which is less ideal in this case and prone to hard-to-detect bugs.

At this point this would probably be a big refactor, but IMO something still worth to take on once we have some bandwidth. I'll create a ticket for it.

@aristidesstaffieri
Copy link
Contributor Author

Thanks for raising this. I agree with you that using navigation parameters and/or hooks to handle those flows would be better instead of using a shared data store managed by the views which is less ideal in this case and prone to hard-to-detect bugs.

At this point this would probably be a big refactor, but IMO something still worth to take on once we have some bandwidth. I'll create a ticket for it.

Thanks yeah I agree this is a whole task on it's own, thanks.

@CassioMG
Copy link
Contributor

Thanks for raising this. I agree with you that using navigation parameters and/or hooks to handle those flows would be better instead of using a shared data store managed by the views which is less ideal in this case and prone to hard-to-detect bugs.
At this point this would probably be a big refactor, but IMO something still worth to take on once we have some bandwidth. I'll create a ticket for it.

Thanks yeah I agree this is a whole task on it's own, thanks.

Update: we've discussed the settings refactor with the team in today's Dev Sync meeting and everyone agreed it would be worth the refactor. We'll plan on tackling it in some upcoming Sprint.

@aristidesstaffieri
Copy link
Contributor Author

@CassioMG thanks for the tip on popTo, that simplified this code quite a bit. Here is what the flow looks like now which I think is what we want in regards to the search contact entry/exit. 054ea8d

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-10-22.at.13.42.32.mp4

Comment on lines 122 to 123
navigation.dispatch(
StackActions.popTo(
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to use navigation.dispatch + StackActions here? Could we do something more straightforward like navigation.popTo() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it looks like you can, updated in d9fd966

navigation.replace(SEND_PAYMENT_ROUTES.SEND_SEARCH_CONTACTS_SCREEN);
// Use popTo to navigate back to SearchContacts
// If SearchContacts exists in stack, pops back to it; otherwise adds it
navigation.dispatch(
Copy link
Contributor

Choose a reason for hiding this comment

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

same here, I thought we could do it like navigation.popTo() directly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated in d9fd966

Copy link
Contributor

@CassioMG CassioMG left a comment

Choose a reason for hiding this comment

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

LGTM! left a suggestion on a cleaner syntax for the popTo() action

@aristidesstaffieri aristidesstaffieri merged commit 3b53639 into main Oct 23, 2025
3 checks passed
@aristidesstaffieri aristidesstaffieri deleted the feature/send-collectibles-ui branch October 23, 2025 15:23
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.

History - Collectible Sent UI History - Collectible Sent mapping Send Collectible UI

3 participants