diff --git a/.claude/skills/freighter-mobile-best-practices b/.claude/skills/freighter-mobile-best-practices new file mode 120000 index 00000000..78d99206 --- /dev/null +++ b/.claude/skills/freighter-mobile-best-practices @@ -0,0 +1 @@ +../../../docs/skills/freighter-mobile-best-practices \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..7dbf5cda --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,178 @@ +# Freighter Mobile + +> Non-custodial Stellar wallet for iOS and Android. React Native app with +> Zustand state management (ducks pattern). + +## Glossary + +Domain terms you will encounter throughout this codebase: + +| Term | Meaning | +| ----------------- | ---------------------------------------------------------------------------------------- | +| **Duck** | A Zustand store module in `src/ducks/` managing a single domain of state | +| **XDR** | Stellar binary serialization format used for transactions and ledger entries | +| **WalletConnect** | Web3 protocol (v2) for dApp-to-wallet connections via `@reown/walletkit` | +| **RPC method** | WalletConnect v2 handler (`stellar_signXDR`, `stellar_signAndSubmitXDR`, etc.) | +| **Metro** | React Native JavaScript bundler (replaces webpack); cache issues are common | +| **Bundle ID** | App identifier — `org.stellar.freighterdev` (dev) / `org.stellar.freighterwallet` (prod) | +| **Fastlane** | Ruby automation for iOS/Android builds and App Store/Play Store submissions | +| **Maestro** | YAML-based mobile e2e test runner; flows live in `e2e/flows/` | +| **NativeWind** | Tailwind CSS utility classes adapted for React Native (v4) | +| **rn-nodeify** | Polyfills Node.js APIs (crypto, stream, buffer) required by the Stellar SDK | +| **jail-monkey** | Jailbreak/root detection library — never bypass in production code | + +## Documentation + +- [Auth Flow Architecture](./docs/auth_flow_diagram.md) +- [WalletConnect RPC Methods](./docs/walletconnect-rpc-methods.md) +- [Release Process](./RELEASE.md) +- [E2E Testing Guide](./e2e/README.md) +- [E2E CI & Triggers](./e2e/docs/ci-and-triggers.md) +- [E2E Local Setup](./e2e/docs/local-setup-and-env.md) +- [E2E Running Tests](./e2e/docs/running-tests.md) +- [E2E Debugging](./e2e/docs/artifacts-and-debugging.md) +- [E2E Creating Tests](./e2e/docs/creating-tests.md) +- [WalletConnect E2E](./e2e/docs/walletconnect-e2e-testing.md) +- [Mock dApp for Testing](./mock-dapp/README.md) +- [Getting Started](./README.md) +- [Contributing](./CONTRIBUTING.md) + +## Quick Reference + +| Item | Value | +| ---------------- | -------------------------------------------------------- | +| Language | TypeScript, React Native 0.81 | +| Node | >= 20 (CI: Node 20; locally 22 also works) | +| Package Manager | Yarn 4.10.0 (Corepack) | +| State Management | Zustand 5 (ducks pattern) | +| Navigation | React Navigation 7 (nested stack/tab) | +| Styling | NativeWind 4 (Tailwind) + Styled Components 6 | +| Testing | Jest 30 (unit), Maestro (e2e) | +| Linting | ESLint Airbnb + TS strict + Prettier | +| iOS Build | Xcode + Fastlane + CocoaPods | +| Android Build | Gradle + Fastlane (SDK 36, min SDK 24, NDK 28.2, JDK 17) | +| Default Branch | `main` | + +## Build & Test Commands + +```bash +yarn install # Install deps (runs Husky, polyfills, pods) +bundle install # Ruby deps (Fastlane, CocoaPods) +yarn ios # Run on iOS simulator +yarn android # Run on Android emulator +yarn start # Metro bundler only +yarn start-c # Metro with cache reset +yarn test # Jest unit tests +yarn check # lint:ts + lint:check + format:check +yarn fix # Auto-fix lint + format +yarn lint:translations # Check for missing i18n keys +yarn test:e2e:ios # Maestro e2e (iOS) +yarn test:e2e:android # Maestro e2e (Android) +``` + +### Cleaning Builds (escalation order) + +```bash +yarn start-c # Clear Metro cache (try first) +yarn pod-install # Reinstall CocoaPods +yarn node-c-install # Remove node_modules + reinstall +yarn c-install # Full clean (Gradle + node_modules + reinstall) +yarn r-install # Nuclear: reset env + rebuild everything +``` + +## Repository Structure + +``` +freighter-mobile/ +├── src/ +│ ├── components/ # RN components (screens, templates, primitives, SDS) +│ ├── ducks/ # Zustand stores (one per domain) +│ ├── hooks/ # Custom React hooks +│ ├── helpers/ # Utility functions +│ ├── services/ # Business logic (analytics, blockaid, storage, backend) +│ ├── navigators/ # React Navigation (Root, Auth, Tab, + 6 feature navigators) +│ ├── providers/ # Context providers (AuthCheck, Network, Toast, WalletKit) +│ ├── config/ # App config (envConfig, constants, theme, routes) +│ ├── i18n/ # Translations (i18next) +│ └── polyfills/ # Node.js API polyfills for Stellar SDK +├── __tests__/ # Jest unit tests (mirrors src/) +├── __mocks__/ # Jest mocks for native modules +├── e2e/flows/ # Maestro e2e test flows +├── mock-dapp/ # WalletConnect mock dApp for local testing +├── ios/ # Native iOS project +├── android/ # Native Android project +├── fastlane/ # Release automation +└── .github/workflows/ # CI: test, ios, android, ios-e2e, android-e2e, new-release +``` + +## Architecture + +State lives in isolated Zustand ducks (`src/ducks/`). Key stores: `auth`, +`balances`, `transactionBuilder`, `swap`, `walletKit`, `preferences`, +`networkInfo`, `history`. Access via hooks (`useAuthStore`, etc.). + +Navigation uses nested React Navigation 7 navigators. Entry point: +`RootNavigator` → `AuthNavigator` or `TabNavigator` → feature navigators. Deep +links: `freighterdev://` (dev) / `freighterwallet://` (prod). + +dApp connectivity via WalletConnect v2 (`src/providers/WalletKitProvider.tsx`). +4 RPC methods: `stellar_signXDR`, `stellar_signAndSubmitXDR`, +`stellar_signMessage`, `stellar_signAuthEntry`. + +## Security-Sensitive Areas + +Do not modify these without fully understanding the security implications: + +- `src/ducks/auth.ts` — authentication state, key management +- `src/services/storage/` — secure storage (iOS Keychain / Android Keystore) +- `src/helpers/` related to signing, encryption, or key derivation +- `src/navigators/` — deep link handling (injection vector) +- `src/providers/WalletKitProvider.tsx` — WalletConnect session management +- `jail-monkey` detection — never bypass in non-test code +- `SecureClipboardService` — use this for all sensitive clipboard operations + +## Known Complexity / Gotchas + +- **Auth flow** is a complex state machine (sign-up, sign-in, import, lock, + biometrics). Read `docs/auth_flow_diagram.md` before touching + `src/ducks/auth.ts`. +- **Dual bundle IDs** mean separate signing configs, push tokens, and deep link + schemes. Don't mix dev/prod identifiers. +- **Version bumps** require touching 5 files simultaneously. Use + `yarn set-app-version` — do not edit them manually. +- **Metro cache** is the first culprit for unexplained build failures; run + `yarn start-c` before investigating further. +- **Polyfills** (`rn-nodeify`) must stay in sync with the Stellar SDK version — + don't remove or update without testing cryptographic operations. +- **Blockaid** transaction scanning is integrated into sign flows. Don't bypass + or weaken — it's a security control. +- **Pre-commit hooks** run the full test suite + TypeScript check. This is + intentional and slow on large changesets. + +## Pre-submission Checklist + +```bash +yarn check # Lint + format must pass +yarn test # Unit tests must pass +# Then test manually on both iOS and Android simulators +``` + +## Best Practices Entry Points + +Read the relevant file when working in that area: + +| Concern | Entry Point | When to Read | +| -------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- | +| Code Style | `docs/skills/freighter-mobile-best-practices/references/code-style.md` | Writing or reviewing any code | +| Architecture | `docs/skills/freighter-mobile-best-practices/references/architecture.md` | Adding features, understanding state/nav structure | +| Styling | `docs/skills/freighter-mobile-best-practices/references/styling.md` | Creating or modifying UI components | +| Security | `docs/skills/freighter-mobile-best-practices/references/security.md` | Touching keys, auth, storage, or dApp interactions | +| Testing | `docs/skills/freighter-mobile-best-practices/references/testing.md` | Writing or fixing tests | +| Performance | `docs/skills/freighter-mobile-best-practices/references/performance.md` | Optimizing renders, lists, images, or startup | +| Error Handling | `docs/skills/freighter-mobile-best-practices/references/error-handling.md` | Adding error states, retries, or user-facing errors | +| Internationalization | `docs/skills/freighter-mobile-best-practices/references/i18n.md` | Adding or modifying user-facing strings | +| WalletConnect | `docs/skills/freighter-mobile-best-practices/references/walletconnect.md` | Working with dApp connections or RPC methods | +| Navigation | `docs/skills/freighter-mobile-best-practices/references/navigation.md` | Adding screens, deep links, or navigation flows | +| Git & PR Workflow | `docs/skills/freighter-mobile-best-practices/references/git-workflow.md` | Branching, committing, opening PRs, CI, releases | +| Dependencies | `docs/skills/freighter-mobile-best-practices/references/dependencies.md` | Adding, updating, or auditing packages | +| Anti-Patterns | `docs/skills/freighter-mobile-best-practices/references/anti-patterns.md` | Code review, avoiding common mistakes | diff --git a/docs/skills/freighter-mobile-best-practices/SKILL.md b/docs/skills/freighter-mobile-best-practices/SKILL.md new file mode 100644 index 00000000..e9c252ec --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/SKILL.md @@ -0,0 +1,37 @@ +--- +name: freighter-mobile-best-practices +description: + Comprehensive best practices for the Freighter Mobile React Native app (iOS + + Android). Covers code style, architecture, security, testing, performance, + error handling, i18n, styling, WalletConnect, navigation, git workflow, + dependencies, and anti-patterns. Use when writing new screens, creating + Zustand stores, adding hooks, working with WalletConnect, handling secure key + storage, writing navigation flows, building transactions, styling components, + reviewing mobile PRs, or any development work in the freighter-mobile repo. + Triggers on any task touching the freighter-mobile codebase. +--- + +# Freighter Mobile Best Practices + +Freighter Mobile is a non-custodial Stellar wallet built with React Native, +targeting both iOS and Android. The app uses Zustand for state management, React +Navigation for routing, NativeWind for styling, and Maestro for end-to-end +testing. + +## Reference Index + +| Concern | File | When to Read | +| -------------------- | ---------------------------- | --------------------------------------------------- | +| Code Style | references/code-style.md | Writing or reviewing any code | +| Architecture | references/architecture.md | Adding features, understanding the codebase | +| Styling | references/styling.md | Creating or modifying UI components | +| Security | references/security.md | Touching keys, auth, storage, or dApp interactions | +| Testing | references/testing.md | Writing or fixing tests | +| Performance | references/performance.md | Optimizing renders, lists, images, or startup | +| Error Handling | references/error-handling.md | Adding error states, retries, or user-facing errors | +| Internationalization | references/i18n.md | Adding or modifying user-facing strings | +| WalletConnect | references/walletconnect.md | Working with dApp connections or RPC methods | +| Navigation | references/navigation.md | Adding screens, deep links, or navigation flows | +| Git & PR Workflow | references/git-workflow.md | Branching, committing, opening PRs, CI, releases | +| Dependencies | references/dependencies.md | Adding, updating, or auditing packages | +| Anti-Patterns | references/anti-patterns.md | Code review, avoiding common mistakes | diff --git a/docs/skills/freighter-mobile-best-practices/references/anti-patterns.md b/docs/skills/freighter-mobile-best-practices/references/anti-patterns.md new file mode 100644 index 00000000..cb7483ec --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/anti-patterns.md @@ -0,0 +1,155 @@ +# Anti-Patterns + +This document lists common mistakes to avoid in the Freighter Mobile codebase. + +## Relative Imports + +ESLint enforces absolute imports from the `src/` root. Relative imports will +fail lint. + +```tsx +// Wrong - will fail lint +import { useAuth } from "../../hooks/useAuth"; + +// Correct +import { useAuth } from "hooks/useAuth"; +``` + +## Function Declarations for Components + +All components must use arrow function expressions. Function declarations will +fail lint (`react/function-component-definition`). + +```tsx +// Wrong - will fail lint +export function MyComponent({ title }: Props) { + return {title}; +} + +// Correct +export const MyComponent: React.FC = ({ title }) => { + return {title}; +}; +``` + +## Class Components + +Class components are not used anywhere in the codebase. Always use functional +components with hooks. + +## Magic Strings in Navigation + +Never use raw strings for route names. Always use route enum constants. + +```tsx +// Wrong +navigation.navigate("EnterAmount"); + +// Correct +navigation.navigate(SEND_PAYMENT_ROUTES.ENTER_AMOUNT); +``` + +## AsyncStorage for Sensitive Data + +Never store keys, seeds, or passwords in AsyncStorage. Use the appropriate tier +from `storageFactory`: + +- `secureDataStorage` for keys and seeds +- `biometricDataStorage` for biometric-gated passwords + +## Untyped navigation.navigate() + +Always type navigation params via the param list types. Untyped navigate calls +bypass TypeScript safety and can cause runtime crashes. + +## Missing JSDoc + +The PR template requires JSDoc on new functions and updated JSDoc on modified +functions. While the codebase currently has few JSDoc comments, all new code +should include them. Help improve this incrementally. + +## Floating Promises + +Even though `@typescript-eslint/no-floating-promises` is not currently enforced +in ESLint, avoid floating promises as a best practice. Every promise should be +`await`ed or have a `.catch()` handler. + +```tsx +// Wrong - unhandled promise +fetchBalances(publicKey); + +// Correct +await fetchBalances(publicKey); + +// Also correct +fetchBalances(publicKey).catch((error) => { + Sentry.captureException(normalizeError(error)); +}); +``` + +## Trusting dApp Metadata + +WalletConnect session info (app name, icon, URL) comes from the dApp itself and +can be spoofed. Never use this metadata for security decisions. Always validate +the transaction content independently. + +## Logging Key Material + +Never `console.log` keys, seeds, or passwords, even in `__DEV__` mode. Logs can +be captured by crash reporting tools or device log viewers. + +## Skipping Network Validation + +Always verify the WalletConnect request chain matches the active Stellar +network. A dApp could request signing on `stellar:pubnet` while the user expects +`stellar:testnet`. + +## Direct Store Mutations + +Zustand's `set()` creates new state objects. Never mutate existing state +directly. + +```tsx +// Wrong - mutating existing state +get().balances.push(newBalance); + +// Correct - creating new state +set({ balances: [...get().balances, newBalance] }); +``` + +## Cross-Store Action Chains + +Do not have store A's action call store B's action which calls store C's action. +This creates hard-to-debug cascading updates. Keep actions independent and +compose at the hook or component level instead. + +## Empty Catch Blocks + +Always handle or log errors. At minimum, use `normalizeError()` + Sentry. + +```tsx +// Wrong +try { + await riskyOp(); +} catch {} + +// Correct +try { + await riskyOp(); +} catch (error) { + Sentry.captureException(normalizeError(error)); +} +``` + +## Hardcoding Test Data + +Use environment variables for test secrets. Never hardcode recovery phrases, +private keys, or test passwords in source code. + +```tsx +// Wrong +const testPhrase = "abandon abandon abandon ..."; + +// Correct +const testPhrase = process.env.E2E_TEST_RECOVERY_PHRASE; +``` diff --git a/docs/skills/freighter-mobile-best-practices/references/architecture.md b/docs/skills/freighter-mobile-best-practices/references/architecture.md new file mode 100644 index 00000000..016d8d73 --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/architecture.md @@ -0,0 +1,154 @@ +# Architecture + +## Layer Structure + +The codebase follows a layered architecture with a strict downward import +direction: + +``` +components (UI) + -> hooks (logic) + -> ducks (state) + -> services (APIs) + -> helpers (utilities) + -> types +``` + +Each layer may import from the layers below it but never from the layers above. +This ensures a clean dependency graph and prevents circular imports. + +## Zustand Duck Pattern + +State management uses Zustand with a "duck" pattern. Each store (duck) lives in +`src/ducks/` and follows this structure: + +```tsx +// src/ducks/prices.ts +import { create } from "zustand"; + +interface PricesState { + // State + prices: Record; + isLoading: boolean; + error: string | null; + + // Actions + fetchPrices: (assetCodes: string[]) => Promise; + clearPrices: () => void; +} + +export const usePricesStore = create((set, get) => ({ + prices: {}, + isLoading: false, + error: null, + + fetchPrices: async (assetCodes) => { + const requestId = generateRequestId(); + set({ isLoading: true, error: null, currentRequestId: requestId }); + + try { + const prices = await pricesService.getPrices(assetCodes); + // Check staleness before setting + if (get().currentRequestId !== requestId) return; + set({ prices, isLoading: false }); + } catch (error) { + if (get().currentRequestId !== requestId) return; + set({ error: normalizeError(error).message, isLoading: false }); + } + }, + + clearPrices: () => set({ prices: {}, error: null }), +})); +``` + +### Async Action Pattern + +Async actions follow this general sequence: + +1. Set loading state: `set({ isLoading: true, error: null })` +2. Call the service layer +3. Set result or error state + +For actions prone to race conditions (e.g., `transactionBuilder.ts`), a +`generateRequestId()` pattern is used to check staleness before applying +results. This pattern is NOT universal — most stores (like `prices.ts`, +`balances.ts`) use simpler try/catch without request IDs. Use the request ID +pattern only when concurrent calls to the same action could produce stale +results. + +### Store Interaction + +- Stores can read each other via `getState()`: + `usePricesStore.getState().prices` +- Prefer passing data as action parameters over cross-store reads +- Never have store A's action call store B's action which calls store C's action + +## Screen Structure + +Each screen follows a consistent directory layout: + +``` +src/screens/SendPayment/ + index.tsx # Screen entry point / coordinator + screens/ # Sub-screens for multi-step flows + SelectAsset.tsx + EnterAmount.tsx + ConfirmTransaction.tsx + components/ # Screen-specific UI components + RecipientInput.tsx + AmountDisplay.tsx + hooks/ # Screen-specific logic hooks + useSendPaymentFlow.ts + useValidateRecipient.ts +``` + +## Hook Composition + +Reusable hooks live in `src/hooks/` and encapsulate common logic. Screens +compose multiple hooks together: + +```tsx +const SendPaymentScreen: React.FC = () => { + const { activeAccount } = useGetActiveAccount(); + const { buildTransaction } = useTransactionBuilderStore(); + const { validateMemo } = useValidateTransactionMemo(); + const { scanResult } = useBlockaidTransaction(); + // ... compose the flow +}; +``` + +## Service Layer + +Services live in `src/services/` and handle all external API communication: + +- `apiFactory.ts` provides `createApiService()` with configurable `baseURL` +- All service functions return typed responses +- Retry configuration is handled at the service level + +## Provider Layer + +`src/providers/` contains app-wide context providers: + +- `WalletKitProvider` for WalletConnect +- Theme provider +- Other app-wide concerns + +## Config + +`src/config/` holds constants, feature flags, and environment-specific values +used across the app. + +## Dual Bundle IDs + +The app maintains separate identities for development and production: + +| Concern | Dev | Prod | +| ---------------- | -------------------------- | ----------------------------- | +| Bundle ID | `org.stellar.freighterdev` | `org.stellar.freighterwallet` | +| Signing | Dev certificates | Prod certificates | +| Push tokens | Separate | Separate | +| Deep link scheme | `freighterdev://` | `freighterwallet://` | +| Keychain entries | Isolated | Isolated | + +This isolation ensures dev builds never interfere with production data or +credentials. diff --git a/docs/skills/freighter-mobile-best-practices/references/code-style.md b/docs/skills/freighter-mobile-best-practices/references/code-style.md new file mode 100644 index 00000000..c0891510 --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/code-style.md @@ -0,0 +1,266 @@ +# Code Style + +## Prettier Configuration (.prettierrc.json) + +- **Quotes**: Double quotes (`singleQuote: false`) +- **Indentation**: 2-space indent +- **Print width**: 80 characters +- **Trailing commas**: `all` (including function parameters) +- **Semicolons**: Always +- **Arrow parens**: Always (`arrowParens: "always"`) +- **Import sorting**: `@trivago/prettier-plugin-sort-imports` plugin. Imports + use absolute paths from `src/` root (no `@/` prefix). ESLint handles grouping: + builtin > external > internal > parent > sibling > index + +## ESLint Configuration (eslint.config.mjs) + +Extends: + +- Airbnb +- Airbnb TypeScript +- Airbnb Hooks +- `@typescript-eslint/recommended` +- `@typescript-eslint/recommended-requiring-type-checking` +- Prettier (disables conflicting rules) + +### Key Rules + +**Arrow functions enforced**: `react/function-component-definition` is set to +`error` for both named and unnamed components. All components must use arrow +function expressions: + +```tsx +// Correct +const MyComponent: React.FC = ({ title }) => { + return {title}; +}; + +// Wrong - will fail lint +export function MyComponent({ title }: Props) { + return {title}; +} +``` + +**Absolute imports only**: `@fnando/eslint-plugin-consistent-import` enforces +imports from `src/` root with `disallowRelative: true`: + +```tsx +// Correct +import { useAuth } from "hooks/useAuth"; + +// Wrong - will fail lint +import { useAuth } from "../../hooks/useAuth"; +``` + +**Import ordering**: Groups ordered as builtin > external > internal > parent > +sibling > index. Newlines required between groups. Alphabetical sorting within +each group. + +**No floating promises**: `@typescript-eslint/no-floating-promises` is currently +**disabled** (`"off"` in eslint.config.mjs). Promises are not required to be +awaited or caught by lint, but best practice is to always handle them. + +**No unsafe assignments/calls/returns**: Enforced in production code. Relaxed +only in test files. + +**Translation enforcement**: Custom ESLint plugin +(`src/eslint-plugin-translations/`) flags missing translation keys as errors. +Every user-facing string must go through `t()`. + +### Test File Relaxation + +TypeScript strict rules are relaxed in these patterns: + +- `**/*.test.ts` +- `**/*.test.tsx` +- `__tests__/**/*` + +## File Naming Conventions + +- **Components**: PascalCase directories (e.g., `SendPayment/`, `AccountCard/`) +- **Hooks**: `useXxx.ts` (e.g., `useAuth.ts`, `useBiometrics.ts`) +- **Ducks (stores)**: `featureName.ts` (e.g., `prices.ts`, `walletKit.ts`) +- **Helpers**: camelCase (e.g., `formatBalance.ts`, `stellarHelpers.ts`) + +## Pre-Commit Hooks (.husky/pre-commit) + +Three checks run in sequence on every commit: + +1. **lint-staged**: Runs ESLint `--fix` and Prettier on staged files +2. **yarn test**: Runs the full Jest test suite +3. **yarn lint:ts**: TypeScript type checking + +## lint-staged Configuration + +- `*.{js,jsx,ts,tsx}` -> `eslint --fix` + `prettier --write` +- `*.{json,md,yml,yaml}` -> `prettier --write` + +## Code Review Guidelines + +The following conventions are enforced during code review (not automated via +ESLint plugins): + +- **enum-over-type** -- prefer enums over type union literals for finite named + sets +- **no-loose-strings** -- all user-facing strings must use i18n `t()` calls +- **no-magic-numbers** -- extract numeric literals into named constants +- **i18n-pt-en** -- every user-facing string must have both English and + Portuguese translations + +The custom ESLint plugin at `src/eslint-plugin-translations/` enforces +translation key presence automatically. + +## Detailed Patterns (Based on 434-file Analysis) + +### Destructuring + +Inline parameter destructuring is the dominant pattern (90%+): + +```tsx +// STANDARD — inline destructuring (90%+ of components) +const BalanceRow = ({ balance, scanResult }: BalanceRowProps) => { ... }; + +// LESS COMMON — separate destructuring +const BalanceRow = (props: BalanceRowProps) => { + const { balance, scanResult } = props; +}; +``` + +For Zustand stores, direct hook calls dominate (70%) over selectors (30%): + +```tsx +// DOMINANT (70%) — direct hook call +const { balances, isLoading } = useBalancesStore(); + +// USED FOR PERF (30%) — selector to subscribe to specific data +const balances = useBalancesStore((state) => state.balances); +``` + +### Optional Chaining and Nullish Coalescing + +`&&` is 1.9x more common than `?.` (2700+ vs 921 occurrences). Both are +acceptable: + +```tsx +// Conditional rendering — always use && +{ + isMalicious && ; +} + +// Property access — use ?. +const fiatTotal = balance?.fiatTotal; +``` + +Nullish coalescing `??` (175 occurrences) is preferred over `||` for new code +when preserving `0`/`""`: + +```tsx +const timeout = config.timeout ?? DEFAULT_TIMEOUT; // PREFERRED +``` + +### Export Patterns + +Named exports dominate (70%), default exports for screens (30%): + +```tsx +// SCREENS — default export (100% of screens) +export default HomeScreen; + +// HOOKS/HELPERS/SERVICES — named export (95%+) +export const useColors = () => { ... }; +export const fetchBalances = async () => { ... }; +``` + +### Component Prop Types + +Separate interfaces are standard (95%+). `React.FC` is used in ~45% of +components — both patterns are acceptable: + +```tsx +// BOTH ACCEPTABLE +const BalanceRow: React.FC = ({ balance }) => { ... }; +const BalanceRow = ({ balance }: BalanceRowProps) => { ... }; +``` + +### Naming Deep Dive + +| Category | Convention | Evidence | +| ----------------- | --------------------------------------------------- | --------------------------- | +| Screen components | `XxxScreen` suffix always | 40+ screens, 100% adherence | +| Hooks | `useXxx` always | 57 hooks, 100% adherence | +| Zustand stores | `useXxxStore` | 24 ducks, 100% adherence | +| Store actions | verb + noun (`fetchBalances`, `clearData`) | 95%+ | +| Constants | SCREAMING_SNAKE_CASE in `config/constants.ts` | 95%+ | +| Types/Interfaces | PascalCase with `Props`, `State`, `Config` suffixes | 500+ types, 100% | +| Enum values | Mixed PascalCase/SCREAMING_SNAKE per enum | Context-dependent | + +### Conditional Rendering + +`&&` pattern dominates (70%), ternary for either/or (25%), early returns rare +(5%): + +```tsx +// MOST COMMON (70%) +{item.icon && {item.icon}} + +// FOR EITHER/OR (25%) +{isLoading ? : } + +// RARE — early returns in hooks, not components +if (!data) return ; +``` + +### Async Patterns + +`async/await` (85%) over `.then()` (15%). `Promise.all` used in 27 occurrences +(13 files): + +```tsx +// STANDARD +const balances = await fetchBalances(publicKey); + +// PARALLEL FETCHING +const [balances, prices] = await Promise.all([ + fetchBalances(publicKey), + fetchPrices(assetCodes), +]); +``` + +### Type Assertions and Generics + +Always `as Type` (TSX requirement). Prefer descriptive generic names (60%) over +single letters (40%): + +```tsx +// PREFERRED +create((set, get) => ({ ... })); + +// ACCEPTABLE for simple cases +function identity(value: T): T { return value; } +``` + +`any` count: 139 occurrences across 70 files (~3%) — mostly external API +integrations. Use `unknown` for new code. + +### Styling Split + +| Approach | Usage | When | +| --------------------- | --------------------- | ----------------------------------- | +| NativeWind (Tailwind) | 45% (921 occurrences) | Layout, spacing, simple styles | +| Inline style props | 50% | Dynamic styles, conditional styling | +| styled-components | 4% (18 occurrences) | Complex themed components | +| StyleSheet.create | <1% (1 occurrence) | Avoid for new code | + +### String/Number Patterns + +Template literals near-universal (95%+). All magic numbers extracted to +`config/constants.ts`: + +```tsx +// ALL constants go in config/constants.ts +export const DEFAULT_PADDING = 24; +export const DEFAULT_ICON_SIZE = 24; +export const NATIVE_TOKEN_CODE = "XLM"; +``` + +Error messages use i18n (`t()`) — no hardcoded error strings. diff --git a/docs/skills/freighter-mobile-best-practices/references/dependencies.md b/docs/skills/freighter-mobile-best-practices/references/dependencies.md new file mode 100644 index 00000000..9fed8b94 --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/dependencies.md @@ -0,0 +1,91 @@ +# Dependencies + +## Package Manager + +**Yarn** is the package manager. Do not use npm. + +## Node Version + +Node >= 20 is required (specified in `package.json` engines). CI uses Node 20. + +## Native Dependencies + +These packages require native linking and affect both iOS and Android build +configurations: + +- `react-native-keychain` — Secure storage (Keychain/Keystore) +- `@d11/react-native-fast-image` — Cached image loading +- `react-native-reanimated` — Animation library +- And others + +When adding a package with native code, you must rebuild both platforms. + +## iOS Dependencies (CocoaPods) + +After adding a native dependency: + +```bash +yarn pod-install +``` + +To clean and reinstall CocoaPods (when encountering pod issues): + +```bash +cd ios && pod deintegrate && pod cache clean --all +cd .. && yarn pod-install +``` + +## Android Dependencies (Gradle) + +Clean the Android build: + +```bash +yarn gradle-clean +``` + +JDK 17+ is recommended for Android builds. + +## Full Environment Reset + +When things go wrong, do a full reset: + +```bash +yarn r-install +``` + +This resets the environment and rebuilds everything from scratch. + +## Metro Cache + +Start Metro bundler with a clean cache: + +```bash +yarn start-c +``` + +This runs `react-native start --reset-cache`. + +## Adding a Native Dependency + +1. Add the package to `package.json` +2. Run `yarn install` +3. Run `yarn pod-install` (for iOS) +4. Rebuild both platforms and verify + +## Upgrading React Native + +1. Follow the official + [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) +2. Test thoroughly on both iOS and Android +3. Check all native module compatibility with the new RN version +4. Pay special attention to `react-native-reanimated`, `react-native-keychain`, + and other native modules + +## Environment Variables + +- Configuration lives in `.env` (created from `.env.example`) +- The `.env.example` template contains 48 variables +- **Never commit `.env`** — it may contain secrets +- Keep `.env.example` updated when adding new variables +- E2E test variables (`IS_E2E_TEST`, `E2E_TEST_RECOVERY_PHRASE`, etc.) are also + configured here diff --git a/docs/skills/freighter-mobile-best-practices/references/error-handling.md b/docs/skills/freighter-mobile-best-practices/references/error-handling.md new file mode 100644 index 00000000..5defcd0b --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/error-handling.md @@ -0,0 +1,140 @@ +# Error Handling + +## Zustand Action Pattern + +Every async action in a Zustand store follows this error handling structure: + +```tsx +fetchData: async (params) => { + set({ isLoading: true, error: null }); + + try { + const result = await service.getData(params); + set({ data: result, isLoading: false }); + } catch (error) { + const normalized = normalizeError(error); + set({ error: normalized.message, isLoading: false }); + } +}, +``` + +Always clear the error state before starting a new request (`error: null`). + +## Error Normalization + +`normalizeError()` in `src/config/logger.ts` converts unknown errors into proper +`Error` objects for Sentry reporting. It handles: + +- Standard `Error` objects (passed through) +- Plain strings (wrapped in `new Error()`) +- `null` / `undefined` (generic fallback message) +- React Native event objects (extracts meaningful info) +- Nested error objects (unwraps to find the root cause) + +```tsx +import { normalizeError } from "config/logger"; + +try { + await riskyOperation(); +} catch (error) { + const normalized = normalizeError(error); + Sentry.captureException(normalized); + set({ error: normalized.message, isLoading: false }); +} +``` + +## Type Guards + +Use runtime type guards for discriminating between different data shapes: + +- `isNativeBalance()` — checks if a balance is the native XLM asset +- `isClassicBalance()` — checks if a balance is a classic Stellar asset + +These prevent runtime type errors when handling polymorphic data from the +network. + +## Network Retry + +Horizon transaction submissions retry on HTTP 504 with exponential backoff: + +| Attempt | Delay | +| ------- | ---------- | +| 1 | 1 second | +| 2 | 2 seconds | +| 3 | 4 seconds | +| 4 | 8 seconds | +| 5 | 16 seconds | + +Maximum of 5 retry attempts before giving up and surfacing the error. + +## Toast Notifications + +Surface user-facing errors via the toast system, not native alerts: + +```tsx +// Correct - use toast for errors +showToast({ message: t("send.errors.insufficientBalance"), type: "error" }); + +// Wrong - avoid alert() for routine errors +Alert.alert("Error", "Insufficient balance"); +``` + +Reserve `Alert.alert()` for critical confirmations only (e.g., "Are you sure you +want to delete this account?"). + +## Transaction Validation + +Use `validateTransactionParams()` before building any transaction. It returns an +error message string or `null`: + +```tsx +const validationError = validateTransactionParams({ + destination, + amount, + asset, +}); + +if (validationError) { + set({ error: validationError }); + return; +} + +// Safe to build the transaction +``` + +## WalletConnect Error Responses + +When rejecting a WalletConnect request, respond with an error message: + +```tsx +await walletKit.respondSessionRequest({ + topic: session.topic, + response: { + id: request.id, + jsonrpc: "2.0", + error: { code: 5000, message: "User rejected the request" }, + }, +}); +``` + +Use `hasRespondedRef` to prevent duplicate responses to the same request: + +```tsx +if (hasRespondedRef.current) return; +hasRespondedRef.current = true; +await walletKit.respondSessionRequest({ ... }); +``` + +## Sentry Integration + +`normalizeError()` feeds directly into Sentry for crash reporting. Always +normalize errors before sending to Sentry to ensure consistent, actionable +reports. + +## Rules + +- **Never** use empty catch blocks. Always handle or log the error. +- **Never** silently swallow errors. At minimum, use `normalizeError()` + + Sentry. +- **Never** show generic "Something went wrong" without additional context. + Include what operation failed. diff --git a/docs/skills/freighter-mobile-best-practices/references/git-workflow.md b/docs/skills/freighter-mobile-best-practices/references/git-workflow.md new file mode 100644 index 00000000..07a5659e --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/git-workflow.md @@ -0,0 +1,90 @@ +# Git & PR Workflow + +## Branching + +- **Main branch**: `main` (not `master`) +- **Branch naming**: Use `{initials}-description` format: + - `lf-feature-biometric-onboarding` — Feature work + - `cg-fix-token-display` — Bug fixes + - `lf-upgrade-react-native` — Maintenance + +## Commit Messages + +- Use action verb in present tense: Add, Fix, Update, Improve, Cleanup, Remove +- Keep messages concise +- PR numbers are auto-added on merge (squash merge) + +Examples: + +- `Add biometric onboarding flow` +- `Fix WalletConnect session disconnect handling` +- `Update price fetching timeout to 3 seconds` +- `Remove deprecated auth middleware` + +## Pull Request Process + +### PR Template (.github/pull_request_template.md) + +The PR template includes a comprehensive checklist. Key requirements: + +- **JSDoc**: Add JSDoc on new functions, update JSDoc on modified functions +- **Both platforms**: Test on both iOS and Android +- **Small screens**: Test on small screen devices/simulators +- **Design approval**: UI changes need design team sign-off +- **Metrics**: Check if changes affect tracked metrics +- **Self-review**: Review your own diff before requesting reviews + +### PR Expectations + +- No mixed concerns (don't combine a bug fix with a refactor) +- Include screenshots or screen recordings for UI changes +- Both iOS and Android must be tested +- Small screens must be verified + +## CI on Pull Requests + +| Workflow | What It Runs | +| ----------------- | -------------------------------- | +| `test.yml` | Jest test suite | +| `ios-e2e.yml` | Maestro iOS end-to-end tests | +| `android-e2e.yml` | Maestro Android end-to-end tests | + +All checks must pass before merging. + +## Release Process (RELEASE.md) + +### Standard Release + +1. Triggered from `main` via GitHub Actions +2. Creates a release branch +3. Bumps version in 5 files: + - `package.json` + - `build.gradle` (Android) + - `Info.plist` (iOS prod) + - `Info-Dev.plist` (iOS dev) + - `project.pbxproj` (Xcode) +4. Auto-generates release notes from commit history + +### Emergency Release + +1. Triggered from a previous tag (not `main`) +2. Creates an `emergency-release` branch +3. Cherry-picks the necessary fixes +4. Follows the same version bump process + +### Post-Merge Steps + +1. Create a manual git tag for the release +2. Trigger iOS and Android builds via GitHub Actions +3. Promote builds in App Store Connect (iOS) and Google Play Console (Android) + +## Nightly Builds + +- Run daily at 8 AM UTC +- Automatically skipped when a release branch is active +- Useful for catching integration issues early + +## Branch Cleanup + +Delete `release/` and `emergency-release/` branches after they are merged back +to `main`. diff --git a/docs/skills/freighter-mobile-best-practices/references/i18n.md b/docs/skills/freighter-mobile-best-practices/references/i18n.md new file mode 100644 index 00000000..9cbb36e3 --- /dev/null +++ b/docs/skills/freighter-mobile-best-practices/references/i18n.md @@ -0,0 +1,94 @@ +# Internationalization (i18n) + +## Framework + +The app uses `i18next` + `react-i18next`, initialized in `src/i18n/index.ts`. + +## Supported Languages + +| Language | Code | Location | +| ---------- | ---- | --------------------------------------- | +| English | `en` | `src/i18n/locales/en/translations.json` | +| Portuguese | `pt` | `src/i18n/locales/pt/translations.json` | + +## Device Language Detection + +`getDeviceLanguage()` detects the device's preferred language and falls back to +English if the detected language is not supported. + +## Custom Hook + +Use `useAppTranslation()` instead of the raw `useTranslation` hook. It wraps +`useTranslation<"translations">()` with proper typing: + +```tsx +import { useAppTranslation } from "hooks/useAppTranslation"; + +const MyComponent: React.FC = () => { + const { t } = useAppTranslation(); + + return {t("home.title")}; +}; +``` + +## Key Structure + +Translation keys use nested JSON objects with dot notation: + +```json +{ + "transaction": { + "errors": { + "amountRequired": "Amount is required", + "insufficientBalance": "Insufficient balance", + "invalidDestination": "Invalid destination address" + }, + "labels": { + "amount": "Amount", + "destination": "Destination" + } + } +} +``` + +Access in code: `t("transaction.errors.amountRequired")` + +## Adding New Strings + +1. Add the key to `src/i18n/locales/en/translations.json` +2. Add the same key to `src/i18n/locales/pt/translations.json` with the + Portuguese translation +3. Use `t("your.new.key")` in the component + +Both language files must be updated simultaneously. Missing translations will +trigger ESLint errors. + +## ESLint Enforcement + +The custom ESLint plugin at `src/eslint-plugin-translations/` enforces +translation usage: + +- **Rule**: `translations/missing-translations` (error level) +- **What it does**: Flags direct string literals in JSX that should use `t()` + instead +- Every user-facing string must come from `t()` calls + +```tsx +// Correct +{t("home.welcome")} +