qualia-framework 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/bin/cli.js +519 -0
- package/framework/agents/architecture-strategist.md +53 -0
- package/framework/agents/backend-agent.md +150 -0
- package/framework/agents/code-simplicity-reviewer.md +86 -0
- package/framework/agents/frontend-agent.md +111 -0
- package/framework/agents/kieran-typescript-reviewer.md +96 -0
- package/framework/agents/performance-oracle.md +111 -0
- package/framework/agents/qualia-codebase-mapper.md +760 -0
- package/framework/agents/qualia-debugger.md +1203 -0
- package/framework/agents/qualia-executor.md +881 -0
- package/framework/agents/qualia-integration-checker.md +423 -0
- package/framework/agents/qualia-phase-researcher.md +453 -0
- package/framework/agents/qualia-plan-checker.md +699 -0
- package/framework/agents/qualia-planner.md +1241 -0
- package/framework/agents/qualia-project-researcher.md +602 -0
- package/framework/agents/qualia-research-synthesizer.md +236 -0
- package/framework/agents/qualia-roadmapper.md +605 -0
- package/framework/agents/qualia-verifier.md +685 -0
- package/framework/agents/team-orchestrator.md +228 -0
- package/framework/agents/teams/full-stack-team.md +48 -0
- package/framework/agents/teams/optimize-team.md +53 -0
- package/framework/agents/teams/review-team.md +62 -0
- package/framework/agents/teams/ship-team.md +86 -0
- package/framework/agents/test-agent.md +182 -0
- package/framework/askpass.sh +2 -0
- package/framework/commands/design.md +53 -0
- package/framework/commands/quick-db.md +22 -0
- package/framework/config/retention.json +35 -0
- package/framework/core/PRINCIPLES.md +77 -0
- package/framework/hooks/auto-format.sh +45 -0
- package/framework/hooks/block-env-edit.sh +42 -0
- package/framework/hooks/branch-guard.sh +46 -0
- package/framework/hooks/confirm-delete.sh +56 -0
- package/framework/hooks/migration-validate.sh +68 -0
- package/framework/hooks/notification-speak.sh +15 -0
- package/framework/hooks/pre-commit.sh +80 -0
- package/framework/hooks/pre-compact.sh +55 -0
- package/framework/hooks/pre-deploy-gate.sh +151 -0
- package/framework/hooks/qualia-colors.sh +32 -0
- package/framework/hooks/retention-cleanup.sh +43 -0
- package/framework/hooks/save-session-state.sh +153 -0
- package/framework/hooks/session-context-loader.sh +28 -0
- package/framework/hooks/session-learn.sh +30 -0
- package/framework/knowledge/claudecode-bible.md +1384 -0
- package/framework/knowledge/client-prefs.md +22 -0
- package/framework/knowledge/common-fixes.md +25 -0
- package/framework/knowledge/deployment-map.md +35 -0
- package/framework/knowledge/email-signature.html +1 -0
- package/framework/knowledge/employees.md +8 -0
- package/framework/knowledge/learned-patterns.md +51 -0
- package/framework/knowledge/optimization-research-2026.md +137 -0
- package/framework/knowledge/qualia-context.md +67 -0
- package/framework/knowledge/supabase-patterns.md +50 -0
- package/framework/knowledge/voice-agent-patterns.md +46 -0
- package/framework/qualia-engine/VERSION +1 -0
- package/framework/qualia-engine/bin/qualia-tools.js +2160 -0
- package/framework/qualia-engine/bin/qualia-tools.test.js +1054 -0
- package/framework/qualia-engine/references/checkpoints.md +775 -0
- package/framework/qualia-engine/references/continuation-format.md +249 -0
- package/framework/qualia-engine/references/decimal-phase-calculation.md +65 -0
- package/framework/qualia-engine/references/design-quality.md +56 -0
- package/framework/qualia-engine/references/git-integration.md +254 -0
- package/framework/qualia-engine/references/git-planning-commit.md +50 -0
- package/framework/qualia-engine/references/model-profile-resolution.md +32 -0
- package/framework/qualia-engine/references/model-profiles.md +73 -0
- package/framework/qualia-engine/references/phase-argument-parsing.md +61 -0
- package/framework/qualia-engine/references/planning-config.md +195 -0
- package/framework/qualia-engine/references/questioning.md +141 -0
- package/framework/qualia-engine/references/tdd.md +263 -0
- package/framework/qualia-engine/references/ui-brand.md +160 -0
- package/framework/qualia-engine/references/verification-patterns.md +612 -0
- package/framework/qualia-engine/templates/DEBUG.md +159 -0
- package/framework/qualia-engine/templates/DESIGN.md +81 -0
- package/framework/qualia-engine/templates/UAT.md +247 -0
- package/framework/qualia-engine/templates/codebase/architecture.md +255 -0
- package/framework/qualia-engine/templates/codebase/concerns.md +310 -0
- package/framework/qualia-engine/templates/codebase/conventions.md +307 -0
- package/framework/qualia-engine/templates/codebase/integrations.md +280 -0
- package/framework/qualia-engine/templates/codebase/stack.md +186 -0
- package/framework/qualia-engine/templates/codebase/structure.md +285 -0
- package/framework/qualia-engine/templates/codebase/testing.md +480 -0
- package/framework/qualia-engine/templates/config.json +35 -0
- package/framework/qualia-engine/templates/context.md +283 -0
- package/framework/qualia-engine/templates/continue-here.md +78 -0
- package/framework/qualia-engine/templates/debug-subagent-prompt.md +91 -0
- package/framework/qualia-engine/templates/discovery.md +146 -0
- package/framework/qualia-engine/templates/milestone-archive.md +123 -0
- package/framework/qualia-engine/templates/milestone.md +115 -0
- package/framework/qualia-engine/templates/phase-prompt.md +567 -0
- package/framework/qualia-engine/templates/planner-subagent-prompt.md +117 -0
- package/framework/qualia-engine/templates/project.md +184 -0
- package/framework/qualia-engine/templates/projects/ai-agent.md +156 -0
- package/framework/qualia-engine/templates/projects/mobile-app.md +181 -0
- package/framework/qualia-engine/templates/projects/voice-agent.md +134 -0
- package/framework/qualia-engine/templates/projects/website.md +137 -0
- package/framework/qualia-engine/templates/requirements.md +231 -0
- package/framework/qualia-engine/templates/research-project/ARCHITECTURE.md +204 -0
- package/framework/qualia-engine/templates/research-project/FEATURES.md +147 -0
- package/framework/qualia-engine/templates/research-project/PITFALLS.md +200 -0
- package/framework/qualia-engine/templates/research-project/STACK.md +120 -0
- package/framework/qualia-engine/templates/research-project/SUMMARY.md +170 -0
- package/framework/qualia-engine/templates/research.md +552 -0
- package/framework/qualia-engine/templates/roadmap.md +202 -0
- package/framework/qualia-engine/templates/state.md +176 -0
- package/framework/qualia-engine/templates/summary-complex.md +59 -0
- package/framework/qualia-engine/templates/summary-minimal.md +41 -0
- package/framework/qualia-engine/templates/summary-standard.md +48 -0
- package/framework/qualia-engine/templates/summary.md +246 -0
- package/framework/qualia-engine/templates/user-setup.md +311 -0
- package/framework/qualia-engine/templates/verification-report.md +322 -0
- package/framework/qualia-engine/workflows/add-phase.md +179 -0
- package/framework/qualia-engine/workflows/add-todo.md +157 -0
- package/framework/qualia-engine/workflows/audit-milestone.md +241 -0
- package/framework/qualia-engine/workflows/check-todos.md +176 -0
- package/framework/qualia-engine/workflows/complete-milestone.md +858 -0
- package/framework/qualia-engine/workflows/diagnose-issues.md +219 -0
- package/framework/qualia-engine/workflows/discovery-phase.md +289 -0
- package/framework/qualia-engine/workflows/discuss-phase.md +534 -0
- package/framework/qualia-engine/workflows/execute-phase.md +559 -0
- package/framework/qualia-engine/workflows/execute-plan.md +438 -0
- package/framework/qualia-engine/workflows/help.md +470 -0
- package/framework/qualia-engine/workflows/insert-phase.md +220 -0
- package/framework/qualia-engine/workflows/list-phase-assumptions.md +178 -0
- package/framework/qualia-engine/workflows/map-codebase.md +327 -0
- package/framework/qualia-engine/workflows/new-milestone.md +363 -0
- package/framework/qualia-engine/workflows/new-project.md +1037 -0
- package/framework/qualia-engine/workflows/pause-work.md +122 -0
- package/framework/qualia-engine/workflows/plan-milestone-gaps.md +256 -0
- package/framework/qualia-engine/workflows/plan-phase.md +422 -0
- package/framework/qualia-engine/workflows/progress.md +354 -0
- package/framework/qualia-engine/workflows/quick.md +252 -0
- package/framework/qualia-engine/workflows/remove-phase.md +326 -0
- package/framework/qualia-engine/workflows/research-phase.md +74 -0
- package/framework/qualia-engine/workflows/resume-project.md +306 -0
- package/framework/qualia-engine/workflows/set-profile.md +80 -0
- package/framework/qualia-engine/workflows/settings.md +145 -0
- package/framework/qualia-engine/workflows/transition.md +556 -0
- package/framework/qualia-engine/workflows/update.md +197 -0
- package/framework/qualia-engine/workflows/verify-phase.md +195 -0
- package/framework/qualia-engine/workflows/verify-work.md +625 -0
- package/framework/rules/context7.md +11 -0
- package/framework/rules/deployment.md +29 -0
- package/framework/rules/frontend.md +33 -0
- package/framework/rules/security.md +12 -0
- package/framework/rules/speed.md +20 -0
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +120 -0
- package/framework/scripts/bootstrap-pop-os.sh +354 -0
- package/framework/scripts/claude-voice +13 -0
- package/framework/scripts/cleanup.sh +131 -0
- package/framework/scripts/cowork-mode.sh +141 -0
- package/framework/scripts/generate-project-claude-md.sh +153 -0
- package/framework/scripts/load-test-webhook.js +172 -0
- package/framework/scripts/say.py +236 -0
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +167 -0
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +216 -0
- package/framework/scripts/speak.py +55 -0
- package/framework/scripts/speak.sh +18 -0
- package/framework/scripts/status.sh +138 -0
- package/framework/scripts/sync-to-framework.sh +65 -0
- package/framework/scripts/voice-hotkey.py +227 -0
- package/framework/scripts/voice-input.sh +51 -0
- package/framework/skills/animate/SKILL.md +202 -0
- package/framework/skills/bolder/SKILL.md +144 -0
- package/framework/skills/browser-qa/SKILL.md +536 -0
- package/framework/skills/clarify/SKILL.md +179 -0
- package/framework/skills/colorize/SKILL.md +170 -0
- package/framework/skills/critique/SKILL.md +126 -0
- package/framework/skills/deep-research/SKILL.md +271 -0
- package/framework/skills/delight/SKILL.md +329 -0
- package/framework/skills/deploy/SKILL.md +261 -0
- package/framework/skills/deploy-verify/SKILL.md +377 -0
- package/framework/skills/deploy-verify/scripts/canary-check.sh +206 -0
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +147 -0
- package/framework/skills/deploy-verify/scripts/check-cwv.js +139 -0
- package/framework/skills/deploy-verify/scripts/project-detect.sh +84 -0
- package/framework/skills/deploy-verify/scripts/verify.sh +548 -0
- package/framework/skills/design-quieter/SKILL.md +130 -0
- package/framework/skills/distill/SKILL.md +149 -0
- package/framework/skills/docs-lookup/SKILL.md +78 -0
- package/framework/skills/fcm-notifications/SKILL.md +125 -0
- package/framework/skills/financial-ledger/SKILL.md +1039 -0
- package/framework/skills/frontend-master/NOTICE.md +4 -0
- package/framework/skills/frontend-master/SKILL.md +127 -0
- package/framework/skills/frontend-master/reference/color-and-contrast.md +132 -0
- package/framework/skills/frontend-master/reference/interaction-design.md +123 -0
- package/framework/skills/frontend-master/reference/motion-design.md +99 -0
- package/framework/skills/frontend-master/reference/responsive-design.md +114 -0
- package/framework/skills/frontend-master/reference/spatial-design.md +100 -0
- package/framework/skills/frontend-master/reference/typography.md +131 -0
- package/framework/skills/frontend-master/reference/ux-writing.md +107 -0
- package/framework/skills/harden/SKILL.md +357 -0
- package/framework/skills/i18n-rtl/SKILL.md +752 -0
- package/framework/skills/learn/SKILL.md +71 -0
- package/framework/skills/memory/SKILL.md +50 -0
- package/framework/skills/mobile-expo/SKILL.md +864 -0
- package/framework/skills/mobile-expo/references/store-checklist.md +550 -0
- package/framework/skills/nestjs-backend/README.md +73 -0
- package/framework/skills/nestjs-backend/SKILL.md +446 -0
- package/framework/skills/nestjs-backend/references/templates.md +1173 -0
- package/framework/skills/normalize/SKILL.md +79 -0
- package/framework/skills/onboard/SKILL.md +242 -0
- package/framework/skills/polish/SKILL.md +209 -0
- package/framework/skills/pr/SKILL.md +66 -0
- package/framework/skills/qualia/SKILL.md +153 -0
- package/framework/skills/qualia-add-todo/SKILL.md +68 -0
- package/framework/skills/qualia-audit-milestone/SKILL.md +92 -0
- package/framework/skills/qualia-check-todos/SKILL.md +55 -0
- package/framework/skills/qualia-complete-milestone/SKILL.md +108 -0
- package/framework/skills/qualia-debug/SKILL.md +149 -0
- package/framework/skills/qualia-design/SKILL.md +203 -0
- package/framework/skills/qualia-discuss-phase/SKILL.md +72 -0
- package/framework/skills/qualia-execute-phase/SKILL.md +86 -0
- package/framework/skills/qualia-help/SKILL.md +67 -0
- package/framework/skills/qualia-idk/SKILL.md +352 -0
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +67 -0
- package/framework/skills/qualia-new-milestone/SKILL.md +72 -0
- package/framework/skills/qualia-new-project/SKILL.md +92 -0
- package/framework/skills/qualia-optimize/SKILL.md +417 -0
- package/framework/skills/qualia-pause-work/SKILL.md +96 -0
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +57 -0
- package/framework/skills/qualia-plan-phase/SKILL.md +101 -0
- package/framework/skills/qualia-progress/SKILL.md +53 -0
- package/framework/skills/qualia-quick/SKILL.md +89 -0
- package/framework/skills/qualia-research-phase/SKILL.md +88 -0
- package/framework/skills/qualia-resume-work/SKILL.md +62 -0
- package/framework/skills/qualia-review/SKILL.md +263 -0
- package/framework/skills/qualia-start/SKILL.md +182 -0
- package/framework/skills/qualia-verify-work/SKILL.md +105 -0
- package/framework/skills/qualia-workflow/SKILL.md +130 -0
- package/framework/skills/rag/SKILL.md +750 -0
- package/framework/skills/responsive/SKILL.md +231 -0
- package/framework/skills/retro/SKILL.md +284 -0
- package/framework/skills/sakani-conventions/SKILL.md +136 -0
- package/framework/skills/sakani-conventions/evals/evals.json +23 -0
- package/framework/skills/sakani-conventions/references/entities.md +365 -0
- package/framework/skills/sakani-conventions/references/error-codes.md +95 -0
- package/framework/skills/seo-master/SKILL.md +490 -0
- package/framework/skills/seo-master/references/checklist.md +199 -0
- package/framework/skills/seo-master/references/structured-data.md +609 -0
- package/framework/skills/ship/SKILL.md +202 -0
- package/framework/skills/stack-researcher/SKILL.md +215 -0
- package/framework/skills/status/SKILL.md +154 -0
- package/framework/skills/status/scripts/health-check.sh +562 -0
- package/framework/skills/subscription-payments/SKILL.md +250 -0
- package/framework/skills/supabase/SKILL.md +973 -0
- package/framework/skills/supabase/references/templates.md +159 -0
- package/framework/skills/team/SKILL.md +67 -0
- package/framework/skills/test-runner/SKILL.md +202 -0
- package/framework/skills/voice-agent/SKILL.md +407 -0
- package/framework/skills/zoho-workflow/SKILL.md +51 -0
- package/framework/statusline-command.sh +117 -0
- package/package.json +24 -0
- package/profiles/fawzi.json +16 -0
- package/profiles/hasan.json +16 -0
- package/profiles/moayad.json +16 -0
- package/templates/CLAUDE-owner.md +52 -0
- package/templates/CLAUDE.md.hbs +58 -0
- package/templates/env.claude.template +12 -0
- package/templates/settings.json +141 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-expo
|
|
3
|
+
description: "React Native Expo mobile app development — project setup, navigation (Expo Router), native modules, push notifications, secure storage, app store submission, and mobile-specific patterns. Use whenever building or modifying a React Native or Expo mobile app, creating screens, navigation flows, native integrations, or preparing for iOS/Android store submission. Triggers on: React Native, Expo, mobile app, iOS, Android, app store, screen, navigation, native module, mobile UI, push notification mobile, secure storage, EAS build."
|
|
4
|
+
tags: [react-native, expo, mobile, ios, android]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# React Native Expo Mobile Development
|
|
8
|
+
|
|
9
|
+
This skill covers React Native Expo mobile app development for Qualia workflow projects.
|
|
10
|
+
|
|
11
|
+
## Project Setup
|
|
12
|
+
|
|
13
|
+
### Expo SDK 52+
|
|
14
|
+
- Always use latest stable Expo SDK (52+)
|
|
15
|
+
- TypeScript is mandatory for all projects
|
|
16
|
+
- Use Expo Router for file-based routing (like Next.js)
|
|
17
|
+
- Configured via `app.json` or `app.config.ts`
|
|
18
|
+
|
|
19
|
+
### Directory Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
app/
|
|
23
|
+
├── (auth)/ # Protected routes (require authentication)
|
|
24
|
+
│ ├── (tabs)/ # Bottom tab navigator (protected)
|
|
25
|
+
│ │ ├── home.tsx
|
|
26
|
+
│ │ ├── units.tsx
|
|
27
|
+
│ │ └── profile.tsx
|
|
28
|
+
│ │ └── _layout.tsx # Tab layout configuration
|
|
29
|
+
│ ├── _layout.tsx # Auth wrapper layout
|
|
30
|
+
│ └── [route].tsx # Catch-all protected route
|
|
31
|
+
├── (public)/ # Unauthenticated screens
|
|
32
|
+
│ ├── login.tsx
|
|
33
|
+
│ ├── register.tsx
|
|
34
|
+
│ ├── verify-otp.tsx
|
|
35
|
+
│ └── _layout.tsx
|
|
36
|
+
├── _layout.tsx # Root layout
|
|
37
|
+
├── index.tsx # Root screen (can redirect)
|
|
38
|
+
└── +not-found.tsx # 404 screen
|
|
39
|
+
|
|
40
|
+
components/
|
|
41
|
+
├── ui/ # Reusable, unstyled UI primitives
|
|
42
|
+
│ ├── Button.tsx
|
|
43
|
+
│ ├── Input.tsx
|
|
44
|
+
│ ├── Modal.tsx
|
|
45
|
+
│ └── Card.tsx
|
|
46
|
+
├── forms/ # Form-specific components
|
|
47
|
+
│ ├── LoginForm.tsx
|
|
48
|
+
│ ├── OTPForm.tsx
|
|
49
|
+
│ └── ProfileForm.tsx
|
|
50
|
+
├── auth/ # Auth-related components
|
|
51
|
+
│ ├── ProtectedRoute.tsx
|
|
52
|
+
│ └── AuthGuard.tsx
|
|
53
|
+
└── [feature]/ # Feature-specific components
|
|
54
|
+
├── UnitsCard.tsx
|
|
55
|
+
└── UnitsList.tsx
|
|
56
|
+
|
|
57
|
+
lib/
|
|
58
|
+
├── api/ # API client layer
|
|
59
|
+
│ ├── client.ts # Axios/fetch with interceptors
|
|
60
|
+
│ ├── endpoints.ts # API endpoint definitions
|
|
61
|
+
│ └── schemas.ts # Zod schemas (shared with backend)
|
|
62
|
+
├── auth/ # Authentication logic
|
|
63
|
+
│ ├── context.tsx # Auth context provider
|
|
64
|
+
│ ├── tokens.ts # Token management
|
|
65
|
+
│ └── useAuth.ts # Auth hook
|
|
66
|
+
├── storage/ # Secure storage wrapper
|
|
67
|
+
│ ├── secureStore.ts # expo-secure-store wrapper
|
|
68
|
+
│ └── useSecureStorage.ts # Custom hook
|
|
69
|
+
├── i18n/ # Internationalization (if needed)
|
|
70
|
+
│ └── i18n.ts
|
|
71
|
+
├── utils/ # Helper functions
|
|
72
|
+
└── constants.ts # App-wide constants
|
|
73
|
+
|
|
74
|
+
hooks/
|
|
75
|
+
├── useAuthCheck.ts # Check if user is authenticated
|
|
76
|
+
├── useNavigation.ts # Type-safe navigation hook
|
|
77
|
+
├── useFetch.ts # Custom hook for API calls
|
|
78
|
+
└── useKeyboard.ts # Keyboard handling
|
|
79
|
+
|
|
80
|
+
constants/
|
|
81
|
+
├── theme.ts # Colors, typography, spacing
|
|
82
|
+
└── config.ts # API base URL, feature flags
|
|
83
|
+
|
|
84
|
+
assets/
|
|
85
|
+
├── icons/
|
|
86
|
+
├── images/
|
|
87
|
+
└── animations/
|
|
88
|
+
|
|
89
|
+
App.tsx or app.json # Entry point configuration
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Navigation with Expo Router
|
|
93
|
+
|
|
94
|
+
### File-Based Routing
|
|
95
|
+
- Expo Router uses file system paths as routes
|
|
96
|
+
- `app/home.tsx` → `/home` route
|
|
97
|
+
- `app/(tabs)/home.tsx` → `/home` with tab layout
|
|
98
|
+
- `app/(auth)/profile.tsx` → Protected `/profile` route
|
|
99
|
+
- Deep linking is automatic with proper configuration
|
|
100
|
+
|
|
101
|
+
### Layout Groups
|
|
102
|
+
Layout groups with parentheses don't create route segments:
|
|
103
|
+
- `(auth)` group shares auth layout without adding to URL
|
|
104
|
+
- `(public)` group for unauthenticated screens
|
|
105
|
+
- `(tabs)` group for tab navigation within auth group
|
|
106
|
+
|
|
107
|
+
### Protected Routes Pattern
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// app/(auth)/_layout.tsx
|
|
111
|
+
import { useAuth } from '@/lib/auth/useAuth';
|
|
112
|
+
import { Redirect, Stack } from 'expo-router';
|
|
113
|
+
|
|
114
|
+
export default function AuthLayout() {
|
|
115
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
116
|
+
|
|
117
|
+
if (isLoading) return <SplashScreen />;
|
|
118
|
+
if (!isAuthenticated) return <Redirect href="/login" />;
|
|
119
|
+
|
|
120
|
+
return <Stack />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// app/(auth)/(tabs)/_layout.tsx
|
|
124
|
+
import { Tabs } from 'expo-router';
|
|
125
|
+
|
|
126
|
+
export default function TabsLayout() {
|
|
127
|
+
return (
|
|
128
|
+
<Tabs>
|
|
129
|
+
<Tabs.Screen name="home" options={{ title: 'Home' }} />
|
|
130
|
+
<Tabs.Screen name="units" options={{ title: 'Units' }} />
|
|
131
|
+
<Tabs.Screen name="profile" options={{ title: 'Profile' }} />
|
|
132
|
+
</Tabs>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Type-Safe Navigation
|
|
138
|
+
Create a custom hook for navigation with proper typing:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// lib/navigation/useTypedNavigation.ts
|
|
142
|
+
import { useNavigation } from '@react-navigation/native';
|
|
143
|
+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
144
|
+
|
|
145
|
+
type RootStackParamList = {
|
|
146
|
+
'/(public)/login': undefined;
|
|
147
|
+
'/(auth)/(tabs)/home': undefined;
|
|
148
|
+
'/(auth)/(tabs)/units': { unitId: string };
|
|
149
|
+
'/(auth)/profile': undefined;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function useTypedNavigation() {
|
|
153
|
+
return useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Deep Linking
|
|
158
|
+
Configure in `app.json`:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"scheme": "sakani",
|
|
163
|
+
"plugins": [
|
|
164
|
+
["expo-router", {
|
|
165
|
+
"origin": "https://sakani.app"
|
|
166
|
+
}]
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## API Client Setup
|
|
172
|
+
|
|
173
|
+
### Centralized API Client
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
// lib/api/client.ts
|
|
177
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
178
|
+
import { getAccessToken, getRefreshToken, setAccessToken } from '@/lib/auth/tokens';
|
|
179
|
+
|
|
180
|
+
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3000';
|
|
181
|
+
|
|
182
|
+
let client: AxiosInstance;
|
|
183
|
+
|
|
184
|
+
export function createApiClient(): AxiosInstance {
|
|
185
|
+
client = axios.create({
|
|
186
|
+
baseURL: API_BASE_URL,
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'application/json',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Request interceptor: attach access token
|
|
193
|
+
client.interceptors.request.use(async (config) => {
|
|
194
|
+
const token = await getAccessToken();
|
|
195
|
+
if (token) {
|
|
196
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
197
|
+
}
|
|
198
|
+
return config;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Response interceptor: handle 401 with refresh
|
|
202
|
+
client.interceptors.response.use(
|
|
203
|
+
(response) => response,
|
|
204
|
+
async (error: AxiosError) => {
|
|
205
|
+
const originalRequest = error.config as any;
|
|
206
|
+
|
|
207
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
208
|
+
originalRequest._retry = true;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const refreshToken = await getRefreshToken();
|
|
212
|
+
const { data } = await axios.post(`${API_BASE_URL}/auth/refresh`, {
|
|
213
|
+
refreshToken,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await setAccessToken(data.accessToken);
|
|
217
|
+
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
|
|
218
|
+
return client(originalRequest);
|
|
219
|
+
} catch (refreshError) {
|
|
220
|
+
// Refresh failed, redirect to login
|
|
221
|
+
// Trigger logout in auth context
|
|
222
|
+
throw refreshError;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return Promise.reject(error);
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return client;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function getApiClient(): AxiosInstance {
|
|
234
|
+
return client || createApiClient();
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Endpoint Definitions
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
// lib/api/endpoints.ts
|
|
242
|
+
export const API_ENDPOINTS = {
|
|
243
|
+
AUTH: {
|
|
244
|
+
LOGIN: '/auth/login',
|
|
245
|
+
VERIFY_OTP: '/auth/verify-otp',
|
|
246
|
+
REFRESH: '/auth/refresh',
|
|
247
|
+
LOGOUT: '/auth/logout',
|
|
248
|
+
},
|
|
249
|
+
UNITS: {
|
|
250
|
+
LIST: '/units',
|
|
251
|
+
GET: (id: string) => `/units/${id}`,
|
|
252
|
+
CREATE: '/units',
|
|
253
|
+
UPDATE: (id: string) => `/units/${id}`,
|
|
254
|
+
},
|
|
255
|
+
PROFILE: {
|
|
256
|
+
GET: '/profile',
|
|
257
|
+
UPDATE: '/profile',
|
|
258
|
+
},
|
|
259
|
+
} as const;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Authentication Flow (Mobile)
|
|
263
|
+
|
|
264
|
+
### Token Management
|
|
265
|
+
|
|
266
|
+
Access tokens are short-lived (15-30 min) and stored in-memory.
|
|
267
|
+
Refresh tokens are long-lived and stored in secure storage.
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// lib/auth/tokens.ts
|
|
271
|
+
import * as SecureStore from 'expo-secure-store';
|
|
272
|
+
|
|
273
|
+
const ACCESS_TOKEN_KEY = 'accessToken';
|
|
274
|
+
const REFRESH_TOKEN_KEY = 'refreshToken';
|
|
275
|
+
|
|
276
|
+
let accessToken: string | null = null;
|
|
277
|
+
|
|
278
|
+
export async function setTokens(access: string, refresh: string) {
|
|
279
|
+
accessToken = access;
|
|
280
|
+
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refresh);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function getAccessToken(): Promise<string | null> {
|
|
284
|
+
return accessToken;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function getRefreshToken(): Promise<string | null> {
|
|
288
|
+
return await SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function clearTokens() {
|
|
292
|
+
accessToken = null;
|
|
293
|
+
await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function initializeTokens() {
|
|
297
|
+
// Called on app start
|
|
298
|
+
// Access token stays in memory, refresh token loaded from secure store
|
|
299
|
+
// Don't restore access token to memory (it expired)
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Auth Context
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
// lib/auth/context.tsx
|
|
307
|
+
import React, { createContext, useState, useEffect } from 'react';
|
|
308
|
+
import { AppState } from 'react-native';
|
|
309
|
+
import { getApiClient } from '@/lib/api/client';
|
|
310
|
+
import { setTokens, clearTokens, getRefreshToken } from './tokens';
|
|
311
|
+
|
|
312
|
+
interface AuthContextType {
|
|
313
|
+
isAuthenticated: boolean;
|
|
314
|
+
isLoading: boolean;
|
|
315
|
+
user: { id: string; email: string } | null;
|
|
316
|
+
login: (email: string, otp: string) => Promise<void>;
|
|
317
|
+
logout: () => Promise<void>;
|
|
318
|
+
refreshAuth: () => Promise<void>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
322
|
+
|
|
323
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
324
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
325
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
326
|
+
const [user, setUser] = useState<AuthContextType['user']>(null);
|
|
327
|
+
|
|
328
|
+
// Initialize auth on app start
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
const initAuth = async () => {
|
|
331
|
+
try {
|
|
332
|
+
const refreshToken = await getRefreshToken();
|
|
333
|
+
if (refreshToken) {
|
|
334
|
+
await refreshAuth();
|
|
335
|
+
} else {
|
|
336
|
+
setIsAuthenticated(false);
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
setIsLoading(false);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
initAuth();
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
// Listen for app foreground (auto-refresh tokens)
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
const subscription = AppState.addEventListener('focus', refreshAuth);
|
|
349
|
+
return () => subscription.remove();
|
|
350
|
+
}, []);
|
|
351
|
+
|
|
352
|
+
const login = async (email: string, otp: string) => {
|
|
353
|
+
const api = getApiClient();
|
|
354
|
+
const { data } = await api.post('/auth/verify-otp', { email, otp });
|
|
355
|
+
await setTokens(data.accessToken, data.refreshToken);
|
|
356
|
+
setUser(data.user);
|
|
357
|
+
setIsAuthenticated(true);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const logout = async () => {
|
|
361
|
+
await clearTokens();
|
|
362
|
+
setUser(null);
|
|
363
|
+
setIsAuthenticated(false);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const refreshAuth = async () => {
|
|
367
|
+
try {
|
|
368
|
+
const api = getApiClient();
|
|
369
|
+
const { data } = await api.post('/auth/refresh');
|
|
370
|
+
await setTokens(data.accessToken, data.refreshToken);
|
|
371
|
+
setUser(data.user);
|
|
372
|
+
setIsAuthenticated(true);
|
|
373
|
+
} catch {
|
|
374
|
+
await logout();
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<AuthContext.Provider value={{ isAuthenticated, isLoading, user, login, logout, refreshAuth }}>
|
|
380
|
+
{children}
|
|
381
|
+
</AuthContext.Provider>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function useAuth() {
|
|
386
|
+
const context = React.useContext(AuthContext);
|
|
387
|
+
if (!context) {
|
|
388
|
+
throw new Error('useAuth must be used within AuthProvider');
|
|
389
|
+
}
|
|
390
|
+
return context;
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### OTP Login Screen
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
// app/(public)/verify-otp.tsx
|
|
398
|
+
import { useState } from 'react';
|
|
399
|
+
import { View, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
|
|
400
|
+
import { useAuth } from '@/lib/auth/context';
|
|
401
|
+
import { useRouter } from 'expo-router';
|
|
402
|
+
|
|
403
|
+
export default function VerifyOTPScreen() {
|
|
404
|
+
const [otp, setOtp] = useState('');
|
|
405
|
+
const [loading, setLoading] = useState(false);
|
|
406
|
+
const { login } = useAuth();
|
|
407
|
+
const router = useRouter();
|
|
408
|
+
|
|
409
|
+
const handleVerify = async () => {
|
|
410
|
+
try {
|
|
411
|
+
setLoading(true);
|
|
412
|
+
await login('user@example.com', otp); // Email should come from previous screen
|
|
413
|
+
router.replace('/(auth)/(tabs)/home');
|
|
414
|
+
} catch (error) {
|
|
415
|
+
Alert.alert('Error', 'Invalid OTP');
|
|
416
|
+
} finally {
|
|
417
|
+
setLoading(false);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<View style={{ flex: 1, padding: 16, justifyContent: 'center' }}>
|
|
423
|
+
<Text style={{ fontSize: 20, marginBottom: 16 }}>Enter OTP</Text>
|
|
424
|
+
<TextInput
|
|
425
|
+
placeholder="000000"
|
|
426
|
+
value={otp}
|
|
427
|
+
onChangeText={setOtp}
|
|
428
|
+
keyboardType="number-pad"
|
|
429
|
+
maxLength={6}
|
|
430
|
+
style={{ borderWidth: 1, padding: 12, marginBottom: 16 }}
|
|
431
|
+
/>
|
|
432
|
+
<TouchableOpacity
|
|
433
|
+
onPress={handleVerify}
|
|
434
|
+
disabled={loading}
|
|
435
|
+
style={{ backgroundColor: '#007AFF', padding: 12, borderRadius: 8 }}
|
|
436
|
+
>
|
|
437
|
+
<Text style={{ color: 'white', textAlign: 'center' }}>Verify</Text>
|
|
438
|
+
</TouchableOpacity>
|
|
439
|
+
</View>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Secure Storage
|
|
445
|
+
|
|
446
|
+
### SecureStore for Sensitive Data
|
|
447
|
+
|
|
448
|
+
Never use AsyncStorage for tokens or secrets. Use `expo-secure-store`:
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
// lib/storage/secureStore.ts
|
|
452
|
+
import * as SecureStore from 'expo-secure-store';
|
|
453
|
+
|
|
454
|
+
const CHUNK_SIZE = 1800; // SecureStore limit is ~2KB per key
|
|
455
|
+
|
|
456
|
+
export async function secureSet(key: string, value: string): Promise<void> {
|
|
457
|
+
if (value.length > CHUNK_SIZE) {
|
|
458
|
+
// Chunk large values
|
|
459
|
+
for (let i = 0; i < value.length; i += CHUNK_SIZE) {
|
|
460
|
+
const chunk = value.substring(i, i + CHUNK_SIZE);
|
|
461
|
+
const chunkKey = `${key}_${Math.floor(i / CHUNK_SIZE)}`;
|
|
462
|
+
await SecureStore.setItemAsync(chunkKey, chunk);
|
|
463
|
+
}
|
|
464
|
+
await SecureStore.setItemAsync(`${key}_chunks`, Math.ceil(value.length / CHUNK_SIZE).toString());
|
|
465
|
+
} else {
|
|
466
|
+
await SecureStore.setItemAsync(key, value);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function secureGet(key: string): Promise<string | null> {
|
|
471
|
+
const chunks = await SecureStore.getItemAsync(`${key}_chunks`);
|
|
472
|
+
if (chunks) {
|
|
473
|
+
let value = '';
|
|
474
|
+
for (let i = 0; i < parseInt(chunks); i++) {
|
|
475
|
+
const chunk = await SecureStore.getItemAsync(`${key}_${i}`);
|
|
476
|
+
if (chunk) value += chunk;
|
|
477
|
+
}
|
|
478
|
+
return value || null;
|
|
479
|
+
}
|
|
480
|
+
return await SecureStore.getItemAsync(key);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function secureRemove(key: string): Promise<void> {
|
|
484
|
+
const chunks = await SecureStore.getItemAsync(`${key}_chunks`);
|
|
485
|
+
if (chunks) {
|
|
486
|
+
for (let i = 0; i < parseInt(chunks); i++) {
|
|
487
|
+
await SecureStore.deleteItemAsync(`${key}_${i}`);
|
|
488
|
+
}
|
|
489
|
+
await SecureStore.deleteItemAsync(`${key}_chunks`);
|
|
490
|
+
} else {
|
|
491
|
+
await SecureStore.deleteItemAsync(key);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## State Management
|
|
497
|
+
|
|
498
|
+
### React Context (Auth)
|
|
499
|
+
Use Context for small, global state like authentication.
|
|
500
|
+
|
|
501
|
+
### TanStack Query (Server State)
|
|
502
|
+
Use React Query for API data caching, refetching, and synchronization:
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
// lib/api/queries.ts
|
|
506
|
+
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
507
|
+
import { getApiClient } from './client';
|
|
508
|
+
|
|
509
|
+
export function useUnits() {
|
|
510
|
+
return useQuery({
|
|
511
|
+
queryKey: ['units'],
|
|
512
|
+
queryFn: async () => {
|
|
513
|
+
const { data } = await getApiClient().get('/units');
|
|
514
|
+
return data;
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function useCreateUnit() {
|
|
520
|
+
return useMutation({
|
|
521
|
+
mutationFn: async (unit: any) => {
|
|
522
|
+
const { data } = await getApiClient().post('/units', unit);
|
|
523
|
+
return data;
|
|
524
|
+
},
|
|
525
|
+
onSuccess: (data, variables, context) => {
|
|
526
|
+
// Invalidate and refetch units list
|
|
527
|
+
queryClient.invalidateQueries({ queryKey: ['units'] });
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Zustand (Complex Client State)
|
|
534
|
+
Use Zustand for complex client state if needed:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
// lib/store/appStore.ts
|
|
538
|
+
import { create } from 'zustand';
|
|
539
|
+
|
|
540
|
+
interface AppState {
|
|
541
|
+
theme: 'light' | 'dark';
|
|
542
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
546
|
+
theme: 'light',
|
|
547
|
+
setTheme: (theme) => set({ theme }),
|
|
548
|
+
}));
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Push Notifications
|
|
552
|
+
|
|
553
|
+
### Setup with expo-notifications
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
npx expo install expo-notifications expo-device
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Request Permissions
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
// lib/notifications/index.ts
|
|
563
|
+
import * as Notifications from 'expo-notifications';
|
|
564
|
+
import * as Device from 'expo-device';
|
|
565
|
+
|
|
566
|
+
export async function requestNotificationPermissions(): Promise<boolean> {
|
|
567
|
+
if (!Device.isDevice) {
|
|
568
|
+
console.log('Must use physical device for push notifications');
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
|
573
|
+
let finalStatus = existingStatus;
|
|
574
|
+
|
|
575
|
+
if (existingStatus !== 'granted') {
|
|
576
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
577
|
+
finalStatus = status;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return finalStatus === 'granted';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export async function getDeviceToken(): Promise<string | null> {
|
|
584
|
+
const token = (await Notifications.getExpoPushTokenAsync()).data;
|
|
585
|
+
return token;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Register device token with backend
|
|
589
|
+
export async function registerDeviceToken(token: string) {
|
|
590
|
+
const api = getApiClient();
|
|
591
|
+
await api.post('/notifications/register', { deviceToken: token });
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Listen for Notifications
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
// Set default notification handler
|
|
599
|
+
Notifications.setNotificationHandler({
|
|
600
|
+
handleNotification: async () => ({
|
|
601
|
+
shouldShowAlert: true,
|
|
602
|
+
shouldPlaySound: true,
|
|
603
|
+
shouldSetBadge: true,
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Listen in your app
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
const subscription = Notifications.addNotificationResponseReceivedListener((response) => {
|
|
610
|
+
// Handle notification tap
|
|
611
|
+
const route = response.notification.request.content.data?.route;
|
|
612
|
+
if (route) {
|
|
613
|
+
router.push(route);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
return () => subscription.remove();
|
|
618
|
+
}, []);
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Forms with react-hook-form + Zod
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
// lib/api/schemas.ts (shared with backend)
|
|
625
|
+
import { z } from 'zod';
|
|
626
|
+
|
|
627
|
+
export const LoginSchema = z.object({
|
|
628
|
+
email: z.string().email('Invalid email'),
|
|
629
|
+
otp: z.string().min(6, 'OTP must be 6 digits'),
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
export const UnitSchema = z.object({
|
|
633
|
+
name: z.string().min(2, 'Name is required'),
|
|
634
|
+
address: z.string().min(5, 'Address is required'),
|
|
635
|
+
price: z.number().positive('Price must be positive'),
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
```tsx
|
|
640
|
+
// components/forms/LoginForm.tsx
|
|
641
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
642
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
643
|
+
import { LoginSchema } from '@/lib/api/schemas';
|
|
644
|
+
|
|
645
|
+
export function LoginForm({ onSubmit }: { onSubmit: (data: any) => void }) {
|
|
646
|
+
const { control, handleSubmit, formState: { errors } } = useForm({
|
|
647
|
+
resolver: zodResolver(LoginSchema),
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
<View>
|
|
652
|
+
<Controller
|
|
653
|
+
control={control}
|
|
654
|
+
name="email"
|
|
655
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
656
|
+
<>
|
|
657
|
+
<TextInput
|
|
658
|
+
placeholder="Email"
|
|
659
|
+
value={value}
|
|
660
|
+
onChangeText={onChange}
|
|
661
|
+
onBlur={onBlur}
|
|
662
|
+
keyboardType="email-address"
|
|
663
|
+
style={{ borderWidth: 1, padding: 12, marginBottom: 8 }}
|
|
664
|
+
/>
|
|
665
|
+
{errors.email && <Text style={{ color: 'red' }}>{errors.email.message}</Text>}
|
|
666
|
+
</>
|
|
667
|
+
)}
|
|
668
|
+
/>
|
|
669
|
+
<TouchableOpacity onPress={handleSubmit(onSubmit)}>
|
|
670
|
+
<Text>Submit</Text>
|
|
671
|
+
</TouchableOpacity>
|
|
672
|
+
</View>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## Performance Optimization
|
|
678
|
+
|
|
679
|
+
### Use FlashList Instead of FlatList
|
|
680
|
+
|
|
681
|
+
```tsx
|
|
682
|
+
import { FlashList } from '@shopify/flash-list';
|
|
683
|
+
|
|
684
|
+
<FlashList
|
|
685
|
+
data={units}
|
|
686
|
+
renderItem={({ item }) => <UnitCard unit={item} />}
|
|
687
|
+
keyExtractor={(item) => item.id}
|
|
688
|
+
estimatedItemSize={100}
|
|
689
|
+
/>
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Image Optimization with expo-image
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
import { Image } from 'expo-image';
|
|
696
|
+
|
|
697
|
+
<Image
|
|
698
|
+
source={require('@/assets/image.png')}
|
|
699
|
+
style={{ width: 200, height: 200 }}
|
|
700
|
+
contentFit="cover"
|
|
701
|
+
transition={200}
|
|
702
|
+
/>
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Avoid Re-renders
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
const MemoizedCard = React.memo(({ unit }: { unit: Unit }) => (
|
|
709
|
+
<UnitCard unit={unit} />
|
|
710
|
+
));
|
|
711
|
+
|
|
712
|
+
const handlePress = useCallback(() => {
|
|
713
|
+
navigation.navigate('details', { id });
|
|
714
|
+
}, [id, navigation]);
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Loading States (Not Spinners)
|
|
718
|
+
|
|
719
|
+
```tsx
|
|
720
|
+
// Use skeleton loaders instead of spinners
|
|
721
|
+
<SkeletonLoader height={100} width="100%" count={3} />
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## Testing
|
|
725
|
+
|
|
726
|
+
### Unit & Component Tests
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
// Use Jest + React Native Testing Library
|
|
730
|
+
import { render, screen } from '@testing-library/react-native';
|
|
731
|
+
import { LoginForm } from '@/components/forms/LoginForm';
|
|
732
|
+
|
|
733
|
+
describe('LoginForm', () => {
|
|
734
|
+
it('renders email input', () => {
|
|
735
|
+
render(<LoginForm onSubmit={jest.fn()} />);
|
|
736
|
+
expect(screen.getByPlaceholderText('Email')).toBeTruthy();
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### E2E Tests
|
|
742
|
+
|
|
743
|
+
Use Detox or Maestro for end-to-end testing on iOS/Android simulators.
|
|
744
|
+
|
|
745
|
+
## Common Gotchas
|
|
746
|
+
|
|
747
|
+
### Platform-Specific Code
|
|
748
|
+
|
|
749
|
+
```tsx
|
|
750
|
+
import { Platform } from 'react-native';
|
|
751
|
+
|
|
752
|
+
// Option 1: Platform check
|
|
753
|
+
{Platform.OS === 'ios' && <View>{/* iOS only */}</View>}
|
|
754
|
+
|
|
755
|
+
// Option 2: Platform extensions
|
|
756
|
+
// Button.ios.tsx, Button.android.tsx
|
|
757
|
+
// import Button from '@/components/Button'
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### SafeAreaView for Notch/Dynamic Island
|
|
761
|
+
|
|
762
|
+
```tsx
|
|
763
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
764
|
+
|
|
765
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
766
|
+
{/* Content */}
|
|
767
|
+
</SafeAreaView>
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### Keyboard Handling
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
import { KeyboardAvoidingView } from 'react-native';
|
|
774
|
+
|
|
775
|
+
<KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
|
|
776
|
+
{/* Form content */}
|
|
777
|
+
</KeyboardAvoidingView>
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Android Back Button
|
|
781
|
+
|
|
782
|
+
```tsx
|
|
783
|
+
useEffect(() => {
|
|
784
|
+
const backAction = () => {
|
|
785
|
+
// Handle back navigation
|
|
786
|
+
return true; // Return true to prevent default behavior
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
|
|
790
|
+
return () => backHandler.remove();
|
|
791
|
+
}, []);
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### iOS Swipe-to-Go-Back
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
// Disable in specific screens if needed
|
|
798
|
+
useNavigationOptions({
|
|
799
|
+
gestureEnabled: false,
|
|
800
|
+
});
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
## Dependencies
|
|
804
|
+
|
|
805
|
+
### Core
|
|
806
|
+
- `expo@52+`
|
|
807
|
+
- `expo-router` (file-based navigation)
|
|
808
|
+
- `react-native` (via Expo)
|
|
809
|
+
- `typescript`
|
|
810
|
+
|
|
811
|
+
### State & Data
|
|
812
|
+
- `@tanstack/react-query`
|
|
813
|
+
- `zustand` (optional, for complex client state)
|
|
814
|
+
|
|
815
|
+
### Forms
|
|
816
|
+
- `react-hook-form`
|
|
817
|
+
- `@hookform/resolvers`
|
|
818
|
+
- `zod`
|
|
819
|
+
|
|
820
|
+
### API
|
|
821
|
+
- `axios`
|
|
822
|
+
|
|
823
|
+
### Storage
|
|
824
|
+
- `expo-secure-store` (tokens, secrets)
|
|
825
|
+
|
|
826
|
+
### Notifications
|
|
827
|
+
- `expo-notifications`
|
|
828
|
+
- `expo-device`
|
|
829
|
+
|
|
830
|
+
### Auth
|
|
831
|
+
- `expo-local-authentication` (biometric unlock)
|
|
832
|
+
|
|
833
|
+
### Images
|
|
834
|
+
- `expo-image`
|
|
835
|
+
|
|
836
|
+
### Lists
|
|
837
|
+
- `@shopify/flash-list`
|
|
838
|
+
|
|
839
|
+
### Testing
|
|
840
|
+
- `jest`
|
|
841
|
+
- `@testing-library/react-native`
|
|
842
|
+
- `detox` or `maestro` (E2E)
|
|
843
|
+
|
|
844
|
+
### Dev
|
|
845
|
+
- `@types/react-native`
|
|
846
|
+
- `@types/react`
|
|
847
|
+
- `expo-dev-client` (for custom dev builds)
|
|
848
|
+
|
|
849
|
+
## Quick Start
|
|
850
|
+
|
|
851
|
+
```bash
|
|
852
|
+
npx create-expo-app sakani --template
|
|
853
|
+
cd sakani
|
|
854
|
+
npx expo install expo-router
|
|
855
|
+
npx expo customize tsconfig
|
|
856
|
+
npx expo install axios zustand @tanstack/react-query zod react-hook-form @hookform/resolvers
|
|
857
|
+
npx expo install expo-secure-store expo-notifications expo-device expo-local-authentication expo-image @shopify/flash-list react-native-safe-area-context
|
|
858
|
+
|
|
859
|
+
# Create basic structure
|
|
860
|
+
mkdir -p app/(auth)/(tabs) app/(public) components/{ui,forms} lib/{api,auth,storage} hooks constants
|
|
861
|
+
|
|
862
|
+
# Start dev server
|
|
863
|
+
npx expo start
|
|
864
|
+
```
|