Skip to content

Commit f1e6009

Browse files
chore(runway): cherry-pick fix: show banner for hardware wallets not supported (#16152)
- fix: cp-7.47.0 show banner for hardware wallets not supported (#16094)
1 parent 192f01f commit f1e6009

5 files changed

Lines changed: 95 additions & 12 deletions

File tree

app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import mockQuotes from '../../_mocks_/mock-quotes-sol-sol.json';
1414
import { SolScope } from '@metamask/keyring-api';
1515
import { mockUseBridgeQuoteData } from '../../_mocks_/useBridgeQuoteData.mock';
1616
import { useBridgeQuoteData } from '../../hooks/useBridgeQuoteData';
17+
import { isHardwareAccount } from '../../../../../util/address';
18+
import { strings } from '../../../../../../locales/i18n';
1719

1820
// TODO remove this mock once we have a real implementation
1921
jest.mock('../../../../../selectors/confirmTransaction');
@@ -69,6 +71,9 @@ jest.mock('../../../../hooks/useAccounts', () => ({
6971
isSelected: true,
7072
},
7173
],
74+
ensByAccountAddress: {
75+
'0x1234567890123456789012345678901234567890': '',
76+
},
7277
}),
7378
}));
7479

@@ -123,6 +128,12 @@ jest.mock('../../hooks/useBridgeQuoteData', () => ({
123128
.mockImplementation(() => mockUseBridgeQuoteData),
124129
}));
125130

131+
jest.mock('../../../../../util/address', () => ({
132+
isHardwareAccount: jest.fn().mockReturnValue(false),
133+
formatAddress: jest.fn().mockImplementation((address) => address),
134+
getLabelTextByAddress: jest.fn().mockReturnValue(''),
135+
}));
136+
126137
describe('BridgeView', () => {
127138
const token2Address = '0x0000000000000000000000000000000000000002' as Hex;
128139

@@ -630,6 +641,47 @@ describe('BridgeView', () => {
630641

631642
expect(toJSON()).toMatchSnapshot();
632643
});
644+
645+
it('displays hardware wallet not supported banner and disables continue button when using hardware wallet with Solana source', async () => {
646+
// Mock isHardwareAccount to return true for this test only
647+
const mockIsHardwareAccount = jest.fn().mockReturnValue(true);
648+
jest.mocked(isHardwareAccount).mockImplementation(mockIsHardwareAccount);
649+
650+
const testState = createBridgeTestState({
651+
bridgeControllerOverrides: {
652+
quoteRequest: {
653+
insufficientBal: false,
654+
},
655+
quotesLoadingStatus: RequestStatus.FETCHED,
656+
quotes: [mockQuotes[0] as unknown as QuoteResponse],
657+
quotesLastFetched: 12,
658+
},
659+
bridgeReducerOverrides: {
660+
sourceAmount: '1.0',
661+
sourceToken: {
662+
address: 'So11111111111111111111111111111111111111112',
663+
chainId: SolScope.Mainnet,
664+
decimals: 9,
665+
image: '',
666+
name: 'Solana',
667+
symbol: 'SOL',
668+
},
669+
},
670+
});
671+
672+
const { getByText } = renderScreen(
673+
BridgeView,
674+
{
675+
name: Routes.BRIDGE.ROOT,
676+
},
677+
{ state: testState },
678+
);
679+
680+
// Wait for the banner text to appear
681+
await waitFor(() => {
682+
expect(getByText(strings('bridge.hardware_wallet_not_supported_solana'))).toBeTruthy();
683+
});
684+
});
633685
});
634686

635687
describe('Error Banner Visibility', () => {

app/components/UI/Bridge/Views/BridgeView/index.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
setIsSubmittingTx,
4040
selectIsSolanaToEvm,
4141
selectDestAddress,
42+
selectIsSolanaSourced,
4243
} from '../../../../../core/redux/slices/bridge';
4344
import {
4445
useNavigation,
@@ -70,6 +71,8 @@ import { BridgeToken, BridgeViewMode } from '../../types';
7071
import { useSwitchTokens } from '../../hooks/useSwitchTokens';
7172
import { ScrollView } from 'react-native';
7273
import useIsInsufficientBalance from '../../hooks/useInsufficientBalance';
74+
import { selectSelectedInternalAccountFormattedAddress } from '../../../../../selectors/accountsController';
75+
import { isHardwareAccount } from '../../../../../util/address';
7376

7477
export interface BridgeRouteParams {
7578
token?: BridgeToken;
@@ -119,10 +122,17 @@ const BridgeView = () => {
119122
} = useBridgeQuoteData();
120123
const { quotesLastFetched } = useSelector(selectBridgeControllerState);
121124
const { handleSwitchTokens } = useSwitchTokens();
125+
const selectedAddress = useSelector(
126+
selectSelectedInternalAccountFormattedAddress,
127+
);
128+
const isHardwareAddress = selectedAddress
129+
? !!isHardwareAccount(selectedAddress)
130+
: false;
122131

123132
const isEvmSolanaBridge = useSelector(selectIsEvmSolanaBridge);
124133
const isSolanaSwap = useSelector(selectIsSolanaSwap);
125134
const isSolanaToEvm = useSelector(selectIsSolanaToEvm);
135+
const isSolanaSourced = useSelector(selectIsSolanaSourced);
126136
// inputRef is used to programmatically blur the input field after a delay
127137
// This gives users time to type before the keyboard disappears
128138
// The ref is typed to only expose the blur method we need
@@ -156,9 +166,9 @@ const BridgeView = () => {
156166
sourceAmount !== undefined && sourceAmount !== '.' && sourceToken?.decimals;
157167

158168
const hasValidBridgeInputs =
159-
isValidSourceAmount &&
160-
!!sourceToken &&
161-
!!destToken &&
169+
isValidSourceAmount &&
170+
!!sourceToken &&
171+
!!destToken &&
162172
// Prevent quote fetching when destination address is not set
163173
// Destinations address is only needed for EVM <> Solana bridges
164174
(!isEvmSolanaBridge || (isEvmSolanaBridge && !!destAddress));
@@ -326,7 +336,9 @@ const BridgeView = () => {
326336
}
327337

328338
const isSwap = route.params.bridgeViewMode === BridgeViewMode.Swap;
329-
return isSwap ? strings('bridge.confirm_swap') : strings('bridge.confirm_bridge');
339+
return isSwap
340+
? strings('bridge.confirm_swap')
341+
: strings('bridge.confirm_bridge');
330342
};
331343

332344
useEffect(() => {
@@ -379,12 +391,26 @@ const BridgeView = () => {
379391
activeQuote &&
380392
quotesLastFetched && (
381393
<Box style={styles.buttonContainer}>
394+
{isHardwareAddress && (
395+
<BannerAlert
396+
severity={BannerAlertSeverity.Error}
397+
description={
398+
isSolanaSourced
399+
? strings('bridge.hardware_wallet_not_supported_solana')
400+
: strings('bridge.hardware_wallet_not_supported')
401+
}
402+
/>
403+
)}
382404
<Button
383405
variant={ButtonVariants.Primary}
384406
label={getButtonLabel()}
385407
onPress={handleContinue}
386408
style={styles.button}
387-
isDisabled={hasInsufficientBalance || isSubmittingTx}
409+
isDisabled={
410+
hasInsufficientBalance ||
411+
isSubmittingTx ||
412+
isHardwareAddress
413+
}
388414
/>
389415
<Button
390416
variant={ButtonVariants.Link}

app/components/UI/Bridge/components/QuoteExpiredModal/__snapshots__/QuoteExpiredModal.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ exports[`QuoteExpiredModal renders correctly 1`] = `
240240
}
241241
}
242242
>
243-
Rates update every 10 seconds, so tap Get new quote when you're ready.
243+
Rates update every 15 seconds, so tap Get new quote when you're ready.
244244
</Text>
245245
</View>
246246
<View

app/core/redux/slices/bridge/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,14 @@ export const selectBridgeQuotes = createSelector(
340340
}),
341341
);
342342

343+
export const selectIsSolanaSourced = createSelector(
344+
selectSourceToken,
345+
(sourceToken) =>
346+
sourceToken?.chainId &&
347+
isSolanaChainId(sourceToken.chainId),
348+
);
349+
350+
343351
export const selectIsEvmToSolana = createSelector(
344352
selectSourceToken,
345353
selectDestToken,

locales/languages/en.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3937,18 +3937,15 @@
39373937
"title": "Bridge",
39383938
"submitting_transaction": "Submitting",
39393939
"fetching_quote": "Fetching quote",
3940-
"fee_disclaimer": "Includes 0.875% MM fee"
3940+
"fee_disclaimer": "Includes 0.875% MM fee",
3941+
"hardware_wallet_not_supported": "Hardware wallets aren't supported yet. Use a hot wallet to continue.",
3942+
"hardware_wallet_not_supported_solana": "Hardware wallets aren't supported for Solana yet. Use a hot wallet to continue."
39413943
},
39423944
"quote_expired_modal": {
39433945
"title": "New quotes are available",
39443946
"description": "Rates update every {{refreshRate}} seconds, so tap Get new quote when you're ready.",
39453947
"get_new_quote": "Get new quote"
39463948
},
3947-
"quote_expired_modal": {
3948-
"title": "New quotes are available",
3949-
"description": "Rates update every 10 seconds, so tap Get new quote when you're ready.",
3950-
"get_new_quote": "Get new quote"
3951-
},
39523949
"bridge_transaction_details": {
39533950
"status": "Status",
39543951
"date": "Date",

0 commit comments

Comments
 (0)