diff --git a/01-requirements/environment-setup.md b/01-requirements/environment-setup.md new file mode 100644 index 0000000..a6a2b1e --- /dev/null +++ b/01-requirements/environment-setup.md @@ -0,0 +1,140 @@ +# Requirements & Environment Setup + +## Hardware Prerequisites + +**Minimum Requirements:** + +- **macOS**: macOS 10.15+ (for iOS development) +- **Windows**: Windows 10+ with WSL2 +- **Linux**: Ubuntu 18.04+ or equivalent +- **RAM**: 8GB minimum, 16GB recommended +- **Storage**: 50GB free space minimum + +## JDK Setup + +```bash +# Install JDK 11 (recommended for RN 0.70+) +# macOS (using Homebrew) +brew install openjdk@11 + +# Windows (using Chocolatey) +choco install openjdk11 + +# Linux (Ubuntu) +sudo apt update +sudo apt install openjdk-11-jdk +``` + +**Configure JAVA_HOME:** + +```bash +# macOS/Linux - Add to ~/.zshrc or ~/.bash_profile +export JAVA_HOME=$(/usr/libexec/java_home -v 11) +export PATH=$JAVA_HOME/bin:$PATH + +# Windows - Set environment variables +JAVA_HOME=C:\Program Files\OpenJDK\openjdk-11.0.x +PATH=%JAVA_HOME%\bin;%PATH% +``` + +## Node.js & Package Manager + +```bash +# Install Node.js LTS (18.x recommended) +# Using nvm (recommended) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +nvm install 18 +nvm use 18 + +# Install Yarn (recommended over npm) +npm install -g yarn + +# Verify installations +node --version # Should show v18.x.x +yarn --version # Should show 1.22.x+ +``` + +## Android Studio & SDK Setup + +1. **Download Android Studio** from https://developer.android.com/studio +2. **Install Android SDK:** + + - SDK Platforms: Android 13 (API 33), Android 12 (API 31) + - SDK Tools: Android SDK Build-Tools 33.0.0, Android Emulator, Android SDK Platform-Tools + +3. **Configure Environment Variables:** + +```bash +# macOS/Linux - Add to ~/.zshrc or ~/.bash_profile +export ANDROID_HOME=$HOME/Library/Android/sdk # macOS +export ANDROID_HOME=$HOME/Android/Sdk # Linux +export PATH=$PATH:$ANDROID_HOME/emulator +export PATH=$PATH:$ANDROID_HOME/platform-tools + +# Windows +ANDROID_HOME=C:\Users\%USERNAME%\AppData\Local\Android\Sdk +PATH=%ANDROID_HOME%\emulator;%ANDROID_HOME%\platform-tools;%PATH% +``` + +4. **Create AVD (Android Virtual Device):** + - Open Android Studio → AVD Manager + - Create Virtual Device → Pixel 4 → API 33 → Finish + +## Xcode & iOS Setup (macOS only) + +```bash +# Install Xcode from App Store +# Install Command Line Tools +xcode-select --install + +# Install CocoaPods +sudo gem install cocoapods + +# Verify installation +pod --version +``` + +## VSCode Setup + +**Required Extensions:** + +```json +{ + "recommendations": [ + "ms-vscode.vscode-typescript-next", + "bradlc.vscode-tailwindcss", + "esbenp.prettier-vscode", + "ms-vscode.vscode-eslint", + "formulahendry.auto-rename-tag", + "ms-vscode-remote.remote-containers", + "ms-vscode.vscode-json" + ] +} +``` + +**VSCode Settings (.vscode/settings.json):** + +```json +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "typescript.preferences.importModuleSpecifier": "relative" +} +``` + +## Global Dependencies + +```bash +# Install global tools +npm install -g @react-native-community/cli +npm install -g react-devtools + +# macOS only +brew install watchman + +# Verify React Native CLI +npx react-native --version +``` diff --git a/02-project-initialization/project-setup.md b/02-project-initialization/project-setup.md new file mode 100644 index 0000000..510a150 --- /dev/null +++ b/02-project-initialization/project-setup.md @@ -0,0 +1,147 @@ +# Project Initialization + +## Create New Project + +```bash +# Create new React Native project with TypeScript +npx react-native init SaayamApp --template react-native-template-typescript + +cd SaayamApp + +# Initialize git repository +git init +git add . +git commit -m "Initial commit" +``` + +## TypeScript Configuration + +**tsconfig.json:** +```json +{ + "extends": "@react-native/typescript-config/tsconfig.json", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@components/*": ["./src/components/*"], + "@screens/*": ["./src/screens/*"], + "@utils/*": ["./src/utils/*"], + "@config/*": ["./src/config/*"], + "@redux/*": ["./src/redux/*"], + "@assets/*": ["./src/assets/*"] + } + }, + "include": ["src/**/*", "index.js"], + "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"] +} +``` + +## Folder Structure + +``` +src/ +├── components/ # Reusable UI components +│ ├── common/ # Generic components +│ ├── forms/ # Form-specific components +│ └── index.ts # Component exports +├── screens/ # Screen components +│ ├── Auth/ +│ ├── Home/ +│ └── index.ts +├── navigation/ # Navigation configuration +├── redux/ # State management +│ ├── store/ +│ ├── slices/ +│ └── types/ +├── services/ # API services +├── utils/ # Utility functions +├── config/ # App configuration +├── assets/ # Images, fonts, etc. +│ ├── images/ +│ ├── fonts/ +│ └── icons/ +├── types/ # TypeScript type definitions +└── constants/ # App constants +``` + +## Code Quality Setup + +**Install dependencies:** +```bash +yarn add -D eslint prettier husky lint-staged @typescript-eslint/parser @typescript-eslint/eslint-plugin +``` + +**.eslintrc.js:** +```javascript +module.exports = { + root: true, + extends: [ + '@react-native-community', + '@typescript-eslint/recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + rules: { + 'react-native/no-inline-styles': 'warn', + '@typescript-eslint/no-unused-vars': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, +}; +``` + +**.prettierrc.js:** +```javascript +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: false, + singleQuote: true, + trailingComma: 'all', + tabWidth: 2, + semi: true, +}; +``` + +**package.json scripts:** +```json +{ + "scripts": { + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json}\"", + "type-check": "tsc --noEmit" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"] + } +} +``` + +## Environment Configuration + +```bash +yarn add react-native-config +``` + +**Create environment files:** +```bash +# .env.development +API_BASE_URL=https://dev-api.saayam.com +APP_ENV=development + +# .env.production +API_BASE_URL=https://api.saayam.com +APP_ENV=production +``` \ No newline at end of file diff --git a/03-development-standards/coding-guidelines.md b/03-development-standards/coding-guidelines.md new file mode 100644 index 0000000..9b685c6 --- /dev/null +++ b/03-development-standards/coding-guidelines.md @@ -0,0 +1,123 @@ +# Development Standards & Guidelines + +## Naming Conventions + +- **Files**: PascalCase for components, camelCase for utilities +- **Components**: PascalCase (e.g., `UserProfile.tsx`) +- **Variables**: camelCase (e.g., `userName`) +- **Constants**: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`) +- **Types/Interfaces**: PascalCase with descriptive names + +## Component Architecture + +**Base Component Structure:** + +```typescript +// src/components/common/Button/Button.tsx +import React from "react"; +import { + TouchableOpacity, + Text, + StyleSheet, + ViewStyle, + TextStyle, +} from "react-native"; + +interface ButtonProps { + title: string; + onPress: () => void; + variant?: "primary" | "secondary" | "outline"; + disabled?: boolean; + style?: ViewStyle; + textStyle?: TextStyle; +} + +export const Button: React.FC = ({ + title, + onPress, + variant = "primary", + disabled = false, + style, + textStyle, +}) => { + return ( + + + {title} + + + ); +}; + +const styles = StyleSheet.create({ + button: { + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + alignItems: "center", + justifyContent: "center", + }, + primary: { + backgroundColor: "#007AFF", + }, + secondary: { + backgroundColor: "#6C757D", + }, + outline: { + backgroundColor: "transparent", + borderWidth: 1, + borderColor: "#007AFF", + }, + disabled: { + opacity: 0.5, + }, + text: { + fontSize: 16, + fontWeight: "600", + }, + primaryText: { + color: "#FFFFFF", + }, + secondaryText: { + color: "#FFFFFF", + }, + outlineText: { + color: "#007AFF", + }, +}); +``` + +## Code Style Guidelines + +### TypeScript Best Practices + +1. **Always use strict mode** +2. **Define interfaces for all props** +3. **Use proper type annotations** +4. **Avoid `any` type** +5. **Use union types for variants** + +### React Native Specific + +1. **Use StyleSheet.create() for styles** +2. **Avoid inline styles when possible** +3. **Use proper component lifecycle methods** +4. **Implement proper error boundaries** +5. **Use React.memo for performance optimization** + +### File Organization + +1. **One component per file** +2. **Index files for clean imports** +3. **Separate styles in same file** +4. **Group related components in folders** diff --git a/03-development-standards/components.md b/03-development-standards/components.md new file mode 100644 index 0000000..6007fd6 --- /dev/null +++ b/03-development-standards/components.md @@ -0,0 +1,520 @@ +# Required Components + +## TextInput Component + +```typescript +// src/components/common/TextInput/TextInput.tsx +import React, {useState} from 'react'; +import { + TextInput as RNTextInput, + View, + Text, + StyleSheet, + TextInputProps, +} from 'react-native'; + +interface CustomTextInputProps extends TextInputProps { + label?: string; + error?: string; + required?: boolean; +} + +export const TextInput: React.FC = ({ + label, + error, + required, + style, + ...props +}) => { + const [isFocused, setIsFocused] = useState(false); + + return ( + + {label && ( + + {label} + {required && *} + + )} + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + {...props} + /> + {error && {error}} + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: '500', + marginBottom: 8, + color: '#333', + }, + required: { + color: '#FF3B30', + }, + input: { + borderWidth: 1, + borderColor: '#E5E5E7', + borderRadius: 8, + paddingHorizontal: 16, + paddingVertical: 12, + fontSize: 16, + backgroundColor: '#FFFFFF', + }, + focused: { + borderColor: '#007AFF', + }, + error: { + borderColor: '#FF3B30', + }, + errorText: { + fontSize: 12, + color: '#FF3B30', + marginTop: 4, + }, +}); +``` + +## Header Component (CHeader) + +```typescript +// src/components/common/CHeader/CHeader.tsx +import React from 'react'; +import {View, Text, TouchableOpacity, StyleSheet, StatusBar} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +interface CHeaderProps { + title: string; + showBackButton?: boolean; + onBackPress?: () => void; + rightComponent?: React.ReactNode; + backgroundColor?: string; +} + +export const CHeader: React.FC = ({ + title, + showBackButton = true, + onBackPress, + rightComponent, + backgroundColor = '#FFFFFF', +}) => { + const insets = useSafeAreaInsets(); + + return ( + + + + {showBackButton && ( + + + + )} + {title} + {rightComponent} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderBottomWidth: 1, + borderBottomColor: '#E5E5E7', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + }, + backButton: { + padding: 8, + }, + backText: { + fontSize: 24, + color: '#007AFF', + }, + title: { + flex: 1, + fontSize: 18, + fontWeight: '600', + textAlign: 'center', + color: '#000', + }, + rightContainer: { + minWidth: 40, + alignItems: 'flex-end', + }, +}); +``` + +## AlertModal Component + +```typescript +// src/components/common/AlertModal/AlertModal.tsx +import React from 'react'; +import { + Modal, + View, + Text, + TouchableOpacity, + StyleSheet, + Dimensions, +} from 'react-native'; + +interface AlertModalProps { + visible: boolean; + title: string; + message: string; + onConfirm?: () => void; + onCancel?: () => void; + confirmText?: string; + cancelText?: string; + type?: 'info' | 'warning' | 'error' | 'success'; +} + +export const AlertModal: React.FC = ({ + visible, + title, + message, + onConfirm, + onCancel, + confirmText = 'OK', + cancelText = 'Cancel', + type = 'info', +}) => { + return ( + + + + {title} + {message} + + {onCancel && ( + + {cancelText} + + )} + {onConfirm && ( + + {confirmText} + + )} + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + container: { + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 20, + width: Dimensions.get('window').width - 40, + maxWidth: 400, + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 12, + textAlign: 'center', + }, + infoTitle: { + color: '#007AFF', + }, + warningTitle: { + color: '#FF9500', + }, + errorTitle: { + color: '#FF3B30', + }, + successTitle: { + color: '#34C759', + }, + message: { + fontSize: 16, + color: '#666', + textAlign: 'center', + marginBottom: 20, + lineHeight: 22, + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + cancelButton: { + flex: 1, + paddingVertical: 12, + marginRight: 8, + borderRadius: 8, + borderWidth: 1, + borderColor: '#E5E5E7', + }, + confirmButton: { + flex: 1, + paddingVertical: 12, + marginLeft: 8, + borderRadius: 8, + }, + infoButton: { + backgroundColor: '#007AFF', + }, + warningButton: { + backgroundColor: '#FF9500', + }, + errorButton: { + backgroundColor: '#FF3B30', + }, + successButton: { + backgroundColor: '#34C759', + }, + cancelText: { + textAlign: 'center', + fontSize: 16, + color: '#666', + }, + confirmText: { + textAlign: 'center', + fontSize: 16, + color: '#FFFFFF', + fontWeight: '600', + }, +}); +``` + +## Dropdown Component + +```typescript +// src/components/common/Dropdown/Dropdown.tsx +import React, {useState} from 'react'; +import { + View, + Text, + TouchableOpacity, + FlatList, + StyleSheet, + Modal, +} from 'react-native'; + +interface DropdownItem { + label: string; + value: string; +} + +interface DropdownProps { + items: DropdownItem[]; + selectedValue?: string; + onSelect: (item: DropdownItem) => void; + placeholder?: string; + label?: string; +} + +export const Dropdown: React.FC = ({ + items, + selectedValue, + onSelect, + placeholder = 'Select an option', + label, +}) => { + const [isVisible, setIsVisible] = useState(false); + + const selectedItem = items.find(item => item.value === selectedValue); + + return ( + + {label && {label}} + setIsVisible(true)}> + + {selectedItem ? selectedItem.label : placeholder} + + + + + + setIsVisible(false)}> + + item.value} + renderItem={({item}) => ( + { + onSelect(item); + setIsVisible(false); + }}> + {item.label} + + )} + /> + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: '500', + marginBottom: 8, + color: '#333', + }, + selector: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderWidth: 1, + borderColor: '#E5E5E7', + borderRadius: 8, + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: '#FFFFFF', + }, + selectorText: { + fontSize: 16, + color: '#333', + }, + placeholder: { + color: '#999', + }, + arrow: { + fontSize: 12, + color: '#666', + }, + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + dropdown: { + backgroundColor: '#FFFFFF', + borderRadius: 8, + maxHeight: 200, + width: '80%', + }, + item: { + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: '#F0F0F0', + }, + itemText: { + fontSize: 16, + color: '#333', + }, +}); +``` + +## RadioButton Component + +```typescript +// src/components/common/RadioButton/RadioButton.tsx +import React from 'react'; +import {View, Text, TouchableOpacity, StyleSheet} from 'react-native'; + +interface RadioOption { + label: string; + value: string; +} + +interface RadioButtonProps { + options: RadioOption[]; + selectedValue?: string; + onSelect: (value: string) => void; + label?: string; +} + +export const RadioButton: React.FC = ({ + options, + selectedValue, + onSelect, + label, +}) => { + return ( + + {label && {label}} + {options.map(option => ( + onSelect(option.value)}> + + {selectedValue === option.value && } + + {option.label} + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: '500', + marginBottom: 8, + color: '#333', + }, + option: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + }, + radio: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 2, + borderColor: '#007AFF', + marginRight: 12, + alignItems: 'center', + justifyContent: 'center', + }, + selected: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: '#007AFF', + }, + optionText: { + fontSize: 16, + color: '#333', + }, +}); +``` \ No newline at end of file diff --git a/03-development-standards/vscode-snippets.md b/03-development-standards/vscode-snippets.md new file mode 100644 index 0000000..4fa9a7d --- /dev/null +++ b/03-development-standards/vscode-snippets.md @@ -0,0 +1,232 @@ +# VSCode Snippets + +## Setup VSCode Snippets + +Create `.vscode/snippets.code-snippets` in your project root: + +```json +{ + "React Native Functional Component": { + "prefix": "rnfc", + "body": [ + "import React from 'react';", + "import {View, Text, StyleSheet} from 'react-native';", + "", + "interface ${1:ComponentName}Props {", + " $2", + "}", + "", + "export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = ({$3}) => {", + " return (", + " ", + " $4", + " ", + " );", + "};", + "", + "const styles = StyleSheet.create({", + " container: {", + " flex: 1,", + " },", + "});" + ], + "description": "Create a React Native functional component" + }, + + "React Native Screen Component": { + "prefix": "rnscreen", + "body": [ + "import React from 'react';", + "import {View, Text, StyleSheet, SafeAreaView} from 'react-native';", + "import {CHeader} from '@components/common';", + "", + "interface ${1:ScreenName}Props {", + " navigation: any;", + " route: any;", + "}", + "", + "export const ${1:ScreenName}: React.FC<${1:ScreenName}Props> = ({navigation, route}) => {", + " return (", + " ", + " navigation.goBack()}", + " />", + " ", + " $3", + " ", + " ", + " );", + "};", + "", + "const styles = StyleSheet.create({", + " container: {", + " flex: 1,", + " backgroundColor: '#FFFFFF',", + " },", + " content: {", + " flex: 1,", + " padding: 16,", + " },", + "});" + ], + "description": "Create a React Native screen component" + }, + + "Redux Slice": { + "prefix": "reduxslice", + "body": [ + "import {createSlice, PayloadAction} from '@reduxjs/toolkit';", + "", + "interface ${1:SliceName}State {", + " $2", + "}", + "", + "const initialState: ${1:SliceName}State = {", + " $3", + "};", + "", + "const ${4:sliceName}Slice = createSlice({", + " name: '${4:sliceName}',", + " initialState,", + " reducers: {", + " ${5:actionName}: (state, action: PayloadAction<$6>) => {", + " $7", + " },", + " },", + "});", + "", + "export const {${5:actionName}} = ${4:sliceName}Slice.actions;", + "export default ${4:sliceName}Slice.reducer;" + ], + "description": "Create a Redux Toolkit slice" + }, + + "API Service": { + "prefix": "apiservice", + "body": [ + "import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react';", + "import Config from 'react-native-config';", + "", + "export const ${1:serviceName}Api = createApi({", + " reducerPath: '${1:serviceName}Api',", + " baseQuery: fetchBaseQuery({", + " baseUrl: `\\${Config.API_BASE_URL}/${2:endpoint}`,", + " prepareHeaders: (headers, {getState}) => {", + " const token = (getState() as any).auth.token;", + " if (token) {", + " headers.set('authorization', `Bearer \\${token}`);", + " }", + " return headers;", + " },", + " }),", + " tagTypes: ['${3:TagType}'],", + " endpoints: builder => ({", + " ${4:endpointName}: builder.query({", + " query: () => '/${5:path}',", + " providesTags: ['${3:TagType}'],", + " }),", + " }),", + "});", + "", + "export const {use${6:EndpointName}Query} = ${1:serviceName}Api;" + ], + "description": "Create an RTK Query API service" + }, + + "React Hook": { + "prefix": "rnhook", + "body": [ + "import {useState, useEffect} from 'react';", + "", + "interface Use${1:HookName}Return {", + " $2", + "}", + "", + "export const use${1:HookName} = ($3): Use${1:HookName}Return => {", + " const [${4:state}, set${5:State}] = useState($6);", + "", + " useEffect(() => {", + " $7", + " }, []);", + "", + " return {", + " ${4:state},", + " set${5:State},", + " $8", + " };", + "};" + ], + "description": "Create a custom React hook" + }, + + "Test Component": { + "prefix": "rntest", + "body": [ + "import React from 'react';", + "import {render, fireEvent} from '@testing-library/react-native';", + "import {${1:ComponentName}} from '../${1:ComponentName}';", + "", + "describe('${1:ComponentName}', () => {", + " it('renders correctly', () => {", + " const {getByText} = render(<${1:ComponentName} $2 />);", + " expect(getByText('$3')).toBeTruthy();", + " });", + "", + " it('$4', () => {", + " const mock${5:Function} = jest.fn();", + " const {getByText} = render(", + " <${1:ComponentName} ${6:prop}={mock${5:Function}} $7 />", + " );", + " ", + " fireEvent.press(getByText('$8'));", + " expect(mock${5:Function}).toHaveBeenCalledTimes(1);", + " });", + "});" + ], + "description": "Create a test file for a component" + } +} +``` + +## Usage + +After creating the snippets file, you can use these shortcuts in VSCode: + +- `rnfc` - Creates a functional component +- `rnscreen` - Creates a screen component with navigation +- `reduxslice` - Creates a Redux Toolkit slice +- `apiservice` - Creates an RTK Query API service +- `rnhook` - Creates a custom React hook +- `rntest` - Creates a test file template + +## Additional Snippets + +You can extend the snippets file with more specific patterns used in your project: + +```json +{ + "Styled Component": { + "prefix": "rnstyle", + "body": [ + "const styles = StyleSheet.create({", + " ${1:styleName}: {", + " $2", + " },", + "});" + ], + "description": "Create StyleSheet styles" + }, + + "Navigation Type": { + "prefix": "navtype", + "body": [ + "type ${1:StackName}ParamList = {", + " ${2:ScreenName}: {${3:param}: ${4:type}};", + " ${5:AnotherScreen}: undefined;", + "};" + ], + "description": "Create navigation param list type" + } +} +``` \ No newline at end of file diff --git a/04-design-integration/assets.md b/04-design-integration/assets.md new file mode 100644 index 0000000..b2f00ba --- /dev/null +++ b/04-design-integration/assets.md @@ -0,0 +1,281 @@ +# Asset Management + +## Folder Structure + +``` +src/assets/ +├── images/ +│ ├── logo.png +│ ├── logo@2x.png +│ └── logo@3x.png +├── icons/ +│ └── index.ts +└── fonts/ + ├── Montserrat-Regular.ttf + └── Montserrat-Bold.ttf +``` + +## Image Management + +**Create image index file:** +```typescript +// src/assets/images/index.ts +export const Images = { + logo: require('./logo.png'), + splash: require('./splash.png'), + placeholder: require('./placeholder.png'), + // Add more images here +}; +``` + +**Usage in components:** +```typescript +import {Images} from '@assets/images'; +import {Image} from 'react-native'; + +const MyComponent = () => ( + +); +``` + +## Icon Management + +**Install react-native-vector-icons:** +```bash +yarn add react-native-vector-icons +yarn add -D @types/react-native-vector-icons + +# iOS setup +cd ios && pod install +``` + +**Create icon component:** +```typescript +// src/components/common/Icon/Icon.tsx +import React from 'react'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; + +type IconFamily = 'MaterialIcons' | 'Ionicons' | 'FontAwesome'; + +interface IconProps { + name: string; + size?: number; + color?: string; + family?: IconFamily; +} + +const IconComponents = { + MaterialIcons, + Ionicons, + FontAwesome, +}; + +export const Icon: React.FC = ({ + name, + size = 24, + color = '#000', + family = 'MaterialIcons', +}) => { + const IconComponent = IconComponents[family]; + return ; +}; +``` + +## SVG Icons + +**Install react-native-svg:** +```bash +yarn add react-native-svg +yarn add -D @types/react-native-svg + +# iOS setup +cd ios && pod install +``` + +**Create SVG icon component:** +```typescript +// src/components/common/SvgIcon/SvgIcon.tsx +import React from 'react'; +import Svg, {Path, Circle, Rect} from 'react-native-svg'; + +interface SvgIconProps { + name: string; + size?: number; + color?: string; +} + +const iconPaths = { + home: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z', + user: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z', + // Add more icon paths +}; + +export const SvgIcon: React.FC = ({ + name, + size = 24, + color = '#000', +}) => { + const path = iconPaths[name as keyof typeof iconPaths]; + + if (!path) { + console.warn(`Icon "${name}" not found`); + return null; + } + + return ( + + + + ); +}; +``` + +## Image Optimization + +**Install react-native-fast-image:** +```bash +yarn add react-native-fast-image + +# iOS setup +cd ios && pod install +``` + +**Optimized image component:** +```typescript +// src/components/common/OptimizedImage/OptimizedImage.tsx +import React from 'react'; +import FastImage, {FastImageProps} from 'react-native-fast-image'; +import {StyleSheet, View, ActivityIndicator} from 'react-native'; + +interface OptimizedImageProps extends FastImageProps { + showLoader?: boolean; + loaderColor?: string; +} + +export const OptimizedImage: React.FC = ({ + showLoader = true, + loaderColor = '#007AFF', + style, + ...props +}) => { + const [loading, setLoading] = React.useState(true); + + return ( + + setLoading(true)} + onLoadEnd={() => setLoading(false)} + resizeMode={FastImage.resizeMode.cover} + /> + {loading && showLoader && ( + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + loader: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.1)', + }, +}); +``` + +## Asset Preloading + +```typescript +// src/utils/assetPreloader.ts +import {Image} from 'react-native'; +import {Images} from '@assets/images'; + +export const preloadImages = async (): Promise => { + const imagePromises = Object.values(Images).map(image => { + return new Promise((resolve, reject) => { + Image.prefetch(Image.resolveAssetSource(image).uri) + .then(() => resolve()) + .catch(reject); + }); + }); + + try { + await Promise.all(imagePromises); + console.log('All images preloaded successfully'); + } catch (error) { + console.error('Error preloading images:', error); + } +}; +``` + +## Image Caching Strategy + +```typescript +// src/utils/imageCache.ts +import AsyncStorage from '@react-native-async-storage/async-storage'; +import RNFS from 'react-native-fs'; + +const CACHE_DIR = `${RNFS.CachesDirectoryPath}/images`; +const CACHE_KEY_PREFIX = 'image_cache_'; + +export class ImageCache { + static async getCachedImagePath(url: string): Promise { + try { + const cacheKey = `${CACHE_KEY_PREFIX}${this.hashUrl(url)}`; + const cachedPath = await AsyncStorage.getItem(cacheKey); + + if (cachedPath && await RNFS.exists(cachedPath)) { + return cachedPath; + } + + return null; + } catch (error) { + console.error('Error getting cached image:', error); + return null; + } + } + + static async cacheImage(url: string): Promise { + try { + const fileName = this.hashUrl(url); + const filePath = `${CACHE_DIR}/${fileName}`; + const cacheKey = `${CACHE_KEY_PREFIX}${fileName}`; + + // Ensure cache directory exists + await RNFS.mkdir(CACHE_DIR); + + // Download and cache the image + await RNFS.downloadFile({ + fromUrl: url, + toFile: filePath, + }).promise; + + // Store the path in AsyncStorage + await AsyncStorage.setItem(cacheKey, filePath); + + return filePath; + } catch (error) { + console.error('Error caching image:', error); + throw error; + } + } + + private static hashUrl(url: string): string { + // Simple hash function for URL + let hash = 0; + for (let i = 0; i < url.length; i++) { + const char = url.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(); + } +} +``` \ No newline at end of file diff --git a/04-design-integration/design-system.md b/04-design-integration/design-system.md new file mode 100644 index 0000000..32b9c98 --- /dev/null +++ b/04-design-integration/design-system.md @@ -0,0 +1,258 @@ +# Design System Setup + +## Theme Configuration + +**Install design dependencies:** +```bash +yarn add react-native-vector-icons react-native-svg +yarn add -D @types/react-native-vector-icons +``` + +**Theme Configuration:** +```typescript +// src/config/theme.ts +export const Colors = { + primary: '#007AFF', + secondary: '#5856D6', + success: '#34C759', + warning: '#FF9500', + error: '#FF3B30', + background: '#F2F2F7', + surface: '#FFFFFF', + text: { + primary: '#000000', + secondary: '#6D6D80', + disabled: '#C7C7CC', + }, + border: '#E5E5E7', +}; + +export const Typography = { + h1: { + fontSize: 32, + fontWeight: '700' as const, + lineHeight: 38, + }, + h2: { + fontSize: 24, + fontWeight: '600' as const, + lineHeight: 30, + }, + body: { + fontSize: 16, + fontWeight: '400' as const, + lineHeight: 22, + }, + caption: { + fontSize: 12, + fontWeight: '400' as const, + lineHeight: 16, + }, +}; + +export const Spacing = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, +}; +``` + +## Design Tokens + +Create a comprehensive design token system: + +```typescript +// src/config/designTokens.ts +export const DesignTokens = { + colors: { + // Primary palette + primary: { + 50: '#E3F2FD', + 100: '#BBDEFB', + 500: '#2196F3', + 700: '#1976D2', + 900: '#0D47A1', + }, + // Semantic colors + semantic: { + success: '#4CAF50', + warning: '#FF9800', + error: '#F44336', + info: '#2196F3', + }, + // Neutral colors + neutral: { + white: '#FFFFFF', + gray50: '#FAFAFA', + gray100: '#F5F5F5', + gray200: '#EEEEEE', + gray300: '#E0E0E0', + gray400: '#BDBDBD', + gray500: '#9E9E9E', + gray600: '#757575', + gray700: '#616161', + gray800: '#424242', + gray900: '#212121', + black: '#000000', + }, + }, + + typography: { + fontFamily: { + regular: 'Montserrat-Regular', + medium: 'Montserrat-Medium', + semiBold: 'Montserrat-SemiBold', + bold: 'Montserrat-Bold', + }, + fontSize: { + xs: 12, + sm: 14, + base: 16, + lg: 18, + xl: 20, + '2xl': 24, + '3xl': 30, + '4xl': 36, + }, + lineHeight: { + tight: 1.25, + normal: 1.5, + relaxed: 1.75, + }, + }, + + spacing: { + 0: 0, + 1: 4, + 2: 8, + 3: 12, + 4: 16, + 5: 20, + 6: 24, + 8: 32, + 10: 40, + 12: 48, + 16: 64, + 20: 80, + }, + + borderRadius: { + none: 0, + sm: 4, + base: 8, + md: 12, + lg: 16, + xl: 24, + full: 9999, + }, + + shadows: { + sm: { + shadowColor: '#000', + shadowOffset: {width: 0, height: 1}, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 1, + }, + base: { + shadowColor: '#000', + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + lg: { + shadowColor: '#000', + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 5, + }, + }, +}; +``` + +## Theme Provider + +```typescript +// src/providers/ThemeProvider.tsx +import React, {createContext, useContext, ReactNode} from 'react'; +import {DesignTokens} from '@config/designTokens'; + +interface ThemeContextType { + colors: typeof DesignTokens.colors; + typography: typeof DesignTokens.typography; + spacing: typeof DesignTokens.spacing; + borderRadius: typeof DesignTokens.borderRadius; + shadows: typeof DesignTokens.shadows; +} + +const ThemeContext = createContext(undefined); + +interface ThemeProviderProps { + children: ReactNode; +} + +export const ThemeProvider: React.FC = ({children}) => { + const theme = { + colors: DesignTokens.colors, + typography: DesignTokens.typography, + spacing: DesignTokens.spacing, + borderRadius: DesignTokens.borderRadius, + shadows: DesignTokens.shadows, + }; + + return ( + + {children} + + ); +}; + +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; +``` + +## Responsive Design + +```typescript +// src/utils/responsive.ts +import {Dimensions, PixelRatio} from 'react-native'; + +const {width: SCREEN_WIDTH, height: SCREEN_HEIGHT} = Dimensions.get('window'); + +// Based on iPhone 6/7/8 dimensions +const BASE_WIDTH = 375; +const BASE_HEIGHT = 667; + +export const wp = (percentage: number): number => { + const value = (percentage * SCREEN_WIDTH) / 100; + return Math.round(PixelRatio.roundToNearestPixel(value)); +}; + +export const hp = (percentage: number): number => { + const value = (percentage * SCREEN_HEIGHT) / 100; + return Math.round(PixelRatio.roundToNearestPixel(value)); +}; + +export const normalize = (size: number): number => { + const scale = SCREEN_WIDTH / BASE_WIDTH; + const newSize = size * scale; + return Math.round(PixelRatio.roundToNearestPixel(newSize)); +}; + +export const isTablet = (): boolean => { + return SCREEN_WIDTH >= 768; +}; + +export const getDeviceType = (): 'phone' | 'tablet' => { + return isTablet() ? 'tablet' : 'phone'; +}; +``` \ No newline at end of file diff --git a/04-design-integration/icons-fonts.md b/04-design-integration/icons-fonts.md new file mode 100644 index 0000000..27e8748 --- /dev/null +++ b/04-design-integration/icons-fonts.md @@ -0,0 +1,295 @@ +# Icons & Fonts + +## App Icon & Splash Screen + +### Generate App Icons + +**Using IconKitchen or manually:** + +1. **Android**: Place icons in `android/app/src/main/res/mipmap-*` +2. **iOS**: Use Xcode Asset Catalog in `ios/SaayamApp/Images.xcassets` + +**Android Icon Sizes:** +- `mipmap-mdpi`: 48x48px +- `mipmap-hdpi`: 72x72px +- `mipmap-xhdpi`: 96x96px +- `mipmap-xxhdpi`: 144x144px +- `mipmap-xxxhdpi`: 192x192px + +**iOS Icon Sizes:** +- iPhone: 60x60pt (120x120px, 180x180px) +- iPad: 76x76pt (152x152px, 228x228px) +- App Store: 1024x1024px + +### Splash Screen Setup + +**Install react-native-splash-screen:** +```bash +yarn add react-native-splash-screen +``` + +**Android Configuration:** + +1. Create `android/app/src/main/res/drawable/launch_screen.xml`: +```xml + + + + + +``` + +2. Update `android/app/src/main/res/values/styles.xml`: +```xml + + + + + +``` + +**iOS Configuration:** + +1. Create LaunchScreen.storyboard in Xcode +2. Add your logo and configure constraints + +## Custom Fonts + +### Font Installation + +**Add fonts to project:** + +1. Create `src/assets/fonts/` directory +2. Add font files (.ttf, .otf) +3. Configure for each platform + +**Android Configuration:** + +1. Copy fonts to `android/app/src/main/assets/fonts/` +2. Use font family name in styles + +**iOS Configuration:** + +1. Add fonts to Xcode project +2. Update `ios/SaayamApp/Info.plist`: +```xml +UIAppFonts + + Montserrat-Regular.ttf + Montserrat-Bold.ttf + Montserrat-SemiBold.ttf + +``` + +### Font Helper + +```typescript +// src/utils/fontHelper.ts +import {Platform} from 'react-native'; + +export const FontFamily = { + regular: Platform.select({ + ios: 'Montserrat-Regular', + android: 'Montserrat-Regular', + }), + medium: Platform.select({ + ios: 'Montserrat-Medium', + android: 'Montserrat-Medium', + }), + semiBold: Platform.select({ + ios: 'Montserrat-SemiBold', + android: 'Montserrat-SemiBold', + }), + bold: Platform.select({ + ios: 'Montserrat-Bold', + android: 'Montserrat-Bold', + }), +}; + +export const getFontFamily = (weight: 'regular' | 'medium' | 'semiBold' | 'bold' = 'regular'): string => { + return FontFamily[weight] || FontFamily.regular; +}; +``` + +## Icon Font Generation + +### Using IcoMoon + +1. **Visit IcoMoon.io** +2. **Import SVG icons** +3. **Generate font** +4. **Download and integrate** + +**IcoMoon Integration:** +```typescript +// src/config/iconFont.ts +import {createIconSetFromIcoMoon} from 'react-native-vector-icons/lib/create-icon-set-from-icomoon'; +import icoMoonConfig from '../assets/fonts/selection.json'; + +export const CustomIcon = createIconSetFromIcoMoon( + icoMoonConfig, + 'CustomIcons', + 'CustomIcons.ttf' +); +``` + +### Custom Icon Component + +```typescript +// src/components/common/CustomIcon/CustomIcon.tsx +import React from 'react'; +import {createIconSetFromIcoMoon} from 'react-native-vector-icons/lib/create-icon-set-from-icomoon'; +import icoMoonConfig from '@assets/fonts/selection.json'; + +const IconFont = createIconSetFromIcoMoon( + icoMoonConfig, + 'SaayamIcons', + 'SaayamIcons.ttf' +); + +interface CustomIconProps { + name: string; + size?: number; + color?: string; + style?: any; + onPress?: () => void; +} + +export const CustomIcon: React.FC = ({ + name, + size = 24, + color = '#000', + style, + onPress, +}) => { + return ( + + ); +}; +``` + +## Icon Library Setup + +```typescript +// src/config/icons.ts +export const IconLibrary = { + // Navigation icons + home: 'home', + profile: 'user', + settings: 'settings', + back: 'arrow-left', + + // Action icons + add: 'plus', + edit: 'edit', + delete: 'trash', + save: 'check', + cancel: 'x', + + // Status icons + success: 'check-circle', + error: 'x-circle', + warning: 'alert-triangle', + info: 'info', + + // Social icons + facebook: 'facebook', + google: 'google', + apple: 'apple', + + // Feature icons + camera: 'camera', + gallery: 'image', + location: 'map-pin', + notification: 'bell', + search: 'search', + filter: 'filter', + share: 'share', + favorite: 'heart', + star: 'star', +}; + +export type IconName = keyof typeof IconLibrary; +``` + +## Dynamic Icon Loading + +```typescript +// src/utils/iconLoader.ts +import {IconLibrary, IconName} from '@config/icons'; + +export class IconLoader { + private static loadedIcons: Set = new Set(); + + static async preloadIcons(iconNames: IconName[]): Promise { + const promises = iconNames.map(async (iconName) => { + if (!this.loadedIcons.has(iconName)) { + // Preload icon font glyphs + await this.loadIcon(iconName); + this.loadedIcons.add(iconName); + } + }); + + await Promise.all(promises); + } + + private static async loadIcon(iconName: IconName): Promise { + // Implementation depends on your icon font setup + return new Promise((resolve) => { + // Simulate icon loading + setTimeout(resolve, 10); + }); + } + + static isIconLoaded(iconName: IconName): boolean { + return this.loadedIcons.has(iconName); + } +} +``` + +## Font Size Scaling + +```typescript +// src/utils/fontScaling.ts +import {Dimensions, PixelRatio} from 'react-native'; + +const {width: SCREEN_WIDTH} = Dimensions.get('window'); +const BASE_WIDTH = 375; // iPhone 6/7/8 width + +export const fontSizer = (size: number): number => { + const scale = SCREEN_WIDTH / BASE_WIDTH; + const newSize = size * scale; + + // Ensure minimum readable size + const minSize = size * 0.8; + const maxSize = size * 1.2; + + return Math.max(minSize, Math.min(maxSize, Math.round(PixelRatio.roundToNearestPixel(newSize)))); +}; + +export const FontSizes = { + xs: fontSizer(10), + sm: fontSizer(12), + base: fontSizer(14), + lg: fontSizer(16), + xl: fontSizer(18), + '2xl': fontSizer(20), + '3xl': fontSizer(24), + '4xl': fontSizer(28), + '5xl': fontSizer(32), +}; +``` \ No newline at end of file diff --git a/05-state-management-navigation/api-integration.md b/05-state-management-navigation/api-integration.md new file mode 100644 index 0000000..fecfd0c --- /dev/null +++ b/05-state-management-navigation/api-integration.md @@ -0,0 +1,536 @@ +# API Integration + +## RTK Query Setup + +```typescript +// src/services/api.ts +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import Config from 'react-native-config'; +import { RootState } from '@redux/store'; + +export const api = createApi({ + reducerPath: 'api', + baseQuery: fetchBaseQuery({ + baseUrl: Config.API_BASE_URL, + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).auth.token; + if (token) { + headers.set('authorization', `Bearer ${token}`); + } + headers.set('Content-Type', 'application/json'); + return headers; + }, + }), + tagTypes: ['User', 'NGO', 'Donation', 'Request'], + endpoints: () => ({}), +}); +``` + +## Auth API + +```typescript +// src/services/authAPI.ts +import { api } from './api'; + +interface LoginRequest { + email: string; + password: string; +} + +interface LoginResponse { + user: { + id: string; + email: string; + firstName: string; + lastName: string; + }; + token: string; + refreshToken: string; +} + +interface RegisterRequest { + firstName: string; + lastName: string; + email: string; + password: string; + phone: string; +} + +export const authAPI = api.injectEndpoints({ + endpoints: (builder) => ({ + login: builder.mutation({ + query: (credentials) => ({ + url: '/auth/login', + method: 'POST', + body: credentials, + }), + }), + register: builder.mutation({ + query: (userData) => ({ + url: '/auth/register', + method: 'POST', + body: userData, + }), + }), + refreshToken: builder.mutation<{ token: string }, { refreshToken: string }>( + { + query: ({ refreshToken }) => ({ + url: '/auth/refresh', + method: 'POST', + body: { refreshToken }, + }), + }, + ), + logout: builder.mutation({ + query: () => ({ + url: '/auth/logout', + method: 'POST', + }), + }), + forgotPassword: builder.mutation({ + query: ({ email }) => ({ + url: '/auth/forgot-password', + method: 'POST', + body: { email }, + }), + }), + verifyOTP: builder.mutation({ + query: ({ email, otp }) => ({ + url: '/auth/verify-otp', + method: 'POST', + body: { email, otp }, + }), + }), + }), +}); + +export const { + useLoginMutation, + useRegisterMutation, + useRefreshTokenMutation, + useLogoutMutation, + useForgotPasswordMutation, + useVerifyOTPMutation, +} = authAPI; +``` + +## User API + +```typescript +// src/services/userAPI.ts +import { api } from './api'; + +interface User { + id: string; + email: string; + firstName: string; + lastName: string; + phone?: string; + avatar?: string; + isNGO: boolean; + ngoDetails?: NGODetails; +} + +interface NGODetails { + ngoName: string; + registrationNumber: string; + address: string; + description: string; + causes: string[]; +} + +interface UpdateProfileRequest { + firstName?: string; + lastName?: string; + phone?: string; + avatar?: string; +} + +export const userAPI = api.injectEndpoints({ + endpoints: (builder) => ({ + getProfile: builder.query({ + query: () => '/user/profile', + providesTags: ['User'], + }), + updateProfile: builder.mutation({ + query: (updates) => ({ + url: '/user/profile', + method: 'PUT', + body: updates, + }), + invalidatesTags: ['User'], + }), + uploadAvatar: builder.mutation<{ avatarUrl: string }, FormData>({ + query: (formData) => ({ + url: '/user/avatar', + method: 'POST', + body: formData, + }), + invalidatesTags: ['User'], + }), + deleteAccount: builder.mutation({ + query: () => ({ + url: '/user/account', + method: 'DELETE', + }), + }), + }), +}); + +export const { + useGetProfileQuery, + useUpdateProfileMutation, + useUploadAvatarMutation, + useDeleteAccountMutation, +} = userAPI; +``` + +## NGO API + +```typescript +// src/services/ngoAPI.ts +import { api } from './api'; + +interface NGO { + id: string; + ngoName: string; + description: string; + registrationNumber: string; + address: string; + causes: string[]; + coverImage?: string; + isVerified: boolean; + totalDonations: number; + activeRequests: number; +} + +interface CreateNGORequest { + ngoName: string; + description: string; + registrationNumber: string; + address: string; + causes: string[]; + documents: string[]; +} + +export const ngoAPI = api.injectEndpoints({ + endpoints: (builder) => ({ + getNGOList: builder.query< + NGO[], + { page?: number; limit?: number; search?: string } + >({ + query: ({ page = 1, limit = 10, search }) => ({ + url: '/ngo', + params: { page, limit, search }, + }), + providesTags: ['NGO'], + }), + getNGOById: builder.query({ + query: (id) => `/ngo/${id}`, + providesTags: ['NGO'], + }), + createNGO: builder.mutation({ + query: (ngoData) => ({ + url: '/ngo', + method: 'POST', + body: ngoData, + }), + invalidatesTags: ['NGO', 'User'], + }), + updateNGO: builder.mutation< + NGO, + { id: string; updates: Partial } + >({ + query: ({ id, updates }) => ({ + url: `/ngo/${id}`, + method: 'PUT', + body: updates, + }), + invalidatesTags: ['NGO'], + }), + getFavoriteNGOs: builder.query({ + query: () => '/ngo/favorites', + providesTags: ['NGO'], + }), + toggleFavoriteNGO: builder.mutation({ + query: (ngoId) => ({ + url: `/ngo/${ngoId}/favorite`, + method: 'POST', + }), + invalidatesTags: ['NGO'], + }), + }), +}); + +export const { + useGetNGOListQuery, + useGetNGOByIdQuery, + useCreateNGOMutation, + useUpdateNGOMutation, + useGetFavoriteNGOsQuery, + useToggleFavoriteNGOMutation, +} = ngoAPI; +``` + +## Request API + +```typescript +// src/services/requestAPI.ts +import { api } from './api'; + +interface FundraisingRequest { + id: string; + title: string; + description: string; + category: string; + targetAmount: number; + currentAmount: number; + images: string[]; + location: { + latitude: number; + longitude: number; + address: string; + }; + status: 'active' | 'completed' | 'cancelled'; + createdBy: string; + ngoId?: string; + isUrgent: boolean; + endDate: string; +} + +interface CreateRequestData { + title: string; + description: string; + category: string; + targetAmount: number; + images: string[]; + location: { + latitude: number; + longitude: number; + address: string; + }; + isUrgent: boolean; + endDate: string; +} + +export const requestAPI = api.injectEndpoints({ + endpoints: (builder) => ({ + getRequests: builder.query< + FundraisingRequest[], + { + page?: number; + limit?: number; + category?: string; + location?: string; + urgent?: boolean; + } + >({ + query: (params) => ({ + url: '/requests', + params, + }), + providesTags: ['Request'], + }), + getRequestById: builder.query({ + query: (id) => `/requests/${id}`, + providesTags: ['Request'], + }), + createRequest: builder.mutation({ + query: (requestData) => ({ + url: '/requests', + method: 'POST', + body: requestData, + }), + invalidatesTags: ['Request'], + }), + updateRequest: builder.mutation< + FundraisingRequest, + { + id: string; + updates: Partial; + } + >({ + query: ({ id, updates }) => ({ + url: `/requests/${id}`, + method: 'PUT', + body: updates, + }), + invalidatesTags: ['Request'], + }), + deleteRequest: builder.mutation({ + query: (id) => ({ + url: `/requests/${id}`, + method: 'DELETE', + }), + invalidatesTags: ['Request'], + }), + getMyRequests: builder.query({ + query: () => '/requests/my-requests', + providesTags: ['Request'], + }), + }), +}); + +export const { + useGetRequestsQuery, + useGetRequestByIdQuery, + useCreateRequestMutation, + useUpdateRequestMutation, + useDeleteRequestMutation, + useGetMyRequestsQuery, +} = requestAPI; +``` + +## Error Handling + +```typescript +// src/utils/errorHandler.ts +import { SerializedError } from '@reduxjs/toolkit'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; + +export interface APIError { + message: string; + status?: number; + code?: string; +} + +export const handleAPIError = ( + error: FetchBaseQueryError | SerializedError, +): APIError => { + if ('status' in error) { + // FetchBaseQueryError + const status = error.status; + const data = error.data as any; + + switch (status) { + case 400: + return { + message: data?.message || 'Bad request', + status: 400, + code: 'BAD_REQUEST', + }; + case 401: + return { + message: 'Authentication required', + status: 401, + code: 'UNAUTHORIZED', + }; + case 403: + return { + message: 'Access denied', + status: 403, + code: 'FORBIDDEN', + }; + case 404: + return { + message: 'Resource not found', + status: 404, + code: 'NOT_FOUND', + }; + case 500: + return { + message: 'Server error. Please try again later.', + status: 500, + code: 'SERVER_ERROR', + }; + default: + return { + message: data?.message || 'An unexpected error occurred', + status: typeof status === 'number' ? status : undefined, + code: 'UNKNOWN_ERROR', + }; + } + } else { + // SerializedError + return { + message: error.message || 'Network error', + code: 'NETWORK_ERROR', + }; + } +}; +``` + +## API Hooks + +```typescript +// src/hooks/useAPI.ts +import { useState, useCallback } from 'react'; +import { handleAPIError, APIError } from '@utils/errorHandler'; + +interface UseAPIState { + data: T | null; + loading: boolean; + error: APIError | null; +} + +export const useAPI = () => { + const [state, setState] = useState>({ + data: null, + loading: false, + error: null, + }); + + const execute = useCallback(async (apiCall: () => Promise) => { + setState((prev) => ({ ...prev, loading: true, error: null })); + + try { + const data = await apiCall(); + setState({ data, loading: false, error: null }); + return data; + } catch (error: any) { + const apiError = handleAPIError(error); + setState((prev) => ({ ...prev, loading: false, error: apiError })); + throw apiError; + } + }, []); + + const reset = useCallback(() => { + setState({ data: null, loading: false, error: null }); + }, []); + + return { + ...state, + execute, + reset, + }; +}; +``` + +## Store Integration + +```typescript +// src/redux/store/index.ts (updated) +import { configureStore } from '@reduxjs/toolkit'; +import { persistStore, persistReducer } from 'redux-persist'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { combineReducers } from '@reduxjs/toolkit'; +import { api } from '@services/api'; +import authSlice from '../slices/authSlice'; + +const persistConfig = { + key: 'root', + storage: AsyncStorage, + whitelist: ['auth'], +}; + +const rootReducer = combineReducers({ + auth: authSlice, + [api.reducerPath]: api.reducer, +}); + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'], + }, + }).concat(api.middleware), +}); + +export const persistor = persistStore(store); +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; +``` diff --git a/05-state-management-navigation/navigation.md b/05-state-management-navigation/navigation.md new file mode 100644 index 0000000..9a37180 --- /dev/null +++ b/05-state-management-navigation/navigation.md @@ -0,0 +1,364 @@ +# Navigation Setup + +## Installation + +```bash +yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs +yarn add react-native-screens react-native-safe-area-context react-native-gesture-handler +``` + +## iOS Setup + +```bash +cd ios && pod install +``` + +## Navigation Types + +```typescript +// src/navigation/types.ts +export type RootStackParamList = { + Auth: undefined; + Main: undefined; + Splash: undefined; +}; + +export type AuthStackParamList = { + Login: undefined; + Register: undefined; + ForgotPassword: undefined; + OTPVerification: {email: string}; +}; + +export type MainTabParamList = { + Home: undefined; + Profile: undefined; + Settings: undefined; + Notifications: undefined; +}; + +export type HomeStackParamList = { + HomeScreen: undefined; + Details: {id: string}; + Search: undefined; +}; +``` + +## Root Navigator + +```typescript +// src/navigation/RootNavigator.tsx +import React from 'react'; +import {NavigationContainer} from '@react-navigation/native'; +import {createStackNavigator} from '@react-navigation/stack'; +import {useAppSelector} from '@redux/hooks'; +import {selectIsAuthenticated} from '@redux/selectors/authSelectors'; + +import AuthNavigator from './AuthNavigator'; +import MainNavigator from './MainNavigator'; +import SplashScreen from '@screens/SplashScreen'; +import {RootStackParamList} from './types'; + +const Stack = createStackNavigator(); + +export const RootNavigator: React.FC = () => { + const isAuthenticated = useAppSelector(selectIsAuthenticated); + const [isLoading, setIsLoading] = React.useState(true); + + React.useEffect(() => { + // Simulate app initialization + const timer = setTimeout(() => { + setIsLoading(false); + }, 2000); + + return () => clearTimeout(timer); + }, []); + + return ( + + + {isLoading ? ( + + ) : isAuthenticated ? ( + + ) : ( + + )} + + + ); +}; +``` + +## Auth Navigator + +```typescript +// src/navigation/AuthNavigator.tsx +import React from 'react'; +import {createStackNavigator} from '@react-navigation/stack'; +import {AuthStackParamList} from './types'; + +import LoginScreen from '@screens/Auth/LoginScreen'; +import RegisterScreen from '@screens/Auth/RegisterScreen'; +import ForgotPasswordScreen from '@screens/Auth/ForgotPasswordScreen'; +import OTPVerificationScreen from '@screens/Auth/OTPVerificationScreen'; + +const Stack = createStackNavigator(); + +const AuthNavigator: React.FC = () => { + return ( + { + return { + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }; + }, + }}> + + + + + + ); +}; + +export default AuthNavigator; +``` + +## Main Navigator (Bottom Tabs) + +```typescript +// src/navigation/MainNavigator.tsx +import React from 'react'; +import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; +import {MainTabParamList} from './types'; +import {CustomIcon} from '@components/common/CustomIcon'; +import {useTheme} from '@providers/ThemeProvider'; + +import HomeNavigator from './HomeNavigator'; +import ProfileScreen from '@screens/Profile/ProfileScreen'; +import SettingsScreen from '@screens/Settings/SettingsScreen'; +import NotificationsScreen from '@screens/Notifications/NotificationsScreen'; + +const Tab = createBottomTabNavigator(); + +const MainNavigator: React.FC = () => { + const {colors} = useTheme(); + + return ( + ({ + headerShown: false, + tabBarIcon: ({focused, color, size}) => { + let iconName: string; + + switch (route.name) { + case 'Home': + iconName = 'home'; + break; + case 'Profile': + iconName = 'user'; + break; + case 'Settings': + iconName = 'settings'; + break; + case 'Notifications': + iconName = 'bell'; + break; + default: + iconName = 'home'; + } + + return ; + }, + tabBarActiveTintColor: colors.primary[500], + tabBarInactiveTintColor: colors.neutral.gray500, + tabBarStyle: { + backgroundColor: colors.neutral.white, + borderTopColor: colors.neutral.gray200, + paddingBottom: 5, + height: 60, + }, + tabBarLabelStyle: { + fontSize: 12, + fontWeight: '500', + }, + })}> + + + + + + ); +}; + +export default MainNavigator; +``` + +## Home Stack Navigator + +```typescript +// src/navigation/HomeNavigator.tsx +import React from 'react'; +import {createStackNavigator} from '@react-navigation/stack'; +import {HomeStackParamList} from './types'; + +import HomeScreen from '@screens/Home/HomeScreen'; +import DetailsScreen from '@screens/Home/DetailsScreen'; +import SearchScreen from '@screens/Home/SearchScreen'; + +const Stack = createStackNavigator(); + +const HomeNavigator: React.FC = () => { + return ( + + + + + + ); +}; + +export default HomeNavigator; +``` + +## Navigation Service + +```typescript +// src/navigation/NavigationService.ts +import {createNavigationContainerRef, StackActions} from '@react-navigation/native'; + +export const navigationRef = createNavigationContainerRef(); + +export function navigate(name: string, params?: any) { + if (navigationRef.isReady()) { + navigationRef.navigate(name as never, params as never); + } +} + +export function goBack() { + if (navigationRef.isReady()) { + navigationRef.goBack(); + } +} + +export function reset(routeName: string) { + if (navigationRef.isReady()) { + navigationRef.reset({ + index: 0, + routes: [{name: routeName}], + }); + } +} + +export function push(name: string, params?: any) { + if (navigationRef.isReady()) { + navigationRef.dispatch(StackActions.push(name, params)); + } +} + +export function replace(name: string, params?: any) { + if (navigationRef.isReady()) { + navigationRef.dispatch(StackActions.replace(name, params)); + } +} +``` + +## Navigation Hooks + +```typescript +// src/hooks/useNavigation.ts +import {useNavigation as useRNNavigation} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {RootStackParamList} from '@navigation/types'; + +export type NavigationProp = StackNavigationProp; + +export const useNavigation = () => { + return useRNNavigation(); +}; +``` + +## Deep Linking + +```typescript +// src/navigation/LinkingConfiguration.ts +import {LinkingOptions} from '@react-navigation/native'; +import {RootStackParamList} from './types'; + +const linking: LinkingOptions = { + prefixes: ['saayam://'], + config: { + screens: { + Auth: { + screens: { + Login: 'login', + Register: 'register', + ForgotPassword: 'forgot-password', + OTPVerification: 'otp-verification', + }, + }, + Main: { + screens: { + Home: { + screens: { + HomeScreen: 'home', + Details: 'details/:id', + Search: 'search', + }, + }, + Profile: 'profile', + Settings: 'settings', + Notifications: 'notifications', + }, + }, + }, + }, +}; + +export default linking; +``` + +## Navigation Container Setup + +```typescript +// App.tsx (updated) +import React from 'react'; +import {NavigationContainer} from '@react-navigation/native'; +import {Provider} from 'react-redux'; +import {PersistGate} from 'redux-persist/integration/react'; +import {store, persistor} from './src/redux/store'; +import {RootNavigator} from './src/navigation/RootNavigator'; +import {navigationRef} from './src/navigation/NavigationService'; +import linking from './src/navigation/LinkingConfiguration'; + +const App: React.FC = () => { + return ( + + + + + + + + ); +}; + +export default App; +``` \ No newline at end of file diff --git a/05-state-management-navigation/redux-setup.md b/05-state-management-navigation/redux-setup.md new file mode 100644 index 0000000..1bd6200 --- /dev/null +++ b/05-state-management-navigation/redux-setup.md @@ -0,0 +1,264 @@ +# Redux Toolkit Setup + +## Installation + +```bash +yarn add @reduxjs/toolkit react-redux redux-persist +yarn add -D @types/react-redux +``` + +## Store Configuration + +```typescript +// src/redux/store/index.ts +import {configureStore} from '@reduxjs/toolkit'; +import {persistStore, persistReducer} from 'redux-persist'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import {combineReducers} from '@reduxjs/toolkit'; +import authSlice from '../slices/authSlice'; +import userSlice from '../slices/userSlice'; + +const persistConfig = { + key: 'root', + storage: AsyncStorage, + whitelist: ['auth'], +}; + +const rootReducer = combineReducers({ + auth: authSlice, + user: userSlice, +}); + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'], + }, + }), +}); + +export const persistor = persistStore(store); +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; +``` + +## Auth Slice + +```typescript +// src/redux/slices/authSlice.ts +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; + +interface User { + id: string; + email: string; + firstName: string; + lastName: string; + avatar?: string; +} + +interface AuthState { + isAuthenticated: boolean; + user: User | null; + token: string | null; + refreshToken: string | null; + loading: boolean; + error: string | null; +} + +const initialState: AuthState = { + isAuthenticated: false, + user: null, + token: null, + refreshToken: null, + loading: false, + error: null, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + loginStart: (state) => { + state.loading = true; + state.error = null; + }, + loginSuccess: (state, action: PayloadAction<{user: User; token: string; refreshToken: string}>) => { + state.loading = false; + state.isAuthenticated = true; + state.user = action.payload.user; + state.token = action.payload.token; + state.refreshToken = action.payload.refreshToken; + state.error = null; + }, + loginFailure: (state, action: PayloadAction) => { + state.loading = false; + state.isAuthenticated = false; + state.user = null; + state.token = null; + state.refreshToken = null; + state.error = action.payload; + }, + logout: (state) => { + state.isAuthenticated = false; + state.user = null; + state.token = null; + state.refreshToken = null; + state.error = null; + }, + updateUser: (state, action: PayloadAction>) => { + if (state.user) { + state.user = {...state.user, ...action.payload}; + } + }, + clearError: (state) => { + state.error = null; + }, + }, +}); + +export const { + loginStart, + loginSuccess, + loginFailure, + logout, + updateUser, + clearError, +} = authSlice.actions; + +export default authSlice.reducer; +``` + +## Typed Hooks + +```typescript +// src/redux/hooks.ts +import {useDispatch, useSelector, TypedUseSelectorHook} from 'react-redux'; +import type {RootState, AppDispatch} from './store'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; +``` + +## Async Thunks + +```typescript +// src/redux/thunks/authThunks.ts +import {createAsyncThunk} from '@reduxjs/toolkit'; +import {authAPI} from '@services/authAPI'; + +interface LoginCredentials { + email: string; + password: string; +} + +export const loginUser = createAsyncThunk( + 'auth/loginUser', + async (credentials: LoginCredentials, {rejectWithValue}) => { + try { + const response = await authAPI.login(credentials); + return response.data; + } catch (error: any) { + return rejectWithValue(error.response?.data?.message || 'Login failed'); + } + } +); + +export const refreshToken = createAsyncThunk( + 'auth/refreshToken', + async (_, {getState, rejectWithValue}) => { + try { + const state = getState() as any; + const refreshToken = state.auth.refreshToken; + + if (!refreshToken) { + throw new Error('No refresh token available'); + } + + const response = await authAPI.refreshToken(refreshToken); + return response.data; + } catch (error: any) { + return rejectWithValue(error.response?.data?.message || 'Token refresh failed'); + } + } +); +``` + +## Provider Setup + +```typescript +// App.tsx +import React from 'react'; +import {Provider} from 'react-redux'; +import {PersistGate} from 'redux-persist/integration/react'; +import {store, persistor} from './src/redux/store'; +import {AppNavigator} from './src/navigation/AppNavigator'; +import {LoadingScreen} from './src/components/common/LoadingScreen'; + +const App: React.FC = () => { + return ( + + } persistor={persistor}> + + + + ); +}; + +export default App; +``` + +## Middleware Configuration + +```typescript +// src/redux/middleware/authMiddleware.ts +import {Middleware} from '@reduxjs/toolkit'; +import {logout} from '../slices/authSlice'; + +export const authMiddleware: Middleware = (store) => (next) => (action) => { + // Handle token expiration + if (action.type.endsWith('/rejected') && action.payload?.status === 401) { + store.dispatch(logout()); + } + + return next(action); +}; +``` + +## Selectors + +```typescript +// src/redux/selectors/authSelectors.ts +import {createSelector} from '@reduxjs/toolkit'; +import {RootState} from '../store'; + +export const selectAuth = (state: RootState) => state.auth; + +export const selectIsAuthenticated = createSelector( + [selectAuth], + (auth) => auth.isAuthenticated +); + +export const selectUser = createSelector( + [selectAuth], + (auth) => auth.user +); + +export const selectAuthLoading = createSelector( + [selectAuth], + (auth) => auth.loading +); + +export const selectAuthError = createSelector( + [selectAuth], + (auth) => auth.error +); + +export const selectUserFullName = createSelector( + [selectUser], + (user) => user ? `${user.firstName} ${user.lastName}` : '' +); +``` \ No newline at end of file diff --git a/06-testing-quality-assurance/e2e-testing.md b/06-testing-quality-assurance/e2e-testing.md new file mode 100644 index 0000000..8aa3a2c --- /dev/null +++ b/06-testing-quality-assurance/e2e-testing.md @@ -0,0 +1,335 @@ +# E2E Testing with Detox + +## Installation + +```bash +yarn add -D detox +npx detox init +``` + +## Configuration + +**detox.config.js:** +```javascript +module.exports = { + testRunner: 'jest', + runnerConfig: 'e2e/config.json', + configurations: { + 'ios.sim.debug': { + device: { + type: 'ios.simulator', + device: { + type: 'iPhone 12', + }, + }, + app: { + type: 'ios.app', + binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/SaayamApp.app', + build: 'xcodebuild -workspace ios/SaayamApp.xcworkspace -scheme SaayamApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', + }, + }, + 'android.emu.debug': { + device: { + type: 'android.emulator', + device: { + avdName: 'Pixel_4_API_30', + }, + }, + app: { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + reversePorts: [8081], + }, + }, + }, +}; +``` + +**e2e/config.json:** +```json +{ + "maxWorkers": 1, + "testTimeout": 120000, + "retries": 2, + "bail": false, + "verbose": true, + "setupFilesAfterEnv": ["./init.js"] +} +``` + +## Test Setup + +```javascript +// e2e/init.js +const detox = require('detox'); +const config = require('../detox.config.js'); +const adapter = require('detox/runners/jest/adapter'); + +jest.setTimeout(300000); +jasmine.getEnv().addReporter(adapter); + +beforeAll(async () => { + await detox.init(config); +}); + +beforeEach(async () => { + await adapter.beforeEach(); +}); + +afterAll(async () => { + await adapter.afterAll(); + await detox.cleanup(); +}); +``` + +## Login Flow Test + +```javascript +// e2e/loginFlow.e2e.js +describe('Login Flow', () => { + beforeEach(async () => { + await device.reloadReactNative(); + }); + + it('should show login screen on app launch', async () => { + await expect(element(by.id('loginScreen'))).toBeVisible(); + await expect(element(by.id('emailInput'))).toBeVisible(); + await expect(element(by.id('passwordInput'))).toBeVisible(); + await expect(element(by.id('loginButton'))).toBeVisible(); + }); + + it('should show validation errors for empty fields', async () => { + await element(by.id('loginButton')).tap(); + + await expect(element(by.text('Email is required'))).toBeVisible(); + await expect(element(by.text('Password is required'))).toBeVisible(); + }); + + it('should login successfully with valid credentials', async () => { + await element(by.id('emailInput')).typeText('test@example.com'); + await element(by.id('passwordInput')).typeText('password123'); + await element(by.id('loginButton')).tap(); + + // Wait for navigation to home screen + await waitFor(element(by.id('homeScreen'))) + .toBeVisible() + .withTimeout(10000); + }); + + it('should show error for invalid credentials', async () => { + await element(by.id('emailInput')).typeText('invalid@example.com'); + await element(by.id('passwordInput')).typeText('wrongpassword'); + await element(by.id('loginButton')).tap(); + + await expect(element(by.text('Invalid credentials'))).toBeVisible(); + }); +}); +``` + +## Navigation Test + +```javascript +// e2e/navigation.e2e.js +describe('Navigation', () => { + beforeEach(async () => { + await device.reloadReactNative(); + // Login first + await element(by.id('emailInput')).typeText('test@example.com'); + await element(by.id('passwordInput')).typeText('password123'); + await element(by.id('loginButton')).tap(); + await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000); + }); + + it('should navigate between tabs', async () => { + // Test Home tab + await expect(element(by.id('homeScreen'))).toBeVisible(); + + // Navigate to Profile tab + await element(by.id('profileTab')).tap(); + await expect(element(by.id('profileScreen'))).toBeVisible(); + + // Navigate to Settings tab + await element(by.id('settingsTab')).tap(); + await expect(element(by.id('settingsScreen'))).toBeVisible(); + + // Navigate back to Home tab + await element(by.id('homeTab')).tap(); + await expect(element(by.id('homeScreen'))).toBeVisible(); + }); + + it('should navigate to detail screen and back', async () => { + await element(by.id('firstRequestItem')).tap(); + await expect(element(by.id('requestDetailScreen'))).toBeVisible(); + + await element(by.id('backButton')).tap(); + await expect(element(by.id('homeScreen'))).toBeVisible(); + }); +}); +``` + +## Form Testing + +```javascript +// e2e/createRequest.e2e.js +describe('Create Request Flow', () => { + beforeEach(async () => { + await device.reloadReactNative(); + // Login and navigate to create request + await element(by.id('emailInput')).typeText('test@example.com'); + await element(by.id('passwordInput')).typeText('password123'); + await element(by.id('loginButton')).tap(); + await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000); + await element(by.id('createRequestButton')).tap(); + }); + + it('should create a new request successfully', async () => { + await element(by.id('titleInput')).typeText('Help for Medical Treatment'); + await element(by.id('descriptionInput')).typeText('Need urgent help for medical treatment'); + await element(by.id('targetAmountInput')).typeText('50000'); + + // Select category + await element(by.id('categoryDropdown')).tap(); + await element(by.text('Health')).tap(); + + // Add image + await element(by.id('addImageButton')).tap(); + await element(by.text('Camera')).tap(); + + // Submit form + await element(by.id('submitButton')).tap(); + + await waitFor(element(by.text('Request created successfully'))) + .toBeVisible() + .withTimeout(10000); + }); + + it('should show validation errors for incomplete form', async () => { + await element(by.id('submitButton')).tap(); + + await expect(element(by.text('Title is required'))).toBeVisible(); + await expect(element(by.text('Description is required'))).toBeVisible(); + await expect(element(by.text('Target amount is required'))).toBeVisible(); + }); +}); +``` + +## Scroll and List Testing + +```javascript +// e2e/requestList.e2e.js +describe('Request List', () => { + beforeEach(async () => { + await device.reloadReactNative(); + // Login first + await element(by.id('emailInput')).typeText('test@example.com'); + await element(by.id('passwordInput')).typeText('password123'); + await element(by.id('loginButton')).tap(); + await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000); + }); + + it('should load and display request list', async () => { + await expect(element(by.id('requestList'))).toBeVisible(); + await expect(element(by.id('requestItem-0'))).toBeVisible(); + }); + + it('should scroll through request list', async () => { + await element(by.id('requestList')).scroll(300, 'down'); + await expect(element(by.id('requestItem-5'))).toBeVisible(); + }); + + it('should pull to refresh', async () => { + await element(by.id('requestList')).swipe('down', 'slow', 0.8); + await waitFor(element(by.id('refreshIndicator'))) + .toBeVisible() + .withTimeout(2000); + }); + + it('should filter requests by category', async () => { + await element(by.id('filterButton')).tap(); + await element(by.text('Health')).tap(); + await element(by.id('applyFilterButton')).tap(); + + await waitFor(element(by.id('requestList'))) + .toBeVisible() + .withTimeout(5000); + }); +}); +``` + +## Device Interactions + +```javascript +// e2e/deviceInteractions.e2e.js +describe('Device Interactions', () => { + beforeEach(async () => { + await device.reloadReactNative(); + }); + + it('should handle device rotation', async () => { + await device.setOrientation('landscape'); + await expect(element(by.id('loginScreen'))).toBeVisible(); + + await device.setOrientation('portrait'); + await expect(element(by.id('loginScreen'))).toBeVisible(); + }); + + it('should handle app backgrounding and foregrounding', async () => { + await device.sendToHome(); + await device.launchApp({newInstance: false}); + await expect(element(by.id('loginScreen'))).toBeVisible(); + }); + + it('should handle deep links', async () => { + await device.openURL({url: 'saayam://request/123'}); + await waitFor(element(by.id('requestDetailScreen'))) + .toBeVisible() + .withTimeout(10000); + }); +}); +``` + +## Test Utilities + +```javascript +// e2e/utils/helpers.js +export const loginUser = async (email = 'test@example.com', password = 'password123') => { + await element(by.id('emailInput')).typeText(email); + await element(by.id('passwordInput')).typeText(password); + await element(by.id('loginButton')).tap(); + await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000); +}; + +export const navigateToScreen = async (screenId) => { + await element(by.id(screenId)).tap(); + await waitFor(element(by.id(`${screenId}Screen`))).toBeVisible().withTimeout(5000); +}; + +export const fillForm = async (formData) => { + for (const [fieldId, value] of Object.entries(formData)) { + await element(by.id(fieldId)).typeText(value); + } +}; + +export const waitForElementToDisappear = async (elementId, timeout = 5000) => { + await waitFor(element(by.id(elementId))).not.toBeVisible().withTimeout(timeout); +}; +``` + +## Running Tests + +```bash +# Build and run iOS tests +detox build --configuration ios.sim.debug +detox test --configuration ios.sim.debug + +# Build and run Android tests +detox build --configuration android.emu.debug +detox test --configuration android.emu.debug + +# Run specific test file +detox test --configuration ios.sim.debug e2e/loginFlow.e2e.js + +# Run tests with verbose output +detox test --configuration ios.sim.debug --loglevel verbose +``` \ No newline at end of file diff --git a/06-testing-quality-assurance/unit-testing.md b/06-testing-quality-assurance/unit-testing.md new file mode 100644 index 0000000..de99d20 --- /dev/null +++ b/06-testing-quality-assurance/unit-testing.md @@ -0,0 +1,437 @@ +# Unit Testing Setup + +## Installation + +```bash +yarn add -D jest @testing-library/react-native @testing-library/jest-native +``` + +## Jest Configuration + +**jest.config.js:** +```javascript +module.exports = { + preset: 'react-native', + setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], + transformIgnorePatterns: [ + 'node_modules/(?!(react-native|@react-native|react-navigation)/)', + ], + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/*.stories.{ts,tsx}', + ], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, +}; +``` + +## Test Setup + +```typescript +// src/__tests__/setup.ts +import 'react-native-gesture-handler/jestSetup'; + +jest.mock('react-native-reanimated', () => { + const Reanimated = require('react-native-reanimated/mock'); + Reanimated.default.call = () => {}; + return Reanimated; +}); + +jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); + +// Mock AsyncStorage +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock') +); + +// Mock react-native-config +jest.mock('react-native-config', () => ({ + API_BASE_URL: 'https://test-api.example.com', + APP_ENV: 'test', +})); +``` + +## Component Testing + +```typescript +// src/components/Button/__tests__/Button.test.tsx +import React from 'react'; +import {render, fireEvent} from '@testing-library/react-native'; +import {Button} from '../Button'; + +describe('Button Component', () => { + it('renders correctly', () => { + const {getByText} = render( +