Skip to content

Commit 2541e58

Browse files
authored
feat: update provider selection modal ui (#26726)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Updates the Provider Selection modal UI with improved quote-driven sorting, provider tags, skeleton loading states, and a redesigned payment method banner. ### Payment Method Banner - Replaced the plain "Quotes displayed for {paymentMethod}" text with a full-width banner component featuring a circular payment method icon on the left and the descriptive text left-aligned beside it, matching the latest design spec. ### Quote-Based Provider Sorting - Providers with quotes are now sorted by the API's `QuoteSortOrder` reliability ranking (via `quotes.sorted`), placing the most reliable provider at the top. - Providers without quotes are pushed to the bottom of the list, sorted alphabetically, with an "Other options" text separator between the two groups. ### Skeleton Loading State - When quotes are loading, the provider list is replaced with a skeleton placeholder (5 rows) instead of showing unsorted providers with individual loading spinners. Once quotes resolve, providers render in their sorted order. ### Provider Tags - Each provider can display one tag below its name: **Previously used**, **Most reliable**, or **Best rate**. - Tags use the quote's `metadata.tags` (`isMostReliable`, `isBestRate`) from the API response, and `getOrdersProviders` from Redux to identify previously used providers from completed order history. - Precedence when a provider qualifies for multiple tags: Previously used > Most reliable > Best rate. ### Conditional Back Button - The header back arrow is now only shown when the Payment Selection modal is in the navigation stack (i.e., the user navigated from payment selection to provider selection). When the provider modal is opened directly (e.g., from error recovery or token-not-available flows), the back button is hidden since there's nothing to go back to. ## Changed Files - `ProviderSelection.tsx` — Banner, skeleton, sorting, tags, conditional back button - `ProviderSelectionModal.tsx` — Passes `ordersProviders` from Redux, `showBackButton` from navigation state - `ProviderSelection.test.tsx` — Updated test for skeleton loading behavior - `en.json` — Added i18n strings: `other_options`, `previously_used`, `best_rate`, `most_reliable` <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Updates ramp provider selection modal UI ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TRAM-3307 https://consensyssoftware.atlassian.net/browse/TRAM-3187 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** ![new-provider-modal](https://github.com/user-attachments/assets/4efe134f-80ba-4c65-9967-662f23479eec) <img width="353" height="355" alt="Screenshot 2026-02-28 at 5 30 22 AM" src="https://github.com/user-attachments/assets/5b089b35-d302-4c82-8ef5-994e697bac4a" /> <img width="360" height="754" alt="Screenshot 2026-02-28 at 5 30 01 AM" src="https://github.com/user-attachments/assets/cc720ccf-02d6-4681-ac88-303b59775d51" /> <img width="353" height="747" alt="Screenshot 2026-02-28 at 5 29 14 AM" src="https://github.com/user-attachments/assets/04cbedf4-d233-4576-8bd8-c79c6ce0115d" /> <img width="341" height="742" alt="Screenshot 2026-02-28 at 5 29 00 AM" src="https://github.com/user-attachments/assets/ec1e2031-0c01-4c59-85ad-e9fdccaddabc" /> <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes provider ordering/visibility and quote presentation logic in the provider selection flow (sorting, skeleton states, tags, and back-button behavior), which could affect user selection and navigation in edge cases. No security- or data-sensitive logic is introduced. > > **Overview** > Updates `ProviderSelection` to be *quote-driven*: providers with successful quotes are sorted by the API reliability order and shown above an **“Other options”** separator, while providers without quotes are pushed below and alphabetized. > > Replaces the prior “show providers while loading” behavior with a dedicated skeleton list, updates the payment-method header to a banner with an icon, and adds per-provider tags (**Previously used**, **Most reliable**, **Best rate**) derived from order history and quote metadata. > > `ProviderSelectionModal` now conditionally shows the back button only when the payment selection modal exists in the navigation stack and passes `ordersProviders` from Redux; tests/snapshots and i18n strings were updated accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 790f8c3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent fad4a12 commit 2541e58

7 files changed

Lines changed: 1789 additions & 534 deletions

File tree

app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelection.test.tsx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,20 @@ interface RenderOptions {
106106
quotes?: QuotesResponse | null;
107107
quotesLoading?: boolean;
108108
quotesError?: string | null;
109+
showQuotes?: boolean;
109110
}
110111

111112
function renderWithProvider(
112113
providers: Provider[] = mockProviders,
113114
selectedProvider: Provider | null = mockProviders[0],
114115
options: RenderOptions = {},
115116
) {
116-
const { quotes = null, quotesLoading = false, quotesError = null } = options;
117+
const {
118+
quotes = null,
119+
quotesLoading = false,
120+
quotesError = null,
121+
showQuotes,
122+
} = options;
117123

118124
jest.mocked(useRampsController).mockReturnValue({
119125
...defaultMockController,
@@ -128,6 +134,7 @@ function renderWithProvider(
128134
quotesError={quotesError}
129135
onProviderSelect={jest.fn()}
130136
onBack={mockOnBack}
137+
{...(showQuotes !== undefined && { showQuotes })}
131138
/>
132139
),
133140
{
@@ -170,18 +177,22 @@ describe('ProviderSelection', () => {
170177
expect(toJSON()).toMatchSnapshot();
171178
});
172179

173-
it('renders providers immediately when quotes are loading', () => {
180+
it('renders skeleton loading state when quotes are loading', () => {
174181
jest.mocked(useRampsController).mockReturnValue({
175182
...defaultMockController,
176183
userRegion: mockUserRegion,
177184
selectedToken: mockSelectedToken,
178185
providers: mockProviders,
179186
selectedProvider: mockProviders[0],
180187
});
181-
const { getByText } = renderWithProvider(mockProviders, mockProviders[0], {
182-
quotesLoading: true,
183-
});
184-
expect(getByText('Transak')).toBeOnTheScreen();
188+
const { queryByText } = renderWithProvider(
189+
mockProviders,
190+
mockProviders[0],
191+
{
192+
quotesLoading: true,
193+
},
194+
);
195+
expect(queryByText('Transak')).toBeNull();
185196
});
186197

187198
it('matches snapshot when quotes fail to load', async () => {
@@ -210,6 +221,34 @@ describe('ProviderSelection', () => {
210221
expect(mockOnBack).toHaveBeenCalled();
211222
});
212223

224+
it('renders providers directly when quotes load but none are available', async () => {
225+
jest.mocked(useRampsController).mockReturnValue({
226+
...defaultMockController,
227+
userRegion: mockUserRegion,
228+
selectedToken: mockSelectedToken,
229+
providers: mockProviders,
230+
selectedProvider: mockProviders[0],
231+
});
232+
const { toJSON, getByText } = renderWithProvider(
233+
mockProviders,
234+
mockProviders[0],
235+
{
236+
showQuotes: true,
237+
quotes: {
238+
success: [],
239+
sorted: [],
240+
error: [],
241+
customActions: [],
242+
},
243+
},
244+
);
245+
246+
await waitFor(() => {
247+
expect(getByText('Transak')).toBeOnTheScreen();
248+
});
249+
expect(toJSON()).toMatchSnapshot();
250+
});
251+
213252
it('filters out quotes for providers not in the providers array', async () => {
214253
const transakQuote = createMockQuote('/providers/transak', 'Transak');
215254
const stripeQuote = createMockQuote('/providers/stripe', 'Stripe');

0 commit comments

Comments
 (0)