Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -23,11 +33,18 @@ jest.mock('../../../hooks/useEarnToasts');
},
});

const mockSelectTronClaimUnstakedTrxButtonEnabled =
selectTronClaimUnstakedTrxButtonEnabled as unknown as jest.Mock;

const renderBanner = (props: { amount: string; chainId: CaipChainId }) =>
renderWithProvider(<TronUnstakedBanner {...props} />, undefined, false);

describe('TronUnstakedBanner', () => {
const mockHandleClaimUnstakedTrx = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
mockSelectTronClaimUnstakedTrxButtonEnabled.mockReturnValue(true);
mockUseTronClaimUnstakedTrx.mockReturnValue({
handleClaimUnstakedTrx: mockHandleClaimUnstakedTrx,
isSubmitting: false,
Expand All @@ -42,9 +59,10 @@ describe('TronUnstakedBanner', () => {
});

it('renders the title with the given amount', () => {
const { getByText } = render(
<TronUnstakedBanner amount="100" chainId="tron:728126428" />,
);
const { getByText } = renderBanner({
amount: '100',
chainId: 'tron:728126428',
});

const expectedTitle = strings('stake.tron.unstaked_banner.title', {
amount: '100',
Expand All @@ -53,30 +71,49 @@ describe('TronUnstakedBanner', () => {
});

it('renders the description', () => {
const { getByText } = render(
<TronUnstakedBanner amount="100" chainId="tron:728126428" />,
);
const { getByText } = renderBanner({
amount: '100',
chainId: 'tron:728126428',
});

const expectedDescription = strings(
'stake.tron.unstaked_banner.description',
);
expect(getByText(expectedDescription)).toBeOnTheScreen();
});

it('renders the Withdraw button', () => {
const { getByTestId } = render(
<TronUnstakedBanner amount="100" chainId="tron:728126428" />,
);
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(
<TronUnstakedBanner amount="100" chainId="tron:728126428" />,
);
const { getByTestId } = renderBanner({
amount: '100',
chainId: 'tron:728126428',
});

fireEvent.press(getByTestId(TronUnstakedBannerTestIds.CLAIM_BUTTON));
expect(mockHandleClaimUnstakedTrx).toHaveBeenCalledTimes(1);
Expand All @@ -89,9 +126,10 @@ describe('TronUnstakedBanner', () => {
errors: undefined,
});

const { getByTestId } = render(
<TronUnstakedBanner amount="100" chainId="tron:728126428" />,
);
const { getByTestId } = renderBanner({
amount: '100',
chainId: 'tron:728126428',
});

const button = getByTestId(TronUnstakedBannerTestIds.CLAIM_BUTTON);
expect(button.props.accessibilityState?.disabled).toBe(true);
Expand All @@ -104,7 +142,7 @@ describe('TronUnstakedBanner', () => {
errors: ['InsufficientBalance'],
});

render(<TronUnstakedBanner amount="100" chainId="tron:728126428" />);
renderBanner({ amount: '100', chainId: 'tron:728126428' });

expect(mockFailedToastFn).toHaveBeenCalledWith(['InsufficientBalance']);
expect(mockShowToast).toHaveBeenCalledWith(mockFailedToastResult);
Expand All @@ -117,14 +155,14 @@ describe('TronUnstakedBanner', () => {
errors: [],
});

render(<TronUnstakedBanner amount="100" chainId="tron:728126428" />);
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(<TronUnstakedBanner amount="100" chainId="tron:728126428" />);
renderBanner({ amount: '100', chainId: 'tron:728126428' });

expect(mockShowToast).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -41,19 +44,21 @@ const TronUnstakedBanner = ({ amount, chainId }: TronUnstakedBannerProps) => {
<>
<Text
testID={TronUnstakedBannerTestIds.BANNER_DESCRIPTION}
twClassName="pt-1 pb-4"
twClassName={showClaimButton ? 'pt-1 pb-4' : 'pt-1'}
>
{strings('stake.tron.unstaked_banner.description')}
</Text>
<Button
testID={TronUnstakedBannerTestIds.CLAIM_BUTTON}
variant={ButtonVariant.Primary}
size={ButtonSize.Md}
onPress={handleClaimUnstakedTrx}
isDisabled={isSubmitting}
>
{strings('stake.tron.unstaked_banner.button')}
</Button>
{showClaimButton ? (
<Button
testID={TronUnstakedBannerTestIds.CLAIM_BUTTON}
variant={ButtonVariant.Primary}
size={ButtonSize.Md}
onPress={handleClaimUnstakedTrx}
isDisabled={isSubmitting}
>
{strings('stake.tron.unstaked_banner.button')}
</Button>
) : null}
</>
}
/>
Expand Down
4 changes: 2 additions & 2 deletions app/components/UI/Perps/utils/wait.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 2 additions & 0 deletions app/constants/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum FeatureFlagNames {
tokenDetailsV2ButtonLayout = 'tokenDetailsV2ButtonLayout',
complianceEnabled = 'complianceEnabled',
legacyIosGoogleConfigEnabled = 'legacyIosGoogleConfigEnabled',
tronClaimUnstakedTrxButtonEnabled = 'tronClaimUnstakedTrxButtonEnabled',
}

export const DEFAULT_FEATURE_FLAG_VALUES: Partial<
Expand All @@ -24,4 +25,5 @@ export const DEFAULT_FEATURE_FLAG_VALUES: Partial<
[FeatureFlagNames.assetsDefiPositionsEnabled]: true,
[FeatureFlagNames.tokenDetailsV2Buttons]: false,
[FeatureFlagNames.tokenDetailsV2ButtonLayout]: false,
[FeatureFlagNames.tronClaimUnstakedTrxButtonEnabled]: false,
};
Original file line number Diff line number Diff line change
@@ -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
],
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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
],
),
);
1 change: 1 addition & 0 deletions tests/feature-flags/feature-flag-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe('Feature Flag Registry', () => {
expect(flagNames).toContain('bridgeConfigV2');
expect(flagNames).toContain('bitcoinAccounts');
expect(flagNames).toContain('tronAccounts');
expect(flagNames).toContain('tronClaimUnstakedTrxButtonEnabled');
});
});

Expand Down
10 changes: 9 additions & 1 deletion tests/feature-flags/feature-flag-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, FeatureFlagRegistryEntry> = {
Expand Down Expand Up @@ -3618,6 +3618,14 @@ export const FEATURE_FLAG_REGISTRY: Record<string, FeatureFlagRegistryEntry> = {
status: FeatureFlagStatus.Active,
},

tronClaimUnstakedTrxButtonEnabled: {
name: 'tronClaimUnstakedTrxButtonEnabled',
type: FeatureFlagType.Remote,
inProd: true,
productionDefault: false,
status: FeatureFlagStatus.Active,
},

tronStaking: {
name: 'tronStaking',
type: FeatureFlagType.Remote,
Expand Down
Loading