Repository: trezor/trezor-suite
Pull request #18776: feat(suite-native): device backup banners
Author: login: PeKne
Labels: name: mobile
description: Suite Lite issues and PRs
Created: 2025-05-07T06:19:19Z | Merged: 2025-05-13T06:42:33Z

## Description
- in case that user connects a device without backup or failed backup a specific banner is displayed
- these banners are using the same logic as device compromised banner, so it needed a bit of refactoring of the origina device compromised banner code

## Related Issue

Resolve #11549 
Resolve #18125 

## Screenshots:
### No backup


https://github.com/user-attachments/assets/e860883e-086f-4da1-b2f2-32546e5c1179



### Failed backup

https://github.com/user-attachments/assets/0b36c850-7111-4222-af83-7fedeac5b057



🔍🖥️ **Suite web test results:** [View in Currents](https://app.currents.dev/projects/Og0NOQ/runs?branch=feat/native/device-backup-banners&lookback=7)

🔍🖥️ **Suite desktop test results:** [View in Currents](https://app.currents.dev/projects/4ytF0E/runs?branch=feat/native/device-backup-banners&lookback=7)

🔍🖥️ **Suite native android test results:** [View in Currents](https://app.currents.dev/projects/iUe1Y4/runs?branch=feat/native/device-backup-banners&lookback=7)

Linked issue #11549: Handle NO BACKUP device (skipped backup)
Current state:
When user connect device without backup to mobile app we show him no warning in the app, only on device screen.
When user with device without backup try to show receive address app just get stucked because Connect waits for confirmation to reveal address without backup but we don't handle it at all yet.


**Desired state:**

- As a user without a wallet backup, I would like to see top banner warning me about the risk of not having a wallet backup and provide me instructions on how to backup my wallet
- As a user without a wallet backup, I would like to see the top banner Extended version on the Home (dashboard) screen, compact version on all the other screens
- As a user without a wallet backup, I would like to be warmed about the risks of updating firmware and have instructions on how to setup my wallet backup

> Updating firmware carries risks. If something goes wrong, the device may be wiped, and you’ll need your wallet backup to restore access. Create a wallet backup before updating.

**Exceptions**
- As a user without a wallet backup, I would NOT like to see any warning during the onboarding flow

[designs](https://www.figma.com/design/xSWRW8Csq7zYJ3TiqwoIxB/Small-Things?node-id=2404-2105&m=dev)

also related to: https://github.com/trezor/trezor-suite/issues/17768
Linked issue comments: user: login: PeKne
created_at: 2025-04-23T12:58:59Z
body: Probably duplicate? https://github.com/trezor/trezor-suite/issues/18125
author_association: CONTRIBUTOR
user: login: matejkriz
created_at: 2025-05-07T10:56:30Z
body: > Probably duplicate? [#18125](https://github.com/trezor/trezor-suite/issues/18125)

It's not an exact duplicate, this one is for handling device without backup (backup skipped, still possible to do it later), the other one is for device with backup failed (there is no way to fix the backup and the device has to be wiped).
author_association: MEMBER
user: login: STew790
created_at: 2025-05-14T06:39:12Z
body: **QA NOK**
I cant do firmware update at all, I don't have the option

![Image](https://github.com/user-attachments/assets/d7b373d1-03c6-40f2-ad39-fa96f24c7c41)

**QA OK**
I can see the banner on dashboard with button, CTA shows me the wallet creating screen and I can do the backup. The warning is not present during this onboarding part

![Image](https://github.com/user-attachments/assets/b7e2402e-35c9-4d57-be6c-bb3e62c9a33f)

Other screens have the minimal warning

![Image](https://github.com/user-attachments/assets/256131ba-a9b6-483d-97ef-aef860eec1fb)

**Info**
25.5.1 c274554
author_association: CONTRIBUTOR
user: login: STew790
created_at: 2025-05-14T07:21:32Z
body: **QA OK**
After discussion, I will create a separate issue for the fw update.
author_association: CONTRIBUTOR

Diff:
diff --git a/packages/suite/src/components/suite/banners/SuiteBanners/SuiteBanners.tsx b/packages/suite/src/components/suite/banners/SuiteBanners/SuiteBanners.tsx
index 0319594b692f..3b7d20a2787d 100644
--- a/packages/suite/src/components/suite/banners/SuiteBanners/SuiteBanners.tsx
+++ b/packages/suite/src/components/suite/banners/SuiteBanners/SuiteBanners.tsx
@@ -3,7 +3,11 @@ import { useEffect, useState } from 'react';
 import styled from 'styled-components';
 
 import { selectBannerMessage } from '@suite-common/message-system';
-import { selectSelectedDevice } from '@suite-common/wallet-core';
+import {
+    selectIsDeviceBackupRequired,
+    selectIsDeviceBackupUnfinished,
+    selectSelectedDevice,
+} from '@suite-common/wallet-core';
 import { spacingsPx } from '@trezor/theme';
 
 import { MAX_CONTENT_WIDTH } from 'src/constants/suite/layout';
@@ -41,6 +45,8 @@ export const SuiteBanners = () => {
     const bannerMessage = useSelector(selectBannerMessage);
     const firmwareRevisionError = useSelector(selectFirmwareRevisionCheckErrorIfEnabled);
     const firmwareHashError = useSelector(selectFirmwareHashCheckErrorIfEnabled);
+    const isDeviceBackupUnfinished = useSelector(selectIsDeviceBackupUnfinished);
+    const isDeviceBackupRequired = useSelector(selectIsDeviceBackupRequired);
 
     // The dismissal doesn't need to outlive the session. Use local state.
     const [safetyChecksDismissed, setSafetyChecksDismissed] = useState(false);
@@ -54,10 +60,10 @@ export const SuiteBanners = () => {
     if (firmwareRevisionError || firmwareHashError) {
         banner = <FirmwareAuthenticityCheckBanner />;
         priority = 91;
-    } else if (device?.features?.unfinished_backup) {
+    } else if (isDeviceBackupUnfinished) {
         banner = <FailedBackup />;
         priority = 90;
-    } else if (device?.features?.backup_availability === 'Required') {
+    } else if (isDeviceBackupRequired) {
         banner = <NoBackup />;
         priority = 70;
     } else if (device?.connected && device?.features?.safety_checks === 'PromptAlways') {
diff --git a/suite-common/wallet-core/src/device/deviceReducer.ts b/suite-common/wallet-core/src/device/deviceReducer.ts
index 155550b1cb26..e5767b954c66 100644
--- a/suite-common/wallet-core/src/device/deviceReducer.ts
+++ b/suite-common/wallet-core/src/device/deviceReducer.ts
@@ -777,6 +777,16 @@ export const selectIsDeviceBackedUp = createMemoizedSelector(
     features => features?.backup_availability !== 'Required' && !features?.unfinished_backup,
 );
 
+export const selectIsDeviceBackupRequired = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.backup_availability === 'Required',
+);
+
+export const selectIsDeviceBackupUnfinished = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.unfinished_backup,
+);
+
 export const selectDeviceButtonRequests = createMemoizedSelector(
     [selectSelectedDevice],
     device => device?.buttonRequests ?? [],
diff --git a/suite-native/app/src/BannersRenderer.tsx b/suite-native/app/src/BannersRenderer.tsx
index 72df5ebad8c3..e8fd49588a32 100644
--- a/suite-native/app/src/BannersRenderer.tsx
+++ b/suite-native/app/src/BannersRenderer.tsx
@@ -5,26 +5,23 @@ import { useSetAtom } from 'jotai';
 
 import { isAnyBannerRenderedAtom } from '@suite-native/atoms';
 import { OfflineBanner, useIsOfflineBannerVisible } from '@suite-native/connection-status';
+import { DeviceDangerBanner, useIsDeviceDangerBannerVisible } from '@suite-native/device';
 import {
     MessageSystemBannerRenderer,
     useIsMessageSystemBannerVisible,
 } from '@suite-native/message-system';
-import {
-    DeviceCompromisedBanner,
-    useIsDeviceCompromisedBannerVisible,
-} from '@suite-native/module-authenticity-checks';
 
 export const BannersRenderer = () => {
     const setIsAnyBannerRenderedAtom = useSetAtom(isAnyBannerRenderedAtom);
 
     const isOfflineBannerVisible = useIsOfflineBannerVisible();
-    const isDeviceCompromisedBannerVisible = useIsDeviceCompromisedBannerVisible();
+    const isDeviceDangerBannerVisible = useIsDeviceDangerBannerVisible();
     const isMessageSystemBannerVisible = useIsMessageSystemBannerVisible();
 
     // If any banner is rendered, let it be known to components rendered below (e.g. Screen),
     // so that they can adjust their top insets accordingly (via useBannerAwareSafeAreaInsets).
     const isAnyBannerVisible =
-        isMessageSystemBannerVisible || isOfflineBannerVisible || isDeviceCompromisedBannerVisible;
+        isMessageSystemBannerVisible || isOfflineBannerVisible || isDeviceDangerBannerVisible;
 
     useEffect(
         () => setIsAnyBannerRenderedAtom(isAnyBannerVisible),
@@ -36,12 +33,12 @@ export const BannersRenderer = () => {
     const topInsetOfflineBanner = top;
     const topInsetDeviceCompromisedBanner = isOfflineBannerVisible ? 0 : top;
     const topInsetMessageSystemBanner =
-        isOfflineBannerVisible || isDeviceCompromisedBannerVisible ? 0 : top;
+        isOfflineBannerVisible || isDeviceDangerBannerVisible ? 0 : top;
 
     return (
         <>
             <OfflineBanner topSafeAreaInset={topInsetOfflineBanner} />
-            <DeviceCompromisedBanner topSafeAreaInset={topInsetDeviceCompromisedBanner} />
+            <DeviceDangerBanner topSafeAreaInset={topInsetDeviceCompromisedBanner} />
             <MessageSystemBannerRenderer topSafeAreaInset={topInsetMessageSystemBanner} />
         </>
     );
diff --git a/suite-native/app/src/hooks/useGlobalHooks.tsx b/suite-native/app/src/hooks/useGlobalHooks.tsx
index 26202f2fb462..45251f3d51cf 100644
--- a/suite-native/app/src/hooks/useGlobalHooks.tsx
+++ b/suite-native/app/src/hooks/useGlobalHooks.tsx
@@ -1,10 +1,10 @@
 import {
     useDetectDeviceError,
     useHandleDeviceConnection,
+    useRenderDeviceDangerBanner,
     useReportDeviceCompromised,
     useRetryFwAuthenticityChecks,
 } from '@suite-native/device';
-import { useRenderDeviceCompromisedBanner } from '@suite-native/module-authenticity-checks';
 import { useConnectPopupNavigation } from '@suite-native/module-connect-popup';
 
 import { useCoinEnablingInitialCheck } from './useCoinEnablingInitialCheck';
@@ -21,7 +21,7 @@ export const useGlobalHooks = () => {
     useHandleDeviceConnection();
     useDetectDeviceError();
     useReportDeviceCompromised();
-    useRenderDeviceCompromisedBanner();
+    useRenderDeviceDangerBanner();
 
     useRetryFwAuthenticityChecks();
 };
diff --git a/suite-native/app/src/navigation/RootStackNavigator.tsx b/suite-native/app/src/navigation/RootStackNavigator.tsx
index 32d19c804a98..6f7f32b6d668 100644
--- a/suite-native/app/src/navigation/RootStackNavigator.tsx
+++ b/suite-native/app/src/navigation/RootStackNavigator.tsx
@@ -17,7 +17,10 @@ import {
     WalletConnectSessionPopupScreen,
 } from '@suite-native/module-connect-popup';
 import { DevUtilsStackNavigator } from '@suite-native/module-dev-utils';
-import { DeviceOnboardingStackNavigator } from '@suite-native/module-device-onboarding';
+import {
+    BackupFailedModalScreen,
+    DeviceOnboardingStackNavigator,
+} from '@suite-native/module-device-onboarding';
 import { DeviceSettingsStackNavigator } from '@suite-native/module-device-settings';
 import { OnboardingStackNavigator } from '@suite-native/module-onboarding';
 import { SendStackNavigator } from '@suite-native/module-send';
@@ -102,6 +105,10 @@ export const RootStackNavigator = () => {
                 name={RootStackRoutes.DeviceCompromisedModal}
                 component={DeviceCompromisedModalScreen}
             />
+            <RootStack.Screen
+                name={RootStackRoutes.BackupFailedModal}
+                component={BackupFailedModalScreen}
+            />
             {/* Navigation flows that start by push from bottom animation on the first screen of its stack. */}
             <RootStack.Group screenOptions={{ animation: 'slide_from_bottom' }}>
                 <RootStack.Screen
diff --git a/suite-native/device/src/components/DeviceDangerBanner.tsx b/suite-native/device/src/components/DeviceDangerBanner.tsx
new file mode 100644
index 000000000000..e843f7d86eb4
--- /dev/null
+++ b/suite-native/device/src/components/DeviceDangerBanner.tsx
@@ -0,0 +1,62 @@
+import { useAtomValue } from 'jotai';
+
+import { Box, HStack, Text, VStack } from '@suite-native/atoms';
+import { DeviceDangerBannerVariant, deviceDangerBannerAtom } from '@suite-native/device';
+import { Icon } from '@suite-native/icons';
+import { Translation, TxKeyPath } from '@suite-native/intl';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+
+import { DeviceDangerBannerExtension, bannerContentPresets } from './DeviceDangerBannerExtension';
+
+const containerStyle = prepareNativeStyle<{ bannerVariant: DeviceDangerBannerVariant }>(
+    (utils, { bannerVariant }) => ({
+        backgroundColor:
+            bannerVariant === 'other-error'
+                ? utils.colors.backgroundAlertYellowBold
+                : utils.colors.backgroundAlertRedSubtleOnElevation0,
+        // MessageSystemBanner critical variant has the same bgColor, so the margin serves to separate them visually
+        marginBottom: utils.spacings.sp1,
+    }),
+);
+
+const contentStyle = prepareNativeStyle<{ topSafeAreaInset: number }>(
+    (utils, { topSafeAreaInset }) => ({
+        marginTop: topSafeAreaInset,
+        paddingTop: utils.spacings.sp8,
+        paddingBottom: utils.spacings.sp16,
+        paddingHorizontal: utils.spacings.sp24,
+        alignItems: 'center',
+    }),
+);
+
+export const useIsDeviceDangerBannerVisible = () => useAtomValue(deviceDangerBannerAtom) !== null;
+
+type DeviceCompromisedBannerProps = { topSafeAreaInset: number };
+
+export const DeviceDangerBanner = ({ topSafeAreaInset }: DeviceCompromisedBannerProps) => {
+    const deviceDanger = useAtomValue(deviceDangerBannerAtom);
+
+    const { applyStyle } = useNativeStyles();
+
+    if (!deviceDanger) return null;
+    const { cause, variant } = deviceDanger;
+
+    const titleTranslationId: TxKeyPath =
+        variant === 'other-error'
+            ? 'generic.banners.deviceDanger.revisionNotChecked.title'
+            : bannerContentPresets[cause].title;
+
+    return (
+        <Box style={applyStyle(containerStyle, { bannerVariant: variant })}>
+            <VStack spacing="sp2" style={applyStyle(contentStyle, { topSafeAreaInset })}>
+                <HStack alignItems="center">
+                    <Icon name="warningCircle" size="mediumLarge" />
+                    <Text variant="highlight">
+                        <Translation id={titleTranslationId} />
+                    </Text>
+                </HStack>
+                {variant === 'extended' && <DeviceDangerBannerExtension />}
+            </VStack>
+        </Box>
+    );
+};
diff --git a/suite-native/device/src/components/DeviceDangerBannerExtension.tsx b/suite-native/device/src/components/DeviceDangerBannerExtension.tsx
new file mode 100644
index 000000000000..87e05a66ee89
--- /dev/null
+++ b/suite-native/device/src/components/DeviceDangerBannerExtension.tsx
@@ -0,0 +1,84 @@
+import { useNavigation } from '@react-navigation/native';
+import { useAtomValue } from 'jotai';
+
+import { Button, Text, VStack } from '@suite-native/atoms';
+import { Translation, TxKeyPath } from '@suite-native/intl';
+import { useOpenLink } from '@suite-native/link';
+import {
+    DeviceOnboardingStackRoutes,
+    HomeStackParamList,
+    HomeStackRoutes,
+    RootStackParamList,
+    RootStackRoutes,
+    TabToStackCompositeNavigationProp,
+} from '@suite-native/navigation';
+import { TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_MOBILE_URL } from '@trezor/urls';
+
+import { DeviceDangerBannerCause, deviceDangerBannerAtom } from '../deviceAtoms';
+
+export const bannerContentPresets = {
+    'device-compromised': {
+        title: 'generic.banners.deviceDanger.compromised.title',
+        subtitle: 'generic.banners.deviceDanger.compromised.subtitle',
+        cta: 'generic.banners.deviceDanger.compromised.cta',
+    },
+    'backup-failed': {
+        title: 'generic.banners.deviceDanger.backupFailed.title',
+        subtitle: 'generic.banners.deviceDanger.backupFailed.subtitle',
+        cta: 'generic.banners.deviceDanger.backupFailed.cta',
+    },
+    'backup-needed': {
+        title: 'generic.banners.deviceDanger.backupNeeded.title',
+        subtitle: 'generic.banners.deviceDanger.backupNeeded.subtitle',
+        cta: 'generic.banners.deviceDanger.backupNeeded.cta',
+    },
+} as const satisfies Record<
+    DeviceDangerBannerCause,
+    {
+        title: TxKeyPath;
+        subtitle: TxKeyPath;
+        cta: TxKeyPath;
+    }
+>;
+
+type NavigationProps = TabToStackCompositeNavigationProp<
+    RootStackParamList,
+    HomeStackRoutes,
+    HomeStackParamList
+>;
+
+export const DeviceDangerBannerExtension = () => {
+    const navigation = useNavigation<NavigationProps>();
+    const openLink = useOpenLink();
+    const deviceDanger = useAtomValue(deviceDangerBannerAtom);
+    const { cause } = deviceDanger ?? {};
+
+    const handleCtaPress = () => {
+        if (cause === 'device-compromised') {
+            openLink(TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_MOBILE_URL);
+        }
+        if (cause === 'backup-needed') {
+            navigation.navigate(RootStackRoutes.DeviceOnboardingStack, {
+                screen: DeviceOnboardingStackRoutes.WalletBackupTutorial,
+            });
+        }
+        if (cause === 'backup-failed') {
+            navigation.navigate(RootStackRoutes.BackupFailedModal);
+        }
+    };
+
+    if (!cause) return null;
+
+    const { subtitle, cta } = bannerContentPresets[cause];
+
+    return (
+        <VStack spacing="sp16">
+            <Text textAlign="center">
+                <Translation id={subtitle} />
+            </Text>
+            <Button colorScheme="redBold" onPress={handleCtaPress}>
+                <Translation id={cta} />
+            </Button>
+        </VStack>
+    );
+};
diff --git a/suite-native/device/src/deviceAtoms.ts b/suite-native/device/src/deviceAtoms.ts
index eabb861b8e0e..9ac9d9fd3977 100644
--- a/suite-native/device/src/deviceAtoms.ts
+++ b/suite-native/device/src/deviceAtoms.ts
@@ -2,3 +2,25 @@ import { atom } from 'jotai';
 
 export const wasDeviceOnboardingCancelledAtom = atom<boolean>(false);
 export const isOnboardingDeviceDisconnectedAlertDisplayedAtom = atom<boolean>(false);
+
+export type DeviceDangerBannerCause = 'device-compromised' | 'backup-needed' | 'backup-failed';
+
+type DeviceDangerBanner =
+    | {
+          variant: 'brief' | 'extended';
+          cause: DeviceDangerBannerCause;
+      }
+    | {
+          cause: 'device-compromised';
+          variant: 'other-error';
+      };
+
+/**
+ * Represents the display variants for the DeviceCompromisedBanner.
+ * - 'brief': Shows banner with only title
+ * - 'extended': Shows banner with title, subtitle and CTA
+ * - 'other-error': Shows only a warning banner that check could not be performed
+ */
+export type DeviceDangerBannerVariant = DeviceDangerBanner['variant'];
+
+export const deviceDangerBannerAtom = atom<DeviceDangerBanner | null>(null);
diff --git a/suite-native/device/src/deviceThunks.ts b/suite-native/device/src/deviceThunks.ts
index 3318f10569c9..b59d1f2beab6 100644
--- a/suite-native/device/src/deviceThunks.ts
+++ b/suite-native/device/src/deviceThunks.ts
@@ -1,6 +1,11 @@
 import { Feature, selectIsFeatureEnabled } from '@suite-common/message-system';
 import { createThunk } from '@suite-common/redux-utils';
-import { deviceActions, selectDevicePath, selectSelectedDevice } from '@suite-common/wallet-core';
+import {
+    deviceActions,
+    selectDevicePath,
+    selectIsDeviceInitialized,
+    selectSelectedDevice,
+} from '@suite-common/wallet-core';
 import { WalletBackupType, reportCheckFail } from '@suite-native/device';
 import TrezorConnect, { PROTO } from '@trezor/connect';
 import { getFirmwareVersion } from '@trezor/device-utils';
@@ -59,6 +64,7 @@ export const createAndBackupWalletThunk = createThunk(
     ) => {
         const device = selectSelectedDevice(getState());
         const devicePath = selectDevicePath(getState());
+        const isDeviceInitialized = selectIsDeviceInitialized(getState());
         const isEntropyCheckEnabled = selectIsFeatureEnabled(
             getState(),
             Feature.entropyCheckMobile,
@@ -69,6 +75,26 @@ export const createAndBackupWalletThunk = createThunk(
             throw new Error('Device not found');
         }
 
+        // If the device already has a seed, backup is created.
+        if (isDeviceInitialized) {
+            const { backup_type } = device.features;
+
+            const backupParams: PROTO.BackupDevice =
+                backup_type === 'Slip39_Basic' || backup_type === 'Slip39_Basic_Extendable'
+                    ? {
+                          group_threshold: 1,
+                          groups: [{ member_count: 1, member_threshold: 1 }],
+                      }
+                    : {};
+
+            return await TrezorConnect.backupDevice({
+                ...backupParams,
+                device: {
+                    path: device.path,
+                },
+            });
+        }
+
         const result = await TrezorConnect.resetDevice({
             device: { path: devicePath },
             skip_backup: false,
diff --git a/suite-native/device/src/hooks/useRenderDeviceDangerBanner.ts b/suite-native/device/src/hooks/useRenderDeviceDangerBanner.ts
new file mode 100644
index 000000000000..c6eb25892122
--- /dev/null
+++ b/suite-native/device/src/hooks/useRenderDeviceDangerBanner.ts
@@ -0,0 +1,85 @@
+import { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+
+import { useNavigationState } from '@react-navigation/native';
+import { useSetAtom } from 'jotai';
+
+import {
+    selectIsDeviceBackupRequired,
+    selectIsDeviceBackupUnfinished,
+} from '@suite-common/wallet-core';
+import {
+    DeviceDangerBannerCause,
+    deviceDangerBannerAtom,
+    revisionCheckErrorScenarios,
+    selectFirmwareRevisionCheckErrorIfEnabled,
+} from '@suite-native/device';
+import {
+    AppTabsRoutes,
+    HomeStackRoutes,
+    RootStackRoutes,
+    useNavigationRouteMatch,
+} from '@suite-native/navigation';
+import { selectIsOnboardingFinished } from '@suite-native/settings';
+
+export const useRenderDeviceDangerBanner = () => {
+    const setBannerVariant = useSetAtom(deviceDangerBannerAtom);
+    const lastRoute = useNavigationState(state => state?.routes.at(-1)?.name);
+    const isDeviceOnboardingStackFocused = lastRoute === RootStackRoutes.DeviceOnboardingStack;
+
+    const isRouteExcluded =
+        useNavigationRouteMatch([
+            RootStackRoutes.DeviceCompromisedModal,
+            RootStackRoutes.BackupFailedModal,
+        ]) || isDeviceOnboardingStackFocused;
+
+    const isBannerExtended = useNavigationRouteMatch([
+        AppTabsRoutes.HomeStack,
+        HomeStackRoutes.Home,
+    ]);
+
+    const isOnboardingFinished = useSelector(selectIsOnboardingFinished);
+    const revisionCheckError = useSelector(selectFirmwareRevisionCheckErrorIfEnabled);
+    const isDeviceBackupUnfinished = useSelector(selectIsDeviceBackupUnfinished);
+    const isDeviceBackupRequired = useSelector(selectIsDeviceBackupRequired);
+
+    useEffect(() => {
+        let dangerCause: DeviceDangerBannerCause | undefined;
+
+        if (revisionCheckError) {
+            dangerCause = 'device-compromised';
+        } else if (isDeviceBackupUnfinished) {
+            dangerCause = 'backup-failed';
+        } else if (isDeviceBackupRequired) {
+            dangerCause = 'backup-needed';
+        }
+
+        const variant = isBannerExtended ? 'extended' : 'brief';
+
+        if (
+            !isOnboardingFinished ||
+            isRouteExcluded ||
+            !dangerCause ||
+            (revisionCheckError &&
+                revisionCheckErrorScenarios[revisionCheckError].type === 'skipped')
+        ) {
+            return setBannerVariant(null);
+        }
+
+        if (dangerCause === 'device-compromised') {
+            if (revisionCheckError === 'other-error') {
+                return setBannerVariant({ variant: 'other-error', cause: 'device-compromised' });
+            }
+        }
+
+        return setBannerVariant({ variant, cause: dangerCause });
+    }, [
+        isRouteExcluded,
+        isBannerExtended,
+        revisionCheckError,
+        isDeviceBackupUnfinished,
+        isDeviceBackupRequired,
+        isOnboardingFinished,
+        setBannerVariant,
+    ]);
+};
diff --git a/suite-native/device/src/index.ts b/suite-native/device/src/index.ts
index 130f25a54bb0..0699f62b5594 100644
--- a/suite-native/device/src/index.ts
+++ b/suite-native/device/src/index.ts
@@ -5,12 +5,14 @@ export * from './hooks/useDetectDeviceError';
 export * from './hooks/useReportDeviceCompromised';
 export * from './hooks/useRetryFwAuthenticityChecks';
 export * from './hooks/usePinAction';
+export * from './hooks/useRenderDeviceDangerBanner';
 export * from './components/ConnectDeviceAnimation';
 export * from './components/ConfirmOnTrezorImage';
 export * from './components/ConnectorImage';
 export * from './components/DeviceImage';
 export * from './components/ContinueOnTrezorScreenContent';
 export * from './components/ConnectAndUnlockDeviceScreenContent';
+export * from './components/DeviceDangerBanner';
 export * from './utils';
 export * from './selectors';
 export * from './deviceThunks';
diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts
index 3808e9da14e4..912002fc9584 100644
--- a/suite-native/intl/src/en.ts
+++ b/suite-native/intl/src/en.ts
@@ -35,13 +35,28 @@ export const en = {
                 fwRevisionCheckOfflineError:
                     "Firmware authenticity check couldn't be performed.\nGo online to verify your firmware version.",
             },
-            deviceCompromised: {
-                title: 'Unofficial firmware detected',
-                subtitle:
-                    'Your Trezor may be counterfeit. To ensure your safety, receiving funds has been disabled. Contact Trezor Support immediately.',
-                contactSupportButton: 'Contact Trezor Support',
+            deviceDanger: {
+                compromised: {
+                    title: 'Unofficial firmware detected',
+                    subtitle:
+                        'Your Trezor may be counterfeit. To ensure your safety, receiving funds has been disabled. Contact Trezor Support immediately.',
+                    cta: 'Contact Trezor Support',
+                },
+                revisionNotChecked: {
+                    title: "Couldn't perform firmware authenticity check.",
+                },
+                backupFailed: {
+                    title: 'Wallet backup failed',
+                    subtitle: 'Wipe your Trezor, then create a wallet backup.',
+                    cta: 'Wipe device & create backup',
+                },
+                backupNeeded: {
+                    title: 'You need a wallet backup',
+                    subtitle:
+                        'A wallet backup is essential for recovering your assets. Don’t send or receive funds with this device until you’ve created a backup.',
+                    cta: 'Create wallet backup',
+                },
             },
-            fwRevisionCheckOtherError: "Couldn't perform firmware authenticity check.",
         },
         tokens: '+ Tokens',
     },
@@ -1064,6 +1079,26 @@ export const en = {
         walletCreatedSuccessScreen: {
             successLabel: 'All good!',
         },
+
+        backupFailedModalScreen: {
+            title: 'Your wallet backup failed.',
+            subtitle: 'You need to wipe your device and create a wallet backup.',
+            steps: {
+                wipe: 'Wipe your device to continue. This will erase all of its existing data.',
+                contact:
+                    'If you have assets associated with this device, contact Trezor support now.',
+            },
+            primaryButton: 'Wipe device',
+            secondaryButton: 'Contact Trezor Support',
+
+            alert: {
+                title: 'Erase all data?',
+                description:
+                    'You should not have any funds associated with this device. Wiping it will erase its data. This action can’t be undone.',
+                primaryButton: 'Wipe device',
+                secondaryButton: 'Contact Trezor Support',
+            },
+        },
         deviceDisconnectedAlert: {
             title: 'Your Trezor has been disconnected',
             description: 'Connect your Trezor to start again.',
diff --git a/suite-native/module-authenticity-checks/package.json b/suite-native/module-authenticity-checks/package.json
index 3ce6a0203feb..cb13a08dbef1 100644
--- a/suite-native/module-authenticity-checks/package.json
+++ b/suite-native/module-authenticity-checks/package.json
@@ -16,13 +16,10 @@
         "@suite-common/wallet-core": "workspace:*",
         "@suite-native/atoms": "workspace:*",
         "@suite-native/device": "workspace:*",
-        "@suite-native/icons": "workspace:*",
         "@suite-native/intl": "workspace:*",
         "@suite-native/link": "workspace:*",
         "@suite-native/navigation": "workspace:*",
-        "@trezor/styles": "workspace:*",
         "@trezor/urls": "workspace:*",
-        "jotai": "1.9.1",
         "react": "18.2.0",
         "react-native": "0.76.9",
         "react-redux": "9.2.0"
diff --git a/suite-native/module-authenticity-checks/src/DeviceCompromisedBannerAtoms.ts b/suite-native/module-authenticity-checks/src/DeviceCompromisedBannerAtoms.ts
deleted file mode 100644
index 50f9e7609120..000000000000
--- a/suite-native/module-authenticity-checks/src/DeviceCompromisedBannerAtoms.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { atom } from 'jotai';
-
-/**
- * Represents the display variants for the DeviceCompromisedBanner.
- * - 'none': Banner is hidden
- * - 'brief': Shows banner with only title
- * - 'extended': Shows banner with title, subtitle and CTA
- * - 'other-error': Shows only a warning banner that check could not be performed
- */
-export type DeviceCompromisedBannerVariant = 'none' | 'brief' | 'extended' | 'other-error';
-
-export const deviceCompromisedBannerAtom = atom<DeviceCompromisedBannerVariant>('none');
diff --git a/suite-native/module-authenticity-checks/src/components/DeviceCompromisedBanner.tsx b/suite-native/module-authenticity-checks/src/components/DeviceCompromisedBanner.tsx
deleted file mode 100644
index 5e48589fd291..000000000000
--- a/suite-native/module-authenticity-checks/src/components/DeviceCompromisedBanner.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { View } from 'react-native';
-
-import { useAtomValue } from 'jotai';
-
-import { Button, HStack, Text, VStack } from '@suite-native/atoms';
-import { Icon } from '@suite-native/icons';
-import { Translation } from '@suite-native/intl';
-import { useOpenLink } from '@suite-native/link';
-import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
-import { TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_MOBILE_URL } from '@trezor/urls';
-
-import {
-    DeviceCompromisedBannerVariant,
-    deviceCompromisedBannerAtom,
-} from '../DeviceCompromisedBannerAtoms';
-
-const containerStyle = prepareNativeStyle<{ bannerVariant: DeviceCompromisedBannerVariant }>(
-    (utils, { bannerVariant }) => ({
-        backgroundColor:
-            bannerVariant === 'other-error'
-                ? utils.colors.backgroundAlertYellowBold
-                : utils.colors.backgroundAlertRedSubtleOnElevation0,
-        // MessageSystemBanner critical variant has the same bgColor, so the margin serves to separate them visually
-        marginBottom: utils.spacings.sp1,
-    }),
-);
-
-const contentStyle = prepareNativeStyle<{ topSafeAreaInset: number }>(
-    (utils, { topSafeAreaInset }) => ({
-        marginTop: topSafeAreaInset,
-        paddingTop: utils.spacings.sp8,
-        paddingBottom: utils.spacings.sp16,
-        paddingHorizontal: utils.spacings.sp24,
-        alignItems: 'center',
-    }),
-);
-
-const ExtendedBannerContent = () => {
-    const openLink = useOpenLink();
-    const handleContactSupportClick = () =>
-        openLink(TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_MOBILE_URL);
-
-    return (
-        <VStack spacing="sp16">
-            <Text textAlign="center">
-                <Translation id="generic.banners.deviceCompromised.subtitle" />
-            </Text>
-            <Button colorScheme="redBold" onPress={handleContactSupportClick}>
-                <Translation id="generic.banners.deviceCompromised.contactSupportButton" />
-            </Button>
-        </VStack>
-    );
-};
-
-export const useIsDeviceCompromisedBannerVisible = () =>
-    useAtomValue(deviceCompromisedBannerAtom) !== 'none';
-
-type DeviceCompromisedBannerProps = { topSafeAreaInset: number };
-
-export const DeviceCompromisedBanner = ({ topSafeAreaInset }: DeviceCompromisedBannerProps) => {
-    const bannerVariant = useAtomValue(deviceCompromisedBannerAtom);
-    const { applyStyle } = useNativeStyles();
-
-    if (bannerVariant === 'none') return null;
-
-    const titleTranslationId =
-        bannerVariant === 'other-error'
-            ? 'generic.banners.fwRevisionCheckOtherError'
-            : 'generic.banners.deviceCompromised.title';
-
-    return (
-        <View style={applyStyle(containerStyle, { bannerVariant })}>
-            <VStack spacing="sp2" style={applyStyle(contentStyle, { topSafeAreaInset })}>
-                <HStack alignItems="center">
-                    <Icon name="warningCircle" size="mediumLarge" />
-                    <Text variant="highlight">
-                        <Translation id={titleTranslationId} />
-                    </Text>
-                </HStack>
-                {bannerVariant === 'extended' && <ExtendedBannerContent />}
-            </VStack>
-        </View>
-    );
-};
diff --git a/suite-native/module-authenticity-checks/src/index.ts b/suite-native/module-authenticity-checks/src/index.ts
index 6e9ca211e712..731d37c6b305 100644
--- a/suite-native/module-authenticity-checks/src/index.ts
+++ b/suite-native/module-authenticity-checks/src/index.ts
@@ -1,3 +1 @@
-export * from './useRenderDeviceCompromisedBanner';
-export * from './components/DeviceCompromisedBanner';
 export * from './screens/DeviceCompromisedModalScreen';
diff --git a/suite-native/module-authenticity-checks/src/useRenderDeviceCompromisedBanner.ts b/suite-native/module-authenticity-checks/src/useRenderDeviceCompromisedBanner.ts
deleted file mode 100644
index de2db34f0845..000000000000
--- a/suite-native/module-authenticity-checks/src/useRenderDeviceCompromisedBanner.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useEffect } from 'react';
-import { useSelector } from 'react-redux';
-
-import { useSetAtom } from 'jotai';
-
-import {
-    revisionCheckErrorScenarios,
-    selectFirmwareRevisionCheckErrorIfEnabled,
-} from '@suite-native/device';
-import {
-    AppTabsRoutes,
-    HomeStackRoutes,
-    RootStackRoutes,
-    useNavigationRouteMatch,
-} from '@suite-native/navigation';
-import { selectIsOnboardingFinished } from '@suite-native/settings';
-
-import { deviceCompromisedBannerAtom } from './DeviceCompromisedBannerAtoms';
-
-export const useRenderDeviceCompromisedBanner = () => {
-    const isRouteExcluded = useNavigationRouteMatch(RootStackRoutes.DeviceCompromisedModal);
-    // need to match both; see note in hook definition
-    const isExtendedBanner = useNavigationRouteMatch([
-        AppTabsRoutes.HomeStack,
-        HomeStackRoutes.Home,
-    ]);
-
-    const isOnboardingFinished = useSelector(selectIsOnboardingFinished);
-    const revisionCheckError = useSelector(selectFirmwareRevisionCheckErrorIfEnabled);
-
-    const setBannerVariant = useSetAtom(deviceCompromisedBannerAtom);
-
-    useEffect(() => {
-        if (
-            !isOnboardingFinished ||
-            isRouteExcluded ||
-            revisionCheckError === null ||
-            revisionCheckErrorScenarios[revisionCheckError].type === 'skipped'
-        ) {
-            return setBannerVariant('none');
-        }
-
-        if (revisionCheckError === 'other-error') {
-            return setBannerVariant('other-error');
-        }
-
-        setBannerVariant(isExtendedBanner ? 'extended' : 'brief');
-    }, [
-        isRouteExcluded,
-        isExtendedBanner,
-        revisionCheckError,
-        setBannerVariant,
-        isOnboardingFinished,
-    ]);
-};
diff --git a/suite-native/module-authenticity-checks/tsconfig.json b/suite-native/module-authenticity-checks/tsconfig.json
index fa3615412ee3..4b831c6534e9 100644
--- a/suite-native/module-authenticity-checks/tsconfig.json
+++ b/suite-native/module-authenticity-checks/tsconfig.json
@@ -7,11 +7,9 @@
         },
         { "path": "../atoms" },
         { "path": "../device" },
-        { "path": "../icons" },
         { "path": "../intl" },
         { "path": "../link" },
         { "path": "../navigation" },
-        { "path": "../../packages/styles" },
         { "path": "../../packages/urls" }
     ]
 }
diff --git a/suite-native/module-device-onboarding/src/components/WalletBackupTutorial/WalletBackupTutorialStep5.tsx b/suite-native/module-device-onboarding/src/components/WalletBackupTutorial/WalletBackupTutorialStep5.tsx
index e059680c2527..2cd7c273b082 100644
--- a/suite-native/module-device-onboarding/src/components/WalletBackupTutorial/WalletBackupTutorialStep5.tsx
+++ b/suite-native/module-device-onboarding/src/components/WalletBackupTutorial/WalletBackupTutorialStep5.tsx
@@ -1,7 +1,9 @@
 import { useState } from 'react';
+import { useSelector } from 'react-redux';
 
 import * as Haptics from 'expo-haptics';
 
+import { selectIsDeviceInitialized } from '@suite-common/wallet-core';
 import { Box, Button, Card, InlineAlertBox, Text, TitleHeader } from '@suite-native/atoms';
 import { WalletBackupType } from '@suite-native/device';
 import { Icon } from '@suite-native/icons';
@@ -46,6 +48,7 @@ export const WalletBackupTutorialStep5 = ({
     selectedType,
     onSelectType,
 }: WalletBackupTutorialStep5Props) => {
+    const isDeviceInitialized = useSelector(selectIsDeviceInitialized);
     const { applyStyle } = useNativeStyles();
     const openLink = useOpenLink();
     const [isModalShown, setIsModalShown] = useState(false);
@@ -107,15 +110,17 @@ export const WalletBackupTutorialStep5 = ({
                         />
                     )}
                 </Card>
-                <Button
-                    viewLeft={<Icon name="caretDown" size="medium" />}
-                    colorScheme="tertiaryElevation0"
-                    size="small"
-                    style={applyStyle(moreOptionsStyle)}
-                    onPress={openBackupSelection}
-                >
-                    <Translation id="moduleDeviceOnboarding.walletBackupTutorialScreen.step5.moreOptionsButton" />
-                </Button>
+                {!isDeviceInitialized && (
+                    <Button
+                        viewLeft={<Icon name="caretDown" size="medium" />}
+                        colorScheme="tertiaryElevation0"
+                        size="small"
+                        style={applyStyle(moreOptionsStyle)}
+                        onPress={openBackupSelection}
+                    >
+                        <Translation id="moduleDeviceOnboarding.walletBackupTutorialScreen.step5.moreOptionsButton" />
+                    </Button>
+                )}
             </Box>
             <WalletBackupSheet
                 isDisplayed={isModalShown}
diff --git a/suite-native/module-device-onboarding/src/hooks/useWipeDeviceAlert.tsx b/suite-native/module-device-onboarding/src/hooks/useWipeDeviceAlert.tsx
new file mode 100644
index 000000000000..1a30ea8b51af
--- /dev/null
+++ b/suite-native/module-device-onboarding/src/hooks/useWipeDeviceAlert.tsx
@@ -0,0 +1,81 @@
+import { useCallback, useState } from 'react';
+import { useSelector } from 'react-redux';
+
+import { useNavigation } from '@react-navigation/core';
+
+import { selectSelectedDevice } from '@suite-common/wallet-core';
+import { useAlert } from '@suite-native/alerts';
+import { requestPrioritizedDeviceAccess } from '@suite-native/device-mutex';
+import { Translation } from '@suite-native/intl';
+import { useOpenLink } from '@suite-native/link';
+import {
+    DeviceOnboardingStackRoutes,
+    RootStackParamList,
+    RootStackRoutes,
+    StackNavigationProps,
+} from '@suite-native/navigation';
+import TrezorConnect from '@trezor/connect';
+
+export const BACKUP_FAILED_SUPPORT_URL =
+    'https://trezor.io/support/a/trezor-recovery-issues#open-chat';
+
+type NavigationProps = StackNavigationProps<RootStackParamList, RootStackRoutes.BackupFailedModal>;
+
+export const useWipeDeviceAlert = () => {
+    const [isWipeInProgress, setIsWipeInProgress] = useState(false);
+    const navigation = useNavigation<NavigationProps>();
+    const { showAlert } = useAlert();
+    const openLink = useOpenLink();
+    const device = useSelector(selectSelectedDevice);
+
+    const wipeDevice = useCallback(async () => {
+        setIsWipeInProgress(true);
+        if (!device) return;
+
+        const response = await requestPrioritizedDeviceAccess({
+            deviceCallback: async () =>
+                await TrezorConnect.wipeDevice({
+                    device: {
+                        path: device.path,
+                    },
+                    // In bootloader mode we need the skip the final reload otherwise we never get the resolution
+                    skipFinalReload: device.mode === 'bootloader',
+                }),
+        });
+
+        if (response.success && response.payload.success) {
+            navigation.navigate(RootStackRoutes.DeviceOnboardingStack, {
+                screen: DeviceOnboardingStackRoutes.UninitializedDeviceLanding,
+            });
+        }
+        setIsWipeInProgress(false);
+    }, [device, navigation]);
+
+    const showWipeDeviceAlert = useCallback(
+        () =>
+            showAlert({
+                title: (
+                    <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.alert.title" />
+                ),
+                description: (
+                    <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.alert.description" />
+                ),
+                primaryButtonTitle: (
+                    <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.alert.primaryButton" />
+                ),
+                primaryButtonVariant: 'redBold',
+                onPressPrimaryButton: wipeDevice,
+                secondaryButtonTitle: (
+                    <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.alert.secondaryButton" />
+                ),
+                secondaryButtonVariant: 'redElevation1',
+                onPressSecondaryButton: () => openLink(BACKUP_FAILED_SUPPORT_URL),
+            }),
+        [showAlert, openLink, wipeDevice],
+    );
+
+    return {
+        showWipeDeviceAlert,
+        isWipeInProgress,
+    };
+};
diff --git a/suite-native/module-device-onboarding/src/index.ts b/suite-native/module-device-onboarding/src/index.ts
index 695c3c49885e..4e3d6ff9eabd 100644
--- a/suite-native/module-device-onboarding/src/index.ts
+++ b/suite-native/module-device-onboarding/src/index.ts
@@ -1 +1,2 @@
 export * from './navigation/DeviceOnboardingStackNavigator';
+export * from './screens/BackupFailedModalScreen';
diff --git a/suite-native/module-device-onboarding/src/screens/BackupFailedModalScreen.tsx b/suite-native/module-device-onboarding/src/screens/BackupFailedModalScreen.tsx
new file mode 100644
index 000000000000..bba641abba7c
--- /dev/null
+++ b/suite-native/module-device-onboarding/src/screens/BackupFailedModalScreen.tsx
@@ -0,0 +1,86 @@
+import { useEffect } from 'react';
+
+import { Button, IconListTextItem, TitleHeader, VStack } from '@suite-native/atoms';
+import { ContinueOnTrezorScreenContent } from '@suite-native/device';
+import { Translation } from '@suite-native/intl';
+import { useOpenLink } from '@suite-native/link';
+import {
+    RootStackParamList,
+    RootStackRoutes,
+    Screen,
+    ScreenHeader,
+    StackProps,
+} from '@suite-native/navigation';
+
+import { BACKUP_FAILED_SUPPORT_URL, useWipeDeviceAlert } from '../hooks/useWipeDeviceAlert';
+
+export const BackupFailedModalScreen = ({
+    navigation,
+}: StackProps<RootStackParamList, RootStackRoutes.BackupFailedModal>) => {
+    const openLink = useOpenLink();
+    const { showWipeDeviceAlert, isWipeInProgress } = useWipeDeviceAlert();
+
+    const handleSecondaryButtonPress = () => openLink(BACKUP_FAILED_SUPPORT_URL);
+
+    useEffect(() => {
+        const unsubscribe = navigation.addListener('beforeRemove', e => {
+            if (isWipeInProgress && e.data.action.type === 'GO_BACK') {
+                e.preventDefault();
+            }
+        });
+
+        return unsubscribe;
+    }, [navigation, isWipeInProgress]);
+
+    if (isWipeInProgress) {
+        return (
+            <Screen header={<ScreenHeader leftIcon={null} />}>
+                <ContinueOnTrezorScreenContent />
+            </Screen>
+        );
+    }
+
+    return (
+        <Screen header={<ScreenHeader />}>
+            <VStack spacing="sp32" flex={1} paddingTop="sp16">
+                <TitleHeader
+                    titleVariant="titleMedium"
+                    titleSpacing="sp12"
+                    title={
+                        <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.title" />
+                    }
+                    subtitle={
+                        <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.subtitle" />
+                    }
+                />
+                <VStack spacing="sp24">
+                    <IconListTextItem
+                        icon="plugs"
+                        variant="red"
+                        textVariant="highlight"
+                        iconSize="large"
+                    >
+                        <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.steps.wipe" />
+                    </IconListTextItem>
+
+                    <IconListTextItem
+                        icon="chatCircle"
+                        variant="red"
+                        textVariant="highlight"
+                        iconSize="large"
+                    >
+                        <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.steps.contact" />
+                    </IconListTextItem>
+                </VStack>
+            </VStack>
+            <VStack spacing="sp12">
+                <Button colorScheme="redBold" onPress={showWipeDeviceAlert}>
+                    <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.primaryButton" />
+                </Button>
+                <Button colorScheme="redElevation1" onPress={handleSecondaryButtonPress}>
+                    <Translation id="moduleDeviceOnboarding.backupFailedModalScreen.secondaryButton" />
+                </Button>
+            </VStack>
+        </Screen>
+    );
+};
diff --git a/suite-native/navigation/src/navigators.ts b/suite-native/navigation/src/navigators.ts
index 102e11f152da..bf3c0c0bf6df 100644
--- a/suite-native/navigation/src/navigators.ts
+++ b/suite-native/navigation/src/navigators.ts
@@ -249,6 +249,7 @@ export type RootStackParamList = {
     [RootStackRoutes.WalletConnectPair]: undefined;
     [RootStackRoutes.SettingsScreenStack]: NavigatorScreenParams<SettingsStackParamList>;
     [RootStackRoutes.DeviceCompromisedModal]: undefined;
+    [RootStackRoutes.BackupFailedModal]: undefined;
     [RootStackRoutes.TradingWebView]: {
         closeCallbackUrl: string;
         source?: { uri?: string; html?: string };
diff --git a/suite-native/navigation/src/routes.ts b/suite-native/navigation/src/routes.ts
index 537f6be1962e..44fd685e649e 100644
--- a/suite-native/navigation/src/routes.ts
+++ b/suite-native/navigation/src/routes.ts
@@ -20,6 +20,7 @@ export enum RootStackRoutes {
     WalletConnectPair = 'WalletConnectPair',
     SettingsScreenStack = 'SettingsScreenStack',
     DeviceCompromisedModal = 'DeviceCompromisedModal',
+    BackupFailedModal = 'BackupFailedModal',
     TradingWebView = 'TradingWebView',
 }
 
diff --git a/yarn.lock b/yarn.lock
index 1cbc0ed29ad1..7ae5b0b565d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10440,13 +10440,10 @@ __metadata:
     "@suite-common/wallet-core": "workspace:*"
     "@suite-native/atoms": "workspace:*"
     "@suite-native/device": "workspace:*"
-    "@suite-native/icons": "workspace:*"
     "@suite-native/intl": "workspace:*"
     "@suite-native/link": "workspace:*"
     "@suite-native/navigation": "workspace:*"
-    "@trezor/styles": "workspace:*"
     "@trezor/urls": "workspace:*"
-    jotai: "npm:1.9.1"
     react: "npm:18.2.0"
     react-native: "npm:0.76.9"
     react-redux: "npm:9.2.0"


Reviews:
user: login: BrantalikP
body: just nit things, other than that it works as expected 🚀
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:31:40Z
user: login: PeKne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T06:19:29Z
user: login: PeKne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T06:27:01Z
user: login: PeKne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T06:27:04Z
user: login: PeKne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T06:27:09Z
user: login: BrantalikP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:27:42Z
user: login: BrantalikP
body: well done 👏
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:28:34Z

Review comments:
diff_hunk: @@ -777,6 +777,16 @@ export const selectIsDeviceBackedUp = createMemoizedSelector(
     features => features?.backup_availability !== 'Required' && !features?.unfinished_backup,
 );
 
+export const selectHasDeviceNoBackup = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.backup_availability === 'Required',
path: suite-common/wallet-core/src/device/deviceReducer.ts
user: login: BrantalikP
body: Nit: Not sure if it’s accurate, but since `backup_availability` allows multiple types, I would explicitly name the selector `selectIsDeviceBackupRequired`, as we have a similar `selectIsDeviceBackedUp` that listens to a different part of the state.
created_at: 2025-05-07T14:30:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -777,6 +777,16 @@ export const selectIsDeviceBackedUp = createMemoizedSelector(
     features => features?.backup_availability !== 'Required' && !features?.unfinished_backup,
 );
 
+export const selectHasDeviceNoBackup = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.backup_availability === 'Required',
+);
+
+export const selectHasDeviceBackupFailed = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.unfinished_backup,
path: suite-common/wallet-core/src/device/deviceReducer.ts
user: login: BrantalikP
body: Nit: Similar to the above: Since the word fails indicates an error, I would prefer a different name like `selectIsDeviceBackupUnfinished`. Maybe it didn’t fail—just got canceled.
created_at: 2025-05-07T14:33:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+import { View } from 'react-native';
+
+import { useAtomValue } from 'jotai';
+
+import { HStack, Text, VStack } from '@suite-native/atoms';
+import { DeviceDangerBannerVariant, deviceDangerBannerAtom } from '@suite-native/device';
+import { Icon } from '@suite-native/icons';
+import { Translation, TxKeyPath } from '@suite-native/intl';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+
+import { DeviceDangerBannerExtension, bannerContentPresets } from './DeviceDangerBannerExtension';
+
+const containerStyle = prepareNativeStyle<{ bannerVariant: DeviceDangerBannerVariant }>(
+    (utils, { bannerVariant }) => ({
+        backgroundColor:
+            bannerVariant === 'other-error'
+                ? utils.colors.backgroundAlertYellowBold
+                : utils.colors.backgroundAlertRedSubtleOnElevation0,
+        // MessageSystemBanner critical variant has the same bgColor, so the margin serves to separate them visually
+        marginBottom: utils.spacings.sp1,
+    }),
+);
+
+const contentStyle = prepareNativeStyle<{ topSafeAreaInset: number }>(
+    (utils, { topSafeAreaInset }) => ({
+        marginTop: topSafeAreaInset,
+        paddingTop: utils.spacings.sp8,
+        paddingBottom: utils.spacings.sp16,
+        paddingHorizontal: utils.spacings.sp24,
+        alignItems: 'center',
+    }),
+);
+
+export const useIsDeviceDangerBannerVisible = () => useAtomValue(deviceDangerBannerAtom) !== null;
+
+type DeviceCompromisedBannerProps = { topSafeAreaInset: number };
+
+export const DeviceDangerBanner = ({ topSafeAreaInset }: DeviceCompromisedBannerProps) => {
+    const deviceDanger = useAtomValue(deviceDangerBannerAtom);
+
+    const { applyStyle } = useNativeStyles();
+
+    if (!deviceDanger) return null;
+    const { cause, variant } = deviceDanger;
+
+    const titleTranslationId: TxKeyPath =
+        variant === 'other-error'
+            ? 'generic.banners.deviceDanger.revisionNotChecked.title'
+            : bannerContentPresets[cause].title;
+
+    return (
+        <View style={applyStyle(containerStyle, { bannerVariant: variant })}>
path: suite-native/device/src/components/DeviceDangerBanner.tsx
user: login: BrantalikP
body: nit: let's use Box from atoms instead?
created_at: 2025-05-08T09:17:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+import { useEffect } from 'react';
+
+import { Button, IconListTextItem, TitleHeader, VStack } from '@suite-native/atoms';
+import { ContinueOnTrezorScreenContent } from '@suite-native/device';
+import { Translation } from '@suite-native/intl';
+import { useOpenLink } from '@suite-native/link';
+import {
+    RootStackParamList,
+    RootStackRoutes,
+    Screen,
+    ScreenHeader,
+    StackProps,
+} from '@suite-native/navigation';
+
+import { BACKUP_FAILED_SUPPORT_URL, useWipeDeviceAlert } from '../hooks/useWipeDeviceAlert';
+
+export const BackupFailedModalScreen = ({
+    navigation,
+}: StackProps<RootStackParamList, RootStackRoutes.BackupFailedModal>) => {
+    const openLink = useOpenLink();
+    const { showWipeDeviceAlert, isWipeInProgress } = useWipeDeviceAlert();
+
+    const handleSecondaryButtonPress = () => openLink(BACKUP_FAILED_SUPPORT_URL);
+
+    useEffect(() => {
+        const unsubscribe = navigation.addListener('beforeRemove', e => {
path: suite-native/module-device-onboarding/src/screens/BackupFailedModalScreen.tsx
user: login: BrantalikP
body: IMO there should not be any dependency when working with listeners. 
created_at: 2025-05-08T11:29:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+import { useEffect } from 'react';
+
+import { Button, IconListTextItem, TitleHeader, VStack } from '@suite-native/atoms';
+import { ContinueOnTrezorScreenContent } from '@suite-native/device';
+import { Translation } from '@suite-native/intl';
+import { useOpenLink } from '@suite-native/link';
+import {
+    RootStackParamList,
+    RootStackRoutes,
+    Screen,
+    ScreenHeader,
+    StackProps,
+} from '@suite-native/navigation';
+
+import { BACKUP_FAILED_SUPPORT_URL, useWipeDeviceAlert } from '../hooks/useWipeDeviceAlert';
+
+export const BackupFailedModalScreen = ({
+    navigation,
+}: StackProps<RootStackParamList, RootStackRoutes.BackupFailedModal>) => {
+    const openLink = useOpenLink();
+    const { showWipeDeviceAlert, isWipeInProgress } = useWipeDeviceAlert();
+
+    const handleSecondaryButtonPress = () => openLink(BACKUP_FAILED_SUPPORT_URL);
+
+    useEffect(() => {
+        const unsubscribe = navigation.addListener('beforeRemove', e => {
path: suite-native/module-device-onboarding/src/screens/BackupFailedModalScreen.tsx
user: login: PeKne
body: Why? Then it would not work; it would never correctly react to the `isWipeInProgress` state. Since there is a return function used for the cleanup of the useEffect, I don't see any problem with having a dependency. 
created_at: 2025-05-12T06:19:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -777,6 +777,16 @@ export const selectIsDeviceBackedUp = createMemoizedSelector(
     features => features?.backup_availability !== 'Required' && !features?.unfinished_backup,
 );
 
+export const selectHasDeviceNoBackup = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.backup_availability === 'Required',
path: suite-common/wallet-core/src/device/deviceReducer.ts
user: login: PeKne
body: fixed a871a90ec3
created_at: 2025-05-12T06:27:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -777,6 +777,16 @@ export const selectIsDeviceBackedUp = createMemoizedSelector(
     features => features?.backup_availability !== 'Required' && !features?.unfinished_backup,
 );
 
+export const selectHasDeviceNoBackup = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.backup_availability === 'Required',
+);
+
+export const selectHasDeviceBackupFailed = createMemoizedSelector(
+    [selectDeviceFeatures],
+    features => features?.unfinished_backup,
path: suite-common/wallet-core/src/device/deviceReducer.ts
user: login: PeKne
body: fixed a871a90ec3
created_at: 2025-05-12T06:27:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+import { View } from 'react-native';
+
+import { useAtomValue } from 'jotai';
+
+import { HStack, Text, VStack } from '@suite-native/atoms';
+import { DeviceDangerBannerVariant, deviceDangerBannerAtom } from '@suite-native/device';
+import { Icon } from '@suite-native/icons';
+import { Translation, TxKeyPath } from '@suite-native/intl';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+
+import { DeviceDangerBannerExtension, bannerContentPresets } from './DeviceDangerBannerExtension';
+
+const containerStyle = prepareNativeStyle<{ bannerVariant: DeviceDangerBannerVariant }>(
+    (utils, { bannerVariant }) => ({
+        backgroundColor:
+            bannerVariant === 'other-error'
+                ? utils.colors.backgroundAlertYellowBold
+                : utils.colors.backgroundAlertRedSubtleOnElevation0,
+        // MessageSystemBanner critical variant has the same bgColor, so the margin serves to separate them visually
+        marginBottom: utils.spacings.sp1,
+    }),
+);
+
+const contentStyle = prepareNativeStyle<{ topSafeAreaInset: number }>(
+    (utils, { topSafeAreaInset }) => ({
+        marginTop: topSafeAreaInset,
+        paddingTop: utils.spacings.sp8,
+        paddingBottom: utils.spacings.sp16,
+        paddingHorizontal: utils.spacings.sp24,
+        alignItems: 'center',
+    }),
+);
+
+export const useIsDeviceDangerBannerVisible = () => useAtomValue(deviceDangerBannerAtom) !== null;
+
+type DeviceCompromisedBannerProps = { topSafeAreaInset: number };
+
+export const DeviceDangerBanner = ({ topSafeAreaInset }: DeviceCompromisedBannerProps) => {
+    const deviceDanger = useAtomValue(deviceDangerBannerAtom);
+
+    const { applyStyle } = useNativeStyles();
+
+    if (!deviceDanger) return null;
+    const { cause, variant } = deviceDanger;
+
+    const titleTranslationId: TxKeyPath =
+        variant === 'other-error'
+            ? 'generic.banners.deviceDanger.revisionNotChecked.title'
+            : bannerContentPresets[cause].title;
+
+    return (
+        <View style={applyStyle(containerStyle, { bannerVariant: variant })}>
path: suite-native/device/src/components/DeviceDangerBanner.tsx
user: login: PeKne
body: fixed a871a90ec3
created_at: 2025-05-12T06:27:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+import { useEffect } from 'react';
+
+import { Button, IconListTextItem, TitleHeader, VStack } from '@suite-native/atoms';
+import { ContinueOnTrezorScreenContent } from '@suite-native/device';
+import { Translation } from '@suite-native/intl';
+import { useOpenLink } from '@suite-native/link';
+import {
+    RootStackParamList,
+    RootStackRoutes,
+    Screen,
+    ScreenHeader,
+    StackProps,
+} from '@suite-native/navigation';
+
+import { BACKUP_FAILED_SUPPORT_URL, useWipeDeviceAlert } from '../hooks/useWipeDeviceAlert';
+
+export const BackupFailedModalScreen = ({
+    navigation,
+}: StackProps<RootStackParamList, RootStackRoutes.BackupFailedModal>) => {
+    const openLink = useOpenLink();
+    const { showWipeDeviceAlert, isWipeInProgress } = useWipeDeviceAlert();
+
+    const handleSecondaryButtonPress = () => openLink(BACKUP_FAILED_SUPPORT_URL);
+
+    useEffect(() => {
+        const unsubscribe = navigation.addListener('beforeRemove', e => {
path: suite-native/module-device-onboarding/src/screens/BackupFailedModalScreen.tsx
user: login: BrantalikP
body: oh, ok its intentional to actual mount/unmount the listener, I wrongly understood the functionality. resolved 👌
created_at: 2025-05-12T15:27:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: trezor-bot[bot]
created_at: 2025-05-12T06:31:34Z
body: ✅ Previously successful run of **[Test] PR Suite Web e2e tests** workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)
author_association: CONTRIBUTOR
user: login: trezor-bot[bot]
created_at: 2025-05-12T06:31:36Z
body: ✅ Previously successful run of **[Test] PR Suite Desktop e2e tests** workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)
author_association: CONTRIBUTOR
user: login: PeKne
created_at: 2025-05-13T05:48:05Z
body: /rebase
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-13T05:48:23Z
body: Start rebasing: https://github.com/trezor/trezor-suite/actions/runs/14989173337
author_association: NONE
user: login: trezor-bot[bot]
created_at: 2025-05-13T05:49:30Z
body: ✅ Previously successful run of **[Test] PR Suite Web e2e tests** workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)
author_association: CONTRIBUTOR
user: login: trezor-bot[bot]
created_at: 2025-05-13T05:49:31Z
body: ✅ Previously successful run of **[Test] PR Suite Desktop e2e tests** workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)
author_association: CONTRIBUTOR