Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4a518a4
fix: iOS-style top toast animation for DS legacy component
amandaye0h Jul 6, 2026
b3d1a09
fix: Refine Base Notification toast to match iOS style
amandaye0h Jul 6, 2026
d9aa059
prototype: Add Storybook stories to visualize DS and feature toasts
amandaye0h Jul 6, 2026
0836d9a
chore: add remaining stories
amandaye0h Jul 7, 2026
7a33ac1
fix (ui-ds-legacy): polish spacing, text, font weights, colors, bg
amandaye0h Jul 7, 2026
5a48322
fix (ui-base-notification): polish spacing, text, font weights, color…
amandaye0h Jul 7, 2026
1bff0e9
fix (ui-money): polish icon padding
amandaye0h Jul 7, 2026
8a4fcdb
fix(ui-quick-buy): polish icon spacing and spinner color
amandaye0h Jul 7, 2026
a0f25ea
fix(ui-earn): polish icon spacing, text style and spinner color
amandaye0h Jul 7, 2026
4df916a
fix(ui-rewards): polish icon spacing and related test
amandaye0h Jul 7, 2026
a7ec635
fix(ui-ramps): polish icon spacing and color
amandaye0h Jul 7, 2026
1d1d9c7
fix(ui-perps): polish icon, colors and spacing for toast
amandaye0h Jul 7, 2026
1f91e0b
fix(ui-perps-websocket): polish overally styling, icon sizing to matc…
amandaye0h Jul 7, 2026
3b3382c
fix(ui-predict): polish icon size, color styling for predict
amandaye0h Jul 7, 2026
b5cb64b
docs: update edge case story
amandaye0h Jul 7, 2026
a46bff3
docs: fix perps withdraw stories
amandaye0h Jul 7, 2026
a4ea274
fix(ui-predict-share): remove custom bottom offset and fix padding
amandaye0h Jul 7, 2026
6ad5082
fix(ui-perps-withdraw): polish spinner icon and bg color
amandaye0h Jul 7, 2026
45c8c7a
Merge origin/main into prototype/toast-stories
amandaye0h Jul 7, 2026
b2c144c
fix(ui-gas): polish text and buttonicon for gas toast
amandaye0h Jul 7, 2026
7f737c8
doc: update gas story message
amandaye0h Jul 7, 2026
dcd0a5b
chore(testflight): restore production app entry for toast QA build
amandaye0h Jul 7, 2026
ca35a4a
fix(ui-predict-error): adjust bg color for error
amandaye0h Jul 7, 2026
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
7 changes: 6 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -909,5 +909,10 @@ module.exports = {
'no-loss-of-precision': 'off',
},

ignorePatterns: ['wdio.conf.js', 'app/util/termsOfUse/termsOfUseContent.ts'],
ignorePatterns: [
'wdio.conf.js',
'app/util/termsOfUse/termsOfUseContent.ts',
'!.storybook',
'!.storybook/**',
],
};
14 changes: 13 additions & 1 deletion .storybook/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import React, { useEffect } from 'react';
import { getStorybookUI } from '@storybook/react-native';
import { hideAsync } from 'expo-splash-screen';

import './storybook.requires';

const StorybookUIRoot = getStorybookUI({
asyncStorage: null,
});

export { StorybookUIRoot as default };
function StorybookRoot() {
useEffect(() => {
hideAsync().catch(() => {
// Non-fatal — Storybook can still render if splash hide fails
});
}, []);

return <StorybookUIRoot />;
}

export { StorybookRoot as default };
250 changes: 130 additions & 120 deletions .storybook/storybook.requires.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useCallback, useState } from 'react';
import { ScrollView, View } from 'react-native';
import { Text, TextVariant } from '@metamask/design-system-react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';

import Button, { ButtonVariants } from '../../components/Buttons/Button';
import BaseNotification from './index';
import type {
BaseNotificationData,
BaseNotificationProps,
BaseNotificationStatus,
} from './BaseNotification.types';

interface NotificationTrigger {
label: string;
status: BaseNotificationStatus;
data?: BaseNotificationData;
autoDismiss?: boolean;
onHide?: () => void;
}

interface ActiveNotification extends NotificationTrigger {
key: number;
}

interface NotificationTriggerSection {
title: string;
triggers: NotificationTrigger[];
}

const STORYBOOK_PROPS: Pick<
BaseNotificationProps,
'persistUntilDismiss' | 'isVisible'
> = {
isVisible: true,
persistUntilDismiss: true,
};

const LOADING_TRIGGERS: NotificationTrigger[] = [
{ label: 'Pending', status: 'pending' },
{ label: 'Pending deposit', status: 'pending_deposit' },
{ label: 'Pending withdrawal', status: 'pending_withdrawal' },
{
label: 'Speedup',
status: 'speedup',
data: { nonce: '12' },
},
];

const SUCCESS_TRIGGERS: NotificationTrigger[] = [
{
label: 'Success',
status: 'success',
data: { nonce: '42' },
},
{ label: 'Success deposit', status: 'success_deposit' },
{ label: 'Success withdrawal', status: 'success_withdrawal' },
{
label: 'Received',
status: 'received',
data: { amount: '0.5', assetType: 'ETH' },
},
{ label: 'Received payment', status: 'received_payment' },
{ label: 'ETH received', status: 'eth_received' },
{ label: 'Import success', status: 'import_success' },
{ label: 'Simple notification', status: 'simple_notification' },
];

const FAILURE_TRIGGERS: NotificationTrigger[] = [
{ label: 'Error', status: 'error' },
{ label: 'Cancelled', status: 'cancelled' },
{
label: 'Simple notification rejected',
status: 'simple_notification_rejected',
},
];

const INTERACTION_TRIGGERS: NotificationTrigger[] = [
{
label: 'Custom copy',
status: 'simple_notification',
data: {
title: 'Transaction submitted',
description: 'Your swap is being processed on Ethereum Mainnet.',
},
},
{
label: 'With close button',
status: 'success',
autoDismiss: true,
data: {
title: 'Wallet ready',
description: 'Your wallet was imported successfully.',
},
onHide: () => {
// eslint-disable-next-line no-console
console.log('BaseNotification dismissed');
},
},
];

const STORY_SECTIONS: NotificationTriggerSection[] = [
{ title: 'Loading', triggers: LOADING_TRIGGERS },
{ title: 'Success', triggers: SUCCESS_TRIGGERS },
{ title: 'Failure', triggers: FAILURE_TRIGGERS },
{ title: 'Interaction', triggers: INTERACTION_TRIGGERS },
];

const StoryContainer = ({ children }: { children: React.ReactNode }) => {
const tw = useTailwind();

return (
<View style={tw.style('relative min-h-[320px] w-full bg-default')}>
{children}
</View>
);
};

const NotificationTriggerStory = ({
sections,
}: {
sections: NotificationTriggerSection[];
}) => {
const tw = useTailwind();
const [active, setActive] = useState<ActiveNotification | null>(null);

const showNotification = useCallback((trigger: NotificationTrigger) => {
setActive(null);
setTimeout(() => {
setActive({
key: Date.now(),
...trigger,
});
}, 50);
}, []);

return (
<StoryContainer>
<ScrollView
contentContainerStyle={tw.style('gap-6 p-4 pb-32')}
keyboardShouldPersistTaps="handled"
>
{sections.map((section) => (
<View key={section.title} style={tw.style('gap-2')}>
<Text variant={TextVariant.HeadingSm}>{section.title}</Text>
{section.triggers.map((trigger) => (
<Button
key={trigger.label}
variant={ButtonVariants.Secondary}
label={trigger.label}
onPress={() => showNotification(trigger)}
/>
))}
</View>
))}
</ScrollView>
{active && (
<BaseNotification
key={active.key}
{...STORYBOOK_PROPS}
status={active.status}
data={active.data}
autoDismiss={active.autoDismiss}
onHide={active.onHide}
onDismissComplete={() => setActive(null)}
/>
)}
</StoryContainer>
);
};

const BaseNotificationMeta = {
title: 'Components Temp / BaseNotification',
component: BaseNotification,
};

export default BaseNotificationMeta;

export const Default = {
render: () => <NotificationTriggerStory sections={STORY_SECTIONS} />,
};
Original file line number Diff line number Diff line change
@@ -1,39 +1,67 @@
import { StyleSheet } from 'react-native';
import { Theme } from '../../../util/theme/models';
import { Dimensions, StyleSheet } from 'react-native';
import { AppThemeKey, Theme } from '../../../util/theme/models';
import {
TOAST_CLOSE_MARGIN_TOP,
TOAST_DESCRIPTION_MARGIN_TOP,
TOAST_PADDING_HORIZONTAL,
TOAST_PADDING_RIGHT_WITH_CLOSE_ICON,
TOAST_PADDING_VERTICAL,
TOAST_ROW_GAP,
} from '../../components/Toast/Toast.constants';

const marginWidth = 16;
const notificationWidth = Dimensions.get('window').width - marginWidth * 2;

const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
const { colors } = theme;
const { colors, shadows } = theme;

return StyleSheet.create({
floatingBackground: {
backgroundColor: colors.background.section,
marginHorizontal: 16,
borderRadius: 8,
base: {
position: 'absolute',
top: 0,
left: marginWidth,
width: notificationWidth,
backgroundColor:
theme.themeAppearance === AppThemeKey.light
? colors.background.default
: colors.background.section,
...(theme.themeAppearance === AppThemeKey.light ? shadows.size.md : {}),
borderRadius: 16,
borderWidth: 1,
borderColor: colors.border.muted,
},
defaultFlashFloating: {
padding: 16,
paddingTop: TOAST_PADDING_VERTICAL,
paddingBottom: TOAST_PADDING_VERTICAL,
paddingLeft: TOAST_PADDING_HORIZONTAL,
paddingRight: TOAST_PADDING_HORIZONTAL,
flexDirection: 'row',
alignItems: 'flex-start',
gap: TOAST_ROW_GAP,
},
baseWithCloseIconButton: {
paddingRight: TOAST_PADDING_RIGHT_WITH_CLOSE_ICON,
},
pressableContent: {
flex: 1,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'flex-start',
gap: TOAST_ROW_GAP,
},
flashLabel: {
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
},
flashTitle: {
color: colors.text.default,
},
flashText: {
flex: 1,
lineHeight: 18,
marginTop: TOAST_DESCRIPTION_MARGIN_TOP,
},
flashTitle: {
flex: 1,
marginBottom: 2,
lineHeight: 18,
startAccessoryContainer: {
alignSelf: 'flex-start',
},
flashIcon: {
marginRight: 15,
closeButton: {
marginTop: TOAST_CLOSE_MARGIN_TOP,
},
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ export interface BaseNotificationProps {
onPress?: () => void;
onHide?: () => void;
autoDismiss?: boolean;
/** When false the notification is not rendered. Defaults to true. */
isVisible?: boolean;
/** Called after the exit spring animation completes. */
onDismissComplete?: () => void;
/** Auto-dismiss delay in ms. Defaults to Toast visibilityDuration (2750ms). */
dismissDuration?: number;
/** When true, the notification stays visible until manually dismissed. */
persistUntilDismiss?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import { act, fireEvent } from '@testing-library/react-native';
import BaseNotification, { getDescription } from './';
import renderWithProvider from '../../../util/test/renderWithProvider';
import { strings } from '../../../../locales/i18n';
Expand Down Expand Up @@ -85,18 +85,30 @@ describe('BaseNotification', () => {
expect(onPress).toHaveBeenCalledTimes(1);
});

it('renders the close affordance when autoDismiss is true', () => {
it('invokes onHide when the close affordance is pressed', async () => {
jest.useFakeTimers();
const onHide = jest.fn();
const { getByTestId } = renderWithProvider(
<BaseNotification
status="success"
data={defaultData}
autoDismiss
persistUntilDismiss
onHide={onHide}
/>,
);
fireEvent.press(getByTestId('base-notification-close'));

fireEvent(getByTestId('base-notification-container'), 'layout', {
nativeEvent: { layout: { height: 100, width: 300, x: 0, y: 0 } },
});

await act(async () => {
fireEvent.press(getByTestId('base-notification-close'));
jest.runAllTimers();
});

expect(onHide).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});

describe('EIP-7702 transactions (without nonce)', () => {
Expand Down
Loading
Loading