feat(native): implemented OutOfQuotaAlert & refactored alerts

This commit is contained in:
Matěj Husák
2026-02-11 16:21:55 +01:00
parent 0f876dedbd
commit 26cf0996ac
15 changed files with 147 additions and 71 deletions

View File

@@ -1,11 +1,9 @@
import { Translation } from '@suite/intl'; import { Translation } from '@suite/intl';
import { selectSelectedDevice } from '@suite-common/device';
import { import {
WithSuiteSyncQuotaManagerState,
noQuotaLeftWarningDismissed, noQuotaLeftWarningDismissed,
selectDeviceDismissedNoQuotaLeftWarning, selectShouldDisplayOutOfQuotaAlert,
selectLeftDeviceQuota,
} from '@suite-common/suite-sync-quota-manager'; } from '@suite-common/suite-sync-quota-manager';
import { selectSelectedDevice } from '@suite-common/wallet-core';
import { Banner, Button, IconButton } from '@trezor/components'; import { Banner, Button, IconButton } from '@trezor/components';
import { TREZOR_SUPPORT_URL } from '@trezor/urls'; import { TREZOR_SUPPORT_URL } from '@trezor/urls';
@@ -17,14 +15,9 @@ export const OutOfQuotaBanner = () => {
const href = useExternalLink(TREZOR_SUPPORT_URL); const href = useExternalLink(TREZOR_SUPPORT_URL);
const device = useSelector(selectSelectedDevice); const device = useSelector(selectSelectedDevice);
const alreadyDismissed = useSelector((state: WithSuiteSyncQuotaManagerState) => const shouldDisplay = useSelector(selectShouldDisplayOutOfQuotaAlert);
selectDeviceDismissedNoQuotaLeftWarning(state, device?.id || ''),
);
const quotaLeft = useSelector((state: WithSuiteSyncQuotaManagerState) =>
selectLeftDeviceQuota(state, device?.id || ''),
);
if (quotaLeft === undefined || quotaLeft > 0 || alreadyDismissed) return null; if (shouldDisplay === false) return null;
const handleDismiss = () => { const handleDismiss = () => {
if (!device || !device.id) return false; if (!device || !device.id) return false;

View File

@@ -36,6 +36,7 @@ export {
selectHasDeviceAllowance, selectHasDeviceAllowance,
selectLeftDeviceQuota, selectLeftDeviceQuota,
selectDeviceDismissedNoQuotaLeftWarning, selectDeviceDismissedNoQuotaLeftWarning,
selectShouldDisplayOutOfQuotaAlert,
} from './quotaManagerSelectors'; } from './quotaManagerSelectors';
export type { WithSuiteSyncQuotaManagerState } from './quotaManagerSelectors'; export type { WithSuiteSyncQuotaManagerState } from './quotaManagerSelectors';

View File

@@ -1,3 +1,5 @@
import { type DeviceRootState, selectDeviceId } from '@suite-common/device';
import { createWeakMapSelector } from '@suite-common/redux-utils';
import { WalletDescriptor } from '@suite-common/wallet-types'; import { WalletDescriptor } from '@suite-common/wallet-types';
import { SuiteSyncQuotaManagerState } from './quotaManagerReducer'; import { SuiteSyncQuotaManagerState } from './quotaManagerReducer';
@@ -6,6 +8,10 @@ export type WithSuiteSyncQuotaManagerState = {
suiteSyncQuotaManager: SuiteSyncQuotaManagerState; suiteSyncQuotaManager: SuiteSyncQuotaManagerState;
}; };
const createMemoizedSelector = createWeakMapSelector.withTypes<
DeviceRootState & WithSuiteSyncQuotaManagerState
>();
export const selectQuotaManagerBaseUrl = (state: WithSuiteSyncQuotaManagerState) => export const selectQuotaManagerBaseUrl = (state: WithSuiteSyncQuotaManagerState) =>
state.suiteSyncQuotaManager.baseUrl; state.suiteSyncQuotaManager.baseUrl;
@@ -28,14 +34,14 @@ export const selectOwnersAllowance = (state: WithSuiteSyncQuotaManagerState) =>
state.suiteSyncQuotaManager.ownersAllowance; state.suiteSyncQuotaManager.ownersAllowance;
export const selectLeftDeviceQuota = (state: WithSuiteSyncQuotaManagerState, deviceId: string) => export const selectLeftDeviceQuota = (state: WithSuiteSyncQuotaManagerState, deviceId: string) =>
state.suiteSyncQuotaManager.registeredDevices.find(di => di.deviceId === deviceId) state.suiteSyncQuotaManager.registeredDevices.find(d => d.deviceId === deviceId)
?.unspentStorageSize; ?.unspentStorageSize;
export const selectDeviceDismissedNoQuotaLeftWarning = ( export const selectDeviceDismissedNoQuotaLeftWarning = (
state: WithSuiteSyncQuotaManagerState, state: WithSuiteSyncQuotaManagerState,
deviceId: string, deviceId: string,
) => ) =>
state.suiteSyncQuotaManager.registeredDevices.find(di => di.deviceId === deviceId) state.suiteSyncQuotaManager.registeredDevices.find(d => d.deviceId === deviceId)
?.dismissedNoQuotaLeftWarning; ?.dismissedNoQuotaLeftWarning;
export const selectHasDeviceAllowance = ( export const selectHasDeviceAllowance = (
@@ -43,3 +49,13 @@ export const selectHasDeviceAllowance = (
deviceId: string, deviceId: string,
walletDescriptor: WalletDescriptor, walletDescriptor: WalletDescriptor,
) => selectIsDeviceRegistered(state, deviceId) && selectHasOwnerAllowance(state, walletDescriptor); ) => selectIsDeviceRegistered(state, deviceId) && selectHasOwnerAllowance(state, walletDescriptor);
export const selectShouldDisplayOutOfQuotaAlert = createMemoizedSelector(
[
(state: WithSuiteSyncQuotaManagerState & DeviceRootState) =>
selectLeftDeviceQuota(state, selectDeviceId(state) ?? ''),
(state: WithSuiteSyncQuotaManagerState & DeviceRootState) =>
selectDeviceDismissedNoQuotaLeftWarning(state, selectDeviceId(state) ?? ''),
],
(quotaLeft, alreadyDismissed) => quotaLeft === 0 && !alreadyDismissed,
);

View File

@@ -0,0 +1,9 @@
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { FullAlertBox, FullAlertBoxProps } from './FullAlertBox';
export const AnimatedFullAlertBox = (props: FullAlertBoxProps) => (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<FullAlertBox {...props} />
</Animated.View>
);

View File

@@ -20,8 +20,8 @@ const containerStyle = prepareNativeStyle<Pick<FullAlertStyles, 'backgroundColor
export type FullAlertBoxProps = { export type FullAlertBoxProps = {
title: React.ReactNode; title: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
primaryButtonLabel?: string; primaryButtonLabel?: string | React.ReactNode;
secondaryButtonLabel?: string; secondaryButtonLabel?: string | React.ReactNode;
onPressPrimaryButton?: () => void; onPressPrimaryButton?: () => void;
onPressSecondaryButton?: () => void; onPressSecondaryButton?: () => void;
primaryButtonProps?: Partial<ButtonProps>; primaryButtonProps?: Partial<ButtonProps>;

View File

@@ -69,6 +69,7 @@ export * from './utils';
export * from './PriceChangeBadge'; export * from './PriceChangeBadge';
export * from './resetLetterSpacingOnAndroidStyle'; export * from './resetLetterSpacingOnAndroidStyle';
export * from './FullAlertBox/FullAlertBox'; export * from './FullAlertBox/FullAlertBox';
export * from './FullAlertBox/AnimatedFullAlertBox';
export * from './CircularSpinner'; export * from './CircularSpinner';
export * from './CardStepper/CardStepper'; export * from './CardStepper/CardStepper';
export * from './Sheet/BottomSheetModal'; export * from './Sheet/BottomSheetModal';

View File

@@ -62,6 +62,13 @@ export const messages = {
cta: 'Create wallet backup', cta: 'Create wallet backup',
}, },
}, },
outOfSuiteSyncQuota: {
title: 'Suite Sync storage is full',
subtitle:
'New labels will be saved locally on this phone, but not synced to your other devices.',
cta: 'Contact support',
dismiss: 'Dismiss',
},
}, },
tokens: '+ Tokens', tokens: '+ Tokens',
warning: 'Warning', warning: 'Warning',

View File

@@ -19,6 +19,7 @@
"@suite-common/message-system": "workspace:*", "@suite-common/message-system": "workspace:*",
"@suite-common/redux-utils": "workspace:*", "@suite-common/redux-utils": "workspace:*",
"@suite-common/suite-sync": "workspace:*", "@suite-common/suite-sync": "workspace:*",
"@suite-common/suite-sync-quota-manager": "workspace:*",
"@suite-common/wallet-config": "workspace:*", "@suite-common/wallet-config": "workspace:*",
"@suite-common/wallet-core": "workspace:*", "@suite-common/wallet-core": "workspace:*",
"@suite-common/wallet-types": "workspace:*", "@suite-common/wallet-types": "workspace:*",
@@ -35,7 +36,6 @@
"@suite-native/discovery": "workspace:*", "@suite-native/discovery": "workspace:*",
"@suite-native/firmware": "workspace:*", "@suite-native/firmware": "workspace:*",
"@suite-native/graph": "workspace:*", "@suite-native/graph": "workspace:*",
"@suite-native/icons": "workspace:*",
"@suite-native/intl": "workspace:*", "@suite-native/intl": "workspace:*",
"@suite-native/link": "workspace:*", "@suite-native/link": "workspace:*",
"@suite-native/navigation": "workspace:*", "@suite-native/navigation": "workspace:*",

View File

@@ -1,12 +1,11 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { atom, useAtomValue, useSetAtom } from 'jotai'; import { atom, useAtomValue, useSetAtom } from 'jotai';
import { selectDeviceId, selectDeviceUpdateFirmwareVersion } from '@suite-common/device'; import { selectDeviceId, selectDeviceUpdateFirmwareVersion } from '@suite-common/device';
import { FullAlertBox } from '@suite-native/atoms'; import { AnimatedFullAlertBox } from '@suite-native/atoms';
import { Translation, useTranslate } from '@suite-native/intl'; import { Translation, useTranslate } from '@suite-native/intl';
import { import {
DeviceSettingsStackRoutes, DeviceSettingsStackRoutes,
@@ -59,22 +58,20 @@ export const FirmwareUpdateAlert = () => {
} }
return ( return (
<Animated.View entering={FadeIn} exiting={FadeOut}> <AnimatedFullAlertBox
<FullAlertBox title={<Translation id="moduleHome.firmwareUpdateAlert.title" />}
title={<Translation id="moduleHome.firmwareUpdateAlert.title" />} description={
description={ <Translation
<Translation id="moduleHome.firmwareUpdateAlert.version"
id="moduleHome.firmwareUpdateAlert.version" values={{ version: updateFirmwareVersion }}
values={{ version: updateFirmwareVersion }} />
/> }
} variant="info"
variant="info" secondaryButtonLabel={translate('moduleHome.firmwareUpdateAlert.button.close')}
secondaryButtonLabel={translate('moduleHome.firmwareUpdateAlert.button.close')} onPressSecondaryButton={handleClose}
onPressSecondaryButton={handleClose} primaryButtonLabel={translate('moduleHome.firmwareUpdateAlert.button.update')}
primaryButtonLabel={translate('moduleHome.firmwareUpdateAlert.button.update')} onPressPrimaryButton={handleUpdateFirmware}
onPressPrimaryButton={handleUpdateFirmware} marginHorizontal="sp16"
marginHorizontal="sp16" />
/>
</Animated.View>
); );
}; };

View File

@@ -0,0 +1,31 @@
import { useSelector } from 'react-redux';
import { selectShouldDisplayOutOfQuotaAlert } from '@suite-common/suite-sync-quota-manager';
import { SuiteSyncKeysAlert } from './SuiteSyncKeysAlert';
import {
selectShouldDisplaySuiteSyncAlert,
selectShouldDisplayUpgradeFirmwareAlert,
} from '../homescreenSelectors';
import { FirmwareUpdateAlert } from './FirmwareUpdateAlert';
import { OutOfQuotaAlert } from './OutOfQuotaAlert';
export const HomescreenAlerts = () => {
const shouldDisplayOutOfQuotaAlert = useSelector(selectShouldDisplayOutOfQuotaAlert);
const shouldDisplaySuiteSyncAlert = useSelector(selectShouldDisplaySuiteSyncAlert);
const shouldDisplayFirmwareUpdateAlert = useSelector(selectShouldDisplayUpgradeFirmwareAlert);
if (shouldDisplaySuiteSyncAlert) {
return <SuiteSyncKeysAlert />;
}
if (shouldDisplayFirmwareUpdateAlert) {
return <FirmwareUpdateAlert />;
}
if (shouldDisplayOutOfQuotaAlert) {
return <OutOfQuotaAlert />;
}
return null;
};

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectSelectedDevice } from '@suite-common/device';
import { noQuotaLeftWarningDismissed } from '@suite-common/suite-sync-quota-manager';
import { AnimatedFullAlertBox } from '@suite-native/atoms';
import { Translation, useTranslate } from '@suite-native/intl';
import { SUITE_MOBILE_SUPPORT_URL, useOpenLink } from '@suite-native/link';
export const OutOfQuotaAlert = () => {
const dispatch = useDispatch();
const openLink = useOpenLink();
const { translate } = useTranslate();
const device = useSelector(selectSelectedDevice);
if (!device?.id) return null;
const deviceId = device.id;
const handleDismiss = () => {
dispatch(noQuotaLeftWarningDismissed({ deviceId }));
};
const handleContactSupport = () => {
openLink(SUITE_MOBILE_SUPPORT_URL);
};
return (
<AnimatedFullAlertBox
marginHorizontal="sp16"
variant="info"
iconName="info"
title={<Translation id="generic.banners.outOfSuiteSyncQuota.title" />}
description={<Translation id="generic.banners.outOfSuiteSyncQuota.subtitle" />}
primaryButtonLabel={translate('generic.banners.outOfSuiteSyncQuota.cta')}
onPressPrimaryButton={handleContactSupport}
secondaryButtonLabel={translate('generic.banners.outOfSuiteSyncQuota.dismiss')}
onPressSecondaryButton={handleDismiss}
/>
);
};

View File

@@ -19,14 +19,9 @@ import {
StackNavigationProps, StackNavigationProps,
} from '@suite-native/navigation'; } from '@suite-native/navigation';
import { FirmwareUpdateAlert } from './FirmwareUpdateAlert'; import { HomescreenAlerts } from './HomescreenAlerts';
import { PortfolioGraph, PortfolioGraphRef } from './PortfolioGraph'; import { PortfolioGraph, PortfolioGraphRef } from './PortfolioGraph';
import { ReferralButton } from './ReferralButton'; import { ReferralButton } from './ReferralButton';
import { SuiteSyncKeysAlert } from './SuiteSyncKeysAlert';
import {
selectShouldDisplaySuiteSyncAlert,
selectShouldDisplayUpgradeFirmwareAlert,
} from '../homescreenSelectors';
export const PortfolioContent = forwardRef<PortfolioGraphRef>((_props, ref) => { export const PortfolioContent = forwardRef<PortfolioGraphRef>((_props, ref) => {
const navigation = useNavigation<StackNavigationProps<RootStackParamList, RootStackRoutes>>(); const navigation = useNavigation<StackNavigationProps<RootStackParamList, RootStackRoutes>>();
@@ -37,9 +32,6 @@ export const PortfolioContent = forwardRef<PortfolioGraphRef>((_props, ref) => {
selectHasFirmwareAuthenticityCheckHardFailed, selectHasFirmwareAuthenticityCheckHardFailed,
); );
const shouldDisplaySuiteSyncAlert = useSelector(selectShouldDisplaySuiteSyncAlert);
const shouldDisplayFirmwareUpdateAlert = useSelector(selectShouldDisplayUpgradeFirmwareAlert);
const isPortfolioTracker = useSelector(selectIsPortfolioTrackerDevice); const isPortfolioTracker = useSelector(selectIsPortfolioTrackerDevice);
const showTransferButtons = isDeviceAuthorized && !hasDiscovery; const showTransferButtons = isDeviceAuthorized && !hasDiscovery;
@@ -58,19 +50,9 @@ export const PortfolioContent = forwardRef<PortfolioGraphRef>((_props, ref) => {
}); });
}; };
const getHomescreenAlert = () => {
if (shouldDisplaySuiteSyncAlert) {
return <SuiteSyncKeysAlert />;
} else if (shouldDisplayFirmwareUpdateAlert) {
return <FirmwareUpdateAlert />;
} else {
return null;
}
};
return ( return (
<VStack spacing="sp32" marginTop="sp8"> <VStack spacing="sp32" marginTop="sp8">
{getHomescreenAlert()} <HomescreenAlerts />
<AnimatedVStack spacing="sp32" layout={LinearTransition}> <AnimatedVStack spacing="sp32" layout={LinearTransition}>
<PortfolioGraph ref={ref} /> <PortfolioGraph ref={ref} />
<VStack spacing="sp64" marginHorizontal="sp16"> <VStack spacing="sp64" marginHorizontal="sp16">

View File

@@ -1,12 +1,11 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { selectDeviceStaticSessionId, selectIsDeviceConnected } from '@suite-common/device'; import { selectDeviceStaticSessionId, selectIsDeviceConnected } from '@suite-common/device';
import { FullAlertBox } from '@suite-native/atoms'; import { AnimatedFullAlertBox } from '@suite-native/atoms';
import { useTranslate } from '@suite-native/intl'; import { Translation } from '@suite-native/intl';
import { import {
AuthorizeDeviceStackParamList, AuthorizeDeviceStackParamList,
AuthorizeDeviceStackRoutes, AuthorizeDeviceStackRoutes,
@@ -25,7 +24,6 @@ type NavigationProp = StackToStackCompositeNavigationProps<
>; >;
export const SuiteSyncKeysAlert = () => { export const SuiteSyncKeysAlert = () => {
const { translate } = useTranslate();
const { suiteSync } = useNativeServices(); const { suiteSync } = useNativeServices();
const isDeviceConnected = useSelector(selectIsDeviceConnected); const isDeviceConnected = useSelector(selectIsDeviceConnected);
@@ -52,15 +50,13 @@ export const SuiteSyncKeysAlert = () => {
if (!shouldDisplaySuiteSyncAlert) return null; if (!shouldDisplaySuiteSyncAlert) return null;
return ( return (
<Animated.View entering={FadeIn} exiting={FadeOut}> <AnimatedFullAlertBox
<FullAlertBox variant="info"
variant="info" title={<Translation id="moduleHome.suiteSyncAlert.title" />}
title={translate('moduleHome.suiteSyncAlert.title')} description={<Translation id="moduleHome.suiteSyncAlert.description" />}
description={translate('moduleHome.suiteSyncAlert.description')} primaryButtonLabel={<Translation id="moduleHome.suiteSyncAlert.button" />}
primaryButtonLabel={translate('moduleHome.suiteSyncAlert.button')} onPressPrimaryButton={allowSuiteSyncForWallet}
onPressPrimaryButton={allowSuiteSyncForWallet} marginHorizontal="sp16"
marginHorizontal="sp16" />
/>
</Animated.View>
); );
}; };

View File

@@ -13,6 +13,9 @@
{ {
"path": "../../suite-common/suite-sync" "path": "../../suite-common/suite-sync"
}, },
{
"path": "../../suite-common/suite-sync-quota-manager"
},
{ {
"path": "../../suite-common/wallet-config" "path": "../../suite-common/wallet-config"
}, },
@@ -35,7 +38,6 @@
{ "path": "../discovery" }, { "path": "../discovery" },
{ "path": "../firmware" }, { "path": "../firmware" },
{ "path": "../graph" }, { "path": "../graph" },
{ "path": "../icons" },
{ "path": "../intl" }, { "path": "../intl" },
{ "path": "../link" }, { "path": "../link" },
{ "path": "../navigation" }, { "path": "../navigation" },

View File

@@ -13327,6 +13327,7 @@ __metadata:
"@suite-common/message-system": "workspace:*" "@suite-common/message-system": "workspace:*"
"@suite-common/redux-utils": "workspace:*" "@suite-common/redux-utils": "workspace:*"
"@suite-common/suite-sync": "workspace:*" "@suite-common/suite-sync": "workspace:*"
"@suite-common/suite-sync-quota-manager": "workspace:*"
"@suite-common/wallet-config": "workspace:*" "@suite-common/wallet-config": "workspace:*"
"@suite-common/wallet-core": "workspace:*" "@suite-common/wallet-core": "workspace:*"
"@suite-common/wallet-types": "workspace:*" "@suite-common/wallet-types": "workspace:*"
@@ -13343,7 +13344,6 @@ __metadata:
"@suite-native/discovery": "workspace:*" "@suite-native/discovery": "workspace:*"
"@suite-native/firmware": "workspace:*" "@suite-native/firmware": "workspace:*"
"@suite-native/graph": "workspace:*" "@suite-native/graph": "workspace:*"
"@suite-native/icons": "workspace:*"
"@suite-native/intl": "workspace:*" "@suite-native/intl": "workspace:*"
"@suite-native/link": "workspace:*" "@suite-native/link": "workspace:*"
"@suite-native/navigation": "workspace:*" "@suite-native/navigation": "workspace:*"