diff --git a/jest.config.native.js b/jest.config.native.js index a82ee2d6cb0..0a08ed3f1eb 100644 --- a/jest.config.native.js +++ b/jest.config.native.js @@ -24,12 +24,15 @@ module.exports = { '\\.(js|jsx|ts|tsx)$': ['babel-jest', babelConfig], }, transformIgnorePatterns: [ - 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)', + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@shopify/react-native-skia)', ], setupFiles: [ '/../../node_modules/@shopify/react-native-skia/jestSetup.js', '/../../node_modules/react-native-gesture-handler/jestSetup.js', '/../../suite-native/test-utils/src/atomsMock.js', '/../../suite-native/test-utils/src/expoMock.js', + '/../../suite-native/firmware/src/jestSetup.js', + '/../../suite-native/connection-status/src/jestSetup.js', + '/../../suite-native/react-native-graph/src/jestSetup.js', ], }; diff --git a/suite-common/icons/generateIconFont.ts b/suite-common/icons/generateIconFont.ts index 92331abac8c..3d109e628a9 100644 --- a/suite-common/icons/generateIconFont.ts +++ b/suite-common/icons/generateIconFont.ts @@ -18,6 +18,7 @@ const usedIcons = [ 'arrowLineUpRight', 'arrowRight', 'arrowsCounterClockwise', + 'arrowsLeftRight', 'arrowSquareOut', 'arrowUp', 'arrowUpRight', diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json index aa54fd7a014..5bc84d79bd8 100644 --- a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json +++ b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json @@ -89,14 +89,15 @@ "bugBeetle": 61784, "bookmarkSimple": 61785, "backspace": 61786, - "arrowsCounterClockwise": 61787, - "arrowUpRight": 61788, - "arrowUp": 61789, - "arrowURightDown": 61790, - "arrowSquareOut": 61791, - "arrowRight": 61792, - "arrowLineUpRight": 61793, - "arrowLineUp": 61794, - "arrowLineDown": 61795, - "arrowDown": 61796 + "arrowsLeftRight": 61787, + "arrowsCounterClockwise": 61788, + "arrowUpRight": 61789, + "arrowUp": 61790, + "arrowURightDown": 61791, + "arrowSquareOut": 61792, + "arrowRight": 61793, + "arrowLineUpRight": 61794, + "arrowLineUp": 61795, + "arrowLineDown": 61796, + "arrowDown": 61797 } diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf index 0077f90e0d2..8002f2c04e4 100644 Binary files a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf and b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf differ diff --git a/suite-native/accounts/package.json b/suite-native/accounts/package.json index 4bcefd84231..dca4445690d 100644 --- a/suite-native/accounts/package.json +++ b/suite-native/accounts/package.json @@ -13,6 +13,7 @@ "dependencies": { "@mobily/ts-belt": "^3.13.1", "@react-navigation/native": "6.1.18", + "@reduxjs/toolkit": "1.9.5", "@suite-common/formatters": "workspace:*", "@suite-common/redux-utils": "workspace:*", "@suite-common/token-definitions": "workspace:*", diff --git a/suite-native/accounts/redux.d.ts b/suite-native/accounts/redux.d.ts new file mode 100644 index 00000000000..df9a0c3f969 --- /dev/null +++ b/suite-native/accounts/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/app/.gitignore b/suite-native/app/.gitignore index ce7ecb2e622..c8a64fabab5 100644 --- a/suite-native/app/.gitignore +++ b/suite-native/app/.gitignore @@ -27,6 +27,7 @@ android/ npm-debug.* yarn-debug.* yarn-error.* +/reports/junit-report.xml # macOS .DS_Store diff --git a/suite-native/app/e2e/jest.config.js b/suite-native/app/e2e/jest.config.js index b2268644eaf..525e17b97fa 100644 --- a/suite-native/app/e2e/jest.config.js +++ b/suite-native/app/e2e/jest.config.js @@ -15,4 +15,5 @@ module.exports = { verbose: true, maxWorkers: 1, setupFilesAfterEnv: ['/e2e/jest.setup.ts'], + testMatch: ['/e2e/tests/*.test.ts'], }; diff --git a/suite-native/app/package.json b/suite-native/app/package.json index 1d07e01300c..878a8ace124 100644 --- a/suite-native/app/package.json +++ b/suite-native/app/package.json @@ -69,6 +69,7 @@ "@suite-native/module-send": "workspace:*", "@suite-native/module-settings": "workspace:*", "@suite-native/module-staking-management": "workspace:*", + "@suite-native/module-trading": "workspace:*", "@suite-native/navigation": "workspace:*", "@suite-native/notifications": "workspace:*", "@suite-native/receive": "workspace:*", diff --git a/suite-native/app/src/navigation/AppTabNavigator.tsx b/suite-native/app/src/navigation/AppTabNavigator.tsx index 1bd9e2f9c60..6de1e06761b 100644 --- a/suite-native/app/src/navigation/AppTabNavigator.tsx +++ b/suite-native/app/src/navigation/AppTabNavigator.tsx @@ -1,3 +1,5 @@ +import { useSelector } from 'react-redux'; + import { BottomTabBarProps, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { HomeStackNavigator } from '@suite-native/module-home'; @@ -5,6 +7,8 @@ import { AccountsStackNavigator } from '@suite-native/module-accounts-management import { SettingsScreen } from '@suite-native/module-settings'; import { AppTabsParamList, AppTabsRoutes, TabBar } from '@suite-native/navigation'; import { useHandleDeviceRequestsPassphrase } from '@suite-native/device-authorization'; +import { createSelectIsFeatureFlagEnabled, FeatureFlag } from '@suite-native/feature-flags'; +import { TradingStackNavigator } from '@suite-native/module-trading'; import { rootTabsOptions } from './routes'; @@ -13,6 +17,10 @@ const Tab = createBottomTabNavigator(); export const AppTabNavigator = () => { useHandleDeviceRequestsPassphrase(); + const isTradingEnabled = useSelector( + createSelectIsFeatureFlagEnabled(FeatureFlag.IsTradingEnabled), + ); + return ( { > + {isTradingEnabled && ( + + )} ); diff --git a/suite-native/app/src/navigation/__tests__/AppTabNavigator.comp.test.tsx b/suite-native/app/src/navigation/__tests__/AppTabNavigator.comp.test.tsx new file mode 100644 index 00000000000..10174cd308e --- /dev/null +++ b/suite-native/app/src/navigation/__tests__/AppTabNavigator.comp.test.tsx @@ -0,0 +1,46 @@ +import { renderWithStore, waitFor, PreloadedState, fireEvent } from '@suite-native/test-utils'; +import { FeatureFlag, featureFlagsInitialState } from '@suite-native/feature-flags'; + +import { AppTabNavigator } from '../AppTabNavigator'; + +describe('AppTabNavigator', () => { + const renderTabs = async (preloadedState?: PreloadedState) => { + const result = renderWithStore(, { preloadedState }); + await waitFor(() => expect(result.getByText('Home')).toBeDefined()); + + return result; + }; + + it('should render 3 buttons', async () => { + const { getByText } = await renderTabs(); + + expect(getByText('Home')).toBeDefined(); + expect(getByText('My assets')).toBeDefined(); + expect(getByText('Settings')).toBeDefined(); + }); + + it('should not render Trade tab when IsTradingEnabled is false', async () => { + const { queryByText } = await renderTabs({ + featureFlags: { + ...featureFlagsInitialState, + [FeatureFlag.IsTradingEnabled]: false, + }, + }); + + expect(queryByText('Trade')).toBe(null); + }); + + it('should render Trade tab when IsTradingEnabled is true', async () => { + const { getByText } = await renderTabs({ + featureFlags: { + ...featureFlagsInitialState, + [FeatureFlag.IsTradingEnabled]: true, + }, + }); + + const tradeTab = getByText('Trade'); + fireEvent.press(tradeTab); + + expect(getByText('Trading placeholder')).toBeDefined(); + }); +}); diff --git a/suite-native/app/src/navigation/routes.ts b/suite-native/app/src/navigation/routes.ts index 81cc1deb0e8..727ceebbbe2 100644 --- a/suite-native/app/src/navigation/routes.ts +++ b/suite-native/app/src/navigation/routes.ts @@ -19,6 +19,13 @@ const accountsStack = enhanceTabOption({ }, }); +const tradeStack = enhanceTabOption({ + routeName: AppTabsRoutes.TradeStack, + iconName: 'arrowsLeftRight', + focusedIconName: 'arrowsLeftRight', + label: 'Trade', +}); + const settings = enhanceTabOption({ routeName: AppTabsRoutes.Settings, iconName: 'gear', @@ -29,5 +36,6 @@ const settings = enhanceTabOption({ export const rootTabsOptions = { ...homeStack, ...accountsStack, + ...tradeStack, ...settings, }; diff --git a/suite-native/app/tsconfig.json b/suite-native/app/tsconfig.json index 6f57b258499..1012708196f 100644 --- a/suite-native/app/tsconfig.json +++ b/suite-native/app/tsconfig.json @@ -60,6 +60,7 @@ { "path": "../module-staking-management" }, + { "path": "../module-trading" }, { "path": "../navigation" }, { "path": "../notifications" }, { "path": "../receive" }, diff --git a/suite-native/atoms/package.json b/suite-native/atoms/package.json index 796edca635c..758b359b227 100644 --- a/suite-native/atoms/package.json +++ b/suite-native/atoms/package.json @@ -13,6 +13,7 @@ "dependencies": { "@gorhom/bottom-sheet": "5.0.5", "@mobily/ts-belt": "^3.13.1", + "@reduxjs/toolkit": "1.9.5", "@shopify/flash-list": "^1.7.2", "@shopify/react-native-skia": "^1.5.10", "@suite-common/wallet-config": "workspace:*", diff --git a/suite-native/atoms/redux.d.ts b/suite-native/atoms/redux.d.ts new file mode 100644 index 00000000000..df9a0c3f969 --- /dev/null +++ b/suite-native/atoms/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/connection-status/src/jestSetup.js b/suite-native/connection-status/src/jestSetup.js new file mode 100644 index 00000000000..98ec2c3c2b2 --- /dev/null +++ b/suite-native/connection-status/src/jestSetup.js @@ -0,0 +1,3 @@ +import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js'; + +jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo); diff --git a/suite-native/feature-flags/package.json b/suite-native/feature-flags/package.json index 8621354ff4c..ba08dfa7961 100644 --- a/suite-native/feature-flags/package.json +++ b/suite-native/feature-flags/package.json @@ -7,7 +7,8 @@ "main": "src/index", "scripts": { "depcheck": "yarn g:depcheck", - "type-check": "yarn g:tsc --build" + "type-check": "yarn g:tsc --build", + "test:unit": "yarn g:jest -c ../../jest.config.native.js" }, "dependencies": { "@reduxjs/toolkit": "1.9.5", diff --git a/suite-native/feature-flags/src/__tests__/featureFlagsSlice.test.ts b/suite-native/feature-flags/src/__tests__/featureFlagsSlice.test.ts new file mode 100644 index 00000000000..c452a48c1a1 --- /dev/null +++ b/suite-native/feature-flags/src/__tests__/featureFlagsSlice.test.ts @@ -0,0 +1,123 @@ +describe('featureFlagsSlice', () => { + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + }); + + describe('initial state', () => { + it('should have correct initial state for debug environment on android', () => { + jest.mock('@suite-native/config', () => ({ + ...jest.requireActual('@suite-native/config'), + isDebugEnv: () => true, + isDevelopOrDebugEnv: () => true, + })); + jest.mock('@trezor/env-utils', () => ({ + ...jest.requireActual('@trezor/env-utils'), + isAndroid: () => true, + })); + + const { featureFlagsReducer } = require('../featureFlagsSlice'); + + const initialState = featureFlagsReducer(undefined, { type: 'undefined_action' }); + + expect(initialState).toEqual({ + isDeviceConnectEnabled: true, + isCardanoSendEnabled: true, + isRegtestEnabled: true, + isConnectPopupEnabled: true, + areEthL2sEnabled: true, + isTradingEnabled: true, + isDeviceOnboardingEnabled: true, + }); + }); + + it('should have correct initial state for production environment on android', () => { + jest.mock('@suite-native/config', () => ({ + ...jest.requireActual('@suite-native/config'), + isDebugEnv: () => false, + isDevelopOrDebugEnv: () => false, + })); + jest.mock('@trezor/env-utils', () => ({ + ...jest.requireActual('@trezor/env-utils'), + isAndroid: () => true, + })); + + const { featureFlagsReducer } = require('../featureFlagsSlice'); + + const initialState = featureFlagsReducer(undefined, { type: 'undefined_action' }); + + expect(initialState).toEqual({ + isDeviceConnectEnabled: true, + isCardanoSendEnabled: false, + isRegtestEnabled: false, + isConnectPopupEnabled: false, + areEthL2sEnabled: false, + isTradingEnabled: false, + isDeviceOnboardingEnabled: false, + }); + }); + + it('should have correct initial state for production environment on iOS', () => { + jest.mock('@suite-native/config', () => ({ + ...jest.requireActual('@suite-native/config'), + isDebugEnv: () => false, + isDevelopOrDebugEnv: () => false, + })); + jest.mock('@trezor/env-utils', () => ({ + ...jest.requireActual('@trezor/env-utils'), + isAndroid: () => false, + })); + + const { featureFlagsReducer } = require('../featureFlagsSlice'); + + const initialState = featureFlagsReducer(undefined, { type: 'undefined_action' }); + + expect(initialState).toEqual({ + isDeviceConnectEnabled: false, + isCardanoSendEnabled: false, + isRegtestEnabled: false, + isConnectPopupEnabled: false, + areEthL2sEnabled: false, + isTradingEnabled: false, + isDeviceOnboardingEnabled: false, + }); + }); + }); + + describe('toggleFeatureFlag', () => { + it('should toggle feature flag', () => { + const { featureFlagsReducer, toggleFeatureFlag } = require('../featureFlagsSlice'); + + const state = featureFlagsReducer( + undefined, + toggleFeatureFlag({ featureFlag: 'isDeviceConnectEnable' }), + ); + expect(state.isDeviceConnectEnabled).toEqual(false); + + const state2 = featureFlagsReducer( + state, + toggleFeatureFlag({ featureFlag: 'isDeviceConnectEnabled' }), + ); + expect(state2.isDeviceConnectEnabled).toEqual(true); + }); + }); + + describe('selectIsFeatureFlagEnabled', () => { + it('should return correct value', () => { + const { + featureFlagsReducer, + toggleFeatureFlag, + selectIsFeatureFlagEnabled, + } = require('../featureFlagsSlice'); + + const state = featureFlagsReducer( + undefined, + toggleFeatureFlag({ featureFlag: 'isDeviceConnectEnabled' }), + ); + + expect( + selectIsFeatureFlagEnabled({ featureFlags: state }, 'isDeviceConnectEnabled'), + ).toEqual(true); + }); + }); +}); diff --git a/suite-native/feature-flags/src/featureFlagsSlice.ts b/suite-native/feature-flags/src/featureFlagsSlice.ts index ef1cec55c9d..e826d1e24cf 100644 --- a/suite-native/feature-flags/src/featureFlagsSlice.ts +++ b/suite-native/feature-flags/src/featureFlagsSlice.ts @@ -10,7 +10,9 @@ export const FeatureFlag = { IsConnectPopupEnabled: 'isConnectPopupEnabled', AreEthL2sEnabled: 'areEthL2sEnabled', IsDeviceOnboardingEnabled: 'isDeviceOnboardingEnabled', + IsTradingEnabled: 'isTradingEnabled', } as const; + export type FeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag]; export type FeatureFlagsState = Record; @@ -26,6 +28,7 @@ export const featureFlagsInitialState: FeatureFlagsState = { [FeatureFlag.IsConnectPopupEnabled]: isDevelopOrDebugEnv(), [FeatureFlag.AreEthL2sEnabled]: isDebugEnv(), [FeatureFlag.IsDeviceOnboardingEnabled]: isDebugEnv() && !isDetoxTestBuild(), + [FeatureFlag.IsTradingEnabled]: isDebugEnv(), }; export const featureFlagsPersistedKeys: Array = [ @@ -35,6 +38,7 @@ export const featureFlagsPersistedKeys: Array = [ FeatureFlag.IsConnectPopupEnabled, FeatureFlag.AreEthL2sEnabled, FeatureFlag.IsDeviceOnboardingEnabled, + FeatureFlag.IsTradingEnabled, ]; export const featureFlagsSlice = createSlice({ diff --git a/suite-native/firmware/src/jestSetup.js b/suite-native/firmware/src/jestSetup.js new file mode 100644 index 00000000000..6655b1bd261 --- /dev/null +++ b/suite-native/firmware/src/jestSetup.js @@ -0,0 +1,6 @@ +jest.mock('@suite-native/firmware', () => ({ + ...jest.requireActual('./nativeFirmwareSlice'), + ...jest.requireActual('./hooks/useIsFirmwareUpdateFeatureEnabled'), + UpdateProgressIndicatorDemo: () => null, + FirmwareUpdateInProgressScreen: () => null, +})); diff --git a/suite-native/module-dev-utils/src/components/FeatureFlags.tsx b/suite-native/module-dev-utils/src/components/FeatureFlags.tsx index 3f5c23adcfa..472b5bb0a55 100644 --- a/suite-native/module-dev-utils/src/components/FeatureFlags.tsx +++ b/suite-native/module-dev-utils/src/components/FeatureFlags.tsx @@ -12,6 +12,7 @@ const featureFlagsTitleMap = { [FeatureFlagEnum.IsConnectPopupEnabled]: 'Connect Popup', [FeatureFlagEnum.AreEthL2sEnabled]: 'Eth L2s', [FeatureFlagEnum.IsDeviceOnboardingEnabled]: 'Device onboarding', + [FeatureFlagEnum.IsTradingEnabled]: 'Trading', } as const satisfies Record; const FeatureFlag = ({ featureFlag }: { featureFlag: FeatureFlagEnum }) => { diff --git a/suite-native/module-trading/package.json b/suite-native/module-trading/package.json new file mode 100644 index 00000000000..25182f5e1e3 --- /dev/null +++ b/suite-native/module-trading/package.json @@ -0,0 +1,21 @@ +{ + "name": "@suite-native/module-trading", + "version": "1.0.0", + "private": true, + "license": "See LICENSE.md in repo root", + "sideEffects": false, + "main": "src/index", + "scripts": { + "depcheck": "yarn g:depcheck", + "type-check": "yarn g:tsc --build", + "test:unit": "yarn g:jest -c ../../jest.config.native.js" + }, + "dependencies": { + "@react-navigation/native-stack": "6.11.0", + "@reduxjs/toolkit": "1.9.5", + "@suite-native/navigation": "workspace:*", + "@suite-native/test-utils": "workspace:*", + "react": "18.2.0", + "react-native": "0.76.1" + } +} diff --git a/suite-native/module-trading/redux.d.ts b/suite-native/module-trading/redux.d.ts new file mode 100644 index 00000000000..df9a0c3f969 --- /dev/null +++ b/suite-native/module-trading/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-trading/src/index.ts b/suite-native/module-trading/src/index.ts new file mode 100644 index 00000000000..4000905f8f1 --- /dev/null +++ b/suite-native/module-trading/src/index.ts @@ -0,0 +1 @@ +export * from './navigation/TradingStackNavigator'; diff --git a/suite-native/module-trading/src/navigation/TradingStackNavigator.tsx b/suite-native/module-trading/src/navigation/TradingStackNavigator.tsx new file mode 100644 index 00000000000..0d7be891a0f --- /dev/null +++ b/suite-native/module-trading/src/navigation/TradingStackNavigator.tsx @@ -0,0 +1,24 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +import { + stackNavigationOptionsConfig, + TradingStackParamList, + TradingStackRoutes, +} from '@suite-native/navigation'; + +import { TradingScreen } from '../screens/TradingScreen'; + +const TradingStack = createNativeStackNavigator(); + +export const TradingStackNavigator = () => ( + + + +); diff --git a/suite-native/module-trading/src/navigation/__tests__/TradingStackNavigator.comp.test.tsx b/suite-native/module-trading/src/navigation/__tests__/TradingStackNavigator.comp.test.tsx new file mode 100644 index 00000000000..7f300814a22 --- /dev/null +++ b/suite-native/module-trading/src/navigation/__tests__/TradingStackNavigator.comp.test.tsx @@ -0,0 +1,10 @@ +import { renderWithStore, waitFor } from '@suite-native/test-utils'; + +import { TradingStackNavigator } from '../TradingStackNavigator'; + +describe('TradingStackNavigator', () => { + it('should render', async () => { + const { getByText } = renderWithStore(); + await waitFor(() => expect(getByText('Trading placeholder')).toBeDefined()); + }); +}); diff --git a/suite-native/module-trading/src/screens/TradingScreen.tsx b/suite-native/module-trading/src/screens/TradingScreen.tsx new file mode 100644 index 00000000000..7c6c8996fec --- /dev/null +++ b/suite-native/module-trading/src/screens/TradingScreen.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { Screen } from '@suite-native/navigation'; +import { Card, Text } from '@suite-native/atoms'; +import { DeviceManagerScreenHeader } from '@suite-native/device-manager'; + +export const TradingScreen = () => ( + }> + + Trading placeholder + + +); diff --git a/suite-native/module-trading/tsconfig.json b/suite-native/module-trading/tsconfig.json new file mode 100644 index 00000000000..ca53899b029 --- /dev/null +++ b/suite-native/module-trading/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "libDev" }, + "references": [ + { "path": "../navigation" }, + { "path": "../test-utils" } + ], + "include": [".", "**/*.json"] +} diff --git a/suite-native/navigation/src/navigators.ts b/suite-native/navigation/src/navigators.ts index 5689ce9bd15..d0330c26832 100644 --- a/suite-native/navigation/src/navigators.ts +++ b/suite-native/navigation/src/navigators.ts @@ -28,6 +28,7 @@ import { RootStackRoutes, SendStackRoutes, SettingsStackRoutes, + TradingStackRoutes, } from './routes'; import { NavigateParameters } from './types'; @@ -102,6 +103,7 @@ export type SendStackParamList = { export type AppTabsParamList = { [AppTabsRoutes.HomeStack]: NavigatorScreenParams; [AppTabsRoutes.AccountsStack]: NavigatorScreenParams; + [AppTabsRoutes.TradeStack]: NavigatorScreenParams; [AppTabsRoutes.Settings]: undefined; }; @@ -222,3 +224,7 @@ export type RootStackParamList = { }; [RootStackRoutes.SettingsScreenStack]: NavigatorScreenParams; }; + +export type TradingStackParamList = { + [TradingStackRoutes.Trading]: undefined; +}; diff --git a/suite-native/navigation/src/routes.ts b/suite-native/navigation/src/routes.ts index b720e01d290..4fef1ea46d4 100644 --- a/suite-native/navigation/src/routes.ts +++ b/suite-native/navigation/src/routes.ts @@ -21,6 +21,7 @@ export enum RootStackRoutes { export enum AppTabsRoutes { HomeStack = 'HomeStack', AccountsStack = 'AccountsStack', + TradeStack = 'TradeStack', Settings = 'Settings', } @@ -124,3 +125,7 @@ export enum SettingsStackRoutes { SettingsFAQ = 'SettingsFAQ', SettingsCoinEnabling = 'SettingsCoinEnabling', } + +export enum TradingStackRoutes { + Trading = 'Trading', +} diff --git a/suite-native/react-native-graph/src/jestSetup.js b/suite-native/react-native-graph/src/jestSetup.js new file mode 100644 index 00000000000..107aa323d32 --- /dev/null +++ b/suite-native/react-native-graph/src/jestSetup.js @@ -0,0 +1 @@ +jest.mock('@suite-native/react-native-graph', () => ({})); diff --git a/suite-native/state/src/index.ts b/suite-native/state/src/index.ts index 5b121d4634d..65d3a26c9bd 100644 --- a/suite-native/state/src/index.ts +++ b/suite-native/state/src/index.ts @@ -1,2 +1,3 @@ export * from './StoreProvider'; export * from './appSlice'; +export * from './store'; diff --git a/suite-native/state/src/store.ts b/suite-native/state/src/store.ts index 7a28112a9f2..f8d1aebed85 100644 --- a/suite-native/state/src/store.ts +++ b/suite-native/state/src/store.ts @@ -12,6 +12,9 @@ import { blockchainMiddleware } from '@suite-native/blockchain'; import { extraDependencies } from './extraDependencies'; import { prepareRootReducers } from './reducers'; +type RootReducerShape = Awaited>; +export type PreloadedState = Partial | undefined; + const ENABLE_REDUX_LOGGER = false; const middlewares: Middleware[] = [ @@ -33,7 +36,7 @@ if (__DEV__) { } } -export const initStore = async () => +export const initStore = async (preloadedState?: PreloadedState) => configureStore({ reducer: await prepareRootReducers(), middleware: getDefaultMiddleware => @@ -45,4 +48,5 @@ export const initStore = async () => immutableCheck: false, }).concat(middlewares), enhancers: defaultEnhancers => defaultEnhancers.concat(enhancers), + preloadedState, }); diff --git a/suite-native/test-utils/package.json b/suite-native/test-utils/package.json index a0183593643..a9587f25ee3 100644 --- a/suite-native/test-utils/package.json +++ b/suite-native/test-utils/package.json @@ -10,9 +10,12 @@ "type-check": "yarn g:tsc --build" }, "dependencies": { + "@react-navigation/native": "6.1.18", + "@reduxjs/toolkit": "1.9.5", "@testing-library/react-native": "13.0.0", "@trezor/styles": "workspace:*", "@trezor/theme": "workspace:*", - "react": "18.2.0" + "react": "18.2.0", + "react-redux": "8.0.7" } } diff --git a/suite-native/test-utils/redux.d.ts b/suite-native/test-utils/redux.d.ts new file mode 100644 index 00000000000..0ba07f44574 --- /dev/null +++ b/suite-native/test-utils/redux.d.ts @@ -0,0 +1,11 @@ +import { AsyncThunkAction } from '@reduxjs/toolkit'; + +declare module 'redux' { + export interface Dispatch { + >(thunk: TThunk): ReturnType; + + ( + thunkAction: ThunkAction, + ): ReturnType; + } +} diff --git a/suite-native/test-utils/src/Provider.tsx b/suite-native/test-utils/src/BasicProvider.tsx similarity index 71% rename from suite-native/test-utils/src/Provider.tsx rename to suite-native/test-utils/src/BasicProvider.tsx index ef86e3814f1..2e35d020427 100644 --- a/suite-native/test-utils/src/Provider.tsx +++ b/suite-native/test-utils/src/BasicProvider.tsx @@ -1,5 +1,7 @@ import { ReactNode } from 'react'; +import { NavigationContainer } from '@react-navigation/native'; + import { createRenderer, StylesProvider } from '@trezor/styles'; import { prepareNativeTheme } from '@trezor/theme'; import { IntlProvider } from '@suite-native/intl'; @@ -7,13 +9,14 @@ import { IntlProvider } from '@suite-native/intl'; type ProviderProps = { children: ReactNode; }; + const renderer = createRenderer(); const theme = prepareNativeTheme({ colorVariant: 'standard' }); -export const Provider = ({ children }: ProviderProps) => ( +export const BasicProvider = ({ children }: ProviderProps) => ( - {children} + {children} ); diff --git a/suite-native/test-utils/src/StoreProviderForTests.tsx b/suite-native/test-utils/src/StoreProviderForTests.tsx new file mode 100644 index 00000000000..7f1c30ebb03 --- /dev/null +++ b/suite-native/test-utils/src/StoreProviderForTests.tsx @@ -0,0 +1,38 @@ +import { ReactNode, useEffect, useState } from 'react'; +import { Provider } from 'react-redux'; + +import { EnhancedStore } from '@reduxjs/toolkit'; + +import { initStore, PreloadedState } from '@suite-native/state'; + +import { BasicProvider } from './BasicProvider'; + +type ReduxProviderProps = { + children: ReactNode; + preloadedState: PreloadedState; +}; + +/* +This is file is a copy of `StoreProvider.tsx` from `suite-native/state` but with ability to pass `preloadedState` as a prop +and without the `Persistor` and `Sentry` logic. + */ +export const StoreProviderForTests = ({ children, preloadedState }: ReduxProviderProps) => { + const [store, setStore] = useState(null); + + useEffect(() => { + const initStoreAsync = async () => { + const freshStore = await initStore(preloadedState); + setStore(freshStore); + }; + + initStoreAsync(); + }, [preloadedState]); + + if (store === null) return null; + + return ( + + {children} + + ); +}; diff --git a/suite-native/test-utils/src/createRender.tsx b/suite-native/test-utils/src/createRender.tsx new file mode 100644 index 00000000000..44cf08970e3 --- /dev/null +++ b/suite-native/test-utils/src/createRender.tsx @@ -0,0 +1,27 @@ +import { ReactElement } from 'react'; + +import { render, RenderOptions } from '@testing-library/react-native'; + +import { PreloadedState } from '@suite-native/state'; + +type ProviderProps = { + preloadedState?: PreloadedState; +} & RenderOptions; + +export const createRender = + (Provider: NonNullable) => + ( + element: ReactElement, + { wrapper: WrapperComponent, preloadedState, ...options }: ProviderProps = {}, + ) => { + const wrapperWithProvider = ({ children }: { children: ReactElement }) => ( + + {WrapperComponent ? {children} : children} + + ); + + return render(element, { + wrapper: wrapperWithProvider, + ...options, + }); + }; diff --git a/suite-native/test-utils/src/expoMock.js b/suite-native/test-utils/src/expoMock.js index be1bc90291c..c9a4d4a7afa 100644 --- a/suite-native/test-utils/src/expoMock.js +++ b/suite-native/test-utils/src/expoMock.js @@ -401,3 +401,5 @@ jest.mock('expo-constants', () => { return Constants; }); + +jest.mock('redux-devtools-expo-dev-plugin', () => () => next => next); diff --git a/suite-native/test-utils/src/index.tsx b/suite-native/test-utils/src/index.tsx index 7b7f3b7c15d..a348833f3dc 100644 --- a/suite-native/test-utils/src/index.tsx +++ b/suite-native/test-utils/src/index.tsx @@ -1,30 +1,8 @@ -import { ReactElement } from 'react'; +import { createRender } from './createRender'; +import { BasicProvider } from './BasicProvider'; +import { StoreProviderForTests } from './StoreProviderForTests'; -import { render as origRender } from '@testing-library/react-native'; - -import { Provider } from './Provider'; - -type Parameters = TParams extends (...args: infer TParamsInferred) => any - ? TParamsInferred - : never; - -export const render = ( - element: ReactElement, - { wrapper: WrapperComponent, ...options }: Parameters[1] = {}, -): ReturnType => { - const wrapperWithProvider = WrapperComponent - ? ({ children }: { children: ReactElement }) => ( - - {children} - - ) - : Provider; - - return origRender(element, { - wrapper: wrapperWithProvider, - ...options, - }); -}; +export type { PreloadedState } from '@suite-native/state'; export { act, @@ -35,3 +13,7 @@ export { waitFor, waitForElementToBeRemoved, } from '@testing-library/react-native'; + +export const render = createRender(BasicProvider); + +export const renderWithStore = createRender(StoreProviderForTests); diff --git a/yarn.lock b/yarn.lock index 4df6c023bf8..e9b2ffaf490 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9871,6 +9871,7 @@ __metadata: dependencies: "@mobily/ts-belt": "npm:^3.13.1" "@react-navigation/native": "npm:6.1.18" + "@reduxjs/toolkit": "npm:1.9.5" "@suite-common/formatters": "workspace:*" "@suite-common/redux-utils": "workspace:*" "@suite-common/token-definitions": "workspace:*" @@ -9995,6 +9996,7 @@ __metadata: "@suite-native/module-send": "workspace:*" "@suite-native/module-settings": "workspace:*" "@suite-native/module-staking-management": "workspace:*" + "@suite-native/module-trading": "workspace:*" "@suite-native/navigation": "workspace:*" "@suite-native/notifications": "workspace:*" "@suite-native/receive": "workspace:*" @@ -10097,6 +10099,7 @@ __metadata: dependencies: "@gorhom/bottom-sheet": "npm:5.0.5" "@mobily/ts-belt": "npm:^3.13.1" + "@reduxjs/toolkit": "npm:1.9.5" "@shopify/flash-list": "npm:^1.7.2" "@shopify/react-native-skia": "npm:^1.5.10" "@suite-common/wallet-config": "workspace:*" @@ -10948,6 +10951,19 @@ __metadata: languageName: unknown linkType: soft +"@suite-native/module-trading@workspace:*, @suite-native/module-trading@workspace:suite-native/module-trading": + version: 0.0.0-use.local + resolution: "@suite-native/module-trading@workspace:suite-native/module-trading" + dependencies: + "@react-navigation/native-stack": "npm:6.11.0" + "@reduxjs/toolkit": "npm:1.9.5" + "@suite-native/navigation": "workspace:*" + "@suite-native/test-utils": "workspace:*" + react: "npm:18.2.0" + react-native: "npm:0.76.1" + languageName: unknown + linkType: soft + "@suite-native/navigation@workspace:*, @suite-native/navigation@workspace:suite-native/navigation": version: 0.0.0-use.local resolution: "@suite-native/navigation@workspace:suite-native/navigation" @@ -11173,10 +11189,13 @@ __metadata: version: 0.0.0-use.local resolution: "@suite-native/test-utils@workspace:suite-native/test-utils" dependencies: + "@react-navigation/native": "npm:6.1.18" + "@reduxjs/toolkit": "npm:1.9.5" "@testing-library/react-native": "npm:13.0.0" "@trezor/styles": "workspace:*" "@trezor/theme": "workspace:*" react: "npm:18.2.0" + react-redux: "npm:8.0.7" languageName: unknown linkType: soft