diff --git a/suite-native/app/src/navigation/RootStackNavigator.tsx b/suite-native/app/src/navigation/RootStackNavigator.tsx
index 7c45eb4e61e..21e90b75323 100644
--- a/suite-native/app/src/navigation/RootStackNavigator.tsx
+++ b/suite-native/app/src/navigation/RootStackNavigator.tsx
@@ -41,7 +41,7 @@ export const RootStackNavigator = () => {
return RootStackRoutes.AppTabs;
}
- return RootStackRoutes.Onboarding;
+ return RootStackRoutes.OnboardingStack;
};
return (
@@ -49,16 +49,7 @@ export const RootStackNavigator = () => {
initialRouteName={getInitialRouteName()}
screenOptions={stackNavigationOptionsConfig}
>
-
-
{
name={RootStackRoutes.DevUtilsStack}
component={DevUtilsStackNavigator}
/>
-
-
-
-
-
+ {/* Navigation flows that start by push from bottom animation on the first screen of its stack. */}
+
+
+
+
+
+
+
+
+
);
};
diff --git a/suite-native/device/src/hooks/useDetectDeviceError.tsx b/suite-native/device/src/hooks/useDetectDeviceError.tsx
index 6e006071abd..e25e0154f12 100644
--- a/suite-native/device/src/hooks/useDetectDeviceError.tsx
+++ b/suite-native/device/src/hooks/useDetectDeviceError.tsx
@@ -34,7 +34,11 @@ import { BootloaderModalAppendix } from '../components/BootloaderModalAppendix';
import { IncompatibleFirmwareModalAppendix } from '../components/IncompatibleFirmwareModalAppendix';
import { UnacquiredDeviceModalAppendix } from '../components/UnacquiredDeviceModalAppendix';
import { UninitializedDeviceModalAppendix } from '../components/UninitializedDeviceModalAppendix';
-import { selectDeviceError, selectIsDeviceFirmwareSupported } from '../selectors';
+import {
+ selectDeviceError,
+ selectIsDeviceFirmwareSupported,
+ selectIsDeviceSetupSupported,
+} from '../selectors';
export const SUITE_WEB_URL = 'https://suite.trezor.io/web/';
@@ -61,6 +65,7 @@ export const useDetectDeviceError = () => {
const isFirmwareInstallationRunning = useSelector(selectIsFirmwareInstallationRunning);
const hasDeviceFirmwareInstalled = useSelector(selectHasDeviceFirmwareInstalled);
const isOnboardingFinished = useSelector(selectIsOnboardingFinished);
+ const isDeviceSetupSupported = useSelector(selectIsDeviceSetupSupported);
const isDeviceFirmwareSupported = useSelector(selectIsDeviceFirmwareSupported);
const deviceError = useSelector(selectDeviceError);
@@ -103,7 +108,8 @@ export const useDetectDeviceError = () => {
!isDeviceFirmwareSupported &&
isOnboardingFinished &&
!isPortfolioTrackerDevice &&
- !wasDeviceEjectedByUser
+ !wasDeviceEjectedByUser &&
+ !isDeviceSetupSupported
) {
showAlert({
title: ,
@@ -130,10 +136,11 @@ export const useDetectDeviceError = () => {
dispatch,
showAlert,
handleDisconnect,
+ isDeviceSetupSupported,
]);
useEffect(() => {
- if (!isOnboardingFinished) return;
+ if (!isOnboardingFinished || isDeviceSetupSupported) return;
if (
isConnectedDeviceUninitialized &&
@@ -194,6 +201,7 @@ export const useDetectDeviceError = () => {
openLink,
deviceError,
handleDisconnect,
+ isDeviceSetupSupported,
]);
useEffect(() => {
diff --git a/suite-native/device/src/hooks/useHandleDeviceConnection.ts b/suite-native/device/src/hooks/useHandleDeviceConnection.ts
index 35bedf2155e..c570b0fd28e 100644
--- a/suite-native/device/src/hooks/useHandleDeviceConnection.ts
+++ b/suite-native/device/src/hooks/useHandleDeviceConnection.ts
@@ -7,6 +7,7 @@ import {
authorizeDeviceThunk,
selectIsDeviceConnected,
selectIsDeviceConnectedAndAuthorized,
+ selectIsDeviceInitialized,
selectIsDeviceRemembered,
selectIsDeviceUsingPassphrase,
selectIsNoPhysicalDeviceConnected,
@@ -21,15 +22,18 @@ import {
AuthorizeDeviceStackParamList,
AuthorizeDeviceStackRoutes,
HomeStackRoutes,
+ OnboardingStackRoutes,
RootStackParamList,
RootStackRoutes,
StackToStackCompositeNavigationProps,
} from '@suite-native/navigation';
import { selectIsOnboardingFinished } from '@suite-native/settings';
+import { selectIsDeviceSetupSupported } from '../selectors';
+
type NavigationProp = StackToStackCompositeNavigationProps<
AuthorizeDeviceStackParamList | RootStackParamList,
- AuthorizeDeviceStackRoutes.PinMatrix | RootStackRoutes.Onboarding,
+ AuthorizeDeviceStackRoutes.PinMatrix | RootStackRoutes.OnboardingStack,
RootStackParamList
>;
@@ -41,9 +45,12 @@ export const useHandleDeviceConnection = () => {
const isDeviceConnectedAndAuthorized = useSelector(selectIsDeviceConnectedAndAuthorized);
const hasDeviceRequestedPin = useSelector(selectDeviceRequestedPin);
const isDeviceConnected = useSelector(selectIsDeviceConnected);
- const { isBiometricsOverlayVisible } = useIsBiometricsOverlayVisible();
+ const isDeviceInitialized = useSelector(selectIsDeviceInitialized);
const isDeviceUsingPassphrase = useSelector(selectIsDeviceUsingPassphrase);
const isFirmwareInstallationRunning = useSelector(selectIsFirmwareInstallationRunning);
+ const isDeviceSetupSupported = useSelector(selectIsDeviceSetupSupported);
+
+ const { isBiometricsOverlayVisible } = useIsBiometricsOverlayVisible();
const navigation = useNavigation();
const dispatch = useDispatch();
@@ -52,12 +59,46 @@ export const useHandleDeviceConnection = () => {
const isSendStackFocused = lastRoute === RootStackRoutes.SendStack;
const shouldBlockSendReviewRedirect = isDeviceRemembered && isSendStackFocused;
+ // When is an uninitialized device model that supports device setup, navigate to device onboarding.
+ useEffect(() => {
+ if (
+ isDeviceSetupSupported &&
+ !isDeviceInitialized &&
+ isDeviceConnected &&
+ isOnboardingFinished &&
+ !isPortfolioTrackerDevice &&
+ !isBiometricsOverlayVisible
+ ) {
+ requestPrioritizedDeviceAccess({
+ deviceCallback: () => dispatch(authorizeDeviceThunk()),
+ });
+
+ if (!isDeviceInitialized) {
+ navigation.navigate(RootStackRoutes.OnboardingStack, {
+ screen: OnboardingStackRoutes.UninitializedDeviceLanding,
+ });
+
+ return;
+ }
+ }
+ }, [
+ dispatch,
+ isDeviceConnected,
+ isOnboardingFinished,
+ isBiometricsOverlayVisible,
+ navigation,
+ isDeviceInitialized,
+ isPortfolioTrackerDevice,
+ isDeviceSetupSupported,
+ ]);
+
// At the moment when unauthorized physical device is selected,
// redirect to the Connecting screen where is handled the connection logic.
useEffect(() => {
if (isFirmwareInstallationRunning) return;
if (
+ isDeviceInitialized &&
isDeviceConnected &&
isOnboardingFinished &&
!isPortfolioTrackerDevice &&
@@ -88,6 +129,7 @@ export const useHandleDeviceConnection = () => {
isDeviceUsingPassphrase,
shouldBlockSendReviewRedirect,
isFirmwareInstallationRunning,
+ isDeviceInitialized,
]);
// In case that the physical device is disconnected, redirect to the home screen and
@@ -96,12 +138,7 @@ export const useHandleDeviceConnection = () => {
if (isFirmwareInstallationRunning) return;
if (isNoPhysicalDeviceConnected && isOnboardingFinished) {
- const previousRoute = navigation.getState()?.routes.at(-1)?.name;
-
- // This accidentally gets triggered by finishing onboarding with no device connected,
- // so this prevents from redirect being duplicated.
- const isPreviousRouteOnboarding = previousRoute === RootStackRoutes.Onboarding;
- if (isPreviousRouteOnboarding || shouldBlockSendReviewRedirect) {
+ if (shouldBlockSendReviewRedirect) {
return;
}
navigation.navigate(RootStackRoutes.AppTabs, {
diff --git a/suite-native/device/src/index.ts b/suite-native/device/src/index.ts
index b21ffa3c21b..7d286134363 100644
--- a/suite-native/device/src/index.ts
+++ b/suite-native/device/src/index.ts
@@ -12,3 +12,4 @@ export * from './utils';
export * from './selectors';
export * from './deviceThunks';
export * from './config/firmware';
+export * from './types';
diff --git a/suite-native/device/src/selectors.ts b/suite-native/device/src/selectors.ts
index 3bcefe803ab..15f35153d51 100644
--- a/suite-native/device/src/selectors.ts
+++ b/suite-native/device/src/selectors.ts
@@ -36,7 +36,7 @@ import { doesCoinSupportStaking } from '@suite-native/staking';
import { BigNumber } from '@trezor/utils';
import { revisionCheckErrorScenarios } from './config/firmware';
-import { isFirmwareVersionSupported } from './utils';
+import { isDeviceSetupSupported, isFirmwareVersionSupported } from './utils';
type NativeDeviceRootState = DeviceRootState &
AccountsRootState &
@@ -185,3 +185,8 @@ export const selectHasFirmwareAuthenticityCheckHardFailed = (state: FwAuthentici
return isRevisionHardError;
};
+
+export const selectIsDeviceSetupSupported = createMemoizedSelector(
+ [selectDeviceModel],
+ model => !!model && isDeviceSetupSupported(model),
+);
diff --git a/suite-native/device/src/types.ts b/suite-native/device/src/types.ts
new file mode 100644
index 00000000000..9232c8735e5
--- /dev/null
+++ b/suite-native/device/src/types.ts
@@ -0,0 +1,9 @@
+import { DeviceModelInternal } from '@trezor/connect';
+
+export type SetupSupportingDeviceModel = Exclude<
+ DeviceModelInternal,
+ | DeviceModelInternal.T1B1
+ | DeviceModelInternal.T2T1
+ | DeviceModelInternal.T3W1
+ | DeviceModelInternal.UNKNOWN
+>;
diff --git a/suite-native/device/src/utils.ts b/suite-native/device/src/utils.ts
index 0758b93adb7..0bd5c6aa78c 100644
--- a/suite-native/device/src/utils.ts
+++ b/suite-native/device/src/utils.ts
@@ -2,6 +2,7 @@ import { G } from '@mobily/ts-belt';
import { AnyAction } from '@reduxjs/toolkit';
import * as semver from 'semver';
+import { UnreachableCaseError } from '@suite-common/suite-utils';
import { DEVICE, Device, DeviceEvent, DeviceModelInternal, VersionArray } from '@trezor/connect';
export const minimalSupportedFirmwareVersion = {
@@ -37,3 +38,20 @@ export const isDeviceEventAction = (
action: AnyAction,
actionType: T,
): action is { type: T; payload: Device } => action.type === actionType;
+
+export const isDeviceSetupSupported = (model: DeviceModelInternal) => {
+ // Exhaustive check for case that new model is introduced later it won't be forgotten.
+ switch (model) {
+ case DeviceModelInternal.T2B1:
+ case DeviceModelInternal.T3B1:
+ case DeviceModelInternal.T3T1:
+ return true;
+ case DeviceModelInternal.T1B1:
+ case DeviceModelInternal.T2T1:
+ case DeviceModelInternal.T3W1:
+ case DeviceModelInternal.UNKNOWN:
+ return false;
+ default:
+ throw new UnreachableCaseError(model);
+ }
+};
diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts
index ad3b806f22b..8e110e5b1fd 100644
--- a/suite-native/intl/src/en.ts
+++ b/suite-native/intl/src/en.ts
@@ -825,6 +825,21 @@ export const en = {
notNow: 'Not now',
},
},
+
+ uninitializedDeviceLandingScreen: {
+ noFirmware: {
+ title: 'Now it’s just you\nand your crypto',
+ button: "Let's get started",
+ },
+ firmware: {
+ title: 'Have you used this Trezor before?',
+ subtitle:
+ 'Firmware is already installed on this Trezor. Continue only if you have used this Trezor before.',
+ confirmButton: 'Yes, set up my Trezor',
+ noButton: 'No, I have not',
+ },
+ lookDifferentLabel: 'My device looks different',
+ },
},
moduleAccountManagement: {
accountSettingsScreen: {
diff --git a/suite-native/module-onboarding/package.json b/suite-native/module-onboarding/package.json
index e3ec921e91c..d1e10db9433 100644
--- a/suite-native/module-onboarding/package.json
+++ b/suite-native/module-onboarding/package.json
@@ -13,15 +13,18 @@
"@react-navigation/core": "^6.4.10",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
+ "@reduxjs/toolkit": "1.9.5",
"@suite-native/atoms": "workspace:*",
"@suite-native/icons": "workspace:*",
"@suite-native/intl": "workspace:*",
"@suite-native/navigation": "workspace:*",
+ "@trezor/connect": "workspace:*",
"@trezor/env-utils": "workspace:*",
"@trezor/styles": "workspace:*",
"expo-linear-gradient": "^14.0.1",
"react": "18.2.0",
"react-native": "0.76.1",
+ "react-native-svg": "^15.9.0",
"react-redux": "8.0.7"
}
}
diff --git a/suite-native/module-onboarding/redux.d.ts b/suite-native/module-onboarding/redux.d.ts
new file mode 100644
index 00000000000..df9a0c3f969
--- /dev/null
+++ b/suite-native/module-onboarding/redux.d.ts
@@ -0,0 +1,7 @@
+import { AsyncThunkAction } from '@reduxjs/toolkit';
+
+declare module 'redux' {
+ export interface Dispatch {
+ >(thunk: TThunk): ReturnType;
+ }
+}
diff --git a/suite-native/module-onboarding/src/assets/t3b1.png b/suite-native/module-onboarding/src/assets/t3b1.png
new file mode 100644
index 00000000000..31943568425
Binary files /dev/null and b/suite-native/module-onboarding/src/assets/t3b1.png differ
diff --git a/suite-native/module-onboarding/src/assets/t3t1.png b/suite-native/module-onboarding/src/assets/t3t1.png
new file mode 100644
index 00000000000..9e0a0488c92
Binary files /dev/null and b/suite-native/module-onboarding/src/assets/t3t1.png differ
diff --git a/suite-native/module-onboarding/src/components/HeaderUnderlineSvg.tsx b/suite-native/module-onboarding/src/components/HeaderUnderlineSvg.tsx
new file mode 100644
index 00000000000..e56ca125660
--- /dev/null
+++ b/suite-native/module-onboarding/src/components/HeaderUnderlineSvg.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import Svg, { Path, SvgProps } from 'react-native-svg';
+
+import { useIllustrationColors } from '@suite-native/atoms';
+
+export const HeaderUnderlineSvg = (props: SvgProps) => {
+ const { lineColor } = useIllustrationColors();
+
+ return (
+
+ );
+};
diff --git a/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx b/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx
index a0ea19adcdc..08cf84e32a4 100644
--- a/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx
+++ b/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx
@@ -8,6 +8,7 @@ import {
import { AnalyticsConsentScreen } from '../screens/AnalyticsConsentScreen';
import { BiometricsScreen } from '../screens/BiometricsScreen';
+import { UninitializedDeviceLandingScreen } from '../screens/UninitializedDeviceLandingScreen';
import { WelcomeScreen } from '../screens/WelcomeScreen';
export const OnboardingStack = createNativeStackNavigator();
@@ -26,5 +27,9 @@ export const OnboardingStackNavigator = () => (
name={OnboardingStackRoutes.Biometrics}
component={BiometricsScreen}
/>
+
);
diff --git a/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx b/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx
new file mode 100644
index 00000000000..543ce5deb25
--- /dev/null
+++ b/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx
@@ -0,0 +1,118 @@
+import { useSelector } from 'react-redux';
+
+import { selectDeviceModel, selectHasDeviceFirmwareInstalled } from '@suite-common/wallet-core';
+import { Box, Button, Image, Text, TextButton, TitleHeader, VStack } from '@suite-native/atoms';
+import { SetupSupportingDeviceModel } from '@suite-native/device';
+import { Translation } from '@suite-native/intl';
+import { Screen, ScreenHeader } from '@suite-native/navigation';
+import { useToast } from '@suite-native/toasts';
+import { DeviceModelInternal } from '@trezor/connect';
+import { getScreenHeight } from '@trezor/env-utils';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+
+import { HeaderUnderlineSvg } from '../components/HeaderUnderlineSvg';
+
+const trezorImageStyle = prepareNativeStyle<{ hasDeviceFirmwareInstalled: boolean }>(
+ (_, { hasDeviceFirmwareInstalled }) => ({
+ width: '100%',
+ height: hasDeviceFirmwareInstalled ? 280 : 360,
+ maxHeight: (getScreenHeight() / 3) * 2,
+ alignItems: 'center',
+ }),
+);
+
+const trezorModelImageMap = {
+ [DeviceModelInternal.T2B1]: require('../assets/t3b1.png'),
+ [DeviceModelInternal.T3B1]: require('../assets/t3b1.png'),
+ [DeviceModelInternal.T3T1]: require('../assets/t3t1.png'),
+} as const satisfies Record;
+
+const UninitializedDeviceLandingScreenContent = () => {
+ const { applyStyle } = useNativeStyles();
+ const deviceModel = useSelector(selectDeviceModel) as SetupSupportingDeviceModel;
+ const hasDeviceFirmwareInstalled = useSelector(selectHasDeviceFirmwareInstalled);
+
+ if (!deviceModel) {
+ return null;
+ }
+
+ return (
+
+ {hasDeviceFirmwareInstalled ? (
+
+ }
+ titleVariant="titleMedium"
+ subtitle={
+
+ }
+ />
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export const UninitializedDeviceLandingScreen = () => {
+ const { showToast } = useToast();
+ const hasDeviceFirmwareInstalled = useSelector(selectHasDeviceFirmwareInstalled);
+
+ const handleConfirmButtonPress = () => {
+ // TODO: navigate to next onboarding screen based on firmware status
+ showToast({ variant: 'warning', message: 'TODO: implement next screen' });
+ };
+
+ const handleDeclineButtonPress = () => {
+ // TODO: navigate to screen where user can see more info regarding tampered devices.
+ showToast({ variant: 'warning', message: 'TODO: implement next screen' });
+ };
+
+ return (
+ // TODO: add handling of close button event
+ }>
+
+
+
+
+
+
+
+
+
+ {hasDeviceFirmwareInstalled && (
+
+ )}
+
+
+
+ );
+};
diff --git a/suite-native/module-onboarding/tsconfig.json b/suite-native/module-onboarding/tsconfig.json
index 261062d51ba..d67c01e421f 100644
--- a/suite-native/module-onboarding/tsconfig.json
+++ b/suite-native/module-onboarding/tsconfig.json
@@ -6,6 +6,7 @@
{ "path": "../icons" },
{ "path": "../intl" },
{ "path": "../navigation" },
+ { "path": "../../packages/connect" },
{ "path": "../../packages/env-utils" },
{ "path": "../../packages/styles" }
]
diff --git a/suite-native/navigation/src/navigators.ts b/suite-native/navigation/src/navigators.ts
index 02da8ea4170..f4e7b4eb52b 100644
--- a/suite-native/navigation/src/navigators.ts
+++ b/suite-native/navigation/src/navigators.ts
@@ -112,6 +112,7 @@ export type OnboardingStackParamList = {
[OnboardingStackRoutes.Welcome]: undefined;
[OnboardingStackRoutes.AnalyticsConsent]: undefined;
[OnboardingStackRoutes.Biometrics]: undefined;
+ [OnboardingStackRoutes.UninitializedDeviceLanding]: undefined;
};
export type AccountsImportStackParamList = {
@@ -193,7 +194,7 @@ export type AuthorizeDeviceStackParamList = {
export type RootStackParamList = {
[RootStackRoutes.AppTabs]: NavigatorScreenParams;
- [RootStackRoutes.Onboarding]: NavigatorScreenParams;
+ [RootStackRoutes.OnboardingStack]: NavigatorScreenParams;
[RootStackRoutes.AuthorizeDeviceStack]: NavigatorScreenParams;
[RootStackRoutes.AccountsImport]: NavigatorScreenParams;
[RootStackRoutes.AccountSettings]: { accountKey: AccountKey };
diff --git a/suite-native/navigation/src/routes.ts b/suite-native/navigation/src/routes.ts
index e8f24416e53..298fa5b139c 100644
--- a/suite-native/navigation/src/routes.ts
+++ b/suite-native/navigation/src/routes.ts
@@ -1,7 +1,7 @@
export enum RootStackRoutes {
AppTabs = 'AppTabs',
LegacyOnboarding = 'LegacyOnboarding',
- Onboarding = 'Onboarding',
+ OnboardingStack = 'OnboardingStack',
AccountsImport = 'AccountsImport',
AuthorizeDeviceStack = 'AuthorizeDeviceStack',
AccountDetail = 'AccountDetail',
@@ -29,6 +29,7 @@ export enum OnboardingStackRoutes {
Welcome = 'Welcome',
AnalyticsConsent = 'AnalyticsConsent',
Biometrics = 'Biometrics',
+ UninitializedDeviceLanding = 'UninitializedDeviceLanding',
}
export enum AccountsImportStackRoutes {
diff --git a/yarn.lock b/yarn.lock
index 138f0265f67..88be7ffaf7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10807,15 +10807,18 @@ __metadata:
"@react-navigation/core": "npm:^6.4.10"
"@react-navigation/native": "npm:6.1.18"
"@react-navigation/native-stack": "npm:6.11.0"
+ "@reduxjs/toolkit": "npm:1.9.5"
"@suite-native/atoms": "workspace:*"
"@suite-native/icons": "workspace:*"
"@suite-native/intl": "workspace:*"
"@suite-native/navigation": "workspace:*"
+ "@trezor/connect": "workspace:*"
"@trezor/env-utils": "workspace:*"
"@trezor/styles": "workspace:*"
expo-linear-gradient: "npm:^14.0.1"
react: "npm:18.2.0"
react-native: "npm:0.76.1"
+ react-native-svg: "npm:^15.9.0"
react-redux: "npm:8.0.7"
languageName: unknown
linkType: soft