Skip to content

Commit 6556ecd

Browse files
authored
feat: restore wallet tailwind migration (#27575)
<!-- 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** Migrates the RestoreWallet component from legacy React Native styling (StyleSheet, View, StyledButton) to the MetaMask design system using @metamask/design-system-react-native components (Box, Text, Button) and Tailwind CSS via useTailwind() Why: Legacy StyleSheet.create(), raw View/Text, and deprecated StyledButton are being phased out across the app in favor of the design system for consistency, accessibility, and maintainability. What changed: * Replaced View with Box (design system) * Replaced deprecated Text with design system Text (updated variants: HeadingLG → HeadingLg, BodyMD → BodyMd) * Replaced deprecated StyledButton + manual ActivityIndicator with design system Button (isLoading prop handles spinner internally) * Replaced useAppThemeFromContext() + createStyles() with useTailwind() hook * Removed ActivityIndicator import (no longer needed) * Updated unit test to assert loading state via spinner-container testID instead of UNSAFE_getByType(ActivityIndicator) styles.ts is not deleted as it is still shared by WalletRestored and WalletResetNeeded Jira: https://consensyssoftware.atlassian.net/browse/TO-596 <!-- 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: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: RestoreWallet screen Tailwind migration Feature: RestoreWallet screen Tailwind migration Scenario: user sees the RestoreWallet screen during vault recovery Given the app detects a corrupted vault on startup or login When user is navigated to the RestoreWallet screen Then the screen displays the device image, "Restore needed" title, description text, and a full-width "Restore wallet" button Scenario: user taps Restore wallet button Given the user is on the RestoreWallet screen When user taps "Restore wallet" Then a loading spinner appears on the button And the app attempts to restore the vault from backup ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <img width="303" height="640" alt="Screenshot 2026-03-18 at 12 19 37 PM" src="https://github.com/user-attachments/assets/abdfb2bb-47a7-452c-8c63-3bffbd22e0f2" /> <img width="303" height="640" alt="Screenshot 2026-03-18 at 12 18 25 PM" src="https://github.com/user-attachments/assets/98e90073-c9f8-440b-a5f7-b5e0cfaf09af" /> <!-- [screenshots/recordings] --> ### **After** <img width="303" height="640" alt="Screenshot 2026-03-18 at 12 28 36 PM" src="https://github.com/user-attachments/assets/ad7f8575-70a9-4451-910e-08e05248d219" /> <img width="303" height="640" alt="Screenshot 2026-03-18 at 12 27 01 PM" src="https://github.com/user-attachments/assets/9205a65d-b9c8-4c0f-8d98-49234ea41d7a" /> <!-- [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] > **Low Risk** > Low risk UI refactor that swaps legacy styling/components for design-system primitives and changes how the loading spinner is rendered/queried in tests. > > **Overview** > Migrates `RestoreWallet` from legacy `StyleSheet`/`View`/`StyledButton`/`ActivityIndicator` to MetaMask design-system components (`Box`, `Text`, `Button`) styled via `useTailwind()`. > > Updates the restore CTA to use `Button`’s `isLoading` state (instead of manually rendering an `ActivityIndicator`) and adjusts the unit test to assert the new loading UI via the design-system spinner test id. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 00ea399. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 39c509d commit 6556ecd

3 files changed

Lines changed: 254 additions & 32 deletions

File tree

app/components/Views/RestoreWallet/RestoreWallet.test.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Image, ActivityIndicator } from 'react-native';
2+
import { Image } from 'react-native';
33
import { fireEvent, waitFor } from '@testing-library/react-native';
44
import RestoreWallet from './RestoreWallet';
55
import Routes from '../../../constants/navigation/Routes';
@@ -93,6 +93,12 @@ describe('RestoreWallet', () => {
9393
const imageElement = UNSAFE_getByType(Image);
9494
expect(imageElement).toBeTruthy();
9595
});
96+
97+
it('renders component tree correctly', () => {
98+
const { toJSON } = renderWithProvider(<RestoreWallet />);
99+
100+
expect(toJSON()).toMatchSnapshot();
101+
});
96102
});
97103

98104
describe('analytics tracking', () => {
@@ -150,23 +156,21 @@ describe('RestoreWallet', () => {
150156
});
151157
});
152158

153-
it('shows loading indicator while restoring', async () => {
159+
it('triggers vault restore on button press', async () => {
154160
let resolveRestore: (value: { success: boolean }) => void;
155161
const restorePromise = new Promise<{ success: boolean }>((resolve) => {
156162
resolveRestore = resolve;
157163
});
158164
(EngineService.initializeVaultFromBackup as jest.Mock).mockReturnValue(
159165
restorePromise,
160166
);
161-
const { getByText, UNSAFE_getByType } = renderWithProvider(
162-
<RestoreWallet />,
163-
);
167+
const { getByText } = renderWithProvider(<RestoreWallet />);
164168

165169
fireEvent.press(
166170
getByText(strings('restore_wallet.restore_needed_action')),
167171
);
168172

169-
expect(UNSAFE_getByType(ActivityIndicator)).toBeTruthy();
173+
expect(EngineService.initializeVaultFromBackup).toHaveBeenCalled();
170174

171175
// @ts-expect-error resolveRestore is assigned in Promise constructor
172176
resolveRestore({ success: true });

app/components/Views/RestoreWallet/RestoreWallet.tsx

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
/* eslint-disable import/no-commonjs */
22
import React, { useCallback, useEffect, useMemo, useState } from 'react';
3-
import { View, Image, ActivityIndicator } from 'react-native';
3+
import { Image } from 'react-native';
44
import { strings } from '../../../../locales/i18n';
5-
import { createStyles } from './styles';
6-
import Text, {
5+
import { useTailwind } from '@metamask/design-system-twrnc-preset';
6+
import {
7+
Box,
8+
Text,
9+
Button,
10+
BoxAlignItems,
11+
BoxJustifyContent,
712
TextVariant,
8-
} from '../../../component-library/components/Texts/Text';
9-
import StyledButton from '../../UI/StyledButton';
13+
TextColor,
14+
ButtonVariant,
15+
ButtonSize,
16+
} from '@metamask/design-system-react-native';
1017
import {
1118
createNavigationDetails,
1219
useParams,
@@ -15,7 +22,6 @@ import Routes from '../../../constants/navigation/Routes';
1522
import EngineService from '../../../core/EngineService';
1623
import { SafeAreaView } from 'react-native-safe-area-context';
1724
import { useNavigation, StackActions } from '@react-navigation/native';
18-
import { useAppThemeFromContext } from '../../../util/theme';
1925
import { createWalletResetNeededNavDetails } from './WalletResetNeeded';
2026
import { createWalletRestoredNavDetails } from './WalletRestored';
2127
import { MetaMetricsEvents } from '../../../core/Analytics';
@@ -44,8 +50,7 @@ export const createRestoreWalletNavDetailsNested =
4450

4551
const RestoreWallet = () => {
4652
const { trackEvent, createEventBuilder } = useMetrics();
47-
const { colors } = useAppThemeFromContext();
48-
const styles = createStyles(colors);
53+
const tw = useTailwind();
4954

5055
const [loading, setLoading] = useState<boolean>(false);
5156

@@ -89,31 +94,42 @@ const RestoreWallet = () => {
8994
}, [deviceMetaData, navigation, trackEvent, createEventBuilder]);
9095

9196
return (
92-
<SafeAreaView style={styles.screen}>
93-
<View style={styles.content}>
94-
<View style={styles.images}>
97+
<SafeAreaView style={tw.style('flex-1 px-6 justify-between items-center')}>
98+
<Box
99+
alignItems={BoxAlignItems.Center}
100+
justifyContent={BoxJustifyContent.Center}
101+
twClassName="flex-1"
102+
>
103+
<Box alignItems={BoxAlignItems.Center} twClassName="p-4">
95104
<Image source={onboardingDeviceImage} />
96-
</View>
97-
<Text variant={TextVariant.HeadingLG} style={styles.title}>
105+
</Box>
106+
<Text
107+
variant={TextVariant.HeadingLg}
108+
color={TextColor.TextDefault}
109+
twClassName="text-center pb-4"
110+
>
98111
{strings('restore_wallet.restore_needed_title')}
99112
</Text>
100-
<Text variant={TextVariant.BodyMD} style={styles.description}>
113+
<Text
114+
variant={TextVariant.BodyMd}
115+
color={TextColor.TextDefault}
116+
twClassName="text-center p-2"
117+
>
101118
{strings('restore_wallet.restore_needed_description')}
102119
</Text>
103-
</View>
104-
<View style={styles.actionButtonWrapper}>
105-
<StyledButton
106-
type="confirm"
107-
containerStyle={styles.actionButton}
120+
</Box>
121+
<Box twClassName="w-full my-2.5">
122+
<Button
123+
variant={ButtonVariant.Primary}
124+
size={ButtonSize.Lg}
125+
isFullWidth
108126
onPress={handleOnNext}
127+
isLoading={loading}
128+
twClassName="bg-primary-default"
109129
>
110-
{loading ? (
111-
<ActivityIndicator size="small" color={colors.primary.inverse} />
112-
) : (
113-
strings('restore_wallet.restore_needed_action')
114-
)}
115-
</StyledButton>
116-
</View>
130+
{strings('restore_wallet.restore_needed_action')}
131+
</Button>
132+
</Box>
117133
</SafeAreaView>
118134
);
119135
};
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`RestoreWallet rendering renders component tree correctly 1`] = `
4+
<RNCSafeAreaView
5+
edges={
6+
{
7+
"bottom": "additive",
8+
"left": "additive",
9+
"right": "additive",
10+
"top": "additive",
11+
}
12+
}
13+
style={
14+
{
15+
"alignItems": "center",
16+
"flexBasis": "0%",
17+
"flexGrow": 1,
18+
"flexShrink": 1,
19+
"justifyContent": "space-between",
20+
"paddingLeft": 24,
21+
"paddingRight": 24,
22+
}
23+
}
24+
>
25+
<View
26+
style={
27+
[
28+
{
29+
"alignItems": "center",
30+
"display": "flex",
31+
"flexBasis": "0%",
32+
"flexGrow": 1,
33+
"flexShrink": 1,
34+
"justifyContent": "center",
35+
},
36+
undefined,
37+
]
38+
}
39+
>
40+
<View
41+
style={
42+
[
43+
{
44+
"alignItems": "center",
45+
"display": "flex",
46+
"paddingBottom": 16,
47+
"paddingLeft": 16,
48+
"paddingRight": 16,
49+
"paddingTop": 16,
50+
},
51+
undefined,
52+
]
53+
}
54+
>
55+
<Image
56+
source={1}
57+
/>
58+
</View>
59+
<Text
60+
accessibilityRole="text"
61+
style={
62+
[
63+
{
64+
"color": "#131416",
65+
"fontFamily": "Geist-Bold",
66+
"fontSize": 24,
67+
"fontWeight": 700,
68+
"letterSpacing": 0,
69+
"lineHeight": 32,
70+
"paddingBottom": 16,
71+
"textAlign": "center",
72+
},
73+
undefined,
74+
]
75+
}
76+
>
77+
Restore needed
78+
</Text>
79+
<Text
80+
accessibilityRole="text"
81+
style={
82+
[
83+
{
84+
"color": "#131416",
85+
"fontFamily": "Geist-Regular",
86+
"fontSize": 16,
87+
"fontWeight": 400,
88+
"letterSpacing": 0,
89+
"lineHeight": 24,
90+
"paddingBottom": 8,
91+
"paddingLeft": 8,
92+
"paddingRight": 8,
93+
"paddingTop": 8,
94+
"textAlign": "center",
95+
},
96+
undefined,
97+
]
98+
}
99+
>
100+
Something went wrong, but don’t worry! Let’s try to restore your wallet.
101+
</Text>
102+
</View>
103+
<View
104+
style={
105+
[
106+
{
107+
"display": "flex",
108+
"marginBottom": 10,
109+
"marginTop": 10,
110+
"width": "100%",
111+
},
112+
undefined,
113+
]
114+
}
115+
>
116+
<View
117+
accessibilityLabel="Restore wallet"
118+
accessibilityRole="button"
119+
accessibilityState={
120+
{
121+
"busy": undefined,
122+
"checked": undefined,
123+
"disabled": false,
124+
"expanded": undefined,
125+
"selected": undefined,
126+
}
127+
}
128+
accessibilityValue={
129+
{
130+
"max": undefined,
131+
"min": undefined,
132+
"now": undefined,
133+
"text": undefined,
134+
}
135+
}
136+
accessible={true}
137+
collapsable={false}
138+
focusable={true}
139+
onBlur={[Function]}
140+
onClick={[Function]}
141+
onFocus={[Function]}
142+
onResponderGrant={[Function]}
143+
onResponderMove={[Function]}
144+
onResponderRelease={[Function]}
145+
onResponderTerminate={[Function]}
146+
onResponderTerminationRequest={[Function]}
147+
onStartShouldSetResponder={[Function]}
148+
style={
149+
[
150+
{
151+
"alignItems": "center",
152+
"backgroundColor": "#4459ff",
153+
"borderRadius": 12,
154+
"columnGap": 8,
155+
"flexDirection": "row",
156+
"height": 48,
157+
"justifyContent": "center",
158+
"minWidth": 80,
159+
"opacity": 1,
160+
"overflow": "hidden",
161+
"paddingLeft": 16,
162+
"paddingRight": 16,
163+
"width": "100%",
164+
},
165+
{
166+
"transform": [
167+
{
168+
"scale": 1,
169+
},
170+
],
171+
},
172+
]
173+
}
174+
>
175+
<Text
176+
accessibilityRole="text"
177+
ellipsizeMode="clip"
178+
numberOfLines={1}
179+
style={
180+
[
181+
{
182+
"color": "#ffffff",
183+
"flexGrow": 0,
184+
"flexShrink": 1,
185+
"flexWrap": "wrap",
186+
"fontFamily": "Geist-Medium",
187+
"fontSize": 16,
188+
"fontWeight": 400,
189+
"letterSpacing": 0,
190+
"lineHeight": 24,
191+
"textAlign": "center",
192+
},
193+
undefined,
194+
]
195+
}
196+
>
197+
Restore wallet
198+
</Text>
199+
</View>
200+
</View>
201+
</RNCSafeAreaView>
202+
`;

0 commit comments

Comments
 (0)