--- name: add-feature description: Use when adding a new screen, route, or user-facing feature to the Ecency mobile React Native app, covering navigator registration, route params, i18n strings and styling. argument-hint: [feature-name] --- # Add Feature Ordered procedure for adding a screen. CLAUDE.md covers Redux, TanStack Query, `@ecency/sdk`, sheets and lint rules in more depth. Easiest to miss: the safe-area root (Step 1) and the params contract (Step 4). Each one breaks something. New screens are functional. A few older screens under `src/screens/` are still classes, plus `application/children/errorBoundary.tsx`, which React requires to be a class. An overlay with no route of its own, shown with `SheetManager.show`, is a bottom sheet instead: `src/navigation/sheets.tsx`, see CLAUDE.md. ```text src/screens// index.ts # local barrel, re-exported from src/screens/index.ts screen/Screen.tsx screen/Styles.ts # or .styles.ts children/ hooks/ # optional ``` ## Step 1: screen rooted in SafeAreaView Use `SafeAreaView` from `react-native-safe-area-context` as the root; that is where screens generally import it from. `src/screens/dappBrowser/screen/dappBrowser.tsx` still takes it from `react-native`, which is the pattern this rule exists to replace. Do not pass `edges`: it **replaces** the defaults rather than extending them, so `edges={['bottom']}` drops the top inset and the header runs under the status bar. That shipped on the Email digests screen; the fix was removing `edges={['bottom']}`. Skeleton for `screen/Screen.tsx` (a template, not a quote of any one file): ```tsx import React from 'react'; import { useIntl } from 'react-intl'; import { SafeAreaView } from 'react-native-safe-area-context'; import { BasicHeader } from '../../../components'; import { useAppSelector } from '../../../hooks'; import { selectCurrentAccount } from '../../../redux/selectors'; import styles from './myFeatureStyles'; const MyFeatureScreen = () => { const intl = useIntl(); const currentAccount = useAppSelector(selectCurrentAccount); return ( {/* content */} ); }; export default MyFeatureScreen; ``` `useAppSelector(selectCurrentAccount)` reads the account; `useAuth()` from `src/hooks` when you only need `{ username, code }`. Reusable UI is exported from the `src/components/index.tsx` barrel (`BasicHeader`, `MainButton`, `TextInput`, `UserAvatar`, `Icon`). The styles file default-exports `EStyleSheet.create({ container: { flex: 1, backgroundColor: '$primaryBackgroundColor' } })`. Variables are defined in `src/themes/lightTheme.ts` and `darkTheme.ts`: `$primaryBlack` text, `$primaryDarkGray` secondary text, `$primaryBlue` accent, `$primaryLightBackground` cards, `$iconColor`, `$primaryRed` destructive. Hex literals do not follow the theme. ## Step 2: both barrels `src/screens//index.ts` does `import MyFeature from './screen/myFeatureScreen';` then `export { MyFeature }; export default MyFeature;`. Add the import plus the name to the export block in `src/screens/index.ts`. `stackNavigator.tsx` imports its screens from that barrel. `src/screens/waves` shows the cost of skipping this: it never reached the barrel, so `botomTabNavigator.tsx` reaches it by path. ## Step 3: route name In `src/constants/routeNames.ts`. Entries are template literals over the shared suffix consts. The object ends `as const`, which is what makes the route names a literal union: ```ts MY_FEATURE: `MyFeature${SCREEN_SUFFIX}`, ``` ## Step 4: params contract (skip it and typecheck fails) `src/navigation/types.ts` derives `RouteName` from ROUTES, then asserts every route has an entry: ```ts export type _MissingRouteContracts = AssertNever>; ``` A ROUTES entry with no `AppParamList` entry breaks that assertion: the route name does not satisfy the constraint `never`. `yarn typecheck` runs against an empty baseline, so this fails CI. Add: ```ts [ROUTES.SCREENS.MY_FEATURE]: { username?: string } | undefined; ``` Append `| undefined` only if the screen renders with no params. Leaving it off makes params required at the call sites, which is what you want for a screen that cannot render empty (`WEB_BROWSER`, `VOTERS`, `ASSET_DETAILS`, `CHAT_THREAD`, `PROFILE_EDIT`). ## Step 5: register in the navigator `src/navigation/stackNavigator.tsx` defines `MainStackNavigator` and the root `StackNavigator`. `MainStackNavigator` registers the drawer as its first screen (`ROUTES.DRAWER.MAIN`), so an ordinary screen added to it is a sibling of the drawer that pushes over it. The root `StackNavigator` holds `MainStackNavigator` itself plus pre-auth and full-screen routes (Login, Register, Welcome, PinCode, WebBrowser). Most new screens go in the main one: ```tsx ``` Put it in the `` block to slide up, add `options={{ presentation: 'modal' }}` for a true modal. The `as any` casts on existing rows are legacy prop debt; a new screen needs none. ## Step 6: navigating `types.ts` declares `ReactNavigation.RootParamList extends AppParamList`, so the untyped hook is already checked against Step 4. Omitting params for a route that requires them is a compile error, not a blank screen. ```tsx const navigation = useNavigation(); navigation.navigate(ROUTES.SCREENS.MY_FEATURE, { username }); ``` Reading the params back is not settled house style. Most screens destructure a `route` prop, usually typed `any`; `useRoute` is rare. Prefer keying off Step 4 instead: ```tsx import { RouteProp, useRoute } from '@react-navigation/native'; import { AppParamList } from '../../../navigation/types'; const route = useRoute>(); ``` Outside a component (deep links, redux actions) use the equally typed object form, `RootNavigation.navigate({ name, params })` from `src/navigation/rootNavigation.tsx`. ## Step 7: strings `src/config/locales/en-US.json` is the catalog you edit; Crowdin owns the translations. It is **nested**: top-level keys are objects, not dotted ids. `src/utils/flattenMessages.ts` joins the levels with dots at load, so a nested block is read with a dotted id. ```json { "myfeature": { "title": "My Feature", "empty": "Nothing here yet" } } ``` ```tsx intl.formatMessage({ id: 'myfeature.title' }); ``` ## Step 8: data Server data uses `@ecency/sdk` query options with TanStack Query; mutations use a wrapper in `src/providers/sdk/mutations/`. See the SDK Migration section of CLAUDE.md. Keep Redux for auth, settings, UI state and the optimistic cache reducer. Older reducers (`postsReducer`, `walletReducer`) still hold server data; do not extend them for a new screen. ## Checklist - [ ] Root is `SafeAreaView` from `react-native-safe-area-context`, no `edges` override - [ ] `src/screens//index.ts` re-exported from `src/screens/index.ts` - [ ] Route in `routeNames.ts` **and** a params entry in `AppParamList` (`src/navigation/types.ts`) - [ ] Registered in `src/navigation/stackNavigator.tsx` - [ ] Nested strings in `en-US.json` only - [ ] Theme variables, no hex literals - [ ] `yarn lint` and `yarn typecheck` clean