diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 577c1afce5c..e7293543910 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -114,6 +114,7 @@ import OnboardingSuccess from '../../Views/OnboardingSuccess'; import DefaultSettings from '../../Views/OnboardingSuccess/DefaultSettings'; import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal'; import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal'; +import ProfileSyncingModal from '../../UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal'; import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { SnapsExecutionWebView } from '../../../lib/snaps'; @@ -656,6 +657,10 @@ const App = ({ userLoggedIn }) => { name={Routes.SHEET.BASIC_FUNCTIONALITY} component={BasicFunctionalityModal} /> + + + + + +`; diff --git a/app/components/UI/Notification/List/index.test.tsx b/app/components/UI/Notification/List/index.test.tsx new file mode 100644 index 00000000000..895f1120775 --- /dev/null +++ b/app/components/UI/Notification/List/index.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import NotificationsList from './'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import MOCK_NOTIFICATIONS from '../__mocks__/mock_notifications'; +import { NavigationProp, ParamListBase } from '@react-navigation/native'; + +const navigationMock = { + navigate: jest.fn(), +} as unknown as NavigationProp; + +describe('NotificationsList', () => { + it('should render correctly', () => { + const { toJSON } = renderWithProvider( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Notification/List/index.tsx b/app/components/UI/Notification/List/index.tsx new file mode 100644 index 00000000000..6f58ae2a15e --- /dev/null +++ b/app/components/UI/Notification/List/index.tsx @@ -0,0 +1,239 @@ +import { NavigationProp, ParamListBase } from '@react-navigation/native'; +import React, { useCallback, useMemo } from 'react'; +import { ActivityIndicator, FlatList, FlatListProps, View } from 'react-native'; +import ScrollableTabView, { + DefaultTabBar, + DefaultTabBarProps, + TabBarProps, +} from 'react-native-scrollable-tab-view'; +import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/NotificationsView.selectors'; +import { strings } from '../../../../../locales/i18n'; +import { + hasNotificationComponents, + hasNotificationModal, + NotificationComponentState, +} from '../../../../util/notifications/notification-states'; +import Routes from '../../../../constants/navigation/Routes'; +import { MetaMetricsEvents } from '../../../../core/Analytics'; +import { Notification } from '../../../../util/notifications'; +import { useMarkNotificationAsRead } from '../../../../util/notifications/hooks/useNotifications'; +import { useMetrics } from '../../../hooks/useMetrics'; +import Empty from '../Empty'; +import { NotificationMenuItem } from '../NotificationMenuItem'; +import useStyles from './useStyles'; + +interface NotificationsListProps { + navigation: NavigationProp; + allNotifications: Notification[]; + walletNotifications: Notification[]; + web3Notifications: Notification[]; + loading: boolean; +} + +interface NotificationsListItemProps { + navigation: NavigationProp; + notification: Notification; +} +interface NotificationsListItemProps { + navigation: NavigationProp; + notification: Notification; +} + +function Loading() { + const { + theme: { colors }, + styles, + } = useStyles(); + + return ( + + + + ); +} + +function NotificationsListItem(props: NotificationsListItemProps) { + const { styles } = useStyles(); + const { markNotificationAsRead } = useMarkNotificationAsRead(); + + const onNotificationClick = useCallback( + (item: Notification) => { + markNotificationAsRead([ + { + id: item.id, + type: item.type, + isRead: item.isRead, + }, + ]); + if (hasNotificationModal(item.type)) { + props.navigation.navigate(Routes.NOTIFICATIONS.DETAILS, { + notification: item, + }); + } + }, + [markNotificationAsRead, props.navigation], + ); + + const menuItemState = useMemo(() => { + const notificationState = + NotificationComponentState[props.notification.type]; + return notificationState.createMenuItem(props.notification); + }, [props.notification]); + + if (!hasNotificationComponents(props.notification.type)) { + return null; + } + + return ( + onNotificationClick(props.notification)} + styles={styles} + simultaneousHandlers={undefined} + > + + + + ); +} + +function useNotificationListProps(props: { + navigation: NavigationProp; +}) { + const { styles } = useStyles(); + + const getListProps = useCallback( + (data: Notification[], tabLabel?: string) => { + const listProps: FlatListProps = { + keyExtractor: (item) => item.id, + data, + ListEmptyComponent: ( + + ), + contentContainerStyle: styles.list, + renderItem: ({ item }) => ( + + ), + initialNumToRender: 10, + maxToRenderPerBatch: 2, + onEndReachedThreshold: 0.5, + }; + + return { ...listProps, tabLabel: tabLabel ?? '' }; + }, + [props.navigation, styles.list], + ); + + return getListProps; +} + +function SingleNotificationList(props: NotificationsListProps) { + const getListProps = useNotificationListProps(props); + + return ; +} + +function TabbedNotificationList(props: NotificationsListProps) { + const { + theme: { colors }, + styles, + } = useStyles(); + const { trackEvent } = useMetrics(); + + const getListProps = useNotificationListProps(props); + + const onTabClick = useCallback( + (tabLabel: string) => { + switch (tabLabel) { + case strings('notifications.list.0'): + trackEvent(MetaMetricsEvents.ALL_NOTIFICATIONS); + break; + case strings('notifications.list.1'): + trackEvent(MetaMetricsEvents.WALLET_NOTIFICATIONS); + break; + case strings('notifications.list.2'): + trackEvent(MetaMetricsEvents.WEB3_NOTIFICATIONS); + break; + default: + break; + } + }, + [trackEvent], + ); + + return ( + ) => ( + + + + )} + onChangeTab={(val) => onTabClick(val.ref.props.tabLabel)} + > + {/* Tab 1 - All Notifications */} + + + {/* Tab 2 - Wallet Notifications */} + + + {/* Tab 3 - Web 3 Notifications */} + + + ); +} + +const Notifications = (props: NotificationsListProps) => { + const { styles } = useStyles(); + if (props.loading) { + return ( + + + + ); + } + + if (props.web3Notifications.length > 0) { + return ( + + + + ); + } + + return ( + + + + ); +}; + +export default Notifications; diff --git a/app/components/UI/Notification/List/styles.ts b/app/components/UI/Notification/List/styles.ts new file mode 100644 index 00000000000..53fb50f0cb8 --- /dev/null +++ b/app/components/UI/Notification/List/styles.ts @@ -0,0 +1,146 @@ +/* eslint-disable import/prefer-default-export */ +import { StyleSheet, TextStyle } from 'react-native'; +import type { Theme } from '@metamask/design-tokens'; + +export type NotificationListStyles = ReturnType; + +export const createStyles = ({ colors, typography }: Theme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + marginHorizontal: 8, + }, + wrapper: { + flex: 1, + paddingVertical: 10, + justifyContent: 'center', + borderRadius: 10, + }, + loaderContainer: { + position: 'absolute', + zIndex: 999, + width: '100%', + height: '100%', + }, + menuItemContainer: { + flexDirection: 'row', + gap: 16, + }, + + tabUnderlineStyle: { + height: 2, + backgroundColor: colors.primary.default, + }, + tabStyle: { + paddingBottom: 0, + paddingVertical: 8, + }, + tabBar: { + borderColor: colors.background.default, + }, + textStyle: { + ...(typography.sBodyMD as TextStyle), + fontWeight: '500', + }, + loader: { + backgroundColor: colors.background.default, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + TabWrapper: { + backgroundColor: colors.background.default, + flex: 1, + }, + list: { flexGrow: 1 }, + fox: { + width: 20, + height: 20, + }, + itemLogoSize: { + width: 32, + height: 32, + }, + containerFill: { flex: 1 }, + badgeWrapper: { + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'flex-start', + position: 'absolute', + top: '10%', + }, + circleLogo: { + width: 32, + height: 32, + borderRadius: 16, + overflow: 'hidden', + borderWidth: 0.5, + borderColor: colors.background.alternative, + }, + circleLogoPlaceholder: { + backgroundColor: colors.background.alternative, + width: 32, + height: 32, + borderRadius: 16, + borderWidth: 0.5, + borderColor: colors.background.alternative, + }, + squareLogo: { + width: 32, + height: 32, + borderRadius: 8, + overflow: 'hidden', + borderWidth: 0.5, + borderColor: colors.background.alternative, + }, + squareLogoPlaceholder: { + backgroundColor: colors.background.alternative, + width: 32, + height: 32, + borderRadius: 8, + borderWidth: 0.5, + borderColor: colors.background.alternative, + }, + rowInsider: { + flex: 1, + flexDirection: 'row', + gap: 8, + justifyContent: 'space-between', + }, + ethLogo: { + width: 32, + height: 32, + borderRadius: 16, + }, + foxWrapper: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: colors.background.alternative, + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'flex-start', + position: 'absolute', + top: '25%', + }, + button: { + marginTop: 16, + width: '100%', + alignSelf: 'center', + }, + trashIconContainer: { + position: 'absolute', + paddingHorizontal: 24, + flex: 1, + flexDirection: 'row', + backgroundColor: colors.background.hover, + justifyContent: 'flex-end', + alignItems: 'center', + overflow: 'hidden', + height: '100%', + right: 0, + left: 0, + zIndex: -1, + }, + }); diff --git a/app/components/UI/Notification/List/useStyles.ts b/app/components/UI/Notification/List/useStyles.ts new file mode 100644 index 00000000000..f230632a969 --- /dev/null +++ b/app/components/UI/Notification/List/useStyles.ts @@ -0,0 +1,9 @@ +import { useMemo } from 'react'; +import { createStyles } from './styles'; +import { useTheme } from '../../../../util/theme'; + +export default function useStyles() { + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + return { theme, styles }; +} diff --git a/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx new file mode 100644 index 00000000000..1a65102a1ad --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; + +import NotificationContent from './Content'; + +describe('NotificationContent', () => { + const title = 'Welcome to the new Test!'; + const createdAt = '2024-04-26T16:35:03.147606Z'; + const description = { + start: + 'We are excited to announce the launch of our brand new website and app!', + end: 'Ethereum', + }; + + it('renders correctly', () => { + const { toJSON } = renderWithProvider( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders title and 1 part of description', () => { + const titleWithTo = 'Sent 0.01 ETH to 0x10000'; + const { getByText } = renderWithProvider( + , + ); + + expect(getByText(titleWithTo)).toBeTruthy(); + }); +}); diff --git a/app/components/UI/Notification/NotificationMenuItem/Content.tsx b/app/components/UI/Notification/NotificationMenuItem/Content.tsx new file mode 100644 index 00000000000..65cb0882e78 --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/Content.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { View } from 'react-native'; +import Text, { + TextColor, + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import { NotificationMenuItem } from '../../../../util/notifications/notification-states/types/NotificationMenuItem'; +import useStyles from '../List/useStyles'; +import { formatMenuItemDate } from '../../../../util/notifications/methods'; + +type NotificationContentProps = Pick< + NotificationMenuItem, + 'title' | 'description' | 'createdAt' +>; + +function NotificationContent(props: NotificationContentProps) { + const { styles } = useStyles(); + + return ( + + {/* Section 1 - Title + Timestamp */} + + + {props.title} + + + {formatMenuItemDate(new Date(props.createdAt))} + + + {/* Section 2 - Left Desc + Right Desc */} + + + {props.description.start} + + {props.description.end && ( + {props.description.end} + )} + + + ); +} + +export default NotificationContent; diff --git a/app/components/UI/Notification/NotificationMenuItem/Icon.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Icon.test.tsx new file mode 100644 index 00000000000..bc8bb54b981 --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/Icon.test.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Linking } from 'react-native'; + +import renderWithProvider from '../../../../util/test/renderWithProvider'; + +import NotificationIcon from './Icon'; +import { IconName } from '../../../../component-library/components/Icons/Icon'; +import initialBackgroundState from '../../../../util/test/initial-background-state.json'; + +import SVG_ETH_LOGO_PATH from '../../../../component-library/components/Icons/Icon/assets/ethereum.svg'; + +Linking.openURL = jest.fn(() => Promise.resolve('opened https://metamask.io!')); + +const mockInitialState = { + engine: { + backgroundState: { + ...initialBackgroundState, + }, + }, +}; + +describe('NotificationIcon', () => { + const walletNotification = { + badgeIcon: IconName.Send2, + imageUrl: SVG_ETH_LOGO_PATH, + }; + + it('matches snapshot when icon is provided', () => { + const { toJSON } = renderWithProvider( + , + { state: mockInitialState }, + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Notification/NotificationMenuItem/Icon.tsx b/app/components/UI/Notification/NotificationMenuItem/Icon.tsx new file mode 100644 index 00000000000..a18b019673d --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/Icon.tsx @@ -0,0 +1,67 @@ +import { NotificationMenuItem } from '../../../../util/notifications/notification-states/types/NotificationMenuItem'; +import React, { useMemo } from 'react'; +import useStyles from '../List/useStyles'; +import BadgeWrapper from '../../../../component-library/components/Badges/BadgeWrapper'; +import Badge, { + BadgeVariant, +} from '../../../../component-library/components/Badges/Badge'; +import { BOTTOM_BADGEWRAPPER_BADGEPOSITION } from '../../../../component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants'; +import RemoteImage from '../../../../components/Base/RemoteImage'; +import METAMASK_FOX from '../../../../images/fox.png'; +import { View } from 'react-native'; + +type NotificationIconProps = Pick; + +function MenuIcon(props: NotificationIconProps) { + const { styles } = useStyles(); + + const menuIconStyles = { + style: + props.image?.variant === 'square' ? styles.squareLogo : styles.circleLogo, + placeholderStyle: + props.image?.variant === 'square' + ? styles.squareLogoPlaceholder + : styles.circleLogoPlaceholder, + }; + + const source = useMemo(() => { + if (!props.image?.url) { + return METAMASK_FOX; + } + if (typeof props.image.url === 'string') { + return { uri: props.image.url }; + } + return props.image.url; + }, [props.image?.url]); + + return ( + + ); +} + +function NotificationIcon(props: NotificationIconProps) { + const { styles } = useStyles(); + + return ( + + + } + style={styles.badgeWrapper} + > + + + + ); +} + +export default NotificationIcon; diff --git a/app/components/UI/Notification/NotificationMenuItem/Root.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Root.test.tsx new file mode 100644 index 00000000000..38783f74ede --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/Root.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; + +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import { useSharedValue, runOnJS, withTiming } from 'react-native-reanimated'; + +import NotificationRoot from './Root'; + +const children = <>; +const styles = { + wrapper: {}, + trashIconContainer: {}, +}; +const handleOnPress = jest.fn(); +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + useSharedValue: jest.fn().mockImplementation((init) => ({ + value: init, + withTiming: jest.fn((toValue, _, callback) => ({ toValue, callback })), + })), + runOnJS: jest.fn((callback) => callback()), +})); + +describe('NotificationRoot', () => { + const SWIPE_THRESHOLD = -100; + const SCREEN_WIDTH = 300; // Assume some screen width + + // Mock callbacks + const onDismiss = jest.fn(); + + // Shared values setup + const transX = useSharedValue(0); + const itemHeight = useSharedValue(100); // Assume initial height + const paddingVertical = useSharedValue(20); // Assume initial padding + const opacity = useSharedValue(1); // Assume full opacity + const onActive = (event: PanGestureHandlerGestureEvent) => { + const isSwipingLeft = event.translationX > 0; + + if (isSwipingLeft) { + transX.value = 0; + return; + } + + transX.value = event.translationX; + }; + const onEnd = () => { + const isDismissed = transX.value < SWIPE_THRESHOLD; + if (isDismissed) { + transX.value = withTiming(-SCREEN_WIDTH); + itemHeight.value = withTiming(0); + paddingVertical.value = withTiming(0); + opacity.value = withTiming(0, undefined, (isFinished: boolean) => { + if (isFinished && onDismiss) { + runOnJS(onDismiss); + } + }); + } else { + transX.value = withTiming(0); + } + }; + + it('matches snapshot', () => { + const { toJSON } = renderWithProvider( + + {children} + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should dismiss the item if transX.value is less than SWIPE_THRESHOLD', () => { + transX.value = -150; + onEnd(); + expect(transX.value).toBe(-SCREEN_WIDTH); + expect(itemHeight.value).toBe(0); + expect(paddingVertical.value).toBe(0); + expect(opacity.value).toBe(0); + expect(onDismiss).toHaveBeenCalled(); + }); + + it('should set transX.value to 0 if swiping left', () => { + const event = { + translationX: 10, + } as unknown as PanGestureHandlerGestureEvent; + onActive(event); + expect(transX.value).toBe(0); + }); + + it('should set transX.value to event.translationX if not swiping left', () => { + const event = { + translationX: -10, + } as unknown as PanGestureHandlerGestureEvent; + onActive(event); + expect(transX.value).toBe(-10); + }); +}); diff --git a/app/components/UI/Notification/NotificationMenuItem/Root.tsx b/app/components/UI/Notification/NotificationMenuItem/Root.tsx new file mode 100644 index 00000000000..a706da80884 --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/Root.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { Dimensions, TouchableOpacity } from 'react-native'; +import { + PanGestureHandler, + PanGestureHandlerGestureEvent, + PanGestureHandlerProps, +} from 'react-native-gesture-handler'; +import Animated, { + runOnJS, + useAnimatedGestureHandler, + useSharedValue, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated'; +import Icon, { + IconColor, + IconName, + IconSize, +} from '../../../../component-library/components/Icons/Icon'; +import { NotificationListStyles } from '../List/styles'; + +interface NotificationRootProps + extends Pick { + children: React.ReactNode; + styles: NotificationListStyles; + handleOnPress: () => void; + onDismiss?: () => void; +} + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const DELETE_BUTTON_WIDTH = -SCREEN_WIDTH * 0.3; +const SWIPE_THRESHOLD = DELETE_BUTTON_WIDTH; + +function NotificationRoot({ + children, + handleOnPress, + styles, + onDismiss, + simultaneousHandlers, +}: NotificationRootProps) { + const transX = useSharedValue(0); + const itemHeight = useSharedValue(); + const paddingVertical = useSharedValue(10); + const opacity = useSharedValue(1); + + const panGesture = useAnimatedGestureHandler({ + onActive: (event: PanGestureHandlerGestureEvent) => { + const isSwipingLeft = event.translationX > 0; + + if (isSwipingLeft) { + transX.value = 0; + return; + } + + transX.value = event.translationX; + }, + onEnd: () => { + const isDismissed = transX.value < SWIPE_THRESHOLD; + if (isDismissed) { + transX.value = withTiming(-SCREEN_WIDTH); + itemHeight.value = withTiming(0); + paddingVertical.value = withTiming(0); + opacity.value = withTiming(0, undefined, (isFinished: boolean) => { + if (isFinished && onDismiss) { + runOnJS(onDismiss); + } + }); + } else { + transX.value = withTiming(0); + } + }, + }); + + const rChildrenStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: transX.value }], + ...styles.container, + })); + + const rIconStyle = useAnimatedStyle(() => { + const opct = withTiming(transX.value < SWIPE_THRESHOLD ? 1 : 0, { + duration: 300, + }); + + return { opacity: opct }; + }); + + const rContainerStyle = useAnimatedStyle(() => ({ + height: itemHeight.value, + paddingVertical: paddingVertical.value, + opacity: opacity.value, + })); + + return ( + + + + + {children} + + + + + + + + ); +} + +export default NotificationRoot; diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap new file mode 100644 index 00000000000..9e11e42af79 --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotificationContent renders correctly 1`] = ` + + + + Welcome to the new Test! + + + Apr 26 + + + + + We are excited to announce the launch of our brand new website and app! + + + Ethereum + + + +`; diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Icon.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Icon.test.tsx.snap new file mode 100644 index 00000000000..4f3b4715133 --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Icon.test.tsx.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotificationIcon matches snapshot when icon is provided 1`] = ` + + + + + + + + + + + + +`; diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Root.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Root.test.tsx.snap new file mode 100644 index 00000000000..d055b2e865b --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Root.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotificationRoot matches snapshot 1`] = ` + + + + + + + + +`; diff --git a/app/components/UI/Notification/NotificationMenuItem/index.tsx b/app/components/UI/Notification/NotificationMenuItem/index.tsx new file mode 100644 index 00000000000..99596dc7970 --- /dev/null +++ b/app/components/UI/Notification/NotificationMenuItem/index.tsx @@ -0,0 +1,10 @@ +/* eslint-disable import/prefer-default-export */ +import NotificationRoot from './Root'; +import NotificationIcon from './Icon'; +import NotificationContent from './Content'; + +export const NotificationMenuItem = { + Root: NotificationRoot, + Icon: NotificationIcon, + Content: NotificationContent, +}; diff --git a/app/components/UI/Notification/SwitchLoadingModal/Loader.tsx b/app/components/UI/Notification/SwitchLoadingModal/Loader.tsx new file mode 100644 index 00000000000..6b4a984b2fa --- /dev/null +++ b/app/components/UI/Notification/SwitchLoadingModal/Loader.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import PropTypes from 'prop-types'; +import Device from '../../../../util/device'; +import { useTheme } from '../../../../util/theme'; +import Text, { + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import Icon, { + IconSize, + IconName, + IconColor, +} from '../../../../component-library/components/Icons/Icon'; + +import Spinner from '../../AnimatedSpinner'; +import Button, { + ButtonSize, + ButtonVariants, + ButtonWidthTypes, +} from '../../../../component-library/components/Buttons/Button'; +import { strings } from '../../../../../locales/i18n'; + +const createStyles = (colors) => + StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + borderColor: colors.border.default, + borderWidth: 2, + borderRadius: 16, + paddingBottom: Device.isIphoneX() ? 24 : 18, + minHeight: 120, + margin: 16, + }, + spinnerWrapper: { + alignItems: 'center', + marginVertical: 12, + }, + text: { + lineHeight: 20, + paddingHorizontal: 24, + marginVertical: 12, + width: '100%', + }, + button: { + alignSelf: 'center', + }, + }); + +const Loader = ({ + loadingText, + onDismiss, + errorText, +}: { + loadingText: string; + onDismiss: () => void; + errorText?: string; +}) => { + const { colors } = useTheme(); + const styles = createStyles(colors); + + return ( + + + {!errorText ? ( + + ) : ( + + )} + + + {errorText || loadingText} + + {!!errorText && ( +