# Mobile Patterns

> Use when working in apps/mobileAppYC. Covers React Native architecture, navigation, Redux Toolkit state management, and mobile-specific conventions.

- Skill: `yosemitecrew/mobile-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add yosemitecrew/mobile-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yosemitecrew/mobile-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: yosemitecrew (https://skillmd.com/u/yosemitecrew)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yosemitecrew/mobile-patterns

---


# Mobile App Patterns — Yosemite Crew

## Description

Use this skill when working on apps/mobileAppYC. Covers React Native architecture, navigation, Redux state management, and mobile-specific conventions.

TRIGGER: any task in apps/mobileAppYC — screens, components, navigation, state, or native integrations.

---

## Architecture

```
apps/mobileAppYC/
  src/
    app/            ← Redux store.ts + typed hooks.ts
    features/       ← feature slices: <domain>/{screens,components,hooks,services,<domain>Slice.ts,selectors.ts}
    shared/         ← cross-feature components, screens, stores, services, utils
    navigation/     ← React Navigation config
    localization/   ← translation files (i18next)
    theme/          ← design tokens and theming
    config/         ← environment/config values
    context/        ← React contexts
    types/          ← shared TS types
    assets/         ← images, fonts
```

New code follows the feature-slice pattern: put screens, components, and services inside the owning `src/features/<domain>/` directory, not in global folders.

---

## State Management

**Redux Toolkit** (not Zustand — that's frontend only). Redux Persist is enabled.

```ts
// Define a slice
import { createSlice } from '@reduxjs/toolkit';

const appointmentSlice = createSlice({
  name: 'appointments',
  initialState,
  reducers: { ... },
});
```

Never mix Redux and local `useState` for the same piece of data. Local state is for ephemeral UI state (modal open, input focus). Shared/persisted state goes in Redux.

---

## Navigation

**React Navigation 7** with bottom tabs + native stack + drawer.

```ts
// Type your navigation params
type RootStackParamList = {
  Home: undefined;
  AppointmentDetail: { appointmentId: string };
};
```

Never navigate with bare strings — always use typed route names.

---

## Forms

**react-hook-form + Yup** for all forms.

```tsx
const schema = yup.object({ name: yup.string().required() });
const { control, handleSubmit } = useForm({ resolver: yupResolver(schema) });
```

---

## Internationalisation

All user-visible strings must go through **i18next**.

```tsx
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
<Text>{t('appointments.title')}</Text>;
```

Never hardcode English strings in components.

### UI copy normalization

- Never render raw backend enums or role acronyms directly in UI text (example: `PAYMENT_AT_CLINIC`, `VET`).
- Map technical values to user-friendly labels before rendering.
- Avoid `Actor` as a user-facing label; prefer contextual labels (`Lead`, `Support`) or neutral `Updated by`.

---

## Authentication

SuperTokens is the mobile auth provider (email OTP + social through the provider; `supertokens-react-native` manages sessions). Firebase remains for push notifications only. Never bypass the SuperTokens session layer for core auth.

---

## Payments

`@stripe/stripe-react-native` — use the SDK's pre-built UI sheets where possible. Never build custom card input from scratch.

---

## Testing

- **Jest 30** + Testing Library for React Native. Jest 30 renamed the targeting flag to `--testPathPatterns` (plural) - the old singular form errors.
- **Detox** for E2E (run separately, not part of standard CI).
- Target tests: `pnpm --filter mobileAppYC run test -- --testPathPatterns="path/to/file"`
- Never run the full suite without `--testPathPatterns`.

### Coverage Mandate — Non-Negotiable

**Target: ≥ 95% Statements, Branches, Functions, Lines across `apps/mobileAppYC`. Every change must move coverage upward, never downward.**

#### Rules that apply to every task — add, modify, remove

1. **Any file you touch must finish with equal or higher coverage than you found it.** Run the targeted test and confirm before handoff.
2. **Any file you create must hit ≥ 90% Statements, Branches, Functions on first commit.** New code with no tests is a blocker — do not declare the task done.
3. **When you delete code**, delete the corresponding test code too. Dead test scaffolding inflates noise and hides real gaps.
4. **When you modify behaviour** (rename, refactor, add a branch, change a conditional), update every existing test covering the changed path AND add new cases for new branches.
5. **Snapshot tests count but do not substitute** for behavioural assertions. Every logical branch needs at least one assertion that validates the outcome.

#### Test types required — use all of them, not just one

| Layer     | Tool                         | When required                                                                      |
| --------- | ---------------------------- | ---------------------------------------------------------------------------------- |
| Unit      | Jest                         | Every service, Redux slice, hook, utility, helper                                  |
| Component | React Testing Library for RN | Every screen and reusable component — render + interaction + conditional rendering |
| Snapshot  | Jest `toMatchSnapshot`       | Stable UI layouts — complement behavioural tests, never replace them               |
| E2E       | Detox                        | Auth flows, booking, checkout, payment, any critical user journey                  |

All four layers must grow together. Do not add unit tests while leaving Detox untouched for critical flows, and vice versa.

#### Coverage enforcement workflow

```bash
# After every change, run coverage for the touched file(s):
pnpm --filter mobileAppYC run test -- --testPathPatterns="<YourFile>" --coverage --collectCoverageFrom="src/path/to/YourFile.tsx"

# Check — if Statements/Branches/Functions dropped vs what you started with, add tests before declaring done.
```

---

### New code = new tests (mandatory)

**Every new module, screen, service, hook, slice, or utility added to `apps/mobileAppYC` must ship with tests in the same batch. No exceptions.**

| What you add                                | What you must also add                                         |
| ------------------------------------------- | -------------------------------------------------------------- |
| Service function / API call                 | Jest unit: success + all error branches                        |
| Redux slice                                 | Jest: every reducer, action creator, selector, and async thunk |
| Custom hook                                 | `renderHook` covering all return values and state branches     |
| Utility function                            | Jest unit with full branch coverage                            |
| Screen component                            | Jest + Testing Library render + key interaction                |
| E2E-critical flow (auth, booking, checkout) | Detox test                                                     |

**Coverage bar for any new file you author: Statements ≥ 90%, Branches ≥ 90%, Functions ≥ 90%.**

Never leave an existing file in a worse coverage state than you found it.

### Mandatory pre-commit checks (run in order, never skip)

```bash
npx tsc --noemit                                    # from apps/mobileAppYC/
pnpm --filter mobileAppYC run lint
pnpm --filter mobileAppYC run test -- --testPathPatterns="<YourFile>"
```

---

## App Store Submission

Full reference: [`docs/guide/mobile-app-submission-guide.md`](../../../docs/guide/mobile-app-submission-guide.md)

### Pre-Submission Checklist

Before every submission run these checks — **do not bump versions mid-review**:

1. **Production config** — `src/config/variables.local.ts`:
   - `USE_DEV_API = false`
   - `UI_FEATURE_FLAGS.forceLiquidGlassBorder = false`
   - `MOBILE_CONFIG_BEHAVIOR.overrides.forceLiquidGlassBorder = false`

2. **Silence console output** - nothing to do. The console-silencing block in
   `App.tsx` is guarded by `if (!__DEV__)`, so a release build is quiet and a
   Debug build keeps its logs (search for `const noop` to confirm it is intact):

   ```ts
   if (!__DEV__) {
     const noop = () => {};
     console.log = noop;
     console.info = noop;
     console.debug = noop;
     console.trace = noop;
   }
   ```

   It used to run unconditionally, which muted Debug builds too and is why a
   token-expiry bug warned on every call for a whole release without anyone
   seeing it. Do not remove the `__DEV__` guard to "make release quieter" - it
   already is.

3. **Version bumps** (only for a new submission or after rejection):

   | Platform | File                              | Field                     |
   | -------- | --------------------------------- | ------------------------- |
   | Android  | `android/app/build.gradle`        | `versionCode`             |
   | Android  | `android/app/build.gradle`        | `versionName`             |
   | iOS      | `mobileAppYC.xcodeproj` (pbxproj) | `MARKETING_VERSION`       |
   | iOS      | `mobileAppYC.xcodeproj` (pbxproj) | `CURRENT_PROJECT_VERSION` |

   Read the current values from those files and bump by one — never trust a hardcoded
   version table in docs; they go stale after every release.

### Android Build

```bash
# From apps/mobileAppYC/android/ — clean first
rm -rf app/build build .cxx .gradle && ./gradlew clean

# APK (testing/sideload)
./gradlew assembleRelease
# Output: android/app/build/outputs/apk/release/app-release.apk

# AAB (Play Store)
./gradlew bundleRelease
# Output: android/app/build/outputs/bundle/release/app-release.aab
```

Keystore `my-release-key.keystore` must be at `android/app/` and `android/gradle.properties` must have `YC_RELEASE_STORE_FILE`, `YC_RELEASE_STORE_PASSWORD`, `YC_RELEASE_KEY_ALIAS`, `YC_RELEASE_KEY_PASSWORD`.

### iOS Build

```bash
# From apps/mobileAppYC/ios/ — clean first
rm -rf Pods build Podfile.lock ~/Library/Developer/Xcode/DerivedData/*
pod deintegrate && pod install
```

Archive in Xcode:

1. Open `ios/mobileAppYC.xcworkspace` (**not** `.xcodeproj`).
2. Select a physical device or "Any iOS Device (arm64)" — **not** a Simulator.
3. **Product → Clean Build Folder** (`⇧⌘K`) → **Product → Archive**.
4. In Organizer: **Distribute App** → upload to both **Preflight** and **App Store Connect**.

### Post-Approval

After both stores go live, update `README.md`:

- **Current production releases** table with new versions.
- **Release history** table — add a row per platform with version + feature summary.

### Common Pitfalls

- Opening `.xcodeproj` instead of `.xcworkspace` — Pods will not be linked.
- Forgetting `pod install` after cleaning — build fails with missing headers.
- `USE_DEV_API = true` left on — app hits dev backend in production.
- Wrong keystore signing AAB — Play Store upload rejected.
- Bumping version mid-review — Apple treats it as a new binary and restarts review.

---

## Gotchas

- `react-native-permissions` requires explicit permission requests before accessing camera, location, contacts — never assume granted.
- `react-native-fs` paths differ between iOS and Android — use `RNFS.DocumentDirectoryPath` not hardcoded paths.
- Redux Persist can cause stale state after schema changes — bump the persist version key when changing slice shape.
- `@gorhom/bottom-sheet` requires `GestureHandlerRootView` at app root — it's already there, don't remove it.
- Reactotron is dev-only — guard with `__DEV__` checks.
- i18n resource files are in `src/localization/` (there is no `src/i18n/`) — add new keys there before using `t()`.

