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

View File

@@ -36,6 +36,7 @@ export {
selectHasDeviceAllowance,
selectLeftDeviceQuota,
selectDeviceDismissedNoQuotaLeftWarning,
selectShouldDisplayOutOfQuotaAlert,
} 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 { SuiteSyncQuotaManagerState } from './quotaManagerReducer';
@@ -6,6 +8,10 @@ export type WithSuiteSyncQuotaManagerState = {
suiteSyncQuotaManager: SuiteSyncQuotaManagerState;
};
const createMemoizedSelector = createWeakMapSelector.withTypes<
DeviceRootState & WithSuiteSyncQuotaManagerState
>();
export const selectQuotaManagerBaseUrl = (state: WithSuiteSyncQuotaManagerState) =>
state.suiteSyncQuotaManager.baseUrl;
@@ -28,14 +34,14 @@ export const selectOwnersAllowance = (state: WithSuiteSyncQuotaManagerState) =>
state.suiteSyncQuotaManager.ownersAllowance;
export const selectLeftDeviceQuota = (state: WithSuiteSyncQuotaManagerState, deviceId: string) =>
state.suiteSyncQuotaManager.registeredDevices.find(di => di.deviceId === deviceId)
state.suiteSyncQuotaManager.registeredDevices.find(d => d.deviceId === deviceId)
?.unspentStorageSize;
export const selectDeviceDismissedNoQuotaLeftWarning = (
state: WithSuiteSyncQuotaManagerState,
deviceId: string,
) =>
state.suiteSyncQuotaManager.registeredDevices.find(di => di.deviceId === deviceId)
state.suiteSyncQuotaManager.registeredDevices.find(d => d.deviceId === deviceId)
?.dismissedNoQuotaLeftWarning;
export const selectHasDeviceAllowance = (
@@ -43,3 +49,13 @@ export const selectHasDeviceAllowance = (
deviceId: string,
walletDescriptor: 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 = {
title: React.ReactNode;
description?: React.ReactNode;
primaryButtonLabel?: string;
secondaryButtonLabel?: string;
primaryButtonLabel?: string | React.ReactNode;
secondaryButtonLabel?: string | React.ReactNode;
onPressPrimaryButton?: () => void;
onPressSecondaryButton?: () => void;
primaryButtonProps?: Partial<ButtonProps>;

View File

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

View File

@@ -62,6 +62,13 @@ export const messages = {
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',
warning: 'Warning',

View File

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

View File

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

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,
} from '@suite-native/navigation';
import { FirmwareUpdateAlert } from './FirmwareUpdateAlert';
import { HomescreenAlerts } from './HomescreenAlerts';
import { PortfolioGraph, PortfolioGraphRef } from './PortfolioGraph';
import { ReferralButton } from './ReferralButton';
import { SuiteSyncKeysAlert } from './SuiteSyncKeysAlert';
import {
selectShouldDisplaySuiteSyncAlert,
selectShouldDisplayUpgradeFirmwareAlert,
} from '../homescreenSelectors';
export const PortfolioContent = forwardRef<PortfolioGraphRef>((_props, ref) => {
const navigation = useNavigation<StackNavigationProps<RootStackParamList, RootStackRoutes>>();
@@ -37,9 +32,6 @@ export const PortfolioContent = forwardRef<PortfolioGraphRef>((_props, ref) => {
selectHasFirmwareAuthenticityCheckHardFailed,
);
const shouldDisplaySuiteSyncAlert = useSelector(selectShouldDisplaySuiteSyncAlert);
const shouldDisplayFirmwareUpdateAlert = useSelector(selectShouldDisplayUpgradeFirmwareAlert);
const isPortfolioTracker = useSelector(selectIsPortfolioTrackerDevice);
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 (
<VStack spacing="sp32" marginTop="sp8">
{getHomescreenAlert()}
<HomescreenAlerts />
<AnimatedVStack spacing="sp32" layout={LinearTransition}>
<PortfolioGraph ref={ref} />
<VStack spacing="sp64" marginHorizontal="sp16">

View File

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

View File

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

View File

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