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
49 changes: 49 additions & 0 deletions .yarn/patches/react-native-ble-plx-npm-3.4.0-401e8b3343.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
diff --git a/android/src/main/java/com/bleplx/utils/SafePromise.java b/android/src/main/java/com/bleplx/utils/SafePromise.java
index 0203473c88dacf979340c324829871af83cf6d02..35c475e3c33cc882b1322ce6a9d811e6522269d2 100644
--- a/android/src/main/java/com/bleplx/utils/SafePromise.java
+++ b/android/src/main/java/com/bleplx/utils/SafePromise.java
@@ -7,6 +7,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;

public class SafePromise {
+ /**
+ * Fallback non-null reject code used when callers (e.g. BlePlxModule) pass
+ * {@code null} as the code argument. React Native's PromiseImpl.reject is
+ * annotated @NonNull on its code parameter; passing null triggers a
+ * NullPointerException ("Parameter specified as non-null is null") on the
+ * native modules thread, which crashes the host app. Substituting a stable
+ * non-null string preserves the existing error-message payload (already
+ * carried in the second argument) while keeping the reject path safe.
+ */
+ private static final String DEFAULT_REJECT_CODE = "BleError";
+
+ private static String safeCode(String code) {
+ return code != null ? code : DEFAULT_REJECT_CODE;
+ }
+
private Promise promise;
private AtomicBoolean isFinished = new AtomicBoolean();

@@ -22,19 +37,19 @@ public class SafePromise {

public void reject(String code, String message) {
if (isFinished.compareAndSet(false, true)) {
- promise.reject(code, message);
+ promise.reject(safeCode(code), message);
}
}

public void reject(String code, Throwable e) {
if (isFinished.compareAndSet(false, true)) {
- promise.reject(code, e);
+ promise.reject(safeCode(code), e);
}
}

public void reject(String code, String message, Throwable e) {
if (isFinished.compareAndSet(false, true)) {
- promise.reject(code, message, e);
+ promise.reject(safeCode(code), message, e);
}
}

37 changes: 36 additions & 1 deletion app/components/UI/Money/hooks/useMoneyTransactionStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ describe('useMoneyTransactionStatus', () => {

it.each([
['dropped', TransactionStatus.dropped],
['rejected', TransactionStatus.rejected],
['cancelled', TransactionStatus.cancelled],
])('statusUpdated with %s → deposit failed toast', (_label, status) => {
const { statusUpdatedHandler } = renderAndGetHandlers();
Expand All @@ -311,6 +310,42 @@ describe('useMoneyTransactionStatus', () => {

expect(depositFailedFn).toHaveBeenCalledTimes(1);
});

it('rejected → no toast (user backed out of confirmation)', () => {
const { statusUpdatedHandler } = renderAndGetHandlers();

statusUpdatedHandler({
transactionMeta: buildTxMeta({
type: TransactionType.moneyAccountDeposit,
status: TransactionStatus.rejected,
}),
});

jest.advanceTimersByTime(IN_PROGRESS_DELAY_MS);

expect(depositFailedFn).not.toHaveBeenCalled();
expect(depositInProgressFn).not.toHaveBeenCalled();
expect(mockShowToast).not.toHaveBeenCalled();
});

it('approved → rejected before delay → no toast and pending in-progress cleared', () => {
const { statusUpdatedHandler } = renderAndGetHandlers();

const tx = buildTxMeta({
type: TransactionType.moneyAccountDeposit,
status: TransactionStatus.approved,
});
statusUpdatedHandler({ transactionMeta: tx });
jest.advanceTimersByTime(IN_PROGRESS_DELAY_MS - 1);
statusUpdatedHandler({
transactionMeta: { ...tx, status: TransactionStatus.rejected },
});
jest.advanceTimersByTime(IN_PROGRESS_DELAY_MS);

expect(depositInProgressFn).not.toHaveBeenCalled();
expect(depositFailedFn).not.toHaveBeenCalled();
expect(mockShowToast).not.toHaveBeenCalled();
});
});

describe('withdraw lifecycle', () => {
Expand Down
4 changes: 3 additions & 1 deletion app/components/UI/Money/hooks/useMoneyTransactionStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,12 @@ export const useMoneyTransactionStatus = () => {
break;
case TransactionStatus.failed:
case TransactionStatus.dropped:
case TransactionStatus.rejected:
case TransactionStatus.cancelled:
showFailedFor(transactionMeta);
break;
case TransactionStatus.rejected:
cancelPendingInProgress(transactionMeta.id);
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { StyleSheet } from 'react-native';

const styleSheet = () =>
StyleSheet.create({
heading: {
marginTop: 16,
},
desc: {
marginTop: 8,
},
accessory: {
marginTop: 16,
},
});

export default styleSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react-native';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import NotificationsDeveloperOptionsSection from './NotificationsDeveloperOptionsSection';
import { resetPushPrePromptShown } from '../../../../util/notifications/constants/notification-storage-keys';

jest.mock(
'../../../../util/notifications/constants/notification-storage-keys',
() => ({
resetPushPrePromptShown: jest.fn().mockResolvedValue(undefined),
}),
);

const mockResetPushPrePromptShown = jest.mocked(resetPushPrePromptShown);

describe('NotificationsDeveloperOptionsSection', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('resets the push pre-prompt state when the reset button is pressed', async () => {
const { getByText } = renderWithProvider(
<NotificationsDeveloperOptionsSection />,
);

fireEvent.press(getByText('Reset push pre-prompt'));

await waitFor(() => {
expect(mockResetPushPrePromptShown).toHaveBeenCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useCallback } from 'react';
import {
Button,
ButtonVariant,
ButtonSize,
Text,
TextVariant,
TextColor,
} from '@metamask/design-system-react-native';
import { strings } from '../../../../../locales/i18n';
import { useStyles } from '../../../../component-library/hooks';
import styleSheet from './NotificationsDeveloperOptionsSection.styles';
import { resetPushPrePromptShown } from '../../../../util/notifications/constants/notification-storage-keys';

export default function NotificationsDeveloperOptionsSection() {
const { styles } = useStyles(styleSheet);

const onResetPrompt = useCallback(async () => {
await resetPushPrePromptShown();
}, []);

return (
<>
<Text
color={TextColor.TextDefault}
variant={TextVariant.HeadingLg}
style={styles.heading}
>
{strings('app_settings.developer_options.notifications.title')}
</Text>
<Text
color={TextColor.TextAlternative}
variant={TextVariant.BodyMd}
style={styles.desc}
>
{strings('app_settings.developer_options.notifications.description')}
</Text>
<Button
variant={ButtonVariant.Secondary}
size={ButtonSize.Lg}
onPress={onResetPrompt}
isFullWidth
style={styles.accessory}
>
{strings('app_settings.developer_options.notifications.reset_prompt')}
</Button>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ jest.mock('../../utils/formatUtils', () => ({
formatPerpsFiat: jest.fn((value) => `$${value.toFixed(2)}`),
formatPositionSize: jest.fn((value) => value.toFixed(4)),
formatOrderCardDate: jest.fn(() => 'Nov 25, 2025'),
PRICE_RANGES_UNIVERSAL: [
{ min: 0, max: Infinity, decimals: 2, threshold: 0.000001 },
],
}));

// Mock component-library Button to be testable
Expand Down Expand Up @@ -280,6 +283,24 @@ describe('PerpsOrderDetailsView', () => {
expect(screen.getByText('perps.order_details.price')).toBeOnTheScreen();
});

it('formats order price with adaptive sig-dig ranges', () => {
// Arrange
const { formatPerpsFiat: mockFormatPerpsFiat } = jest.requireMock(
'../../utils/formatUtils',
) as { formatPerpsFiat: jest.Mock };

// Act
render(<PerpsOrderDetailsView />);

// Assert — the price call (50000) must pass a ranges option
const priceCall = mockFormatPerpsFiat.mock.calls.find(
(call: unknown[]) => call[0] === 50000,
);
expect(priceCall).toBeDefined();
expect(priceCall[1]).toBeDefined();
expect(priceCall[1].ranges).toBeDefined();
});

it('renders original size, order value and reduce-only rows', () => {
render(<PerpsOrderDetailsView />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
formatPerpsFiat,
formatPositionSize,
formatOrderCardDate,
PRICE_RANGES_UNIVERSAL,
} from '../../utils/formatUtils';
import {
formatOrderLabel,
Expand Down Expand Up @@ -131,11 +132,15 @@ const PerpsOrderDetailsView: React.FC = () => {
const priceText =
isMarketExecution || validOrderPrice === null
? strings('perps.order_details.market')
: formatPerpsFiat(validOrderPrice);
: formatPerpsFiat(validOrderPrice, {
ranges: PRICE_RANGES_UNIVERSAL,
});

let triggerCondition: string | undefined;
if (order.isTrigger && validTriggerPrice !== null) {
const formattedTriggerPrice = formatPerpsFiat(validTriggerPrice);
const formattedTriggerPrice = formatPerpsFiat(validTriggerPrice, {
ranges: PRICE_RANGES_UNIVERSAL,
});
const conditionKey = inferTriggerConditionKey({
detailedOrderType: order.detailedOrderType,
side: order.side,
Expand Down Expand Up @@ -175,10 +180,14 @@ const PerpsOrderDetailsView: React.FC = () => {
? strings('perps.order_details.yes')
: strings('perps.order_details.no'),
takeProfitPriceText: hasTakeProfitPrice
? formatPerpsFiat(parsedTakeProfitPrice)
? formatPerpsFiat(parsedTakeProfitPrice, {
ranges: PRICE_RANGES_UNIVERSAL,
})
: undefined,
stopLossPriceText: hasStopLossPrice
? formatPerpsFiat(parsedStopLossPrice)
? formatPerpsFiat(parsedStopLossPrice, {
ranges: PRICE_RANGES_UNIVERSAL,
})
: undefined,
};
}, [order, priceMetrics]);
Expand Down
18 changes: 18 additions & 0 deletions app/components/UI/Perps/components/PerpsCard/PerpsCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,24 @@ describe('PerpsCard', () => {
expect(getByText('Limit short')).toBeDefined();
});

it('formats low-price order with adaptive sig-dig instead of threshold', () => {
// Arrange
const lowPriceOrder = {
...mockOrder,
symbol: 'PUMP',
price: '0.001',
};

// Act
const { getByText, queryByText } = render(
<PerpsCard order={lowPriceOrder} testID="test-card" />,
);

// Assert — must show actual price, not <$0.01
expect(getByText('$0.001')).toBeOnTheScreen();
expect(queryByText('<$0.01')).toBeNull();
});

it('uses trigger price label for trigger orders', () => {
const triggerOrder = {
...mockOrder,
Expand Down
3 changes: 2 additions & 1 deletion app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
formatPnl,
formatPercentage,
PRICE_RANGES_MINIMAL_VIEW,
PRICE_RANGES_UNIVERSAL,
} from '../../utils/formatUtils';
import {
formatOrderLabel,
Expand Down Expand Up @@ -77,7 +78,7 @@ const getOrderDisplayData = (order: Order): CardDisplayData => {
const valueText =
priceValue !== null
? formatPerpsFiat(priceValue, {
ranges: PRICE_RANGES_MINIMAL_VIEW,
ranges: PRICE_RANGES_UNIVERSAL,
})
: strings('perps.order.market');
const labelText = strings(labelKey);
Expand Down
2 changes: 2 additions & 0 deletions app/components/Views/Settings/DeveloperOptions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { MusdDeveloperOptionsSection } from '../../../UI/Earn/components/MusdDev
import { CardDeveloperOptionsSection } from '../../../UI/Card/components/CardDeveloperOptionsSection';
import { selectMoneyHomeScreenEnabledFlag } from '../../../UI/Money/selectors/featureFlags';
import { MoneyUiDeveloperOptionsSection } from '../../../UI/Money/components/MoneyUiDeveloperOptionsSection';
import NotificationsDeveloperOptionsSection from '../../../UI/Notification/DeveloperOptionsSection/NotificationsDeveloperOptionsSection';

const DeveloperOptions = () => {
const navigation = useNavigation();
Expand Down Expand Up @@ -71,6 +72,7 @@ const DeveloperOptions = () => {
{isMoneyHomeEnabled && <MoneyUiDeveloperOptionsSection />}
<CardDeveloperOptionsSection />
<IdentityDeveloperOptionsSection />
<NotificationsDeveloperOptionsSection />
<HapticsDeveloperOptionsSection />
</ScrollView>
);
Expand Down
Loading
Loading