: Initial Commit

main
Drashti Patel 2025-09-14 09:37:04 +05:30
parent 1cc64f3731
commit b7ff79f08c
24 changed files with 8656 additions and 6 deletions

View File

@ -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
```

View File

@ -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
```

View File

@ -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<ButtonProps> = ({
title,
onPress,
variant = "primary",
disabled = false,
style,
textStyle,
}) => {
return (
<TouchableOpacity
style={[
styles.button,
styles[variant],
disabled && styles.disabled,
style,
]}
onPress={onPress}
disabled={disabled}
activeOpacity={0.7}
>
<Text style={[styles.text, styles[`${variant}Text`], textStyle]}>
{title}
</Text>
</TouchableOpacity>
);
};
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**

View File

@ -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<CustomTextInputProps> = ({
label,
error,
required,
style,
...props
}) => {
const [isFocused, setIsFocused] = useState(false);
return (
<View style={styles.container}>
{label && (
<Text style={styles.label}>
{label}
{required && <Text style={styles.required}> *</Text>}
</Text>
)}
<RNTextInput
style={[
styles.input,
isFocused && styles.focused,
error && styles.error,
style,
]}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
{...props}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
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<CHeaderProps> = ({
title,
showBackButton = true,
onBackPress,
rightComponent,
backgroundColor = '#FFFFFF',
}) => {
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, {paddingTop: insets.top, backgroundColor}]}>
<StatusBar barStyle="dark-content" backgroundColor={backgroundColor} />
<View style={styles.header}>
{showBackButton && (
<TouchableOpacity onPress={onBackPress} style={styles.backButton}>
<Text style={styles.backText}></Text>
</TouchableOpacity>
)}
<Text style={styles.title}>{title}</Text>
<View style={styles.rightContainer}>{rightComponent}</View>
</View>
</View>
);
};
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<AlertModalProps> = ({
visible,
title,
message,
onConfirm,
onCancel,
confirmText = 'OK',
cancelText = 'Cancel',
type = 'info',
}) => {
return (
<Modal visible={visible} transparent animationType="fade">
<View style={styles.overlay}>
<View style={styles.container}>
<Text style={[styles.title, styles[`${type}Title`]]}>{title}</Text>
<Text style={styles.message}>{message}</Text>
<View style={styles.buttonContainer}>
{onCancel && (
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
<Text style={styles.cancelText}>{cancelText}</Text>
</TouchableOpacity>
)}
{onConfirm && (
<TouchableOpacity
style={[styles.confirmButton, styles[`${type}Button`]]}
onPress={onConfirm}>
<Text style={styles.confirmText}>{confirmText}</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
</Modal>
);
};
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<DropdownProps> = ({
items,
selectedValue,
onSelect,
placeholder = 'Select an option',
label,
}) => {
const [isVisible, setIsVisible] = useState(false);
const selectedItem = items.find(item => item.value === selectedValue);
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TouchableOpacity
style={styles.selector}
onPress={() => setIsVisible(true)}>
<Text style={[styles.selectorText, !selectedItem && styles.placeholder]}>
{selectedItem ? selectedItem.label : placeholder}
</Text>
<Text style={styles.arrow}></Text>
</TouchableOpacity>
<Modal visible={isVisible} transparent animationType="fade">
<TouchableOpacity
style={styles.overlay}
onPress={() => setIsVisible(false)}>
<View style={styles.dropdown}>
<FlatList
data={items}
keyExtractor={item => item.value}
renderItem={({item}) => (
<TouchableOpacity
style={styles.item}
onPress={() => {
onSelect(item);
setIsVisible(false);
}}>
<Text style={styles.itemText}>{item.label}</Text>
</TouchableOpacity>
)}
/>
</View>
</TouchableOpacity>
</Modal>
</View>
);
};
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<RadioButtonProps> = ({
options,
selectedValue,
onSelect,
label,
}) => {
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
{options.map(option => (
<TouchableOpacity
key={option.value}
style={styles.option}
onPress={() => onSelect(option.value)}>
<View style={styles.radio}>
{selectedValue === option.value && <View style={styles.selected} />}
</View>
<Text style={styles.optionText}>{option.label}</Text>
</TouchableOpacity>
))}
</View>
);
};
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',
},
});
```

View File

@ -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 (",
" <View style={styles.container}>",
" <Text>$4</Text>",
" </View>",
" );",
"};",
"",
"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 (",
" <SafeAreaView style={styles.container}>",
" <CHeader",
" title=\"${2:Screen Title}\"",
" onBackPress={() => navigation.goBack()}",
" />",
" <View style={styles.content}>",
" <Text>$3</Text>",
" </View>",
" </SafeAreaView>",
" );",
"};",
"",
"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"
}
}
```

View File

@ -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 = () => (
<Image source={Images.logo} style={{width: 100, height: 100}} />
);
```
## 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<IconProps> = ({
name,
size = 24,
color = '#000',
family = 'MaterialIcons',
}) => {
const IconComponent = IconComponents[family];
return <IconComponent name={name} size={size} color={color} />;
};
```
## 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<SvgIconProps> = ({
name,
size = 24,
color = '#000',
}) => {
const path = iconPaths[name as keyof typeof iconPaths];
if (!path) {
console.warn(`Icon "${name}" not found`);
return null;
}
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
<Path d={path} />
</Svg>
);
};
```
## 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<OptimizedImageProps> = ({
showLoader = true,
loaderColor = '#007AFF',
style,
...props
}) => {
const [loading, setLoading] = React.useState(true);
return (
<View style={style}>
<FastImage
{...props}
style={[StyleSheet.absoluteFillObject, style]}
onLoadStart={() => setLoading(true)}
onLoadEnd={() => setLoading(false)}
resizeMode={FastImage.resizeMode.cover}
/>
{loading && showLoader && (
<View style={styles.loader}>
<ActivityIndicator color={loaderColor} />
</View>
)}
</View>
);
};
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<void> => {
const imagePromises = Object.values(Images).map(image => {
return new Promise<void>((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<string | null> {
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<string> {
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();
}
}
```

View File

@ -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<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({children}) => {
const theme = {
colors: DesignTokens.colors,
typography: DesignTokens.typography,
spacing: DesignTokens.spacing,
borderRadius: DesignTokens.borderRadius,
shadows: DesignTokens.shadows,
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};
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';
};
```

View File

@ -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
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/primary_color" />
<item
android:width="200dp"
android:height="200dp"
android:drawable="@mipmap/launch_icon"
android:gravity="center" />
</layer-list>
```
2. Update `android/app/src/main/res/values/styles.xml`:
```xml
<resources>
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/launch_screen</item>
</style>
</resources>
```
**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
<key>UIAppFonts</key>
<array>
<string>Montserrat-Regular.ttf</string>
<string>Montserrat-Bold.ttf</string>
<string>Montserrat-SemiBold.ttf</string>
</array>
```
### 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<CustomIconProps> = ({
name,
size = 24,
color = '#000',
style,
onPress,
}) => {
return (
<IconFont
name={name}
size={size}
color={color}
style={style}
onPress={onPress}
/>
);
};
```
## 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<string> = new Set();
static async preloadIcons(iconNames: IconName[]): Promise<void> {
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<void> {
// 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),
};
```

View File

@ -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<LoginResponse, LoginRequest>({
query: (credentials) => ({
url: '/auth/login',
method: 'POST',
body: credentials,
}),
}),
register: builder.mutation<LoginResponse, RegisterRequest>({
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<void, void>({
query: () => ({
url: '/auth/logout',
method: 'POST',
}),
}),
forgotPassword: builder.mutation<void, { email: string }>({
query: ({ email }) => ({
url: '/auth/forgot-password',
method: 'POST',
body: { email },
}),
}),
verifyOTP: builder.mutation<void, { email: string; otp: string }>({
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<User, void>({
query: () => '/user/profile',
providesTags: ['User'],
}),
updateProfile: builder.mutation<User, UpdateProfileRequest>({
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<void, void>({
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<NGO, string>({
query: (id) => `/ngo/${id}`,
providesTags: ['NGO'],
}),
createNGO: builder.mutation<NGO, CreateNGORequest>({
query: (ngoData) => ({
url: '/ngo',
method: 'POST',
body: ngoData,
}),
invalidatesTags: ['NGO', 'User'],
}),
updateNGO: builder.mutation<
NGO,
{ id: string; updates: Partial<CreateNGORequest> }
>({
query: ({ id, updates }) => ({
url: `/ngo/${id}`,
method: 'PUT',
body: updates,
}),
invalidatesTags: ['NGO'],
}),
getFavoriteNGOs: builder.query<NGO[], void>({
query: () => '/ngo/favorites',
providesTags: ['NGO'],
}),
toggleFavoriteNGO: builder.mutation<void, string>({
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<FundraisingRequest, string>({
query: (id) => `/requests/${id}`,
providesTags: ['Request'],
}),
createRequest: builder.mutation<FundraisingRequest, CreateRequestData>({
query: (requestData) => ({
url: '/requests',
method: 'POST',
body: requestData,
}),
invalidatesTags: ['Request'],
}),
updateRequest: builder.mutation<
FundraisingRequest,
{
id: string;
updates: Partial<CreateRequestData>;
}
>({
query: ({ id, updates }) => ({
url: `/requests/${id}`,
method: 'PUT',
body: updates,
}),
invalidatesTags: ['Request'],
}),
deleteRequest: builder.mutation<void, string>({
query: (id) => ({
url: `/requests/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Request'],
}),
getMyRequests: builder.query<FundraisingRequest[], void>({
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<T> {
data: T | null;
loading: boolean;
error: APIError | null;
}
export const useAPI = <T>() => {
const [state, setState] = useState<UseAPIState<T>>({
data: null,
loading: false,
error: null,
});
const execute = useCallback(async (apiCall: () => Promise<T>) => {
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<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```

View File

@ -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<RootStackParamList>();
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 (
<NavigationContainer>
<Stack.Navigator screenOptions={{headerShown: false}}>
{isLoading ? (
<Stack.Screen name="Splash" component={SplashScreen} />
) : isAuthenticated ? (
<Stack.Screen name="Main" component={MainNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
</NavigationContainer>
);
};
```
## 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<AuthStackParamList>();
const AuthNavigator: React.FC = () => {
return (
<Stack.Navigator
initialRouteName="Login"
screenOptions={{
headerShown: false,
cardStyleInterpolator: ({current, layouts}) => {
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
],
},
};
},
}}>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
<Stack.Screen name="OTPVerification" component={OTPVerificationScreen} />
</Stack.Navigator>
);
};
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<MainTabParamList>();
const MainNavigator: React.FC = () => {
const {colors} = useTheme();
return (
<Tab.Navigator
screenOptions={({route}) => ({
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 <CustomIcon name={iconName} size={size} color={color} />;
},
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',
},
})}>
<Tab.Screen name="Home" component={HomeNavigator} />
<Tab.Screen name="Profile" component={ProfileScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
<Tab.Screen name="Notifications" component={NotificationsScreen} />
</Tab.Navigator>
);
};
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<HomeStackParamList>();
const HomeNavigator: React.FC = () => {
return (
<Stack.Navigator
initialRouteName="HomeScreen"
screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="HomeScreen" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
<Stack.Screen name="Search" component={SearchScreen} />
</Stack.Navigator>
);
};
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<RootStackParamList>;
export const useNavigation = () => {
return useRNNavigation<NavigationProp>();
};
```
## Deep Linking
```typescript
// src/navigation/LinkingConfiguration.ts
import {LinkingOptions} from '@react-navigation/native';
import {RootStackParamList} from './types';
const linking: LinkingOptions<RootStackParamList> = {
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 (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<NavigationContainer ref={navigationRef} linking={linking}>
<RootNavigator />
</NavigationContainer>
</PersistGate>
</Provider>
);
};
export default App;
```

View File

@ -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<typeof store.getState>;
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<string>) => {
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<Partial<User>>) => {
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<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = 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 (
<Provider store={store}>
<PersistGate loading={<LoadingScreen />} persistor={persistor}>
<AppNavigator />
</PersistGate>
</Provider>
);
};
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}` : ''
);
```

View File

@ -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
```

View File

@ -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(
<Button title="Test Button" onPress={() => {}} />
);
expect(getByText('Test Button')).toBeTruthy();
});
it('calls onPress when pressed', () => {
const mockOnPress = jest.fn();
const {getByText} = render(
<Button title="Test Button" onPress={mockOnPress} />
);
fireEvent.press(getByText('Test Button'));
expect(mockOnPress).toHaveBeenCalledTimes(1);
});
it('applies correct styles for different variants', () => {
const {getByText, rerender} = render(
<Button title="Primary" onPress={() => {}} variant="primary" />
);
const button = getByText('Primary').parent;
expect(button).toHaveStyle({backgroundColor: '#007AFF'});
rerender(
<Button title="Secondary" onPress={() => {}} variant="secondary" />
);
const secondaryButton = getByText('Secondary').parent;
expect(secondaryButton).toHaveStyle({backgroundColor: '#6C757D'});
});
it('is disabled when disabled prop is true', () => {
const mockOnPress = jest.fn();
const {getByText} = render(
<Button title="Disabled" onPress={mockOnPress} disabled />
);
const button = getByText('Disabled').parent;
expect(button).toHaveStyle({opacity: 0.5});
fireEvent.press(getByText('Disabled'));
expect(mockOnPress).not.toHaveBeenCalled();
});
});
```
## Screen Testing
```typescript
// src/screens/Login/__tests__/LoginScreen.test.tsx
import React from 'react';
import {render, fireEvent, waitFor} from '@testing-library/react-native';
import {Provider} from 'react-redux';
import {configureStore} from '@reduxjs/toolkit';
import LoginScreen from '../LoginScreen';
import authSlice from '@redux/slices/authSlice';
const createMockStore = (initialState = {}) => {
return configureStore({
reducer: {
auth: authSlice,
},
preloadedState: initialState,
});
};
const renderWithProvider = (component: React.ReactElement, store = createMockStore()) => {
return render(
<Provider store={store}>
{component}
</Provider>
);
};
describe('LoginScreen', () => {
it('renders login form correctly', () => {
const {getByPlaceholderText, getByText} = renderWithProvider(
<LoginScreen navigation={{} as any} route={{} as any} />
);
expect(getByPlaceholderText('Email')).toBeTruthy();
expect(getByPlaceholderText('Password')).toBeTruthy();
expect(getByText('Login')).toBeTruthy();
});
it('shows validation errors for empty fields', async () => {
const {getByText, getByTestId} = renderWithProvider(
<LoginScreen navigation={{} as any} route={{} as any} />
);
fireEvent.press(getByText('Login'));
await waitFor(() => {
expect(getByText('Email is required')).toBeTruthy();
expect(getByText('Password is required')).toBeTruthy();
});
});
it('calls login API with correct credentials', async () => {
const mockLogin = jest.fn();
const {getByPlaceholderText, getByText} = renderWithProvider(
<LoginScreen navigation={{} as any} route={{} as any} />
);
fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com');
fireEvent.changeText(getByPlaceholderText('Password'), 'password123');
fireEvent.press(getByText('Login'));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
});
```
## Redux Testing
```typescript
// src/redux/slices/__tests__/authSlice.test.ts
import authSlice, {
loginStart,
loginSuccess,
loginFailure,
logout,
updateUser,
} from '../authSlice';
describe('authSlice', () => {
const initialState = {
isAuthenticated: false,
user: null,
token: null,
refreshToken: null,
loading: false,
error: null,
};
it('should return the initial state', () => {
expect(authSlice(undefined, {type: 'unknown'})).toEqual(initialState);
});
it('should handle loginStart', () => {
const actual = authSlice(initialState, loginStart());
expect(actual.loading).toBe(true);
expect(actual.error).toBe(null);
});
it('should handle loginSuccess', () => {
const payload = {
user: {
id: '1',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
},
token: 'access-token',
refreshToken: 'refresh-token',
};
const actual = authSlice(initialState, loginSuccess(payload));
expect(actual.loading).toBe(false);
expect(actual.isAuthenticated).toBe(true);
expect(actual.user).toEqual(payload.user);
expect(actual.token).toBe(payload.token);
expect(actual.refreshToken).toBe(payload.refreshToken);
});
it('should handle loginFailure', () => {
const errorMessage = 'Invalid credentials';
const actual = authSlice(initialState, loginFailure(errorMessage));
expect(actual.loading).toBe(false);
expect(actual.isAuthenticated).toBe(false);
expect(actual.error).toBe(errorMessage);
});
it('should handle logout', () => {
const loggedInState = {
...initialState,
isAuthenticated: true,
user: {id: '1', email: 'test@example.com', firstName: 'John', lastName: 'Doe'},
token: 'token',
};
const actual = authSlice(loggedInState, logout());
expect(actual).toEqual(initialState);
});
});
```
## API Testing
```typescript
// src/services/__tests__/authAPI.test.ts
import {authAPI} from '../authAPI';
import {store} from '@redux/store';
describe('authAPI', () => {
beforeEach(() => {
// Reset store state
store.dispatch({type: 'RESET'});
});
it('should login successfully', async () => {
const credentials = {
email: 'test@example.com',
password: 'password123',
};
const mockResponse = {
user: {
id: '1',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
},
token: 'access-token',
refreshToken: 'refresh-token',
};
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockResponse),
})
) as jest.Mock;
const result = await store.dispatch(
authAPI.endpoints.login.initiate(credentials)
);
expect(result.data).toEqual(mockResponse);
});
it('should handle login error', async () => {
const credentials = {
email: 'test@example.com',
password: 'wrongpassword',
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
status: 401,
json: () => Promise.resolve({message: 'Invalid credentials'}),
})
) as jest.Mock;
const result = await store.dispatch(
authAPI.endpoints.login.initiate(credentials)
);
expect(result.error).toBeDefined();
});
});
```
## Custom Hooks Testing
```typescript
// src/hooks/__tests__/useAPI.test.ts
import {renderHook, act} from '@testing-library/react-hooks';
import {useAPI} from '../useAPI';
describe('useAPI', () => {
it('should initialize with correct default state', () => {
const {result} = renderHook(() => useAPI());
expect(result.current.data).toBe(null);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle successful API call', async () => {
const {result} = renderHook(() => useAPI());
const mockData = {id: 1, name: 'Test'};
const mockApiCall = jest.fn(() => Promise.resolve(mockData));
await act(async () => {
await result.current.execute(mockApiCall);
});
expect(result.current.data).toEqual(mockData);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle API call error', async () => {
const {result} = renderHook(() => useAPI());
const mockError = new Error('API Error');
const mockApiCall = jest.fn(() => Promise.reject(mockError));
await act(async () => {
try {
await result.current.execute(mockApiCall);
} catch (error) {
// Expected to throw
}
});
expect(result.current.data).toBe(null);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeDefined();
});
});
```
## Test Utilities
```typescript
// src/__tests__/utils/testUtils.tsx
import React from 'react';
import {render, RenderOptions} from '@testing-library/react-native';
import {Provider} from 'react-redux';
import {NavigationContainer} from '@react-navigation/native';
import {configureStore} from '@reduxjs/toolkit';
import authSlice from '@redux/slices/authSlice';
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
preloadedState?: any;
store?: any;
}
const createTestStore = (preloadedState = {}) => {
return configureStore({
reducer: {
auth: authSlice,
},
preloadedState,
});
};
const AllTheProviders: React.FC<{children: React.ReactNode; store: any}> = ({
children,
store,
}) => {
return (
<Provider store={store}>
<NavigationContainer>
{children}
</NavigationContainer>
</Provider>
);
};
const customRender = (
ui: React.ReactElement,
{
preloadedState = {},
store = createTestStore(preloadedState),
...renderOptions
}: CustomRenderOptions = {}
) => {
const Wrapper: React.FC<{children: React.ReactNode}> = ({children}) => (
<AllTheProviders store={store}>{children}</AllTheProviders>
);
return render(ui, {wrapper: Wrapper, ...renderOptions});
};
export * from '@testing-library/react-native';
export {customRender as render};
```

View File

@ -0,0 +1,477 @@
# Performance Optimization
## Hermes Setup
### Enable Hermes
**Android (android/app/build.gradle):**
```gradle
project.ext.react = [
enableHermes: true
]
```
**iOS (Podfile):**
```ruby
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true
)
```
### Hermes Benefits
- Faster app startup time
- Reduced memory usage
- Smaller bundle size
- Better performance on lower-end devices
## List Optimization
### Optimized FlatList
```typescript
// src/components/OptimizedList/OptimizedList.tsx
import React, {useCallback, useMemo} from 'react';
import {FlatList, ListRenderItem, ViewToken} from 'react-native';
interface OptimizedListProps<T> {
data: T[];
renderItem: ListRenderItem<T>;
keyExtractor: (item: T, index: number) => string;
itemHeight?: number;
onEndReached?: () => void;
onViewableItemsChanged?: (info: {viewableItems: ViewToken[]}) => void;
}
export const OptimizedList = <T,>({
data,
renderItem,
keyExtractor,
itemHeight = 100,
onEndReached,
onViewableItemsChanged,
}: OptimizedListProps<T>) => {
const memoizedRenderItem = useCallback(renderItem, []);
const memoizedKeyExtractor = useCallback(keyExtractor, []);
const getItemLayout = useMemo(
() =>
itemHeight
? (data: any, index: number) => ({
length: itemHeight,
offset: itemHeight * index,
index,
})
: undefined,
[itemHeight]
);
const viewabilityConfig = useMemo(
() => ({
itemVisiblePercentThreshold: 50,
minimumViewTime: 300,
}),
[]
);
return (
<FlatList
data={data}
renderItem={memoizedRenderItem}
keyExtractor={memoizedKeyExtractor}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
/>
);
};
```
### Virtualized List for Large Datasets
```typescript
// src/components/VirtualizedList/VirtualizedList.tsx
import React from 'react';
import {VirtualizedList, ListRenderItem} from 'react-native';
interface VirtualizedListProps<T> {
data: T[];
renderItem: ListRenderItem<T>;
keyExtractor: (item: T, index: number) => string;
itemHeight: number;
}
export const CustomVirtualizedList = <T,>({
data,
renderItem,
keyExtractor,
itemHeight,
}: VirtualizedListProps<T>) => {
const getItem = (data: T[], index: number) => data[index];
const getItemCount = (data: T[]) => data.length;
return (
<VirtualizedList
data={data}
initialNumToRender={4}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemCount={getItemCount}
getItem={getItem}
getItemLayout={(data, index) => ({
length: itemHeight,
offset: itemHeight * index,
index,
})}
/>
);
};
```
## Image Optimization
### Lazy Loading Images
```typescript
// src/components/LazyImage/LazyImage.tsx
import React, {useState, useRef} from 'react';
import {View, Image, Animated, StyleSheet} from 'react-native';
import FastImage from 'react-native-fast-image';
interface LazyImageProps {
source: {uri: string};
style?: any;
placeholder?: React.ReactNode;
}
export const LazyImage: React.FC<LazyImageProps> = ({
source,
style,
placeholder,
}) => {
const [loaded, setLoaded] = useState(false);
const opacity = useRef(new Animated.Value(0)).current;
const onLoad = () => {
setLoaded(true);
Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
};
return (
<View style={style}>
{!loaded && placeholder}
<Animated.View style={[StyleSheet.absoluteFill, {opacity}]}>
<FastImage
source={source}
style={StyleSheet.absoluteFill}
onLoad={onLoad}
resizeMode={FastImage.resizeMode.cover}
/>
</Animated.View>
</View>
);
};
```
## Memory Management
### Component Memoization
```typescript
// src/components/MemoizedComponent/MemoizedComponent.tsx
import React, {memo, useMemo, useCallback} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
interface ItemProps {
id: string;
title: string;
description: string;
onPress: (id: string) => void;
}
const ItemComponent: React.FC<ItemProps> = memo(({
id,
title,
description,
onPress,
}) => {
const handlePress = useCallback(() => {
onPress(id);
}, [id, onPress]);
const formattedDescription = useMemo(() => {
return description.length > 100
? `${description.substring(0, 100)}...`
: description;
}, [description]);
return (
<TouchableOpacity onPress={handlePress}>
<View>
<Text>{title}</Text>
<Text>{formattedDescription}</Text>
</View>
</TouchableOpacity>
);
});
export default ItemComponent;
```
### Custom Hooks Optimization
```typescript
// src/hooks/useOptimizedData.ts
import {useMemo, useCallback} from 'react';
import {useAppSelector} from '@redux/hooks';
export const useOptimizedData = (filter?: string) => {
const rawData = useAppSelector(state => state.data.items);
const filteredData = useMemo(() => {
if (!filter) return rawData;
return rawData.filter(item =>
item.title.toLowerCase().includes(filter.toLowerCase())
);
}, [rawData, filter]);
const processItem = useCallback((item: any) => {
return {
...item,
displayTitle: item.title.toUpperCase(),
isNew: Date.now() - item.createdAt < 86400000, // 24 hours
};
}, []);
const processedData = useMemo(() =>
filteredData.map(processItem),
[filteredData, processItem]
);
return processedData;
};
```
## Bundle Optimization
### Code Splitting
```typescript
// src/utils/lazyImports.ts
import {lazy} from 'react';
// Lazy load heavy screens
export const ProfileScreen = lazy(() => import('@screens/Profile/ProfileScreen'));
export const SettingsScreen = lazy(() => import('@screens/Settings/SettingsScreen'));
export const ReportsScreen = lazy(() => import('@screens/Reports/ReportsScreen'));
// Lazy load heavy components
export const ChartComponent = lazy(() => import('@components/Chart/ChartComponent'));
export const MapComponent = lazy(() => import('@components/Map/MapComponent'));
```
### Dynamic Imports
```typescript
// src/utils/dynamicImports.ts
export const loadChartLibrary = async () => {
const {Chart} = await import('react-native-chart-kit');
return Chart;
};
export const loadMapLibrary = async () => {
const MapView = await import('react-native-maps');
return MapView.default;
};
// Usage in component
const MyComponent = () => {
const [ChartComponent, setChartComponent] = useState(null);
useEffect(() => {
loadChartLibrary().then(setChartComponent);
}, []);
return ChartComponent ? <ChartComponent /> : <LoadingSpinner />;
};
```
## Animation Optimization
### Native Driver Usage
```typescript
// src/animations/optimizedAnimations.ts
import {Animated, Easing} from 'react-native';
export const createOptimizedAnimation = (
animatedValue: Animated.Value,
toValue: number,
duration: number = 300
) => {
return Animated.timing(animatedValue, {
toValue,
duration,
easing: Easing.out(Easing.quad),
useNativeDriver: true, // Use native driver for better performance
});
};
export const createSpringAnimation = (
animatedValue: Animated.Value,
toValue: number
) => {
return Animated.spring(animatedValue, {
toValue,
tension: 100,
friction: 8,
useNativeDriver: true,
});
};
```
## Network Optimization
### Request Batching
```typescript
// src/utils/requestBatcher.ts
class RequestBatcher {
private queue: Array<{
url: string;
options: RequestInit;
resolve: (value: any) => void;
reject: (reason: any) => void;
}> = [];
private batchTimeout: NodeJS.Timeout | null = null;
public request(url: string, options: RequestInit = {}): Promise<any> {
return new Promise((resolve, reject) => {
this.queue.push({url, options, resolve, reject});
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
}
this.batchTimeout = setTimeout(() => {
this.processBatch();
}, 50); // Batch requests within 50ms
});
}
private async processBatch() {
const batch = [...this.queue];
this.queue = [];
this.batchTimeout = null;
// Group similar requests
const grouped = batch.reduce((acc, req) => {
const key = `${req.options.method || 'GET'}-${req.url.split('?')[0]}`;
if (!acc[key]) acc[key] = [];
acc[key].push(req);
return acc;
}, {} as Record<string, typeof batch>);
// Process each group
for (const requests of Object.values(grouped)) {
try {
const results = await Promise.all(
requests.map(req => fetch(req.url, req.options))
);
results.forEach((result, index) => {
requests[index].resolve(result);
});
} catch (error) {
requests.forEach(req => req.reject(error));
}
}
}
}
export const requestBatcher = new RequestBatcher();
```
## Performance Monitoring
### Performance Metrics
```typescript
// src/utils/performanceMonitor.ts
class PerformanceMonitor {
private metrics: Map<string, number> = new Map();
startTiming(label: string) {
this.metrics.set(label, Date.now());
}
endTiming(label: string): number {
const startTime = this.metrics.get(label);
if (!startTime) {
console.warn(`No start time found for ${label}`);
return 0;
}
const duration = Date.now() - startTime;
this.metrics.delete(label);
console.log(`${label}: ${duration}ms`);
return duration;
}
measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
this.startTiming(label);
return fn().finally(() => {
this.endTiming(label);
});
}
measureSync<T>(label: string, fn: () => T): T {
this.startTiming(label);
try {
return fn();
} finally {
this.endTiming(label);
}
}
}
export const performanceMonitor = new PerformanceMonitor();
// Usage
performanceMonitor.measureAsync('API_CALL', () =>
fetch('/api/data').then(res => res.json())
);
```
### Memory Usage Tracking
```typescript
// src/utils/memoryTracker.ts
export const trackMemoryUsage = () => {
if (__DEV__) {
const memoryInfo = (performance as any).memory;
if (memoryInfo) {
console.log('Memory Usage:', {
used: Math.round(memoryInfo.usedJSHeapSize / 1048576) + ' MB',
total: Math.round(memoryInfo.totalJSHeapSize / 1048576) + ' MB',
limit: Math.round(memoryInfo.jsHeapSizeLimit / 1048576) + ' MB',
});
}
}
};
// Track memory usage periodically
setInterval(trackMemoryUsage, 30000); // Every 30 seconds in development
```

View File

@ -0,0 +1,471 @@
# Security
## Secure Storage
### Installation
```bash
yarn add react-native-keychain
```
### Secure Storage Implementation
```typescript
// src/utils/secureStorage.ts
import * as Keychain from 'react-native-keychain';
export class SecureStorage {
static async setItem(key: string, value: string): Promise<void> {
try {
await Keychain.setInternetCredentials(key, key, value);
} catch (error) {
console.error('Error storing secure item:', error);
throw error;
}
}
static async getItem(key: string): Promise<string | null> {
try {
const credentials = await Keychain.getInternetCredentials(key);
return credentials ? credentials.password : null;
} catch (error) {
console.error('Error retrieving secure item:', error);
return null;
}
}
static async removeItem(key: string): Promise<void> {
try {
await Keychain.resetInternetCredentials(key);
} catch (error) {
console.error('Error removing secure item:', error);
throw error;
}
}
static async clear(): Promise<void> {
try {
await Keychain.resetGenericPassword();
} catch (error) {
console.error('Error clearing secure storage:', error);
throw error;
}
}
// Biometric authentication
static async setItemWithBiometrics(key: string, value: string): Promise<void> {
try {
await Keychain.setInternetCredentials(key, key, value, {
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS,
});
} catch (error) {
console.error('Error storing item with biometrics:', error);
throw error;
}
}
static async getItemWithBiometrics(key: string): Promise<string | null> {
try {
const credentials = await Keychain.getInternetCredentials(key, {
authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS,
showModal: true,
kLocalizedFallbackTitle: 'Please use your passcode',
});
return credentials ? credentials.password : null;
} catch (error) {
console.error('Error retrieving item with biometrics:', error);
return null;
}
}
}
```
## Data Encryption
### Encryption Utilities
```typescript
// src/utils/encryption.ts
import CryptoJS from 'crypto-js';
export class EncryptionService {
private static readonly SECRET_KEY = 'your-secret-key-here'; // Should be from secure config
static encrypt(text: string): string {
try {
return CryptoJS.AES.encrypt(text, this.SECRET_KEY).toString();
} catch (error) {
console.error('Encryption error:', error);
throw new Error('Failed to encrypt data');
}
}
static decrypt(encryptedText: string): string {
try {
const bytes = CryptoJS.AES.decrypt(encryptedText, this.SECRET_KEY);
return bytes.toString(CryptoJS.enc.Utf8);
} catch (error) {
console.error('Decryption error:', error);
throw new Error('Failed to decrypt data');
}
}
static hash(text: string): string {
return CryptoJS.SHA256(text).toString();
}
static generateSalt(): string {
return CryptoJS.lib.WordArray.random(128/8).toString();
}
static hashWithSalt(text: string, salt: string): string {
return CryptoJS.SHA256(text + salt).toString();
}
}
```
## API Security
### Request Interceptors
```typescript
// src/services/secureAPI.ts
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
import {SecureStorage} from '@utils/secureStorage';
import {EncryptionService} from '@utils/encryption';
const secureAPI = axios.create({
baseURL: 'https://api.saayam.com',
timeout: 10000,
});
// Request interceptor for authentication
secureAPI.interceptors.request.use(
async (config: AxiosRequestConfig) => {
// Add authentication token
const token = await SecureStorage.getItem('auth_token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
// Add request signature
const timestamp = Date.now().toString();
const signature = EncryptionService.hash(
`${config.method}${config.url}${timestamp}`
);
config.headers = {
...config.headers,
'X-Timestamp': timestamp,
'X-Signature': signature,
};
// Encrypt sensitive data
if (config.data && config.method === 'post') {
config.data = {
...config.data,
encrypted: true,
payload: EncryptionService.encrypt(JSON.stringify(config.data)),
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for decryption
secureAPI.interceptors.response.use(
(response: AxiosResponse) => {
// Decrypt response if needed
if (response.data?.encrypted) {
try {
response.data = JSON.parse(
EncryptionService.decrypt(response.data.payload)
);
} catch (error) {
console.error('Failed to decrypt response:', error);
}
}
return response;
},
async (error) => {
// Handle token refresh
if (error.response?.status === 401) {
try {
const refreshToken = await SecureStorage.getItem('refresh_token');
if (refreshToken) {
const response = await axios.post('/auth/refresh', {
refreshToken,
});
const newToken = response.data.token;
await SecureStorage.setItem('auth_token', newToken);
// Retry original request
error.config.headers.Authorization = `Bearer ${newToken}`;
return secureAPI.request(error.config);
}
} catch (refreshError) {
// Redirect to login
await SecureStorage.clear();
// Navigate to login screen
}
}
return Promise.reject(error);
}
);
export default secureAPI;
```
## Input Validation
### Validation Schemas
```typescript
// src/utils/validation.ts
import * as yup from 'yup';
export const validationSchemas = {
email: yup
.string()
.email('Invalid email format')
.required('Email is required')
.matches(
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
'Invalid email format'
),
password: yup
.string()
.required('Password is required')
.min(8, 'Password must be at least 8 characters')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain uppercase, lowercase, number and special character'
),
phone: yup
.string()
.required('Phone number is required')
.matches(
/^\+?[1-9]\d{1,14}$/,
'Invalid phone number format'
),
amount: yup
.number()
.required('Amount is required')
.positive('Amount must be positive')
.max(1000000, 'Amount cannot exceed 1,000,000'),
};
export const sanitizeInput = (input: string): string => {
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/[<>]/g, '')
.trim();
};
export const validateAndSanitize = (
value: string,
schema: yup.StringSchema
): {isValid: boolean; sanitized: string; error?: string} => {
const sanitized = sanitizeInput(value);
try {
schema.validateSync(sanitized);
return {isValid: true, sanitized};
} catch (error) {
return {
isValid: false,
sanitized,
error: error instanceof Error ? error.message : 'Validation failed',
};
}
};
```
## Biometric Authentication
### Biometric Setup
```typescript
// src/utils/biometricAuth.ts
import TouchID from 'react-native-touch-id';
import {Alert, Platform} from 'react-native';
export class BiometricAuth {
static async isSupported(): Promise<boolean> {
try {
const biometryType = await TouchID.isSupported();
return !!biometryType;
} catch (error) {
return false;
}
}
static async getSupportedType(): Promise<string | null> {
try {
const biometryType = await TouchID.isSupported();
return biometryType;
} catch (error) {
return null;
}
}
static async authenticate(reason: string = 'Authenticate to continue'): Promise<boolean> {
try {
const optionalConfigObject = {
title: 'Authentication Required',
subtitle: reason,
description: 'This app uses biometric authentication to protect your data',
fallbackLabel: 'Use Passcode',
cancelLabel: 'Cancel',
disableDeviceFallback: false,
showModal: true,
kLocalizedFallbackTitle: 'Use Passcode',
};
await TouchID.authenticate(reason, optionalConfigObject);
return true;
} catch (error: any) {
console.error('Biometric authentication failed:', error);
if (error.name === 'LAErrorUserFallback') {
// User chose to use passcode
return this.authenticateWithPasscode();
}
return false;
}
}
private static async authenticateWithPasscode(): Promise<boolean> {
return new Promise((resolve) => {
Alert.prompt(
'Enter Passcode',
'Please enter your device passcode',
[
{text: 'Cancel', onPress: () => resolve(false)},
{
text: 'OK',
onPress: (passcode) => {
// In a real app, you would validate the passcode
resolve(!!passcode);
},
},
],
'secure-text'
);
});
}
}
```
## Certificate Pinning
### SSL Pinning Implementation
```typescript
// src/utils/certificatePinning.ts
import {NetworkingModule} from 'react-native';
export class CertificatePinning {
private static readonly PINNED_CERTIFICATES = [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Your server's certificate hash
];
static setupPinning() {
if (Platform.OS === 'ios') {
// iOS certificate pinning setup
NetworkingModule.addRequestInterceptor((request: any) => {
request.trusty = {
hosts: [
{
host: 'api.saayam.com',
certificates: this.PINNED_CERTIFICATES,
},
],
};
return request;
});
}
}
static validateCertificate(hostname: string, certificate: string): boolean {
return this.PINNED_CERTIFICATES.includes(certificate);
}
}
```
## Security Headers
### Request Security Headers
```typescript
// src/utils/securityHeaders.ts
export const getSecurityHeaders = () => ({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'",
'Referrer-Policy': 'strict-origin-when-cross-origin',
});
```
## Security Best Practices
### Security Checklist
```typescript
// src/utils/securityAudit.ts
export class SecurityAudit {
static performSecurityCheck(): {passed: boolean; issues: string[]} {
const issues: string[] = [];
// Check for debug mode in production
if (__DEV__ && process.env.NODE_ENV === 'production') {
issues.push('Debug mode is enabled in production');
}
// Check for console logs in production
if (process.env.NODE_ENV === 'production' && console.log.toString().includes('native code')) {
issues.push('Console logs are not disabled in production');
}
// Check for secure storage usage
if (!this.isUsingSecureStorage()) {
issues.push('Sensitive data is not stored securely');
}
// Check for HTTPS usage
if (!this.isUsingHTTPS()) {
issues.push('API calls are not using HTTPS');
}
return {
passed: issues.length === 0,
issues,
};
}
private static isUsingSecureStorage(): boolean {
// Check if secure storage is properly implemented
return true; // Implement actual check
}
private static isUsingHTTPS(): boolean {
// Check if all API endpoints use HTTPS
return true; // Implement actual check
}
}
```

View File

@ -0,0 +1,542 @@
# CI/CD Pipeline
## GitHub Actions Setup
### Basic Workflow
**.github/workflows/ci.yml:**
```yaml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run linter
run: yarn lint
- name: Run tests
run: yarn test --coverage
- name: Type check
run: yarn type-check
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build-android:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Build Android APK
run: |
cd android
./gradlew assembleRelease
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: android-apk
path: android/app/build/outputs/apk/release/app-release.apk
build-ios:
needs: test
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Build iOS
run: |
cd ios
xcodebuild -workspace SaayamApp.xcworkspace \
-scheme SaayamApp \
-configuration Release \
-destination generic/platform=iOS \
-archivePath $PWD/build/SaayamApp.xcarchive \
archive
```
### Advanced Workflow with Fastlane
**.github/workflows/deploy.yml:**
```yaml
name: Deploy to Stores
on:
push:
tags:
- 'v*'
jobs:
deploy-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Decode Keystore
env:
ENCODED_STRING: ${{ secrets.KEYSTORE_BASE64 }}
run: |
echo $ENCODED_STRING | base64 -d > android/app/keystore.jks
- name: Deploy to Play Store
env:
SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
cd android
bundle exec fastlane deploy
deploy-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Deploy to App Store
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }}
run: |
cd ios
bundle exec fastlane deploy
```
## Fastlane Setup
### Installation
```bash
# Install Fastlane
sudo gem install fastlane
# Initialize Fastlane for iOS
cd ios && fastlane init
# Initialize Fastlane for Android
cd android && fastlane init
```
### iOS Fastfile
**ios/fastlane/Fastfile:**
```ruby
default_platform(:ios)
platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
setup_ci if ENV['CI']
match(
type: "appstore",
readonly: true
)
increment_build_number(
xcodeproj: "SaayamApp.xcodeproj"
)
build_app(
scheme: "SaayamApp",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.saayam.app" => "match AppStore com.saayam.app"
}
}
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
slack(
message: "Successfully uploaded a new build to TestFlight! 🚀",
channel: "#releases"
)
end
desc "Deploy to App Store"
lane :deploy do
setup_ci if ENV['CI']
match(
type: "appstore",
readonly: true
)
build_app(
scheme: "SaayamApp",
export_method: "app-store"
)
upload_to_app_store(
force: true,
reject_if_possible: true,
skip_metadata: false,
skip_screenshots: false,
submit_for_review: true,
automatic_release: false,
submission_information: {
add_id_info_limits_tracking: true,
add_id_info_serves_ads: false,
add_id_info_tracks_action: true,
add_id_info_tracks_install: true,
add_id_info_uses_idfa: true,
content_rights_has_rights: true,
content_rights_contains_third_party_content: true,
export_compliance_platform: 'ios',
export_compliance_compliance_required: false,
export_compliance_encryption_updated: false,
export_compliance_app_type: nil,
export_compliance_uses_encryption: false,
export_compliance_is_exempt: false,
export_compliance_contains_third_party_cryptography: false,
export_compliance_contains_proprietary_cryptography: false,
export_compliance_available_on_french_store: false
}
)
slack(
message: "Successfully deployed to App Store! 🎉",
channel: "#releases"
)
end
desc "Create screenshots"
lane :screenshots do
capture_screenshots
upload_to_app_store(skip_binary_upload: true)
end
error do |lane, exception|
slack(
message: "Error in #{lane}: #{exception.message}",
success: false,
channel: "#releases"
)
end
end
```
### Android Fastfile
**android/fastlane/Fastfile:**
```ruby
default_platform(:android)
platform :android do
desc "Build and upload to Play Store Internal Testing"
lane :beta do
gradle(
task: "clean bundleRelease",
project_dir: "."
)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
slack(
message: "Successfully uploaded a new build to Play Store Internal Testing! 🚀",
channel: "#releases"
)
end
desc "Deploy to Play Store"
lane :deploy do
gradle(
task: "clean bundleRelease",
project_dir: "."
)
upload_to_play_store(
track: 'production',
release_status: 'completed',
skip_upload_metadata: false,
skip_upload_changelogs: false,
skip_upload_images: false,
skip_upload_screenshots: false
)
slack(
message: "Successfully deployed to Play Store! 🎉",
channel: "#releases"
)
end
desc "Create screenshots"
lane :screenshots do
capture_android_screenshots
upload_to_play_store(skip_upload_apk: true)
end
error do |lane, exception|
slack(
message: "Error in #{lane}: #{exception.message}",
success: false,
channel: "#releases"
)
end
end
```
## Environment Configuration
### Secrets Management
**GitHub Secrets to configure:**
- `KEYSTORE_BASE64`: Base64 encoded Android keystore
- `KEYSTORE_PASSWORD`: Android keystore password
- `KEY_ALIAS`: Android key alias
- `KEY_PASSWORD`: Android key password
- `GOOGLE_PLAY_SERVICE_ACCOUNT`: Google Play service account JSON
- `MATCH_PASSWORD`: iOS certificates password
- `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`: Apple app-specific password
- `FASTLANE_SESSION`: Fastlane session for Apple ID
- `SLACK_URL`: Slack webhook URL for notifications
### Match Setup (iOS)
```bash
# Initialize match
fastlane match init
# Generate certificates
fastlane match development
fastlane match appstore
```
**Matchfile:**
```ruby
git_url("https://github.com/your-org/certificates")
storage_mode("git")
type("development")
app_identifier(["com.saayam.app"])
username("your-apple-id@example.com")
```
## Branch Protection
### GitHub Branch Protection Rules
```yaml
# .github/branch-protection.yml
protection_rules:
main:
required_status_checks:
strict: true
contexts:
- "test"
- "build-android"
- "build-ios"
enforce_admins: true
required_pull_request_reviews:
required_approving_review_count: 2
dismiss_stale_reviews: true
require_code_owner_reviews: true
restrictions:
users: []
teams: ["core-team"]
```
## Automated Testing in CI
### E2E Testing in CI
**.github/workflows/e2e.yml:**
```yaml
name: E2E Tests
on:
pull_request:
branches: [main]
jobs:
e2e-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Build for Detox
run: detox build --configuration ios.sim.release
- name: Run Detox tests
run: detox test --configuration ios.sim.release --cleanup
e2e-android:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-29
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run Detox tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: |
detox build --configuration android.emu.release
detox test --configuration android.emu.release --cleanup
```

View File

@ -0,0 +1,404 @@
# Android Release Process
## Keystore Generation
### Create Release Keystore
```bash
# Generate a new keystore
keytool -genkeypair -v -storetype PKCS12 -keystore saayam-release-key.keystore -alias saayam-key-alias -keyalg RSA -keysize 2048 -validity 10000
# Verify keystore
keytool -list -v -keystore saayam-release-key.keystore
```
### Keystore Security
```bash
# Store keystore securely
mkdir -p ~/.android/keystores
mv saayam-release-key.keystore ~/.android/keystores/
# Set proper permissions
chmod 600 ~/.android/keystores/saayam-release-key.keystore
```
## Gradle Configuration
### Configure Signing
**android/gradle.properties:**
```properties
# Keystore configuration
SAAYAM_UPLOAD_STORE_FILE=saayam-release-key.keystore
SAAYAM_UPLOAD_KEY_ALIAS=saayam-key-alias
SAAYAM_UPLOAD_STORE_PASSWORD=your_keystore_password
SAAYAM_UPLOAD_KEY_PASSWORD=your_key_password
# Build optimization
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.daemon=true
# Android configuration
android.useAndroidX=true
android.enableJetifier=true
android.enableR8.fullMode=true
```
**android/app/build.gradle:**
```gradle
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.saayam.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
multiDexEnabled true
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
release {
if (project.hasProperty('SAAYAM_UPLOAD_STORE_FILE')) {
storeFile file(SAAYAM_UPLOAD_STORE_FILE)
storePassword SAAYAM_UPLOAD_STORE_PASSWORD
keyAlias SAAYAM_UPLOAD_KEY_ALIAS
keyPassword SAAYAM_UPLOAD_KEY_PASSWORD
}
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
applicationIdSuffix ".debug"
debuggable true
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
debuggable false
zipAlignEnabled true
}
}
splits {
abi {
reset()
enable true
universalApk false
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
}
```
## ProGuard Configuration
**android/app/proguard-rules.pro:**
```proguard
# React Native
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.** { *; }
-keep class com.facebook.jni.** { *; }
# Keep our application class
-keep class com.saayam.** { *; }
# Keep native methods
-keepclassmembers class * {
native <methods>;
}
# Keep enums
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Keep Parcelable implementations
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# Keep serializable classes
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# Firebase
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
# Retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
# Gson
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
```
## Build Process
### Build Commands
```bash
# Clean build
cd android && ./gradlew clean
# Build debug APK
./gradlew assembleDebug
# Build release APK
./gradlew assembleRelease
# Build release AAB (recommended for Play Store)
./gradlew bundleRelease
# Install debug APK
./gradlew installDebug
# Install release APK
./gradlew installRelease
```
### Build Variants
```gradle
android {
flavorDimensions "version"
productFlavors {
development {
dimension "version"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
buildConfigField "String", "API_BASE_URL", '"https://dev-api.saayam.com"'
resValue "string", "app_name", "Saayam Dev"
}
staging {
dimension "version"
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField "String", "API_BASE_URL", '"https://staging-api.saayam.com"'
resValue "string", "app_name", "Saayam Staging"
}
production {
dimension "version"
buildConfigField "String", "API_BASE_URL", '"https://api.saayam.com"'
resValue "string", "app_name", "Saayam"
}
}
}
```
## Google Play Console Setup
### Create Application
1. **Go to Google Play Console**
2. **Create Application**
- App name: Saayam
- Default language: English
- App or game: App
- Free or paid: Free
3. **App Content**
- Privacy Policy URL
- App category
- Content rating questionnaire
- Target audience
- Data safety form
### Upload Release
```bash
# Build release AAB
cd android && ./gradlew bundleRelease
# AAB file location
# android/app/build/outputs/bundle/release/app-release.aab
```
### Release Tracks
1. **Internal Testing**
- Upload AAB
- Add internal testers
- Release to internal testing
2. **Closed Testing**
- Create closed testing track
- Add test users via email lists
- Release to closed testing
3. **Open Testing**
- Create open testing track
- Set country availability
- Release to open testing
4. **Production**
- Review release
- Set rollout percentage
- Release to production
## Version Management
### Automated Version Bumping
**scripts/bump-android-version.sh:**
```bash
#!/bin/bash
VERSION_TYPE=${1:-patch} # major, minor, patch
BUILD_GRADLE="android/app/build.gradle"
# Get current version
CURRENT_VERSION=$(grep "versionName" $BUILD_GRADLE | sed 's/.*versionName "\(.*\)"/\1/')
CURRENT_CODE=$(grep "versionCode" $BUILD_GRADLE | sed 's/.*versionCode \(.*\)/\1/')
echo "Current version: $CURRENT_VERSION ($CURRENT_CODE)"
# Calculate new version
IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
MAJOR=${VERSION_PARTS[0]}
MINOR=${VERSION_PARTS[1]}
PATCH=${VERSION_PARTS[2]}
case $VERSION_TYPE in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
NEW_CODE=$((CURRENT_CODE + 1))
echo "New version: $NEW_VERSION ($NEW_CODE)"
# Update build.gradle
sed -i "s/versionCode $CURRENT_CODE/versionCode $NEW_CODE/" $BUILD_GRADLE
sed -i "s/versionName \"$CURRENT_VERSION\"/versionName \"$NEW_VERSION\"/" $BUILD_GRADLE
echo "Version updated successfully!"
```
### Git Tagging
```bash
# Create and push tag
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
# List tags
git tag -l
# Delete tag
git tag -d v1.0.0
git push origin --delete v1.0.0
```
## Release Checklist
### Pre-Release Checklist
- [ ] Update version number
- [ ] Update changelog
- [ ] Run all tests
- [ ] Test on multiple devices
- [ ] Check ProGuard configuration
- [ ] Verify signing configuration
- [ ] Test release build locally
- [ ] Update app store metadata
- [ ] Prepare release notes
### Post-Release Checklist
- [ ] Monitor crash reports
- [ ] Check app store reviews
- [ ] Monitor performance metrics
- [ ] Update documentation
- [ ] Create git tag
- [ ] Notify team of release
- [ ] Plan next release
## Troubleshooting
### Common Build Issues
```bash
# Clean and rebuild
cd android
./gradlew clean
./gradlew assembleRelease
# Clear React Native cache
npx react-native start --reset-cache
# Clear Metro cache
npx react-native start --reset-cache
# Reset Android build
cd android
rm -rf build/
rm -rf app/build/
./gradlew clean
```
### Keystore Issues
```bash
# Verify keystore
keytool -list -v -keystore your-keystore.keystore
# Check keystore alias
keytool -list -keystore your-keystore.keystore
# Export certificate
keytool -export -alias your-alias -keystore your-keystore.keystore -file certificate.crt
```

View File

@ -0,0 +1,492 @@
# iOS Release Process
## Apple Developer Account Setup
### Prerequisites
1. **Apple Developer Account** ($99/year)
2. **Xcode** (latest version)
3. **Valid certificates and provisioning profiles**
### App Store Connect Setup
1. **Create App Record**
- App name: Saayam
- Bundle ID: com.saayam.app
- SKU: unique identifier
- Primary language: English
2. **App Information**
- Category: Social Networking / Lifestyle
- Content rights
- Age rating
- App Review Information
## Certificates & Provisioning
### Certificate Types
```bash
# Development Certificate
# - Used for development and testing
# - Install on development devices
# Distribution Certificate
# - Used for App Store distribution
# - Required for production builds
```
### Using Fastlane Match
```bash
# Initialize match
fastlane match init
# Generate development certificates
fastlane match development
# Generate App Store certificates
fastlane match appstore
# Generate Ad Hoc certificates (for testing)
fastlane match adhoc
```
**Matchfile:**
```ruby
git_url("https://github.com/your-org/certificates")
storage_mode("git")
type("development")
app_identifier(["com.saayam.app"])
username("your-apple-id@example.com")
team_id("YOUR_TEAM_ID")
```
### Manual Certificate Management
1. **Create Certificate Signing Request (CSR)**
- Open Keychain Access
- Certificate Assistant → Request Certificate from Certificate Authority
- Save CSR file
2. **Create Certificates in Developer Portal**
- iOS Development Certificate
- iOS Distribution Certificate
3. **Create App ID**
- Bundle ID: com.saayam.app
- Enable required capabilities (Push Notifications, etc.)
4. **Create Provisioning Profiles**
- Development Profile
- App Store Distribution Profile
## Xcode Configuration
### Project Settings
**Build Settings:**
```
# Code Signing
CODE_SIGN_IDENTITY = "iPhone Distribution"
DEVELOPMENT_TEAM = "YOUR_TEAM_ID"
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.saayam.app"
# Build Configuration
ENABLE_BITCODE = YES
SWIFT_OPTIMIZATION_LEVEL = "-O"
GCC_OPTIMIZATION_LEVEL = "s"
# Deployment
IPHONEOS_DEPLOYMENT_TARGET = "12.0"
TARGETED_DEVICE_FAMILY = "1,2" # iPhone and iPad
```
### Info.plist Configuration
**ios/SaayamApp/Info.plist:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Saayam</string>
<key>CFBundleIdentifier</key>
<string>com.saayam.app</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location to show nearby fundraising requests.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to camera to take photos for fundraising requests.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to select images for fundraising requests.</string>
<key>NSContactsUsageDescription</key>
<string>This app needs access to contacts to help you share fundraising requests.</string>
</dict>
</plist>
```
## Build Process
### Manual Build
```bash
# Clean build folder
cd ios
rm -rf build/
# Install dependencies
pod install
# Build for device
xcodebuild -workspace SaayamApp.xcworkspace \
-scheme SaayamApp \
-configuration Release \
-destination generic/platform=iOS \
-archivePath $PWD/build/SaayamApp.xcarchive \
archive
# Export IPA
xcodebuild -exportArchive \
-archivePath $PWD/build/SaayamApp.xcarchive \
-exportPath $PWD/build/ \
-exportOptionsPlist ExportOptions.plist
```
### Export Options
**ios/ExportOptions.plist:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>uploadBitcode</key>
<true/>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<true/>
<key>provisioningProfiles</key>
<dict>
<key>com.saayam.app</key>
<string>match AppStore com.saayam.app</string>
</dict>
</dict>
</plist>
```
### Fastlane Build
**ios/fastlane/Fastfile:**
```ruby
default_platform(:ios)
platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
setup_ci if ENV['CI']
# Sync certificates and provisioning profiles
match(
type: "appstore",
readonly: true
)
# Increment build number
increment_build_number(
xcodeproj: "SaayamApp.xcodeproj"
)
# Build the app
build_app(
scheme: "SaayamApp",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.saayam.app" => "match AppStore com.saayam.app"
}
}
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: "Bug fixes and performance improvements"
)
end
desc "Deploy to App Store"
lane :release do
setup_ci if ENV['CI']
match(
type: "appstore",
readonly: true
)
build_app(
scheme: "SaayamApp",
export_method: "app-store"
)
upload_to_app_store(
force: true,
reject_if_possible: true,
skip_metadata: false,
skip_screenshots: false,
submit_for_review: true,
automatic_release: false,
submission_information: {
add_id_info_limits_tracking: true,
add_id_info_serves_ads: false,
add_id_info_tracks_action: true,
add_id_info_tracks_install: true,
add_id_info_uses_idfa: true,
content_rights_has_rights: true,
content_rights_contains_third_party_content: true,
export_compliance_platform: 'ios',
export_compliance_compliance_required: false,
export_compliance_encryption_updated: false,
export_compliance_uses_encryption: false,
export_compliance_is_exempt: false
}
)
end
end
```
## Version Management
### Automated Version Bumping
**scripts/bump-ios-version.sh:**
```bash
#!/bin/bash
VERSION_TYPE=${1:-patch} # major, minor, patch
PLIST_PATH="ios/SaayamApp/Info.plist"
# Get current version
CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PLIST_PATH")
CURRENT_BUILD=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$PLIST_PATH")
echo "Current version: $CURRENT_VERSION ($CURRENT_BUILD)"
# Calculate new version
IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
MAJOR=${VERSION_PARTS[0]}
MINOR=${VERSION_PARTS[1]}
PATCH=${VERSION_PARTS[2]}
case $VERSION_TYPE in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
NEW_BUILD=$((CURRENT_BUILD + 1))
echo "New version: $NEW_VERSION ($NEW_BUILD)"
# Update Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $NEW_VERSION" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $NEW_BUILD" "$PLIST_PATH"
# Update Xcode project
xcrun agvtool new-marketing-version $NEW_VERSION
xcrun agvtool new-version -all $NEW_BUILD
echo "Version updated successfully!"
```
## TestFlight Distribution
### Internal Testing
```ruby
# Fastlane lane for internal testing
lane :internal do
build_app(scheme: "SaayamApp")
upload_to_testflight(
groups: ["Internal Testers"],
changelog: "Internal build for testing",
distribute_external: false
)
end
```
### External Testing
```ruby
# Fastlane lane for external testing
lane :external do
build_app(scheme: "SaayamApp")
upload_to_testflight(
groups: ["Beta Testers"],
changelog: "Beta version with new features",
distribute_external: true,
notify_external_testers: true
)
end
```
## App Store Submission
### App Store Metadata
**fastlane/metadata/en-US/:**
```
name.txt # App name
subtitle.txt # App subtitle
description.txt # App description
keywords.txt # App Store keywords
marketing_url.txt # Marketing URL
support_url.txt # Support URL
privacy_url.txt # Privacy policy URL
```
**description.txt:**
```
Saayam is a revolutionary fundraising platform that connects donors with meaningful causes in their local community.
Key Features:
• Browse local fundraising requests
• Support NGOs and individual causes
• Track your donation impact
• Secure payment processing
• Real-time updates on funded projects
Join thousands of users making a difference in their communities. Download Saayam today and start supporting causes that matter to you.
```
### Screenshots
```bash
# Generate screenshots using Fastlane
fastlane snapshot
# Upload screenshots
fastlane deliver --skip_binary_upload
```
**Screenshotfile:**
```ruby
devices([
"iPhone 14 Pro Max",
"iPhone 14 Pro",
"iPhone SE (3rd generation)",
"iPad Pro (12.9-inch) (6th generation)"
])
languages([
"en-US"
])
scheme("SaayamApp")
clear_previous_screenshots(true)
```
## Release Checklist
### Pre-Submission Checklist
- [ ] Update version and build numbers
- [ ] Test on multiple devices and iOS versions
- [ ] Verify all app store metadata
- [ ] Check app icons and screenshots
- [ ] Test in-app purchases (if applicable)
- [ ] Verify privacy policy and terms of service
- [ ] Test deep linking
- [ ] Check push notifications
- [ ] Verify analytics integration
- [ ] Test crash reporting
### App Store Review Guidelines
- [ ] App follows iOS Human Interface Guidelines
- [ ] No crashes or major bugs
- [ ] App provides value to users
- [ ] Respects user privacy
- [ ] Follows App Store Review Guidelines
- [ ] Proper use of Apple APIs
- [ ] Appropriate content rating
- [ ] Valid contact information
### Post-Submission
- [ ] Monitor App Store Connect for review status
- [ ] Respond to reviewer feedback if needed
- [ ] Prepare for release day
- [ ] Monitor crash reports and user feedback
- [ ] Plan post-launch updates
## Troubleshooting
### Common Issues
```bash
# Certificate issues
# - Ensure certificates are valid and not expired
# - Check provisioning profile matches bundle ID
# - Verify team ID is correct
# Build issues
# - Clean build folder: rm -rf ios/build
# - Clean derived data: rm -rf ~/Library/Developer/Xcode/DerivedData
# - Reset CocoaPods: cd ios && pod deintegrate && pod install
# Archive issues
# - Check scheme is set to Release
# - Verify code signing settings
# - Ensure all dependencies are properly linked
```
### Debugging Build Failures
```bash
# Verbose build output
xcodebuild -workspace SaayamApp.xcworkspace \
-scheme SaayamApp \
-configuration Release \
-destination generic/platform=iOS \
archive | xcpretty
# Check build settings
xcodebuild -workspace SaayamApp.xcworkspace \
-scheme SaayamApp \
-showBuildSettings
```

View File

@ -0,0 +1,475 @@
# Version Management
## Semantic Versioning
### Version Format
```
MAJOR.MINOR.PATCH
Examples:
1.0.0 - Initial release
1.0.1 - Bug fix
1.1.0 - New feature
2.0.0 - Breaking changes
```
### Version Rules
- **MAJOR**: Breaking changes, incompatible API changes
- **MINOR**: New features, backward compatible
- **PATCH**: Bug fixes, backward compatible
## Automated Versioning
### Package.json Version Management
```bash
# Install version management tools
npm install -g standard-version
# Bump version automatically
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# With standard-version (recommended)
npx standard-version --release-as patch
npx standard-version --release-as minor
npx standard-version --release-as major
```
### Cross-Platform Version Script
**scripts/version-bump.js:**
```javascript
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const versionType = process.argv[2] || 'patch';
const packageJsonPath = path.join(__dirname, '../package.json');
const androidBuildGradle = path.join(__dirname, '../android/app/build.gradle');
const iosPlistPath = path.join(__dirname, '../ios/SaayamApp/Info.plist');
// Read current version from package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
console.log(`Current version: ${currentVersion}`);
// Bump version in package.json
execSync(`npm version ${versionType} --no-git-tag-version`, { stdio: 'inherit' });
// Read new version
const updatedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const newVersion = updatedPackageJson.version;
console.log(`New version: ${newVersion}`);
// Update Android version
updateAndroidVersion(newVersion);
// Update iOS version
updateiOSVersion(newVersion);
// Create git commit and tag
execSync(`git add .`, { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${newVersion}"`, { stdio: 'inherit' });
execSync(`git tag -a v${newVersion} -m "Release version ${newVersion}"`, { stdio: 'inherit' });
console.log(`Version bumped to ${newVersion} successfully!`);
function updateAndroidVersion(version) {
let buildGradleContent = fs.readFileSync(androidBuildGradle, 'utf8');
// Update versionName
buildGradleContent = buildGradleContent.replace(
/versionName\s+"[^"]*"/,
`versionName "${version}"`
);
// Update versionCode (increment by 1)
const versionCodeMatch = buildGradleContent.match(/versionCode\s+(\d+)/);
if (versionCodeMatch) {
const currentVersionCode = parseInt(versionCodeMatch[1]);
const newVersionCode = currentVersionCode + 1;
buildGradleContent = buildGradleContent.replace(
/versionCode\s+\d+/,
`versionCode ${newVersionCode}`
);
console.log(`Android versionCode updated to: ${newVersionCode}`);
}
fs.writeFileSync(androidBuildGradle, buildGradleContent);
console.log(`Android versionName updated to: ${version}`);
}
function updateiOSVersion(version) {
try {
// Update CFBundleShortVersionString
execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${version}" "${iosPlistPath}"`, { stdio: 'inherit' });
// Get current build number and increment
const currentBuild = execSync(`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${iosPlistPath}"`, { encoding: 'utf8' }).trim();
const newBuild = parseInt(currentBuild) + 1;
// Update CFBundleVersion
execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${newBuild}" "${iosPlistPath}"`, { stdio: 'inherit' });
console.log(`iOS version updated to: ${version} (${newBuild})`);
} catch (error) {
console.error('Error updating iOS version:', error.message);
}
}
```
### Make Script Executable
```bash
chmod +x scripts/version-bump.js
```
### Usage
```bash
# Bump patch version
./scripts/version-bump.js patch
# Bump minor version
./scripts/version-bump.js minor
# Bump major version
./scripts/version-bump.js major
```
## Changelog Management
### Conventional Commits
```bash
# Install commitizen for conventional commits
npm install -g commitizen cz-conventional-changelog
# Configure commitizen
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc
# Use commitizen for commits
git cz
```
### Commit Message Format
```
<type>(<scope>): <subject>
<body>
<footer>
```
**Types:**
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Code style changes
- `refactor`: Code refactoring
- `test`: Adding tests
- `chore`: Maintenance tasks
**Examples:**
```
feat(auth): add biometric authentication
fix(api): resolve login timeout issue
docs(readme): update installation instructions
```
### Automated Changelog
**Install standard-version:**
```bash
npm install -D standard-version
```
**package.json scripts:**
```json
{
"scripts": {
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:patch": "standard-version --release-as patch"
}
}
```
**.versionrc.json:**
```json
{
"types": [
{"type": "feat", "section": "Features"},
{"type": "fix", "section": "Bug Fixes"},
{"type": "chore", "hidden": true},
{"type": "docs", "hidden": true},
{"type": "style", "hidden": true},
{"type": "refactor", "section": "Code Refactoring"},
{"type": "perf", "section": "Performance Improvements"},
{"type": "test", "hidden": true}
],
"commitUrlFormat": "https://github.com/your-org/saayam-app/commit/{{hash}}",
"compareUrlFormat": "https://github.com/your-org/saayam-app/compare/{{previousTag}}...{{currentTag}}"
}
```
## Release Branches
### Git Flow Strategy
```bash
# Install git-flow
brew install git-flow-avh # macOS
apt-get install git-flow # Ubuntu
# Initialize git-flow
git flow init
# Start a release branch
git flow release start 1.2.0
# Finish a release branch
git flow release finish 1.2.0
```
### Branch Structure
```
main (production)
├── develop (development)
├── feature/user-authentication
├── feature/payment-integration
├── release/1.2.0
├── hotfix/critical-bug-fix
```
### Release Process
```bash
# 1. Create release branch from develop
git checkout develop
git pull origin develop
git checkout -b release/1.2.0
# 2. Update version numbers
./scripts/version-bump.js minor
# 3. Run final tests
npm test
npm run e2e
# 4. Merge to main
git checkout main
git merge release/1.2.0
# 5. Tag release
git tag -a v1.2.0 -m "Release version 1.2.0"
# 6. Merge back to develop
git checkout develop
git merge release/1.2.0
# 7. Push everything
git push origin main
git push origin develop
git push origin v1.2.0
# 8. Delete release branch
git branch -d release/1.2.0
```
## Build Numbers
### Platform-Specific Build Numbers
**Android (versionCode):**
- Integer that increases with each release
- Used by Google Play to determine newer versions
- Must be incremented for each upload
**iOS (CFBundleVersion):**
- String that increases with each build
- Used by App Store to determine newer builds
- Can be numeric or alphanumeric
### Build Number Strategy
```javascript
// Generate build number based on timestamp
function generateBuildNumber() {
const now = new Date();
const year = now.getFullYear().toString().slice(-2);
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
return `${year}${month}${day}${hour}${minute}`;
}
// Example: 2312151430 (23-12-15 14:30)
```
## Version Validation
### Pre-Release Checks
**scripts/validate-version.js:**
```javascript
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const packageJsonPath = path.join(__dirname, '../package.json');
const androidBuildGradle = path.join(__dirname, '../android/app/build.gradle');
const iosPlistPath = path.join(__dirname, '../ios/SaayamApp/Info.plist');
function validateVersions() {
// Get package.json version
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageVersion = packageJson.version;
// Get Android version
const buildGradleContent = fs.readFileSync(androidBuildGradle, 'utf8');
const androidVersionMatch = buildGradleContent.match(/versionName\s+"([^"]*)"/);
const androidVersion = androidVersionMatch ? androidVersionMatch[1] : null;
// Get iOS version
let iosVersion = null;
try {
const { execSync } = require('child_process');
iosVersion = execSync(`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${iosPlistPath}"`, { encoding: 'utf8' }).trim();
} catch (error) {
console.error('Could not read iOS version');
}
console.log('Version Validation:');
console.log(`Package.json: ${packageVersion}`);
console.log(`Android: ${androidVersion}`);
console.log(`iOS: ${iosVersion}`);
const allVersionsMatch = packageVersion === androidVersion && packageVersion === iosVersion;
if (allVersionsMatch) {
console.log('✅ All versions match!');
process.exit(0);
} else {
console.log('❌ Version mismatch detected!');
process.exit(1);
}
}
validateVersions();
```
### CI/CD Integration
**.github/workflows/version-check.yml:**
```yaml
name: Version Check
on:
pull_request:
branches: [main]
jobs:
version-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Validate versions
run: node scripts/validate-version.js
```
## Release Notes
### Automated Release Notes
**scripts/generate-release-notes.js:**
```javascript
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
function generateReleaseNotes(fromTag, toTag) {
// Get commits between tags
const commits = execSync(`git log ${fromTag}..${toTag} --pretty=format:"%h %s"`, { encoding: 'utf8' })
.split('\n')
.filter(line => line.trim());
const features = [];
const fixes = [];
const others = [];
commits.forEach(commit => {
if (commit.includes('feat:') || commit.includes('feat(')) {
features.push(commit.replace(/^\w+\s+/, '').replace(/^feat(\([^)]+\))?:\s*/, ''));
} else if (commit.includes('fix:') || commit.includes('fix(')) {
fixes.push(commit.replace(/^\w+\s+/, '').replace(/^fix(\([^)]+\))?:\s*/, ''));
} else {
others.push(commit.replace(/^\w+\s+/, ''));
}
});
let releaseNotes = `# Release Notes\n\n`;
if (features.length > 0) {
releaseNotes += `## 🚀 New Features\n`;
features.forEach(feature => {
releaseNotes += `- ${feature}\n`;
});
releaseNotes += '\n';
}
if (fixes.length > 0) {
releaseNotes += `## 🐛 Bug Fixes\n`;
fixes.forEach(fix => {
releaseNotes += `- ${fix}\n`;
});
releaseNotes += '\n';
}
if (others.length > 0) {
releaseNotes += `## 🔧 Other Changes\n`;
others.forEach(other => {
releaseNotes += `- ${other}\n`;
});
}
return releaseNotes;
}
// Usage: node generate-release-notes.js v1.0.0 v1.1.0
const fromTag = process.argv[2];
const toTag = process.argv[3] || 'HEAD';
if (!fromTag) {
console.error('Usage: node generate-release-notes.js <from-tag> [to-tag]');
process.exit(1);
}
const releaseNotes = generateReleaseNotes(fromTag, toTag);
console.log(releaseNotes);
// Save to file
fs.writeFileSync('RELEASE_NOTES.md', releaseNotes);
console.log('\nRelease notes saved to RELEASE_NOTES.md');
```

View File

@ -0,0 +1,591 @@
# Analytics Setup
## Firebase Analytics
### Installation
```bash
yarn add @react-native-firebase/app
yarn add @react-native-firebase/analytics
```
### Configuration
**analytics.config.js:**
```typescript
import analytics from '@react-native-firebase/analytics';
import Config from 'react-native-config';
class AnalyticsService {
static async initialize() {
if (__DEV__) {
// Disable analytics in development
await analytics().setAnalyticsCollectionEnabled(false);
} else {
await analytics().setAnalyticsCollectionEnabled(true);
}
}
static async setUserId(userId: string) {
await analytics().setUserId(userId);
}
static async setUserProperties(properties: Record<string, string>) {
for (const [key, value] of Object.entries(properties)) {
await analytics().setUserProperty(key, value);
}
}
static async logEvent(eventName: string, parameters?: Record<string, any>) {
try {
await analytics().logEvent(eventName, parameters);
console.log(`Analytics: ${eventName}`, parameters);
} catch (error) {
console.error('Analytics error:', error);
}
}
static async logScreenView(screenName: string, screenClass?: string) {
await this.logEvent('screen_view', {
screen_name: screenName,
screen_class: screenClass || screenName,
});
}
static async logLogin(method: string) {
await this.logEvent('login', {
method,
});
}
static async logSignUp(method: string) {
await this.logEvent('sign_up', {
method,
});
}
static async logPurchase(transactionId: string, value: number, currency: string = 'USD') {
await this.logEvent('purchase', {
transaction_id: transactionId,
value,
currency,
});
}
static async logShare(contentType: string, itemId: string) {
await this.logEvent('share', {
content_type: contentType,
item_id: itemId,
});
}
static async logSearch(searchTerm: string) {
await this.logEvent('search', {
search_term: searchTerm,
});
}
}
export default AnalyticsService;
```
## Custom Event Tracking
### User Journey Tracking
```typescript
// src/utils/userJourneyTracker.ts
import AnalyticsService from '@config/analytics.config';
export class UserJourneyTracker {
private static sessionStartTime: number = Date.now();
private static currentScreen: string = '';
static startSession() {
this.sessionStartTime = Date.now();
AnalyticsService.logEvent('session_start', {
timestamp: this.sessionStartTime,
});
}
static endSession() {
const sessionDuration = Date.now() - this.sessionStartTime;
AnalyticsService.logEvent('session_end', {
session_duration: sessionDuration,
});
}
static trackScreenView(screenName: string) {
const previousScreen = this.currentScreen;
this.currentScreen = screenName;
AnalyticsService.logScreenView(screenName);
if (previousScreen) {
AnalyticsService.logEvent('screen_transition', {
from_screen: previousScreen,
to_screen: screenName,
});
}
}
static trackUserAction(action: string, context?: Record<string, any>) {
AnalyticsService.logEvent('user_action', {
action,
screen: this.currentScreen,
...context,
});
}
static trackFeatureUsage(feature: string, parameters?: Record<string, any>) {
AnalyticsService.logEvent('feature_usage', {
feature_name: feature,
screen: this.currentScreen,
...parameters,
});
}
static trackError(error: string, context?: Record<string, any>) {
AnalyticsService.logEvent('app_error', {
error_message: error,
screen: this.currentScreen,
...context,
});
}
}
```
### Donation Tracking
```typescript
// src/utils/donationTracker.ts
import AnalyticsService from '@config/analytics.config';
export class DonationTracker {
static trackDonationStart(requestId: string, amount: number) {
AnalyticsService.logEvent('donation_start', {
request_id: requestId,
amount,
currency: 'USD',
});
}
static trackDonationComplete(
requestId: string,
amount: number,
paymentMethod: string,
transactionId: string
) {
AnalyticsService.logEvent('donation_complete', {
request_id: requestId,
amount,
currency: 'USD',
payment_method: paymentMethod,
transaction_id: transactionId,
});
// Also log as purchase for e-commerce tracking
AnalyticsService.logPurchase(transactionId, amount);
}
static trackDonationFailed(
requestId: string,
amount: number,
errorReason: string
) {
AnalyticsService.logEvent('donation_failed', {
request_id: requestId,
amount,
currency: 'USD',
error_reason: errorReason,
});
}
static trackRequestCreated(
requestId: string,
category: string,
targetAmount: number
) {
AnalyticsService.logEvent('request_created', {
request_id: requestId,
category,
target_amount: targetAmount,
currency: 'USD',
});
}
static trackRequestViewed(requestId: string, category: string) {
AnalyticsService.logEvent('request_viewed', {
request_id: requestId,
category,
});
}
}
```
## Performance Analytics
### App Performance Monitoring
```typescript
// src/utils/performanceAnalytics.ts
import AnalyticsService from '@config/analytics.config';
export class PerformanceAnalytics {
private static metrics: Map<string, number> = new Map();
static startTiming(operation: string) {
this.metrics.set(operation, Date.now());
}
static endTiming(operation: string) {
const startTime = this.metrics.get(operation);
if (!startTime) return;
const duration = Date.now() - startTime;
this.metrics.delete(operation);
AnalyticsService.logEvent('performance_timing', {
operation,
duration,
});
// Track slow operations
if (duration > 3000) {
AnalyticsService.logEvent('slow_operation', {
operation,
duration,
});
}
}
static trackAppLaunchTime(launchTime: number) {
AnalyticsService.logEvent('app_launch_time', {
launch_time: launchTime,
});
}
static trackAPIResponse(endpoint: string, duration: number, status: number) {
AnalyticsService.logEvent('api_response', {
endpoint,
duration,
status,
});
}
static trackMemoryUsage(usedMemory: number, totalMemory: number) {
AnalyticsService.logEvent('memory_usage', {
used_memory: usedMemory,
total_memory: totalMemory,
usage_percentage: (usedMemory / totalMemory) * 100,
});
}
}
```
## User Behavior Analytics
### Engagement Tracking
```typescript
// src/utils/engagementTracker.ts
import AnalyticsService from '@config/analytics.config';
export class EngagementTracker {
private static sessionEvents: string[] = [];
static trackAppOpen() {
AnalyticsService.logEvent('app_open');
this.sessionEvents = [];
}
static trackAppBackground() {
AnalyticsService.logEvent('app_background', {
session_events: this.sessionEvents.length,
events: this.sessionEvents.slice(-10), // Last 10 events
});
}
static trackUserEngagement(action: string, value?: number) {
this.sessionEvents.push(action);
AnalyticsService.logEvent('user_engagement', {
engagement_time_msec: value || 1000,
action,
});
}
static trackContentInteraction(
contentType: string,
contentId: string,
action: string
) {
AnalyticsService.logEvent('content_interaction', {
content_type: contentType,
content_id: contentId,
action,
});
}
static trackSocialShare(platform: string, contentType: string) {
AnalyticsService.logEvent('social_share', {
platform,
content_type: contentType,
});
}
static trackSearchUsage(query: string, resultsCount: number) {
AnalyticsService.logEvent('search_usage', {
search_term: query,
results_count: resultsCount,
});
}
}
```
## A/B Testing Integration
### Feature Flag Analytics
```typescript
// src/utils/abTestingTracker.ts
import AnalyticsService from '@config/analytics.config';
export class ABTestingTracker {
static trackExperimentExposure(
experimentId: string,
variantId: string,
userId: string
) {
AnalyticsService.logEvent('experiment_exposure', {
experiment_id: experimentId,
variant_id: variantId,
user_id: userId,
});
}
static trackExperimentConversion(
experimentId: string,
variantId: string,
conversionType: string,
value?: number
) {
AnalyticsService.logEvent('experiment_conversion', {
experiment_id: experimentId,
variant_id: variantId,
conversion_type: conversionType,
value,
});
}
static trackFeatureFlagUsage(flagName: string, enabled: boolean) {
AnalyticsService.logEvent('feature_flag_usage', {
flag_name: flagName,
enabled,
});
}
}
```
## Custom Dashboards
### Analytics Dashboard Data
```typescript
// src/utils/analyticsDashboard.ts
import AnalyticsService from '@config/analytics.config';
export class AnalyticsDashboard {
static trackKPI(kpiName: string, value: number, unit?: string) {
AnalyticsService.logEvent('kpi_metric', {
kpi_name: kpiName,
value,
unit: unit || 'count',
timestamp: Date.now(),
});
}
static trackBusinessMetric(
metricName: string,
value: number,
category: string
) {
AnalyticsService.logEvent('business_metric', {
metric_name: metricName,
value,
category,
timestamp: Date.now(),
});
}
static trackUserRetention(daysSinceInstall: number, isActive: boolean) {
AnalyticsService.logEvent('user_retention', {
days_since_install: daysSinceInstall,
is_active: isActive,
});
}
static trackFunnelStep(funnelName: string, step: string, completed: boolean) {
AnalyticsService.logEvent('funnel_step', {
funnel_name: funnelName,
step,
completed,
});
}
}
```
## Privacy Compliance
### GDPR/CCPA Compliance
```typescript
// src/utils/privacyCompliance.ts
import AnalyticsService from '@config/analytics.config';
import AsyncStorage from '@react-native-async-storage/async-storage';
export class PrivacyCompliance {
private static readonly CONSENT_KEY = 'analytics_consent';
static async hasUserConsent(): Promise<boolean> {
try {
const consent = await AsyncStorage.getItem(this.CONSENT_KEY);
return consent === 'true';
} catch {
return false;
}
}
static async setUserConsent(hasConsent: boolean) {
try {
await AsyncStorage.setItem(this.CONSENT_KEY, hasConsent.toString());
if (hasConsent) {
await AnalyticsService.initialize();
AnalyticsService.logEvent('analytics_consent_granted');
} else {
await AnalyticsService.setAnalyticsCollectionEnabled(false);
}
} catch (error) {
console.error('Error setting analytics consent:', error);
}
}
static async revokeConsent() {
await this.setUserConsent(false);
await AsyncStorage.removeItem(this.CONSENT_KEY);
// Clear any stored analytics data
AnalyticsService.logEvent('analytics_consent_revoked');
}
static trackConsentRequest(source: string) {
AnalyticsService.logEvent('consent_request_shown', {
source,
});
}
static trackConsentResponse(granted: boolean, source: string) {
AnalyticsService.logEvent('consent_response', {
granted,
source,
});
}
}
```
## Analytics Integration
### Navigation Analytics
```typescript
// src/navigation/AnalyticsNavigationContainer.tsx
import React from 'react';
import {NavigationContainer, NavigationState} from '@react-navigation/native';
import {UserJourneyTracker} from '@utils/userJourneyTracker';
interface Props {
children: React.ReactNode;
}
export const AnalyticsNavigationContainer: React.FC<Props> = ({children}) => {
const routeNameRef = React.useRef<string>();
const onStateChange = (state: NavigationState | undefined) => {
const previousRouteName = routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
UserJourneyTracker.trackScreenView(currentRouteName || 'Unknown');
}
routeNameRef.current = currentRouteName;
};
return (
<NavigationContainer onStateChange={onStateChange}>
{children}
</NavigationContainer>
);
};
function getActiveRouteName(state: NavigationState | undefined): string | undefined {
if (!state) return undefined;
const route = state.routes[state.index];
if (route.state) {
return getActiveRouteName(route.state as NavigationState);
}
return route.name;
}
```
### Component Analytics HOC
```typescript
// src/hoc/withAnalytics.tsx
import React, {useEffect} from 'react';
import {UserJourneyTracker} from '@utils/userJourneyTracker';
interface AnalyticsProps {
screenName?: string;
trackProps?: string[];
}
export function withAnalytics<P extends object>(
WrappedComponent: React.ComponentType<P>,
analyticsProps: AnalyticsProps
) {
return function AnalyticsComponent(props: P) {
useEffect(() => {
if (analyticsProps.screenName) {
UserJourneyTracker.trackScreenView(analyticsProps.screenName);
}
// Track specific props if specified
if (analyticsProps.trackProps) {
const trackedData = analyticsProps.trackProps.reduce((acc, propName) => {
if (propName in props) {
acc[propName] = (props as any)[propName];
}
return acc;
}, {} as Record<string, any>);
if (Object.keys(trackedData).length > 0) {
UserJourneyTracker.trackUserAction('component_rendered', trackedData);
}
}
}, []);
return <WrappedComponent {...props} />;
};
}
// Usage example:
// export default withAnalytics(MyComponent, {
// screenName: 'MyScreen',
// trackProps: ['userId', 'category']
// });
```

View File

@ -0,0 +1,612 @@
# Crash Monitoring
## Bugsnag Setup
### Installation
```bash
yarn add @bugsnag/react-native
yarn add @bugsnag/react-native-cli
```
### Configuration
**bugsnag.config.js:**
```javascript
import Bugsnag from '@bugsnag/react-native';
import Config from 'react-native-config';
Bugsnag.start({
apiKey: Config.BUGSNAG_API_KEY,
appVersion: Config.APP_VERSION,
releaseStage: Config.APP_ENV,
enabledReleaseStages: ['production', 'staging'],
autoDetectErrors: true,
autoCaptureSessions: true,
enabledErrorTypes: {
unhandledExceptions: true,
unhandledRejections: true,
nativeCrashes: true,
},
onError: (event) => {
// Add custom metadata
event.addMetadata('app', {
buildNumber: Config.BUILD_NUMBER,
environment: Config.APP_ENV,
});
// Filter sensitive data
event.request.url = event.request.url?.replace(/token=[^&]+/, 'token=***');
return true;
},
onSession: (session) => {
// Add user context
session.user = {
id: 'user-id',
name: 'User Name',
email: 'user@example.com',
};
return true;
},
});
export default Bugsnag;
```
### Integration
**App.tsx:**
```typescript
import React from 'react';
import Bugsnag from './src/config/bugsnag.config';
import {AppNavigator} from './src/navigation/AppNavigator';
const App: React.FC = () => {
React.useEffect(() => {
// Set user context when available
Bugsnag.setUser('user-123', 'john@example.com', 'John Doe');
// Leave breadcrumb for app start
Bugsnag.leaveBreadcrumb('App started');
}, []);
return <AppNavigator />;
};
export default App;
```
## Firebase Crashlytics
### Installation
```bash
yarn add @react-native-firebase/app
yarn add @react-native-firebase/crashlytics
```
### Configuration
**firebase.config.js:**
```typescript
import crashlytics from '@react-native-firebase/crashlytics';
import Config from 'react-native-config';
class CrashlyticsService {
static initialize() {
if (__DEV__) {
// Disable crashlytics in development
crashlytics().setCrashlyticsCollectionEnabled(false);
} else {
crashlytics().setCrashlyticsCollectionEnabled(true);
}
}
static setUserId(userId: string) {
crashlytics().setUserId(userId);
}
static setUserAttributes(attributes: Record<string, string>) {
Object.entries(attributes).forEach(([key, value]) => {
crashlytics().setAttribute(key, value);
});
}
static logError(error: Error, context?: Record<string, any>) {
if (context) {
Object.entries(context).forEach(([key, value]) => {
crashlytics().setAttribute(key, String(value));
});
}
crashlytics().recordError(error);
}
static log(message: string) {
crashlytics().log(message);
}
static crash() {
crashlytics().crash();
}
}
export default CrashlyticsService;
```
## Error Boundary
### React Error Boundary
```typescript
// src/components/ErrorBoundary/ErrorBoundary.tsx
import React, {Component, ErrorInfo, ReactNode} from 'react';
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native';
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {hasError: false};
}
static getDerivedStateFromError(error: Error): State {
return {hasError: true, error};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
// Log to crash reporting services
Bugsnag.notify(error, (event) => {
event.addMetadata('errorBoundary', {
componentStack: errorInfo.componentStack,
errorBoundary: true,
});
});
CrashlyticsService.logError(error, {
componentStack: errorInfo.componentStack,
errorBoundary: 'true',
});
}
handleRetry = () => {
this.setState({hasError: false, error: undefined});
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<View style={styles.container}>
<Text style={styles.title}>Oops! Something went wrong</Text>
<Text style={styles.message}>
We're sorry for the inconvenience. The error has been reported and we're working on a fix.
</Text>
<TouchableOpacity style={styles.button} onPress={this.handleRetry}>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return this.props.children;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 16,
textAlign: 'center',
color: '#333',
},
message: {
fontSize: 16,
textAlign: 'center',
marginBottom: 24,
color: '#666',
lineHeight: 24,
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});
```
## Global Error Handler
### Unhandled Promise Rejections
```typescript
// src/utils/errorHandler.ts
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';
export class GlobalErrorHandler {
static initialize() {
// Handle unhandled promise rejections
const originalHandler = global.Promise.prototype.catch;
global.Promise.prototype.catch = function(onRejected) {
return originalHandler.call(this, (error) => {
GlobalErrorHandler.handleError(error, 'unhandledRejection');
if (onRejected) {
return onRejected(error);
}
throw error;
});
};
// Handle uncaught exceptions
if (global.ErrorUtils) {
const originalGlobalHandler = global.ErrorUtils.getGlobalHandler();
global.ErrorUtils.setGlobalHandler((error, isFatal) => {
GlobalErrorHandler.handleError(error, isFatal ? 'fatal' : 'nonfatal');
originalGlobalHandler(error, isFatal);
});
}
}
static handleError(error: Error, type: string) {
console.error(`${type} error:`, error);
// Log to crash reporting services
Bugsnag.notify(error, (event) => {
event.addMetadata('error', {
type,
timestamp: new Date().toISOString(),
});
});
CrashlyticsService.logError(error, {
errorType: type,
timestamp: new Date().toISOString(),
});
}
static logError(error: Error, context?: Record<string, any>) {
this.handleError(error, 'manual');
if (context) {
Bugsnag.addMetadata('context', context);
CrashlyticsService.setUserAttributes(
Object.fromEntries(
Object.entries(context).map(([k, v]) => [k, String(v)])
)
);
}
}
}
```
## Network Error Monitoring
### API Error Tracking
```typescript
// src/services/errorTrackingAPI.ts
import {AxiosError, AxiosResponse} from 'axios';
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';
export class APIErrorTracker {
static trackRequest(config: any) {
Bugsnag.leaveBreadcrumb(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
CrashlyticsService.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
}
static trackResponse(response: AxiosResponse) {
const {status, config} = response;
Bugsnag.leaveBreadcrumb(`API Response: ${status} ${config.url}`);
CrashlyticsService.log(`API Response: ${status} ${config.url}`);
}
static trackError(error: AxiosError) {
const {response, config, message} = error;
const errorData = {
url: config?.url,
method: config?.method,
status: response?.status,
statusText: response?.statusText,
message,
responseData: response?.data,
};
// Log to crash reporting
Bugsnag.notify(error, (event) => {
event.addMetadata('apiError', errorData);
event.severity = response?.status && response.status >= 500 ? 'error' : 'warning';
});
CrashlyticsService.logError(error, {
apiUrl: config?.url || 'unknown',
apiMethod: config?.method || 'unknown',
apiStatus: String(response?.status || 0),
});
console.error('API Error:', errorData);
}
}
```
## Performance Monitoring
### Performance Metrics
```typescript
// src/utils/performanceMonitor.ts
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';
export class PerformanceMonitor {
private static metrics: Map<string, number> = new Map();
static startTiming(label: string) {
this.metrics.set(label, Date.now());
CrashlyticsService.log(`Performance: Started ${label}`);
}
static endTiming(label: string): number {
const startTime = this.metrics.get(label);
if (!startTime) {
console.warn(`No start time found for ${label}`);
return 0;
}
const duration = Date.now() - startTime;
this.metrics.delete(label);
// Log performance metrics
Bugsnag.leaveBreadcrumb(`Performance: ${label} took ${duration}ms`);
CrashlyticsService.log(`Performance: ${label} took ${duration}ms`);
// Alert on slow operations
if (duration > 5000) {
Bugsnag.notify(new Error(`Slow operation: ${label}`), (event) => {
event.severity = 'warning';
event.addMetadata('performance', {
operation: label,
duration,
threshold: 5000,
});
});
}
return duration;
}
static measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
this.startTiming(label);
return fn().finally(() => {
this.endTiming(label);
});
}
}
```
## Custom Crash Reports
### User Feedback Integration
```typescript
// src/components/FeedbackModal/FeedbackModal.tsx
import React, {useState} from 'react';
import {Modal, View, Text, TextInput, TouchableOpacity, StyleSheet} from 'react-native';
import Bugsnag from '@config/bugsnag.config';
interface FeedbackModalProps {
visible: boolean;
onClose: () => void;
error?: Error;
}
export const FeedbackModal: React.FC<FeedbackModalProps> = ({
visible,
onClose,
error,
}) => {
const [feedback, setFeedback] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = () => {
if (error) {
Bugsnag.notify(error, (event) => {
event.addMetadata('userFeedback', {
feedback,
email,
timestamp: new Date().toISOString(),
});
});
} else {
Bugsnag.leaveBreadcrumb('User feedback submitted', {
feedback,
email,
});
}
setFeedback('');
setEmail('');
onClose();
};
return (
<Modal visible={visible} transparent animationType="slide">
<View style={styles.overlay}>
<View style={styles.container}>
<Text style={styles.title}>Help us improve</Text>
<Text style={styles.subtitle}>
Tell us what happened so we can fix the issue
</Text>
<TextInput
style={styles.input}
placeholder="Your email (optional)"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
/>
<TextInput
style={[styles.input, styles.textArea]}
placeholder="Describe what you were doing when the error occurred"
value={feedback}
onChangeText={setFeedback}
multiline
numberOfLines={4}
/>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.cancelButton} onPress={onClose}>
<Text style={styles.cancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.submitButton} onPress={handleSubmit}>
<Text style={styles.submitText}>Send Feedback</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
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: '90%',
maxWidth: 400,
},
title: {
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 20,
},
input: {
borderWidth: 1,
borderColor: '#E5E5E7',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
marginBottom: 12,
fontSize: 16,
},
textArea: {
height: 80,
textAlignVertical: 'top',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 20,
},
cancelButton: {
flex: 1,
paddingVertical: 12,
marginRight: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: '#E5E5E7',
},
submitButton: {
flex: 1,
paddingVertical: 12,
marginLeft: 8,
borderRadius: 8,
backgroundColor: '#007AFF',
},
cancelText: {
textAlign: 'center',
fontSize: 16,
color: '#666',
},
submitText: {
textAlign: 'center',
fontSize: 16,
color: '#FFFFFF',
fontWeight: '600',
},
});
```
## Crash Report Analysis
### Automated Alerts
```typescript
// src/utils/crashAlerts.ts
export class CrashAlerts {
static setupAlerts() {
// Configure Bugsnag alerts
// This would typically be done in the Bugsnag dashboard
// High-priority alerts:
// - App crashes affecting > 1% of users
// - New error types
// - Errors in critical user flows (login, payment)
// Medium-priority alerts:
// - Performance degradation
// - API errors > 5% error rate
// - Memory warnings
}
static generateCrashReport(timeframe: string = '24h') {
// This would integrate with Bugsnag API to generate reports
return {
totalCrashes: 0,
uniqueErrors: 0,
affectedUsers: 0,
topErrors: [],
performanceMetrics: {},
};
}
}
```

View File

@ -0,0 +1,580 @@
# OTA Updates with CodePush
## CodePush Setup
### Installation
```bash
# Install CodePush CLI
npm install -g code-push-cli
# Install React Native CodePush
yarn add react-native-code-push
```
### App Center Configuration
```bash
# Login to App Center
code-push login
# Create apps
code-push app add SaayamApp-iOS ios react-native
code-push app add SaayamApp-Android android react-native
# Get deployment keys
code-push deployment ls SaayamApp-iOS -k
code-push deployment ls SaayamApp-Android -k
```
### Environment Configuration
**.env files:**
```bash
# .env.staging
CODEPUSH_DEPLOYMENT_KEY_IOS=staging-ios-key
CODEPUSH_DEPLOYMENT_KEY_ANDROID=staging-android-key
# .env.production
CODEPUSH_DEPLOYMENT_KEY_IOS=production-ios-key
CODEPUSH_DEPLOYMENT_KEY_ANDROID=production-android-key
```
## CodePush Integration
### App Configuration
**App.tsx:**
```typescript
import React, { useEffect, useState } from "react";
import { Alert, Platform } from "react-native";
import codePush from "react-native-code-push";
import Config from "react-native-config";
import { AppNavigator } from "./src/navigation/AppNavigator";
import { LoadingScreen } from "./src/components/LoadingScreen";
const codePushOptions = {
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.ON_NEXT_RESUME,
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
deploymentKey: Platform.select({
ios: Config.CODEPUSH_DEPLOYMENT_KEY_IOS,
android: Config.CODEPUSH_DEPLOYMENT_KEY_ANDROID,
}),
updateDialog: {
appendReleaseDescription: true,
descriptionPrefix: "\n\nWhat's New:\n",
mandatoryContinueButtonLabel: "Install Now",
mandatoryUpdateMessage:
"A critical update is available and must be installed.",
optionalIgnoreButtonLabel: "Later",
optionalInstallButtonLabel: "Install",
optionalUpdateMessage:
"An update is available. Would you like to install it?",
title: "Update Available",
},
};
const App: React.FC = () => {
const [syncInProgress, setSyncInProgress] = useState(false);
const [syncMessage, setSyncMessage] = useState("");
useEffect(() => {
codePush.notifyAppReady();
}, []);
const codePushStatusDidChange = (syncStatus: codePush.SyncStatus) => {
switch (syncStatus) {
case codePush.SyncStatus.CHECKING_FOR_UPDATE:
setSyncMessage("Checking for updates...");
break;
case codePush.SyncStatus.DOWNLOADING_PACKAGE:
setSyncMessage("Downloading update...");
setSyncInProgress(true);
break;
case codePush.SyncStatus.INSTALLING_UPDATE:
setSyncMessage("Installing update...");
break;
case codePush.SyncStatus.UP_TO_DATE:
setSyncMessage("App is up to date");
setSyncInProgress(false);
break;
case codePush.SyncStatus.UPDATE_INSTALLED:
setSyncMessage("Update installed successfully");
setSyncInProgress(false);
break;
default:
setSyncInProgress(false);
break;
}
};
const codePushDownloadDidProgress = (progress: codePush.DownloadProgress) => {
const percentage = Math.round(
(progress.receivedBytes / progress.totalBytes) * 100
);
setSyncMessage(`Downloading update... ${percentage}%`);
};
if (syncInProgress) {
return <LoadingScreen message={syncMessage} />;
}
return <AppNavigator />;
};
export default codePush(codePushOptions)(App);
```
### Manual Update Check
```typescript
// src/utils/updateManager.ts
import codePush from "react-native-code-push";
import { Alert } from "react-native";
export class UpdateManager {
static async checkForUpdate(): Promise<void> {
try {
const update = await codePush.checkForUpdate();
if (update) {
Alert.alert(
"Update Available",
`Version ${update.appVersion} is available. Would you like to install it now?`,
[
{ text: "Later", style: "cancel" },
{ text: "Install", onPress: () => this.downloadAndInstall() },
]
);
} else {
Alert.alert("No Updates", "Your app is up to date!");
}
} catch (error) {
console.error("Error checking for updates:", error);
}
}
static async downloadAndInstall(): Promise<void> {
try {
await codePush.sync({
installMode: codePush.InstallMode.IMMEDIATE,
updateDialog: {
appendReleaseDescription: true,
descriptionPrefix: "\n\nChanges:\n",
},
});
} catch (error) {
console.error("Error downloading update:", error);
Alert.alert(
"Update Failed",
"Failed to download update. Please try again later."
);
}
}
static async getUpdateMetadata(): Promise<codePush.LocalPackage | null> {
try {
return await codePush.getUpdateMetadata();
} catch (error) {
console.error("Error getting update metadata:", error);
return null;
}
}
static async rollback(): Promise<void> {
try {
await codePush.restartApp();
} catch (error) {
console.error("Error rolling back:", error);
}
}
}
```
## Deployment Strategies
### Staged Rollout
```bash
# Deploy to staging first
code-push release-react SaayamApp-iOS ios --deploymentName Staging
# Test staging deployment
# If successful, promote to production with rollout percentage
# Deploy to 10% of production users
code-push release-react SaayamApp-iOS ios --deploymentName Production --rollout 10%
# Monitor metrics and gradually increase rollout
code-push patch SaayamApp-iOS Production --rollout 25%
code-push patch SaayamApp-iOS Production --rollout 50%
code-push patch SaayamApp-iOS Production --rollout 100%
```
### Automated Deployment
**scripts/deploy-codepush.js:**
```javascript
#!/usr/bin/env node
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const platform = process.argv[2]; // ios or android
const environment = process.argv[3] || "staging"; // staging or production
const rollout = process.argv[4] || "100"; // rollout percentage
if (!platform || !["ios", "android"].includes(platform)) {
console.error(
"Usage: node deploy-codepush.js <ios|android> [staging|production] [rollout%]"
);
process.exit(1);
}
const appName = platform === "ios" ? "SaayamApp-iOS" : "SaayamApp-Android";
const deploymentName = environment === "production" ? "Production" : "Staging";
// Get version from package.json
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
const version = packageJson.version;
// Generate release notes from git commits
const releaseNotes = execSync("git log --oneline -10", { encoding: "utf8" })
.split("\n")
.filter((line) => line.trim())
.map((line) => `• ${line.split(" ").slice(1).join(" ")}`)
.join("\n");
console.log(`Deploying to ${appName} ${deploymentName}...`);
console.log(`Version: ${version}`);
console.log(`Rollout: ${rollout}%`);
try {
const command = [
"code-push release-react",
appName,
platform,
`--deploymentName ${deploymentName}`,
`--description "${releaseNotes}"`,
`--rollout ${rollout}%`,
"--mandatory false",
].join(" ");
execSync(command, { stdio: "inherit" });
console.log(`✅ Successfully deployed to ${appName} ${deploymentName}`);
// Send notification (Slack, email, etc.)
notifyDeployment(appName, deploymentName, version, rollout);
} catch (error) {
console.error("❌ Deployment failed:", error.message);
process.exit(1);
}
function notifyDeployment(appName, deployment, version, rollout) {
// Implementation for sending notifications
console.log(
`📱 ${appName} ${deployment} v${version} deployed to ${rollout}% of users`
);
}
```
### Rollback Strategy
```bash
# Check deployment history
code-push deployment history SaayamApp-iOS Production
# Rollback to previous version
code-push rollback SaayamApp-iOS Production
# Rollback to specific label
code-push rollback SaayamApp-iOS Production --targetRelease v1.2.3
```
## Update Policies
### Update Configuration
```typescript
// src/config/updatePolicy.ts
import codePush from "react-native-code-push";
export const UpdatePolicy = {
// Check for updates when app resumes from background
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
// Install updates on next app restart (non-disruptive)
installMode: codePush.InstallMode.ON_NEXT_RESTART,
// For mandatory updates, install immediately
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
// Minimum background duration before checking (in seconds)
minimumBackgroundDuration: 60,
// Update dialog configuration
updateDialog: {
appendReleaseDescription: true,
descriptionPrefix: "\n\nWhat's New:\n",
mandatoryContinueButtonLabel: "Install Now",
mandatoryUpdateMessage:
"A critical update is required to continue using the app.",
optionalIgnoreButtonLabel: "Not Now",
optionalInstallButtonLabel: "Install",
optionalUpdateMessage:
"An update is available with new features and improvements.",
title: "Update Available",
},
};
```
### Conditional Updates
```typescript
// src/utils/conditionalUpdates.ts
import codePush from "react-native-code-push";
import { Platform } from "react-native";
import DeviceInfo from "react-native-device-info";
export class ConditionalUpdates {
static async shouldCheckForUpdate(): Promise<boolean> {
// Don't check for updates in development
if (__DEV__) return false;
// Check device conditions
const batteryLevel = await DeviceInfo.getBatteryLevel();
const isCharging = await DeviceInfo.isBatteryCharging();
const connectionType = await DeviceInfo.getConnectionType();
// Only update if:
// - Battery > 20% OR device is charging
// - Connected to WiFi (for large updates)
const batteryOk = batteryLevel > 0.2 || isCharging;
const connectionOk = connectionType === "wifi";
return batteryOk && connectionOk;
}
static async checkForUpdateWithConditions(): Promise<void> {
const shouldCheck = await this.shouldCheckForUpdate();
if (!shouldCheck) {
console.log("Skipping update check due to device conditions");
return;
}
try {
const update = await codePush.checkForUpdate();
if (update) {
// Check update size
const updateSizeMB = (update.packageSize || 0) / (1024 * 1024);
if (updateSizeMB > 10) {
// Large update - require WiFi and user confirmation
this.promptForLargeUpdate(update, updateSizeMB);
} else {
// Small update - download automatically
this.downloadUpdate(update);
}
}
} catch (error) {
console.error("Error checking for conditional update:", error);
}
}
private static promptForLargeUpdate(
update: codePush.RemotePackage,
sizeMB: number
) {
// Show custom dialog for large updates
// Implementation depends on your UI framework
}
private static async downloadUpdate(update: codePush.RemotePackage) {
try {
await update.download();
await update.install(codePush.InstallMode.ON_NEXT_RESTART);
} catch (error) {
console.error("Error downloading update:", error);
}
}
}
```
## Monitoring & Analytics
### Update Analytics
```typescript
// src/utils/updateAnalytics.ts
import AnalyticsService from "@config/analytics.config";
import codePush from "react-native-code-push";
export class UpdateAnalytics {
static trackUpdateCheck() {
AnalyticsService.logEvent("codepush_check_for_update");
}
static trackUpdateAvailable(update: codePush.RemotePackage) {
AnalyticsService.logEvent("codepush_update_available", {
app_version: update.appVersion,
package_size: update.packageSize,
is_mandatory: update.isMandatory,
deployment_key: update.deploymentKey,
});
}
static trackUpdateDownloadStart(update: codePush.RemotePackage) {
AnalyticsService.logEvent("codepush_download_start", {
app_version: update.appVersion,
package_size: update.packageSize,
});
}
static trackUpdateDownloadComplete(update: codePush.LocalPackage) {
AnalyticsService.logEvent("codepush_download_complete", {
app_version: update.appVersion,
package_size: update.packageSize,
});
}
static trackUpdateInstalled(update: codePush.LocalPackage) {
AnalyticsService.logEvent("codepush_update_installed", {
app_version: update.appVersion,
install_mode: update.installMode,
});
}
static trackUpdateFailed(error: Error, context?: string) {
AnalyticsService.logEvent("codepush_update_failed", {
error_message: error.message,
context: context || "unknown",
});
}
static trackRollback(reason: string) {
AnalyticsService.logEvent("codepush_rollback", {
reason,
});
}
}
```
### Health Monitoring
```typescript
// src/utils/updateHealthMonitor.ts
import codePush from "react-native-code-push";
import CrashlyticsService from "@config/firebase.config";
export class UpdateHealthMonitor {
private static readonly CRASH_THRESHOLD = 3;
private static crashCount = 0;
static async monitorUpdateHealth() {
try {
const updateMetadata = await codePush.getUpdateMetadata();
if (updateMetadata && updateMetadata.isFirstRun) {
// This is the first run after an update
this.trackFirstRunAfterUpdate(updateMetadata);
// Start monitoring for crashes
this.startCrashMonitoring();
}
} catch (error) {
console.error("Error monitoring update health:", error);
}
}
private static trackFirstRunAfterUpdate(update: codePush.LocalPackage) {
CrashlyticsService.log(
`First run after CodePush update: ${update.appVersion}`
);
// Set custom attributes for crash reporting
CrashlyticsService.setUserAttributes({
codepush_version: update.appVersion || "unknown",
codepush_label: update.label || "unknown",
is_codepush_update: "true",
});
}
private static startCrashMonitoring() {
// Monitor app crashes for the next 24 hours
const monitoringDuration = 24 * 60 * 60 * 1000; // 24 hours
setTimeout(() => {
this.evaluateUpdateStability();
}, monitoringDuration);
}
private static async evaluateUpdateStability() {
if (this.crashCount >= this.CRASH_THRESHOLD) {
console.warn(
`Update appears unstable (${this.crashCount} crashes). Consider rollback.`
);
// Optionally trigger automatic rollback
// await this.performAutomaticRollback();
} else {
console.log("Update appears stable");
CrashlyticsService.log("CodePush update validated as stable");
}
}
private static async performAutomaticRollback() {
try {
await codePush.restartApp();
CrashlyticsService.log("Automatic rollback performed due to instability");
} catch (error) {
console.error("Failed to perform automatic rollback:", error);
}
}
static reportCrash() {
this.crashCount++;
CrashlyticsService.log(`Crash reported. Count: ${this.crashCount}`);
}
}
```
## Best Practices
### Update Guidelines
1. **Test Thoroughly**
- Test updates on staging environment
- Verify on multiple devices and OS versions
- Test offline scenarios
2. **Gradual Rollout**
- Start with small percentage (5-10%)
- Monitor crash rates and user feedback
- Gradually increase rollout
3. **Rollback Strategy**
- Have rollback plan ready
- Monitor key metrics after deployment
- Set up automated alerts for issues
4. **Update Frequency**
- Don't update too frequently (user fatigue)
- Bundle related changes together
- Consider user timezone for deployments
5. **Communication**
- Provide clear release notes
- Notify users of important changes
- Use appropriate update messaging

View File

@ -2,12 +2,46 @@
## Complete Guide from Setup to Production Release ## Complete Guide from Setup to Production Release
_Version 1.0 | Last Updated: December 2024_ *Version 1.0 | Last Updated: December 2024*
_Created by Drashti Patel_
Welcome to the comprehensive React Native Development Standard Operating Procedure (SOP). This documentation provides a complete guide for building React Native applications from scratch, covering everything from initial environment setup to production release and post-launch maintenance.
## What You'll Learn
This SOP is designed for developers at all levels and covers:
- **Environment Setup**: Complete development environment configuration for Mac, Windows, and Linux
- **Project Architecture**: Best practices for structuring React Native applications
- **Development Standards**: Code quality, testing, and security guidelines
- **Production Deployment**: Step-by-step release process for both Android and iOS
- **Maintenance**: Post-release monitoring, updates, and optimization
## Who This Is For
- **Beginner Developers**: Step-by-step instructions to get started with React Native
- **Experienced Teams**: Enterprise-level standards and best practices
- **Project Managers**: Understanding of the complete development lifecycle
- **DevOps Engineers**: CI/CD pipeline setup and deployment processes
## Key Features
**Comprehensive Coverage**: From setup to production release
**Platform Support**: Android and iOS deployment guides
**Code Quality**: ESLint, Prettier, and testing configurations
**Security**: Best practices for secure mobile app development
**Performance**: Optimization techniques and monitoring
**Automation**: CI/CD pipelines and automated testing
## Getting Started
Begin with the [Requirements & Environment Setup](01-requirements/environment-setup.md) section to configure your development environment, then follow the guide sequentially for the best learning experience.
## Prerequisites
- Basic knowledge of JavaScript/TypeScript
- Familiarity with React concepts
- Understanding of mobile app development concepts
--- ---
This GitBook provides a **complete SOP** for building, testing, deploying, and maintaining React Native applications. *This SOP serves as the single source of truth for React Native development standards and can be adapted to fit specific project requirements while maintaining core principles of quality, security, and maintainability.*
It covers everything from environment setup to production release and ongoing maintenance.
Use the sidebar to navigate through each stage of the development lifecycle.

40
SUMMARY.md Normal file
View File

@ -0,0 +1,40 @@
# Table of Contents
- [Introduction](README.md)
## Getting Started
- [Requirements & Environment Setup](01-requirements/environment-setup.md)
- [Project Initialization](02-project-initialization/project-setup.md)
## Development
- [Development Standards & Guidelines](03-development-standards/coding-guidelines.md)
- [Components](03-development-standards/components.md)
- [VSCode Snippets](03-development-standards/vscode-snippets.md)
- [Design Integration & Assets](04-design-integration/design-system.md)
- [Asset Management](04-design-integration/assets.md)
- [Icons & Fonts](04-design-integration/icons-fonts.md)
- [State Management & Navigation](05-state-management-navigation/redux-setup.md)
- [Navigation Setup](05-state-management-navigation/navigation.md)
- [API Integration](05-state-management-navigation/api-integration.md)
## Quality Assurance
- [Testing & Quality Assurance](06-testing-quality-assurance/unit-testing.md)
- [E2E Testing](06-testing-quality-assurance/e2e-testing.md)
- [Performance Optimization](07-performance-optimization/optimization.md)
- [Security](08-security/secure-storage.md)
## Deployment
- [CI/CD Pipeline](09-ci-cd-pipeline/github-actions-fastlane.md)
- [Release Process](10-release-process/android-release.md)
- [iOS Release](10-release-process/ios-release.md)
- [Version Management](10-release-process/versioning.md)
## Maintenance
- [Post-Release Maintenance](11-post-release-maintenance/crash-monitoring.md)
- [Analytics](11-post-release-maintenance/analytics.md)
- [OTA Updates](11-post-release-maintenance/ota-updates.md)