Skip to content

Commit e06f20c

Browse files
authored
Merge branch 'main' into ogp/22207
2 parents 0a207ff + 042b0db commit e06f20c

File tree

14 files changed

+350
-103
lines changed

14 files changed

+350
-103
lines changed

app/components/Views/confirmations/components/confirm/confirm-component.styles.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ const styleSheet = (params: {
1616
maxHeight: '100%',
1717
},
1818
flatContainer: {
19-
position: 'absolute',
20-
top: 0,
21-
left: 0,
22-
right: 0,
23-
bottom: 0,
19+
flex: 1,
2420
zIndex: 9999,
2521
backgroundColor: theme.colors.background.alternative,
2622
justifyContent: 'space-between',

app/components/Views/confirmations/components/confirm/confirm-component.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import React, { useEffect } from 'react';
2-
import {
3-
BackHandler,
4-
StyleSheet,
5-
TouchableWithoutFeedback,
6-
View,
7-
} from 'react-native';
2+
import { BackHandler, TouchableWithoutFeedback, View } from 'react-native';
83
import { ScrollView } from 'react-native-gesture-handler';
94
import { useNavigation } from '@react-navigation/native';
105

@@ -31,6 +26,7 @@ import { TransactionType } from '@metamask/transaction-controller';
3126
import { useParams } from '../../../../../util/navigation/navUtils';
3227
import AnimatedSpinner, { SpinnerSize } from '../../../../UI/AnimatedSpinner';
3328
import { CustomAmountInfoSkeleton } from '../info/custom-amount-info';
29+
import { SafeAreaView } from 'react-native-safe-area-context';
3430

3531
export enum ConfirmationLoader {
3632
Default = 'default',
@@ -46,7 +42,7 @@ const ConfirmWrapped = ({
4642
styles,
4743
route,
4844
}: {
49-
styles: StyleSheet.NamedStyles<Record<string, unknown>>;
45+
styles: ReturnType<typeof styleSheet>;
5046
route?: UnstakeConfirmationViewProps['route'];
5147
}) => {
5248
const alerts = useConfirmationAlerts();
@@ -59,11 +55,7 @@ const ConfirmWrapped = ({
5955
<LedgerContextProvider>
6056
<Title />
6157
<ScrollView
62-
// @ts-expect-error - React Native style type mismatch due to outdated @types/react-native
63-
// See: https://github.com/MetaMask/metamask-mobile/pull/18956#discussion_r2316407382
6458
style={styles.scrollView}
65-
// @ts-expect-error - React Native style type mismatch due to outdated @types/react-native
66-
// See: https://github.com/MetaMask/metamask-mobile/pull/18956#discussion_r2316407382
6759
contentContainerStyle={styles.scrollViewContent}
6860
nestedScrollEnabled
6961
>
@@ -133,9 +125,13 @@ export const Confirm = ({ route }: ConfirmProps) => {
133125
// Show confirmation in a flat container if the confirmation is full screen
134126
if (isFullScreenConfirmation) {
135127
return (
136-
<View style={styles.flatContainer} testID={ConfirmationUIType.FLAT}>
128+
<SafeAreaView
129+
edges={['right', 'bottom', 'left']}
130+
style={styles.flatContainer}
131+
testID={ConfirmationUIType.FLAT}
132+
>
137133
<ConfirmWrapped styles={styles} route={route} />
138-
</View>
134+
</SafeAreaView>
139135
);
140136
}
141137

@@ -160,14 +156,18 @@ function Loader() {
160156

161157
if (loader === ConfirmationLoader.CustomAmount) {
162158
return (
163-
<View style={styles.flatContainer} testID="confirm-loader-custom-amount">
159+
<SafeAreaView
160+
edges={['right', 'bottom', 'left']}
161+
style={styles.flatContainer}
162+
testID="confirm-loader-custom-amount"
163+
>
164164
<ScrollView
165165
style={styles.scrollView}
166166
contentContainerStyle={styles.scrollViewContent}
167167
>
168168
<CustomAmountInfoSkeleton />
169169
</ScrollView>
170-
</View>
170+
</SafeAreaView>
171171
);
172172
}
173173

app/components/Views/confirmations/components/rows/transactions/gas-fee-details-row/gas-fee-details-row.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import useBalanceChanges from '../../../../../../UI/SimulationDetails/useBalance
2323
import { useFeeCalculations } from '../../../../hooks/gas/useFeeCalculations';
2424
import { useFeeCalculationsTransactionBatch } from '../../../../hooks/gas/useFeeCalculationsTransactionBatch';
2525
import { useSelectedGasFeeToken } from '../../../../hooks/gas/useGasFeeToken';
26+
import { useIsGaslessSupported } from '../../../../hooks/gas/useIsGaslessSupported';
2627
import { useConfirmationMetricEvents } from '../../../../hooks/metrics/useConfirmationMetricEvents';
2728
import { useTransactionBatchesMetadata } from '../../../../hooks/transactions/useTransactionBatchesMetadata';
2829
import { useTransactionMetadataRequest } from '../../../../hooks/transactions/useTransactionMetadataRequest';
@@ -228,14 +229,20 @@ const GasFeesDetailsRow = ({
228229
const transactionBatchesMetadata = useTransactionBatchesMetadata();
229230
const gasFeeToken = useSelectedGasFeeToken();
230231
const metamaskFeeFiat = gasFeeToken?.metamaskFeeFiat;
232+
const {
233+
userFeeLevel: isUserFeeLevelExists,
234+
isGasFeeSponsored: doesSentinelAllowSponsorship,
235+
} = transactionMetadata ?? {};
231236

232237
const hideFiatForTestnet = useHideFiatForTestnet(
233238
transactionMetadata?.chainId,
234239
);
235240
const { trackTooltipClickedEvent } = useConfirmationMetricEvents();
236241

237-
const isUserFeeLevelExists = transactionMetadata?.userFeeLevel;
238-
const isGasFeeSponsored = transactionMetadata?.isGasFeeSponsored;
242+
// This prevents the gas fee row from showing as sponsored if stx is disabled
243+
// by the user and 7702 is not supported in the chain.
244+
const { isSupported: isGaslessSupported } = useIsGaslessSupported();
245+
const isGasFeeSponsored = isGaslessSupported && doesSentinelAllowSponsorship;
239246

240247
const handleNetworkFeeTooltipClickedEvent = () => {
241248
trackTooltipClickedEvent({

app/components/Views/confirmations/hooks/alerts/useInsufficientBalanceAlert.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useConfirmActions } from '../useConfirmActions';
1212
import { useTransactionPayToken } from '../pay/useTransactionPayToken';
1313
import { noop } from 'lodash';
1414
import { useConfirmationContext } from '../../context/confirmation-context';
15+
import { useIsGaslessSupported } from '../gas/useIsGaslessSupported';
1516

1617
jest.mock('../../../../../util/navigation/navUtils', () => ({
1718
useParams: jest.fn().mockReturnValue({
@@ -44,6 +45,7 @@ jest.mock('../../../../../reducers/transaction', () => ({
4445
selectTransactionState: jest.fn(),
4546
}));
4647
jest.mock('../../context/confirmation-context');
48+
jest.mock('../gas/useIsGaslessSupported');
4749

4850
describe('useInsufficientBalanceAlert', () => {
4951
const mockUseTransactionMetadataRequest = jest.mocked(
@@ -56,6 +58,8 @@ describe('useInsufficientBalanceAlert', () => {
5658
);
5759
const mockUseTransactionPayToken = jest.mocked(useTransactionPayToken);
5860
const mockUseConfirmationContext = jest.mocked(useConfirmationContext);
61+
const useIsGaslessSupportedMock = jest.mocked(useIsGaslessSupported);
62+
5963
const mockChainId = '0x1';
6064
const mockFromAddress = '0x123';
6165
const mockNativeCurrency = 'ETH';
@@ -72,6 +76,10 @@ describe('useInsufficientBalanceAlert', () => {
7276
beforeEach(() => {
7377
jest.clearAllMocks();
7478

79+
useIsGaslessSupportedMock.mockReturnValue({
80+
isSmartTransaction: false,
81+
isSupported: false,
82+
});
7583
mockUseAccountNativeBalance.mockReturnValue({
7684
balanceWeiInHex: '0x8', // 8 wei
7785
} as unknown as ReturnType<typeof useAccountNativeBalance>);

app/components/Views/confirmations/hooks/alerts/useInsufficientBalanceAlert.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useAccountNativeBalance } from '../useAccountNativeBalance';
1919
import { useConfirmActions } from '../useConfirmActions';
2020
import { useTransactionPayToken } from '../pay/useTransactionPayToken';
2121
import { useConfirmationContext } from '../../context/confirmation-context';
22+
import { useIsGaslessSupported } from '../gas/useIsGaslessSupported';
2223

2324
const HEX_ZERO = '0x0';
2425

@@ -37,6 +38,7 @@ export const useInsufficientBalanceAlert = ({
3738
const { isTransactionValueUpdating } = useConfirmationContext();
3839
const { onReject } = useConfirmActions();
3940
const { payToken } = useTransactionPayToken();
41+
const { isSupported: isGaslessSupported } = useIsGaslessSupported();
4042

4143
return useMemo(() => {
4244
if (!transactionMetadata || isTransactionValueUpdating) {
@@ -65,11 +67,13 @@ export const useInsufficientBalanceAlert = ({
6567
totalTransactionValueBN,
6668
);
6769

70+
const isSponsoredTransaction = isGasFeeSponsored && isGaslessSupported;
71+
6872
const showAlert =
6973
hasInsufficientBalance &&
7074
(ignoreGasFeeToken || !selectedGasFeeToken) &&
7175
!payToken &&
72-
!isGasFeeSponsored;
76+
!isSponsoredTransaction;
7377

7478
if (!showAlert) {
7579
return [];
@@ -100,6 +104,7 @@ export const useInsufficientBalanceAlert = ({
100104
}, [
101105
balanceWeiInHex,
102106
ignoreGasFeeToken,
107+
isGaslessSupported,
103108
isTransactionValueUpdating,
104109
navigation,
105110
networkConfigurations,
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { waitFor } from '@testing-library/react-native';
2+
import { merge } from 'lodash';
3+
import { useGaslessSupportedSmartTransactions } from './useGaslessSupportedSmartTransactions';
4+
import { isSendBundleSupported } from '../../../../../util/transactions/sentinel-api';
5+
import { selectShouldUseSmartTransaction } from '../../../../../selectors/smartTransactionsController';
6+
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
7+
import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
8+
import { transferConfirmationState } from '../../../../../util/test/confirm-data-helpers';
9+
import { transferTransactionStateMock } from '../../__mocks__/transfer-transaction-mock';
10+
import { TransactionMeta } from '@metamask/transaction-controller';
11+
12+
jest.mock('../../../../../util/transactions/sentinel-api');
13+
jest.mock('../../../../../selectors/smartTransactionsController');
14+
jest.mock('../transactions/useTransactionMetadataRequest');
15+
16+
const CHAIN_ID_MOCK = '0x1';
17+
18+
describe('useGaslessSupportedSmartTransactions (mobile)', () => {
19+
const isSendBundleSupportedMock = jest.mocked(isSendBundleSupported);
20+
const selectShouldUseSmartTransactionMock = jest.mocked(
21+
selectShouldUseSmartTransaction,
22+
);
23+
const useTransactionMetadataRequestMock = jest.mocked(
24+
useTransactionMetadataRequest,
25+
);
26+
27+
beforeEach(() => {
28+
jest.resetAllMocks();
29+
useTransactionMetadataRequestMock.mockReturnValue({
30+
chainId: CHAIN_ID_MOCK,
31+
} as unknown as TransactionMeta);
32+
33+
isSendBundleSupportedMock.mockResolvedValue(false);
34+
selectShouldUseSmartTransactionMock.mockReturnValue(false);
35+
});
36+
37+
it('returns isSupported = true when both smart transaction and bundle supported', async () => {
38+
isSendBundleSupportedMock.mockResolvedValue(true);
39+
selectShouldUseSmartTransactionMock.mockReturnValue(true);
40+
41+
const { result } = renderHookWithProvider(
42+
() => useGaslessSupportedSmartTransactions(),
43+
{
44+
state: merge({}, transferConfirmationState),
45+
},
46+
);
47+
await waitFor(() =>
48+
expect(result.current).toStrictEqual({
49+
isSmartTransaction: true,
50+
isSupported: true,
51+
pending: false,
52+
}),
53+
);
54+
});
55+
56+
it('returns isSupported = false when smart transaction enabled but bundle not supported', async () => {
57+
isSendBundleSupportedMock.mockResolvedValue(false);
58+
selectShouldUseSmartTransactionMock.mockReturnValue(true);
59+
60+
const { result } = renderHookWithProvider(
61+
() => useGaslessSupportedSmartTransactions(),
62+
{
63+
state: merge({}, transferConfirmationState),
64+
},
65+
);
66+
await waitFor(() =>
67+
expect(result.current).toStrictEqual({
68+
isSmartTransaction: true,
69+
isSupported: false,
70+
pending: false,
71+
}),
72+
);
73+
});
74+
75+
it('returns isSupported = false when bundle supported but not a smart transaction', async () => {
76+
isSendBundleSupportedMock.mockResolvedValue(true);
77+
selectShouldUseSmartTransactionMock.mockReturnValue(false);
78+
79+
const { result } = renderHookWithProvider(
80+
() => useGaslessSupportedSmartTransactions(),
81+
{
82+
state: merge({}, transferConfirmationState),
83+
},
84+
);
85+
await waitFor(() =>
86+
expect(result.current).toStrictEqual({
87+
isSmartTransaction: false,
88+
isSupported: false,
89+
pending: false,
90+
}),
91+
);
92+
});
93+
94+
it('returns isSupported = false when neither smart transaction nor bundle is supported', async () => {
95+
isSendBundleSupportedMock.mockResolvedValue(false);
96+
selectShouldUseSmartTransactionMock.mockReturnValue(false);
97+
98+
const { result } = renderHookWithProvider(
99+
() => useGaslessSupportedSmartTransactions(),
100+
{
101+
state: merge({}, transferConfirmationState),
102+
},
103+
);
104+
await waitFor(() =>
105+
expect(result.current).toStrictEqual({
106+
isSmartTransaction: false,
107+
isSupported: false,
108+
pending: false,
109+
}),
110+
);
111+
});
112+
113+
it('returns pending = true while sendBundleSupported is still pending', async () => {
114+
let resolvePromise: (value: boolean) => void = () => {
115+
// no-op
116+
};
117+
const pendingPromise = new Promise<boolean>((resolve) => {
118+
resolvePromise = resolve;
119+
});
120+
isSendBundleSupportedMock.mockReturnValue(
121+
pendingPromise as Promise<boolean>,
122+
);
123+
selectShouldUseSmartTransactionMock.mockReturnValue(true);
124+
125+
const { result, rerender } = renderHookWithProvider(
126+
() => useGaslessSupportedSmartTransactions(),
127+
{ state: merge({}, transferConfirmationState) },
128+
);
129+
expect(result.current.pending).toBe(true);
130+
131+
// Resolve and trigger update
132+
resolvePromise(true);
133+
rerender(transferConfirmationState);
134+
await waitFor(() =>
135+
expect(result.current).toStrictEqual({
136+
isSmartTransaction: true,
137+
isSupported: true,
138+
pending: false,
139+
}),
140+
);
141+
});
142+
143+
it('returns false if chainId is missing', async () => {
144+
useTransactionMetadataRequestMock.mockReturnValue({
145+
chainId: undefined,
146+
} as unknown as TransactionMeta);
147+
148+
const { result } = renderHookWithProvider(
149+
() => useGaslessSupportedSmartTransactions(),
150+
{ state: transferTransactionStateMock },
151+
);
152+
await waitFor(() =>
153+
expect(result.current).toStrictEqual({
154+
isSmartTransaction: false,
155+
isSupported: false,
156+
pending: false,
157+
}),
158+
);
159+
});
160+
161+
it('returns false if transactionMeta is null', async () => {
162+
useTransactionMetadataRequestMock.mockReturnValue(undefined);
163+
164+
const { result } = renderHookWithProvider(
165+
() => useGaslessSupportedSmartTransactions(),
166+
{ state: transferTransactionStateMock },
167+
);
168+
await waitFor(() =>
169+
expect(result.current).toStrictEqual({
170+
isSmartTransaction: false,
171+
isSupported: false,
172+
pending: false,
173+
}),
174+
);
175+
});
176+
});

0 commit comments

Comments
 (0)