diff --git a/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.test.tsx b/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.test.tsx
index c50b458e01a..9ba6d9e7854 100644
--- a/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.test.tsx
+++ b/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.test.tsx
@@ -1,11 +1,21 @@
import React from 'react';
-import { render, fireEvent } from '@testing-library/react-native';
+import { fireEvent } from '@testing-library/react-native';
+import type { CaipChainId } from '@metamask/utils';
import TronUnstakedBanner from './TronUnstakedBanner';
import { strings } from '../../../../../../../locales/i18n';
import useTronClaimUnstakedTrx from '../../../hooks/useTronClaimUnstakedTrx';
import useEarnToasts from '../../../hooks/useEarnToasts';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import { selectTronClaimUnstakedTrxButtonEnabled } from '../../../../../../selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled';
import { TronUnstakedBannerTestIds } from './TronUnstakedBanner.testIds';
+jest.mock(
+ '../../../../../../selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled',
+ () => ({
+ selectTronClaimUnstakedTrxButtonEnabled: jest.fn(),
+ }),
+);
+
jest.mock('../../../hooks/useTronClaimUnstakedTrx');
const mockUseTronClaimUnstakedTrx =
useTronClaimUnstakedTrx as jest.MockedFunction<
@@ -23,11 +33,18 @@ jest.mock('../../../hooks/useEarnToasts');
},
});
+const mockSelectTronClaimUnstakedTrxButtonEnabled =
+ selectTronClaimUnstakedTrxButtonEnabled as unknown as jest.Mock;
+
+const renderBanner = (props: { amount: string; chainId: CaipChainId }) =>
+ renderWithProvider(, undefined, false);
+
describe('TronUnstakedBanner', () => {
const mockHandleClaimUnstakedTrx = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
+ mockSelectTronClaimUnstakedTrxButtonEnabled.mockReturnValue(true);
mockUseTronClaimUnstakedTrx.mockReturnValue({
handleClaimUnstakedTrx: mockHandleClaimUnstakedTrx,
isSubmitting: false,
@@ -42,9 +59,10 @@ describe('TronUnstakedBanner', () => {
});
it('renders the title with the given amount', () => {
- const { getByText } = render(
- ,
- );
+ const { getByText } = renderBanner({
+ amount: '100',
+ chainId: 'tron:728126428',
+ });
const expectedTitle = strings('stake.tron.unstaked_banner.title', {
amount: '100',
@@ -53,9 +71,10 @@ describe('TronUnstakedBanner', () => {
});
it('renders the description', () => {
- const { getByText } = render(
- ,
- );
+ const { getByText } = renderBanner({
+ amount: '100',
+ chainId: 'tron:728126428',
+ });
const expectedDescription = strings(
'stake.tron.unstaked_banner.description',
@@ -63,20 +82,38 @@ describe('TronUnstakedBanner', () => {
expect(getByText(expectedDescription)).toBeOnTheScreen();
});
- it('renders the Withdraw button', () => {
- const { getByTestId } = render(
- ,
- );
+ it('renders the claim button when tronClaimUnstakedTrxButtonEnabled is true', () => {
+ const { getByTestId } = renderBanner({
+ amount: '100',
+ chainId: 'tron:728126428',
+ });
expect(
getByTestId(TronUnstakedBannerTestIds.CLAIM_BUTTON),
).toBeOnTheScreen();
});
+ it('does not render the claim button when tronClaimUnstakedTrxButtonEnabled is false', () => {
+ mockSelectTronClaimUnstakedTrxButtonEnabled.mockReturnValue(false);
+
+ const { getByText, queryByTestId } = renderBanner({
+ amount: '100',
+ chainId: 'tron:728126428',
+ });
+
+ expect(
+ queryByTestId(TronUnstakedBannerTestIds.CLAIM_BUTTON),
+ ).not.toBeOnTheScreen();
+ expect(
+ getByText(strings('stake.tron.unstaked_banner.description')),
+ ).toBeOnTheScreen();
+ });
+
it('calls handleClaimUnstakedTrx when button is pressed', () => {
- const { getByTestId } = render(
- ,
- );
+ const { getByTestId } = renderBanner({
+ amount: '100',
+ chainId: 'tron:728126428',
+ });
fireEvent.press(getByTestId(TronUnstakedBannerTestIds.CLAIM_BUTTON));
expect(mockHandleClaimUnstakedTrx).toHaveBeenCalledTimes(1);
@@ -89,9 +126,10 @@ describe('TronUnstakedBanner', () => {
errors: undefined,
});
- const { getByTestId } = render(
- ,
- );
+ const { getByTestId } = renderBanner({
+ amount: '100',
+ chainId: 'tron:728126428',
+ });
const button = getByTestId(TronUnstakedBannerTestIds.CLAIM_BUTTON);
expect(button.props.accessibilityState?.disabled).toBe(true);
@@ -104,7 +142,7 @@ describe('TronUnstakedBanner', () => {
errors: ['InsufficientBalance'],
});
- render();
+ renderBanner({ amount: '100', chainId: 'tron:728126428' });
expect(mockFailedToastFn).toHaveBeenCalledWith(['InsufficientBalance']);
expect(mockShowToast).toHaveBeenCalledWith(mockFailedToastResult);
@@ -117,14 +155,14 @@ describe('TronUnstakedBanner', () => {
errors: [],
});
- render();
+ renderBanner({ amount: '100', chainId: 'tron:728126428' });
expect(mockFailedToastFn).toHaveBeenCalledWith([]);
expect(mockShowToast).toHaveBeenCalledWith(mockFailedToastResult);
});
it('does not show error toast when there are no errors', () => {
- render();
+ renderBanner({ amount: '100', chainId: 'tron:728126428' });
expect(mockShowToast).not.toHaveBeenCalled();
});
diff --git a/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.tsx b/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.tsx
index 5e245729048..65c6ba3d3d1 100644
--- a/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.tsx
+++ b/app/components/UI/Earn/components/Tron/TronUnstakedBanner/TronUnstakedBanner.tsx
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
+import { useSelector } from 'react-redux';
import type { CaipChainId } from '@metamask/utils';
import { strings } from '../../../../../../../locales/i18n';
import Banner, {
@@ -13,6 +14,7 @@ import {
} from '@metamask/design-system-react-native';
import useTronClaimUnstakedTrx from '../../../hooks/useTronClaimUnstakedTrx';
import useEarnToasts from '../../../hooks/useEarnToasts';
+import { selectTronClaimUnstakedTrxButtonEnabled } from '../../../../../../selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled';
import { TronUnstakedBannerTestIds } from './TronUnstakedBanner.testIds';
interface TronUnstakedBannerProps {
@@ -21,6 +23,7 @@ interface TronUnstakedBannerProps {
}
const TronUnstakedBanner = ({ amount, chainId }: TronUnstakedBannerProps) => {
+ const showClaimButton = useSelector(selectTronClaimUnstakedTrxButtonEnabled);
const { handleClaimUnstakedTrx, isSubmitting, errors } =
useTronClaimUnstakedTrx({ chainId });
const { showToast, EarnToastOptions } = useEarnToasts();
@@ -41,19 +44,21 @@ const TronUnstakedBanner = ({ amount, chainId }: TronUnstakedBannerProps) => {
<>
{strings('stake.tron.unstaked_banner.description')}
-
+ {showClaimButton ? (
+
+ ) : null}
>
}
/>
diff --git a/app/components/UI/Perps/utils/wait.test.ts b/app/components/UI/Perps/utils/wait.test.ts
index aea87705d79..ac38e76a03a 100644
--- a/app/components/UI/Perps/utils/wait.test.ts
+++ b/app/components/UI/Perps/utils/wait.test.ts
@@ -13,14 +13,14 @@ describe('wait', () => {
const promise = wait(100);
jest.advanceTimersByTime(100);
await promise;
- expect(promise).resolves.toBeUndefined();
+ await expect(promise).resolves.toBeUndefined();
});
it('should handle zero duration', async () => {
const promise = wait(0);
jest.advanceTimersByTime(0);
await promise;
- expect(promise).resolves.toBeUndefined();
+ await expect(promise).resolves.toBeUndefined();
});
it('should return a Promise that resolves to undefined', async () => {
diff --git a/app/constants/featureFlags.ts b/app/constants/featureFlags.ts
index a439f406657..67da83992fe 100644
--- a/app/constants/featureFlags.ts
+++ b/app/constants/featureFlags.ts
@@ -16,6 +16,7 @@ export enum FeatureFlagNames {
tokenDetailsV2ButtonLayout = 'tokenDetailsV2ButtonLayout',
complianceEnabled = 'complianceEnabled',
legacyIosGoogleConfigEnabled = 'legacyIosGoogleConfigEnabled',
+ tronClaimUnstakedTrxButtonEnabled = 'tronClaimUnstakedTrxButtonEnabled',
}
export const DEFAULT_FEATURE_FLAG_VALUES: Partial<
@@ -24,4 +25,5 @@ export const DEFAULT_FEATURE_FLAG_VALUES: Partial<
[FeatureFlagNames.assetsDefiPositionsEnabled]: true,
[FeatureFlagNames.tokenDetailsV2Buttons]: false,
[FeatureFlagNames.tokenDetailsV2ButtonLayout]: false,
+ [FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled]: false,
};
diff --git a/app/selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled/index.test.ts b/app/selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled/index.test.ts
new file mode 100644
index 00000000000..55cb6066b58
--- /dev/null
+++ b/app/selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled/index.test.ts
@@ -0,0 +1,49 @@
+import { Json } from '@metamask/utils';
+import { selectTronClaimUnstakedTrxButtonEnabled } from '.';
+import {
+ DEFAULT_FEATURE_FLAG_VALUES,
+ FeatureFlagNames,
+} from '../../../constants/featureFlags';
+
+describe('Tron claim unstaked TRX button enabled feature flag selector', () => {
+ describe('selectTronClaimUnstakedTrxButtonEnabled', () => {
+ it('returns true when remote flag is explicitly true', () => {
+ const result = selectTronClaimUnstakedTrxButtonEnabled.resultFunc({
+ [FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled]: true,
+ });
+
+ expect(result).toBe(true);
+ });
+
+ it('returns false when remote flag is explicitly false', () => {
+ const result = selectTronClaimUnstakedTrxButtonEnabled.resultFunc({
+ [FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled]: false,
+ });
+
+ expect(result).toBe(false);
+ });
+
+ it('returns default value when remote flag is not set', () => {
+ const result = selectTronClaimUnstakedTrxButtonEnabled.resultFunc({});
+
+ expect(result).toBe(
+ DEFAULT_FEATURE_FLAG_VALUES[
+ FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled
+ ],
+ );
+ });
+
+ it('returns default value when remote flag is undefined', () => {
+ const result = selectTronClaimUnstakedTrxButtonEnabled.resultFunc({
+ [FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled]:
+ undefined as unknown as Json,
+ });
+
+ expect(result).toBe(
+ DEFAULT_FEATURE_FLAG_VALUES[
+ FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled
+ ],
+ );
+ });
+ });
+});
diff --git a/app/selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled/index.ts b/app/selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled/index.ts
new file mode 100644
index 00000000000..390dbe3a5a1
--- /dev/null
+++ b/app/selectors/featureFlagController/tronClaimUnstakedTrxButtonEnabled/index.ts
@@ -0,0 +1,17 @@
+import { createSelector } from 'reselect';
+import { selectRemoteFeatureFlags } from '..';
+import {
+ DEFAULT_FEATURE_FLAG_VALUES,
+ FeatureFlagNames,
+} from '../../../constants/featureFlags';
+
+export const selectTronClaimUnstakedTrxButtonEnabled = createSelector(
+ selectRemoteFeatureFlags,
+ (remoteFeatureFlags) =>
+ Boolean(
+ remoteFeatureFlags[FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled] ??
+ DEFAULT_FEATURE_FLAG_VALUES[
+ FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled
+ ],
+ ),
+);
diff --git a/tests/feature-flags/feature-flag-registry.test.ts b/tests/feature-flags/feature-flag-registry.test.ts
index ed238d077e2..a86028cd6b1 100644
--- a/tests/feature-flags/feature-flag-registry.test.ts
+++ b/tests/feature-flags/feature-flag-registry.test.ts
@@ -85,6 +85,7 @@ describe('Feature Flag Registry', () => {
expect(flagNames).toContain('bridgeConfigV2');
expect(flagNames).toContain('bitcoinAccounts');
expect(flagNames).toContain('tronAccounts');
+ expect(flagNames).toContain('tronClaimUnstakedTrxButtonEnabled');
});
});
diff --git a/tests/feature-flags/feature-flag-registry.ts b/tests/feature-flags/feature-flag-registry.ts
index 2c696a8020c..c16c13a2fb3 100644
--- a/tests/feature-flags/feature-flag-registry.ts
+++ b/tests/feature-flags/feature-flag-registry.ts
@@ -62,7 +62,7 @@ export interface FeatureFlagRegistryEntry {
* Remote flag values are stored in the exact format returned by the production
* client-config API, so they can be served directly by the E2E mock server.
*
- * Production defaults last synced: 2026-03-02
+ * Production defaults last synced: 2026-03-25
* Source: https://client-config.api.cx.metamask.io/v1/flags?client=mobile&distribution=main&environment=prod
*/
export const FEATURE_FLAG_REGISTRY: Record = {
@@ -3618,6 +3618,14 @@ export const FEATURE_FLAG_REGISTRY: Record = {
status: FeatureFlagStatus.Active,
},
+ tronClaimUnstakedTrxButtonEnabled: {
+ name: 'tronClaimUnstakedTrxButtonEnabled',
+ type: FeatureFlagType.Remote,
+ inProd: true,
+ productionDefault: false,
+ status: FeatureFlagStatus.Active,
+ },
+
tronStaking: {
name: 'tronStaking',
type: FeatureFlagType.Remote,