ginskill-init 2.7.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/.wrangler/cache/pages.json +4 -0
- package/.wrangler/cache/wrangler-account.json +6 -0
- package/DEVELOPMENT.md +510 -0
- package/README.md +104 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +461 -0
- package/landing/ai-build-ai.png +0 -0
- package/landing/index.html +1495 -0
- package/landing/logo.png +0 -0
- package/package.json +37 -0
- package/skills/active-life-dev/SKILL.md +157 -0
- package/skills/active-life-dev/docs/auth.md +187 -0
- package/skills/active-life-dev/docs/customers.md +216 -0
- package/skills/active-life-dev/docs/integrations.md +209 -0
- package/skills/active-life-dev/docs/inventory.md +192 -0
- package/skills/active-life-dev/docs/modules.md +181 -0
- package/skills/active-life-dev/docs/orders.md +180 -0
- package/skills/active-life-dev/docs/patterns.md +319 -0
- package/skills/active-life-dev/docs/products.md +216 -0
- package/skills/active-life-dev/docs/schema.md +502 -0
- package/skills/active-life-dev/docs/setup.md +169 -0
- package/skills/active-life-dev/docs/vouchers.md +144 -0
- package/skills/ai-asset-generator/SKILL.md +247 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +48 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +100 -0
- package/skills/ai-build-ai/SKILL.md +127 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/docs/team-lead-workflow.md +648 -0
- package/skills/ant-design/SKILL.md +323 -0
- package/skills/ant-design/docs/components.md +160 -0
- package/skills/ant-design/docs/data-entry.md +406 -0
- package/skills/ant-design/docs/display.md +594 -0
- package/skills/ant-design/docs/feedback.md +451 -0
- package/skills/ant-design/docs/key-components.md +414 -0
- package/skills/ant-design/docs/navigation.md +310 -0
- package/skills/ant-design/docs/pro-components.md +543 -0
- package/skills/ant-design/docs/setup.md +213 -0
- package/skills/ant-design/docs/theme.md +265 -0
- package/skills/flutter-performance/SKILL.md +803 -0
- package/skills/flutter-performance/references/flutter-patterns.md +595 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-native-expo/SKILL.md +128 -0
- package/skills/react-native-expo/references/data-layer.md +252 -0
- package/skills/react-native-expo/references/design-system.md +252 -0
- package/skills/react-native-expo/references/navigation.md +199 -0
- package/skills/react-native-expo/references/performance.md +229 -0
- package/skills/react-native-expo/references/platform-services.md +179 -0
- package/skills/react-native-expo/references/state-management.md +209 -0
- package/skills/react-native-expo/references/ui-patterns.md +301 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +374 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/security-scanner/SKILL.md +366 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/traefik/SKILL.md +105 -0
- package/skills/traefik/docs/advanced-routing.md +186 -0
- package/skills/traefik/docs/auth-providers.md +137 -0
- package/skills/traefik/docs/cicd-devops.md +396 -0
- package/skills/traefik/docs/core-config.md +171 -0
- package/skills/traefik/docs/distributed-config.md +96 -0
- package/skills/traefik/docs/docker-compose.md +182 -0
- package/skills/traefik/docs/ha-performance.md +177 -0
- package/skills/traefik/docs/kubernetes.md +278 -0
- package/skills/traefik/docs/middleware.md +205 -0
- package/skills/traefik/docs/monitoring.md +357 -0
- package/skills/traefik/docs/security.md +391 -0
- package/skills/traefik/docs/tls-acme.md +155 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Navigation — Expo Router v5
|
|
2
|
+
|
|
3
|
+
## Route Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/app/
|
|
7
|
+
_layout.tsx # Root: providers + auth guards (Stack.Protected)
|
|
8
|
+
+native-intent.tsx # Native deep link handler
|
|
9
|
+
(auth)/
|
|
10
|
+
_layout.tsx # Auth stack (fade animation, no gesture back)
|
|
11
|
+
login.tsx # Apple, Google, email OAuth
|
|
12
|
+
(onboarding)/
|
|
13
|
+
_layout.tsx # Onboarding stack (slide_from_right, no gesture back)
|
|
14
|
+
select-language.tsx # Language selection
|
|
15
|
+
step1-7.tsx # 7-step setup flow
|
|
16
|
+
(main)/
|
|
17
|
+
_layout.tsx # Main stack: registers all push/modal screens
|
|
18
|
+
index.tsx # Main entry redirect
|
|
19
|
+
(tabs)/ # 4 tabs: Home, Wardrobe, Stylist, Profile
|
|
20
|
+
_layout.tsx # Dual-mode: NativeTabs (iOS 26+ Liquid Glass) or ClassicTabs
|
|
21
|
+
home/
|
|
22
|
+
stylist/
|
|
23
|
+
profile/
|
|
24
|
+
(wardrobe)/ # Items + outfits sub-tabs
|
|
25
|
+
(wardrobe)/ # Push screens: item-detail, overview, upload-animation
|
|
26
|
+
(outfits)/ # Modals: maker (slide_from_bottom), save-modal (transparentModal)
|
|
27
|
+
(styling)/ # Modals: tryon, feedback (slide_from_bottom), about-stylist (push)
|
|
28
|
+
(discover)/ # Transparent modal: product-modal
|
|
29
|
+
(calendar)/ # Outfit calendar screens
|
|
30
|
+
(shared)/ # Shared modals: earn-credits, feedback-modal, referral, sty-history
|
|
31
|
+
notifications/
|
|
32
|
+
(global)/ # Transparent modals accessible from anywhere: loading, product-modal
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Route-Screen Delegation Pattern
|
|
36
|
+
|
|
37
|
+
Route files in `src/app/` are **thin delegators** (under 10 lines). Screen logic lives in `src/screens/` mirroring the same path:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// src/app/(main)/(tabs)/home/index.tsx — THIN DELEGATOR
|
|
41
|
+
import HomeScreen from "@/screens/(main)/(tabs)/home"
|
|
42
|
+
export default function HomeRoute() {
|
|
43
|
+
return <HomeScreen />
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Some route files wrap screens with feature dialogs:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// src/app/(main)/(wardrobe)/item-detail.tsx
|
|
51
|
+
import ClosetItemDetailScreen from "@/screens/(main)/(wardrobe)/item-detail"
|
|
52
|
+
import { ItemDialogs } from "@/features/item/_ui/item-dialogs"
|
|
53
|
+
import { useLocalSearchParams } from "expo-router"
|
|
54
|
+
|
|
55
|
+
export default function ClosetItemDetailRoute() {
|
|
56
|
+
const { item_id } = useLocalSearchParams<{ item_id: string }>()
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<ClosetItemDetailScreen />
|
|
60
|
+
<ItemDialogs item_id={item_id} />
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Screen folders contain `index.tsx` plus `_components/` for screen-specific sub-components:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
src/screens/(main)/(tabs)/home/
|
|
70
|
+
index.tsx
|
|
71
|
+
_components/
|
|
72
|
+
compact-header.tsx
|
|
73
|
+
weather-row.tsx
|
|
74
|
+
todays-picks-section.tsx
|
|
75
|
+
ai-stylist-row.tsx
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Auth Flow
|
|
79
|
+
|
|
80
|
+
Root `_layout.tsx` uses `Stack.Protected guard={condition}` to gate access:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const isLoggedIn = isAuthenticated && !!user
|
|
84
|
+
const needsOnboarding = isLoggedIn && !user?.is_onboarding_completed
|
|
85
|
+
const isAppReady = !isLoading && isAppInitialized && isOtaReady
|
|
86
|
+
|
|
87
|
+
<Stack>
|
|
88
|
+
<Stack.Protected guard={!isLoggedIn}>
|
|
89
|
+
<Stack.Screen name="(auth)" />
|
|
90
|
+
</Stack.Protected>
|
|
91
|
+
<Stack.Protected guard={isLoggedIn && needsOnboarding}>
|
|
92
|
+
<Stack.Screen name="(onboarding)" />
|
|
93
|
+
</Stack.Protected>
|
|
94
|
+
<Stack.Protected guard={isLoggedIn && !needsOnboarding}>
|
|
95
|
+
<Stack.Screen name="(main)" />
|
|
96
|
+
</Stack.Protected>
|
|
97
|
+
<Stack.Screen name="(global)" />
|
|
98
|
+
</Stack>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Global overlays in root layout: `NetworkErrorOverlay`, `ForceUpdateOverlay`, `DailyCheckInSheet`, `QuotaExceededDialog`, `RemoteDialogContainer`.
|
|
102
|
+
|
|
103
|
+
## Tab Layout (Dual-Mode)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// iOS 26+ uses NativeTabs (Liquid Glass native zoom + slide animations)
|
|
107
|
+
// Older iOS + Android uses ClassicTabs (custom tab bar)
|
|
108
|
+
const isLiquidGlass = Platform.OS === "ios" && parseInt(Platform.Version, 10) >= 26
|
|
109
|
+
|
|
110
|
+
// Tabs: home, (wardrobe), stylist, profile
|
|
111
|
+
// Tab config loaded from @/screens/(main)/(tabs)/_constants/tabs.const
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Navigation Patterns
|
|
115
|
+
|
|
116
|
+
### Typed Routes
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { router } from "expo-router"
|
|
120
|
+
import { ROUTES } from "@/shared/constant/route"
|
|
121
|
+
|
|
122
|
+
// Navigate with typed routes
|
|
123
|
+
router.push(ROUTES.ITEM_DETAIL.path)
|
|
124
|
+
router.push({ pathname: "/(main)/(wardrobe)/item-detail", params: { item_id: "123" } })
|
|
125
|
+
router.back()
|
|
126
|
+
router.replace(ROUTES.LOGIN.path)
|
|
127
|
+
router.setParams({ date: newDate }) // Update current route params
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Route Params
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Extract params in route/screen files
|
|
134
|
+
const { item_id } = useLocalSearchParams<{ item_id: string }>()
|
|
135
|
+
|
|
136
|
+
// Complex params (JSON-encoded)
|
|
137
|
+
const { productData } = useLocalSearchParams()
|
|
138
|
+
const product = productData ? JSON.parse(productData) : null
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Modal Presentation
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
<Stack.Screen
|
|
145
|
+
name="(outfits)/maker"
|
|
146
|
+
options={{ presentation: "modal", animation: "slide_from_bottom", headerShown: false }}
|
|
147
|
+
/>
|
|
148
|
+
<Stack.Screen
|
|
149
|
+
name="(discover)/product-modal"
|
|
150
|
+
options={{ presentation: "transparentModal", animation: "fade" }}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Creating a New Screen
|
|
155
|
+
|
|
156
|
+
1. Create screen: `src/screens/(main)/(section)/screen-name/index.tsx`
|
|
157
|
+
2. Add sub-components: `src/screens/(main)/(section)/screen-name/_components/`
|
|
158
|
+
3. Create thin route: `src/app/(main)/(section)/screen-name.tsx`
|
|
159
|
+
4. Register in parent `_layout.tsx` Stack
|
|
160
|
+
5. Add to `ROUTES` constant in `src/shared/constant/route.ts`
|
|
161
|
+
|
|
162
|
+
## Screen Component Pattern
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
export default function HomeScreen() {
|
|
166
|
+
const { t } = useTranslation()
|
|
167
|
+
const { top } = useSafeAreaInsets()
|
|
168
|
+
const queryClient = useQueryClient()
|
|
169
|
+
|
|
170
|
+
const onRefresh = useCallback(async () => {
|
|
171
|
+
await queryClient.invalidateQueries()
|
|
172
|
+
}, [queryClient])
|
|
173
|
+
|
|
174
|
+
useFocusEffect(useCallback(() => {
|
|
175
|
+
logEvent("home_viewed")
|
|
176
|
+
}, []))
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<View style={styles.screen}>
|
|
180
|
+
<ScrollView>{/* Content */}</ScrollView>
|
|
181
|
+
</View>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const styles = StyleSheet.create({
|
|
186
|
+
screen: { flex: 1, backgroundColor: Theme.color.backgroundSubtle },
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Best Practices
|
|
191
|
+
|
|
192
|
+
- Keep route files under 10 lines — delegate to screens
|
|
193
|
+
- Use route groups `()` for organization without affecting URL
|
|
194
|
+
- Prefer `router.push()` over `<Link>` for programmatic navigation
|
|
195
|
+
- Use `useLocalSearchParams()` for type-safe params
|
|
196
|
+
- Use `useFocusEffect()` for screen focus side effects (analytics, refresh)
|
|
197
|
+
- Avoid nesting more than 3 levels of stack navigators
|
|
198
|
+
- Use `(shared)` group for screens accessible from multiple tabs
|
|
199
|
+
- Feature dialogs live alongside route files, not inside screens
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Performance — Animations, Lists, and Optimization
|
|
2
|
+
|
|
3
|
+
## React Native Reanimated v4
|
|
4
|
+
|
|
5
|
+
Runs animations on the UI thread for 60fps regardless of JS thread activity. Uses `react-native-worklets` (separate package in v4).
|
|
6
|
+
|
|
7
|
+
### Shared Values & Animated Styles
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import Animated, {
|
|
11
|
+
useSharedValue, useAnimatedStyle,
|
|
12
|
+
withTiming, withSpring, withDelay, withSequence, withRepeat,
|
|
13
|
+
interpolate, Extrapolation, runOnJS,
|
|
14
|
+
} from "react-native-reanimated"
|
|
15
|
+
|
|
16
|
+
const Component = () => {
|
|
17
|
+
const opacity = useSharedValue(0)
|
|
18
|
+
const translateY = useSharedValue(50)
|
|
19
|
+
|
|
20
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
21
|
+
opacity: opacity.value,
|
|
22
|
+
transform: [{ translateY: translateY.value }],
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
const show = () => {
|
|
26
|
+
opacity.value = withTiming(1, { duration: 300 })
|
|
27
|
+
translateY.value = withSpring(0, { damping: 15, stiffness: 150 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return <Animated.View style={animatedStyle} />
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Animation Functions
|
|
35
|
+
|
|
36
|
+
| Function | Use Case |
|
|
37
|
+
|----------|----------|
|
|
38
|
+
| `withTiming(toValue, { duration })` | Duration-based, predictable |
|
|
39
|
+
| `withSpring(toValue, { damping, stiffness })` | Physics-based, natural |
|
|
40
|
+
| `withDelay(ms, animation)` | Delayed start |
|
|
41
|
+
| `withSequence(anim1, anim2, ...)` | Sequential |
|
|
42
|
+
| `withRepeat(animation, count, reverse)` | Looping |
|
|
43
|
+
| `interpolate(value, inputRange, outputRange)` | Value mapping |
|
|
44
|
+
|
|
45
|
+
### Spring Presets
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const snappy = { damping: 15, stiffness: 200, mass: 0.8 } // Buttons, toggles
|
|
49
|
+
const gentle = { damping: 20, stiffness: 100, mass: 1 } // Modals, panels
|
|
50
|
+
const bouncy = { damping: 8, stiffness: 150, mass: 0.5 } // Playful elements
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Gesture Handler Integration
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler"
|
|
57
|
+
|
|
58
|
+
const DraggableCard = () => {
|
|
59
|
+
const translateX = useSharedValue(0)
|
|
60
|
+
const translateY = useSharedValue(0)
|
|
61
|
+
|
|
62
|
+
const gesture = Gesture.Pan()
|
|
63
|
+
.onUpdate((e) => {
|
|
64
|
+
translateX.value = e.translationX
|
|
65
|
+
translateY.value = e.translationY
|
|
66
|
+
})
|
|
67
|
+
.onEnd(() => {
|
|
68
|
+
translateX.value = withSpring(0)
|
|
69
|
+
translateY.value = withSpring(0)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const style = useAnimatedStyle(() => ({
|
|
73
|
+
transform: [{ translateX: translateX.value }, { translateY: translateY.value }],
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<GestureDetector gesture={gesture}>
|
|
78
|
+
<Animated.View style={style}>{/* content */}</Animated.View>
|
|
79
|
+
</GestureDetector>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Entering/Exiting Layout Animations
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft } from "react-native-reanimated"
|
|
88
|
+
|
|
89
|
+
<Animated.View entering={FadeIn.duration(300)} exiting={FadeOut.duration(200)}>
|
|
90
|
+
{/* content */}
|
|
91
|
+
</Animated.View>
|
|
92
|
+
|
|
93
|
+
// Staggered list items
|
|
94
|
+
<Animated.View entering={FadeIn.delay(index * 50).duration(300)}>
|
|
95
|
+
{/* item */}
|
|
96
|
+
</Animated.View>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Performance Limits
|
|
100
|
+
|
|
101
|
+
- Max **100 animated components** on low-end Android
|
|
102
|
+
- Max **500 animated components** on iOS
|
|
103
|
+
- Never read shared values on JS thread — only in worklets/useAnimatedStyle
|
|
104
|
+
- For complex visuals, use `react-native-skia` instead of many animated components
|
|
105
|
+
|
|
106
|
+
## FlashList v2 — High-Performance Lists
|
|
107
|
+
|
|
108
|
+
FlashList v2 is New Architecture only, uses recycling for near-native perf. **Always use FlashList, never FlatList.**
|
|
109
|
+
|
|
110
|
+
### Basic Usage
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { FlashList } from "@shopify/flash-list"
|
|
114
|
+
|
|
115
|
+
<FlashList
|
|
116
|
+
data={items}
|
|
117
|
+
renderItem={({ item }) => <ItemCard item={item} />}
|
|
118
|
+
estimatedItemSize={120} // Required: approximate item height
|
|
119
|
+
keyExtractor={(item) => item._id}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Optimized Pattern
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const renderItem = useCallback(({ item }: { item: ItemRes }) => (
|
|
127
|
+
<MemoizedItemCard item={item} />
|
|
128
|
+
), [])
|
|
129
|
+
|
|
130
|
+
const MemoizedItemCard = React.memo(({ item }: { item: ItemRes }) => (
|
|
131
|
+
<View><Typography variant="b2">{item.name}</Typography></View>
|
|
132
|
+
))
|
|
133
|
+
|
|
134
|
+
<FlashList
|
|
135
|
+
data={items}
|
|
136
|
+
renderItem={renderItem}
|
|
137
|
+
estimatedItemSize={120}
|
|
138
|
+
keyExtractor={(item) => item._id}
|
|
139
|
+
drawDistance={250} // Pre-render distance
|
|
140
|
+
onEndReached={onEndReached} // Infinite scroll
|
|
141
|
+
onEndReachedThreshold={0.5}
|
|
142
|
+
numColumns={2} // Grid layout
|
|
143
|
+
getItemType={(item) => item.type} // CRITICAL for mixed item types
|
|
144
|
+
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
|
|
145
|
+
ListEmptyComponent={<EmptyState />}
|
|
146
|
+
/>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Key Rules
|
|
150
|
+
|
|
151
|
+
1. **Never use `key` prop** inside item components — defeats recycling
|
|
152
|
+
2. **Use `getItemType`** for heterogeneous lists
|
|
153
|
+
3. **Always provide `estimatedItemSize`** — measure one item
|
|
154
|
+
4. **Profile in release mode** — dev mode perf is misleading
|
|
155
|
+
5. **Memoize `renderItem`** with `useCallback`
|
|
156
|
+
6. **Memoize item components** with `React.memo`
|
|
157
|
+
|
|
158
|
+
## expo-image — Optimized Image Loading
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { Image } from "expo-image"
|
|
162
|
+
|
|
163
|
+
<Image
|
|
164
|
+
source={{ uri: imageUrl }}
|
|
165
|
+
style={{ width: 100, height: 100 }}
|
|
166
|
+
contentFit="cover"
|
|
167
|
+
transition={200} // Fade-in ms
|
|
168
|
+
cachePolicy="memory-disk" // Default, recommended
|
|
169
|
+
placeholder={blurhash} // Optional blurhash
|
|
170
|
+
recyclingKey={item._id} // For list recycling
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
// Prefetch
|
|
174
|
+
await Image.prefetch(urls)
|
|
175
|
+
// Clear cache
|
|
176
|
+
await Image.clearDiskCache()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| Cache Policy | Description |
|
|
180
|
+
|-------------|-------------|
|
|
181
|
+
| `memory-disk` | Default. Memory + disk |
|
|
182
|
+
| `memory` | Memory only |
|
|
183
|
+
| `disk` | Disk only |
|
|
184
|
+
| `none` | No caching |
|
|
185
|
+
|
|
186
|
+
## General Performance Best Practices
|
|
187
|
+
|
|
188
|
+
### Component Optimization
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Memoize expensive components
|
|
192
|
+
const ExpensiveChild = React.memo(({ data }) => { /* ... */ })
|
|
193
|
+
|
|
194
|
+
// Memoize callbacks passed as props
|
|
195
|
+
const handlePress = useCallback(() => { /* ... */ }, [dep])
|
|
196
|
+
|
|
197
|
+
// Memoize computed values
|
|
198
|
+
const filtered = useMemo(() => items.filter(i => i.category === selected), [items, selected])
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Avoid Common Pitfalls
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// BAD: Inline styles (new object every render)
|
|
205
|
+
<View style={{ padding: 16, backgroundColor: "#fff" }}>
|
|
206
|
+
|
|
207
|
+
// GOOD: StyleSheet.create (cached)
|
|
208
|
+
const styles = StyleSheet.create({
|
|
209
|
+
container: { padding: Spacing.lg, backgroundColor: Theme.color.backgroundElevated },
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// BAD: Arrow in render (new reference)
|
|
213
|
+
<Button onPress={() => handlePress(item._id)} />
|
|
214
|
+
|
|
215
|
+
// GOOD: Memoized callback
|
|
216
|
+
const onPress = useCallback(() => handlePress(item._id), [item._id])
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Metro Optimization
|
|
220
|
+
|
|
221
|
+
- `inlineRequires: true` in `metro.config.js` — defers module loading, improves cold start
|
|
222
|
+
- `.lottie` and `.cjs` added as asset/source extensions
|
|
223
|
+
- SVG support via `react-native-svg-transformer`
|
|
224
|
+
|
|
225
|
+
### Platform-Specific
|
|
226
|
+
|
|
227
|
+
- **Shimmer**: `shimmer.ios.tsx` / `shimmer.android.tsx` (platform file extensions)
|
|
228
|
+
- **Android**: `removeClippedSubviews={true}` on large lists
|
|
229
|
+
- **Lottie**: Keep files under 100KB; for complex animations use Skia
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Platform Services — Auth, Push, Deep Links, i18n, Analytics
|
|
2
|
+
|
|
3
|
+
## Authentication
|
|
4
|
+
|
|
5
|
+
### OAuth Providers
|
|
6
|
+
|
|
7
|
+
- **Apple**: `expo-apple-authentication` → `src/features/auth/sign-in-with-apple.service.ts`
|
|
8
|
+
- **Google**: `@react-native-google-signin/google-signin` → `src/features/auth/sign-in-with-google.service.ts`
|
|
9
|
+
- **Facebook**: `react-native-fbsdk-next` (auto-initialized via app.config.ts)
|
|
10
|
+
- **Sign out**: `src/features/auth/sign-out.service.ts`
|
|
11
|
+
|
|
12
|
+
### Auth Bootstrap (`src/shared/hooks/use-auth-init.ts`)
|
|
13
|
+
|
|
14
|
+
Runs once in root layout:
|
|
15
|
+
1. Wait for Zustand hydration (`isHydrated`)
|
|
16
|
+
2. Validate token integrity (`validateTokenIntegrity()`)
|
|
17
|
+
3. If valid → `setAuthenticated(true)`, else → `logout()`
|
|
18
|
+
4. Fetch user profile → set `useUserStore`
|
|
19
|
+
5. Init Adapty (subscriptions) + sync Adapty user ID
|
|
20
|
+
6. Sync Sty balance + retry pending verifications
|
|
21
|
+
7. **15-second timeout guard** → force sign-out if user never loads
|
|
22
|
+
|
|
23
|
+
Error handling: Network errors keep auth state; 401/404 triggers `handleAuthFailure()` → sign out.
|
|
24
|
+
|
|
25
|
+
### Token Management
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { storeTokens, getAccessToken, clearTokens, hasValidTokens } from "@/shared/libs/token-storage"
|
|
29
|
+
|
|
30
|
+
await storeTokens({ accessToken, refreshToken }) // After login
|
|
31
|
+
// Auto-injected by api-client interceptor
|
|
32
|
+
// 401 → auto-refresh (single-flight, max 2 retries) → if fails, sign out
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Push Notifications
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import * as Notifications from "expo-notifications"
|
|
39
|
+
import { usePushNotifications } from "@/shared/hooks/use-push-notifications"
|
|
40
|
+
|
|
41
|
+
// Hook handles: permission request, token registration, notification handlers
|
|
42
|
+
Notifications.setNotificationHandler({
|
|
43
|
+
handleNotification: async () => ({
|
|
44
|
+
shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true,
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Deep Linking
|
|
50
|
+
|
|
51
|
+
### Branch.io
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import branch from "react-native-branch"
|
|
55
|
+
|
|
56
|
+
branch.subscribe(({ error, params }) => {
|
|
57
|
+
if (params["+clicked_branch_link"]) {
|
|
58
|
+
router.push({ pathname: params.screen, params: { id: params.id } })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Airbridge
|
|
64
|
+
|
|
65
|
+
Configured via `airbridge-expo-sdk` plugin in `app.config.ts`. Associated domains for universal links.
|
|
66
|
+
|
|
67
|
+
### Native Intent
|
|
68
|
+
|
|
69
|
+
`src/app/+native-intent.tsx` processes incoming URLs.
|
|
70
|
+
|
|
71
|
+
## Internationalization (i18n)
|
|
72
|
+
|
|
73
|
+
9 languages via `i18next` + `react-i18next`. Config in `src/features/multi-languages/`.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { useTranslation } from "react-i18next"
|
|
77
|
+
|
|
78
|
+
const { t } = useTranslation()
|
|
79
|
+
<Typography variant="b1">{t("home.welcome")}</Typography>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Language stored in MMKV for instant startup access. Locale JSON files in `src/features/multi-languages/locales/`.
|
|
83
|
+
|
|
84
|
+
## Analytics
|
|
85
|
+
|
|
86
|
+
### Airbridge (Primary) + Facebook
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { logEvent } from "@/shared/utils/log-event"
|
|
90
|
+
|
|
91
|
+
logEvent("item_created", { category: "tops", source: "camera" })
|
|
92
|
+
logEvent("outfit_saved", { itemCount: 3 })
|
|
93
|
+
logEvent("subscription_started", { plan: "premium" })
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### User Properties
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { setUserProperty } from "@/shared/utils/set-user-property"
|
|
100
|
+
setUserProperty("subscription_tier", "premium")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Subscriptions — Adapty
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { adapty } from "react-native-adapty"
|
|
107
|
+
// Managed via createAdaptySlice in app-store
|
|
108
|
+
// Paywall presentation, subscription status, receipt validation
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Store Review
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import * as StoreReview from "expo-store-review"
|
|
115
|
+
if (await StoreReview.hasAction()) await StoreReview.requestReview()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Live Activity (iOS)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { useLiveActivityManager } from "@/shared/live-activity"
|
|
122
|
+
// Start/update/end live activities for long-running tasks (e.g., outfit generation)
|
|
123
|
+
// Uses expo-live-activity with push notification support
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Location
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { useLocation } from "@/features/location"
|
|
130
|
+
// Flow: permission → device location → sync to backend → weather recommendations
|
|
131
|
+
// Services in src/features/location/_services/
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Haptics
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import * as Haptics from "expo-haptics"
|
|
138
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) // Buttons
|
|
139
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium) // Toggles
|
|
140
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) // Confirmations
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Clipboard / Linking / Browser
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import * as Clipboard from "expo-clipboard"
|
|
147
|
+
import * as WebBrowser from "expo-web-browser"
|
|
148
|
+
import * as Linking from "expo-linking"
|
|
149
|
+
|
|
150
|
+
await Clipboard.setStringAsync("text")
|
|
151
|
+
await WebBrowser.openBrowserAsync(url) // In-app browser
|
|
152
|
+
await Linking.openURL(url) // System browser
|
|
153
|
+
await Linking.openSettings() // App settings
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Environment Configuration
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
.env.local → Development (default)
|
|
160
|
+
.env.staging → Staging/Preview
|
|
161
|
+
.env.prod → Production
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Key env vars: `EXPO_PUBLIC_API_URL`, `EXPO_PUBLIC_APP_NAME`, `APP_VARIANT=development` (appends `.dev` to bundle IDs), `EXPO_ENV` (controls env file loading).
|
|
165
|
+
|
|
166
|
+
`app.config.ts` loads env files based on `EXPO_ENV`. Production builds set `SKIP_LOCAL_ENV=1`.
|
|
167
|
+
|
|
168
|
+
## Logging
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { Logger } from "@/shared/utils/logger"
|
|
172
|
+
|
|
173
|
+
Logger.info("User signed in", { userId })
|
|
174
|
+
Logger.warn("Slow response", { endpoint, duration })
|
|
175
|
+
Logger.error("Upload failed", { error: err.message })
|
|
176
|
+
// Dev: console with emojis/colors. Prod: errors shipped via clientLogger (batched, with backoff)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Client logger (`src/shared/services/client-logger.service.ts`): max 200 entries queue, batch flush at 20 or every 10s, 429 backoff, connectivity check before flush.
|