This is page 1 of 2. Use http://codebase.md/lallen30/mcp-remote-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── build │ └── index.js ├── package-lock.json ├── package.json ├── README.md ├── resources │ ├── code-examples │ │ └── react-native │ │ ├── assets │ │ │ └── images │ │ │ ├── event.png │ │ │ ├── qr-codeeee13.png │ │ │ └── [email protected] │ │ ├── components │ │ │ ├── Button.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── LoadingSpinner.tsx │ │ │ └── UpdateModal.tsx │ │ ├── config │ │ │ ├── apiConfig.ts │ │ │ └── environment.ts │ │ ├── hooks │ │ │ ├── useAppUpdate.ts │ │ │ └── useForm.ts │ │ ├── navigation │ │ │ ├── AppNavigator.tsx │ │ │ ├── DrawerNavigator.tsx │ │ │ ├── NavigationWrapper.tsx │ │ │ └── TabNavigator.tsx │ │ ├── screens │ │ │ ├── AboutUs.tsx │ │ │ ├── HomeScreen.js │ │ │ ├── HomeScreen.tsx │ │ │ ├── PostLogin │ │ │ │ ├── AboutUs │ │ │ │ │ ├── AboutUsScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── BluestoneAppsAI │ │ │ │ │ ├── BluestoneAppsAIScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── Calendar │ │ │ │ │ ├── CalendarScreen.tsx │ │ │ │ │ ├── EventDetails.tsx │ │ │ │ │ ├── EventDetailsStyles.ts │ │ │ │ │ └── Styles.ts │ │ │ │ ├── ChangePassword │ │ │ │ │ ├── ChangePasswordScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── Contact │ │ │ │ │ ├── ContactScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── CustomerSupport │ │ │ │ │ ├── CustomerSupportScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── EditProfile │ │ │ │ │ ├── EditProfileScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── Home │ │ │ │ │ └── HomeScreen.tsx │ │ │ │ ├── MyProfile │ │ │ │ │ ├── MyProfileScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ └── Posts │ │ │ │ ├── PostScreen.tsx │ │ │ │ ├── PostsScreen.tsx │ │ │ │ ├── PostStyles.ts │ │ │ │ └── Styles.ts │ │ │ └── PreLogin │ │ │ ├── ForgotPassword │ │ │ │ ├── ForgotPasswordScreen.tsx │ │ │ │ └── Styles.ts │ │ │ ├── Legal │ │ │ │ ├── PrivacyPolicyScreen.tsx │ │ │ │ └── TermsAndConditionsScreen.tsx │ │ │ ├── Login │ │ │ │ ├── LoginScreen.tsx │ │ │ │ └── Styles.tsx │ │ │ ├── SignUp │ │ │ │ ├── SignUpScreen.tsx │ │ │ │ └── Styles.ts │ │ │ └── VerifyEmail │ │ │ ├── Styles.ts │ │ │ └── VerifyEmailScreen.tsx │ │ ├── services │ │ │ ├── apiService.ts │ │ │ ├── authService.ts │ │ │ ├── axiosRequest.ts │ │ │ ├── config.ts │ │ │ ├── NavigationMonitorService.ts │ │ │ ├── storageService.ts │ │ │ ├── types.ts │ │ │ ├── UpdateService.ts │ │ │ └── userService.ts │ │ ├── theme │ │ │ ├── colors_original.ts │ │ │ ├── colors.ts │ │ │ ├── README.md │ │ │ ├── theme.ts │ │ │ └── typography.ts │ │ ├── tsconfig.json │ │ ├── types │ │ │ └── react-native.d.ts │ │ └── utils │ │ ├── axiosUtils.ts │ │ └── LogSuppressor.ts │ └── standards │ ├── api_communication.md │ ├── component_design.md │ ├── project_structure.md │ └── state_management.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | dist/ 7 | 8 | # Environment variables 9 | .env 10 | .env.local 11 | .env.*.local 12 | 13 | # Logs 14 | logs/ 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # Editor directories and files 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | .DS_Store 26 | 27 | # Testing 28 | coverage/ 29 | 30 | # Temporary files 31 | .tmp/ 32 | .temp/ 33 | 34 | # Debug 35 | .node-version 36 | .npm/ 37 | 38 | # TypeScript cache 39 | *.tsbuildinfo 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Remote Server (Node.js Version) 2 | 3 | This is a Node.js implementation of the BluestoneApps Coding Standards and Examples MCP server. 4 | 5 | ## Overview 6 | 7 | This MCP server provides access to React Native coding standards and code examples through the Model Context Protocol (MCP). It can be used with MCP clients like Windsurf IDE. 8 | 9 | ## Features 10 | 11 | - Access to React Native coding standards 12 | - Component, hook, screen, service, and theme code examples 13 | - Fuzzy matching for finding examples by name 14 | - Full integration with the MCP protocol 15 | 16 | ## Installation 17 | 18 | 1. Clone this repository 19 | 2. Install dependencies: 20 | ```bash 21 | npm install 22 | ``` 23 | 3. Build the project: 24 | ```bash 25 | npm run build 26 | ``` 27 | 28 | ## Usage 29 | 30 | Start the server: 31 | 32 | ```bash 33 | npm start 34 | ``` 35 | 36 | To use with MCP clients, configure them to connect to this server. 37 | 38 | ## Tools 39 | 40 | The server provides the following tools: 41 | 42 | - `get_project_structure`: Get project structure standards 43 | - `get_api_communication`: Get API communication standards 44 | - `get_component_design`: Get component design standards 45 | - `get_state_management`: Get state management standards 46 | - `get_component_example`: Get a specific component example 47 | - `get_hook_example`: Get a specific hook example 48 | - `get_service_example`: Get a specific service example 49 | - `get_screen_example`: Get a specific screen example 50 | - `get_theme_example`: Get a specific theme example 51 | - `list_available_examples`: List all available code examples 52 | 53 | ## Configuring with MCP Clients 54 | 55 | For Windsurf IDE, update the `mcp_config.json` with: 56 | 57 | ```json 58 | { 59 | "servers": { 60 | "bluestoneapps": { 61 | "command": "node", 62 | "args": ["/path/to/build/index.js"], 63 | "description": "BluestoneApps Coding Standards and Examples", 64 | "displayName": "BluestoneApps MCP Server", 65 | "timeout": 30000 66 | } 67 | } 68 | } 69 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/theme/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # React Native Theme System 2 | 3 | This directory contains the TypeScript theme files for React Native applications. The theme system provides a consistent design language across the application. 4 | 5 | ## Standards 6 | 7 | - All theme files are written in TypeScript (.ts) for better type safety and developer experience 8 | - Theme files export both the values and TypeScript types for proper type checking 9 | - The theme system is modular and composable, with separate files for different aspects of the design system 10 | 11 | ## Files 12 | 13 | - `colors.ts` - Color palette definitions and utility functions 14 | - `typography.ts` - Typography styles, font families, sizes, and text styles 15 | - `theme.ts` - Main theme configuration that combines all theme elements 16 | 17 | ## Usage 18 | 19 | Import the theme elements in your components: 20 | 21 | ```tsx 22 | import { colors } from './theme/colors'; 23 | import typography from './theme/typography'; 24 | import theme from './theme/theme'; 25 | 26 | // Using colors 27 | const styles = StyleSheet.create({ 28 | container: { 29 | backgroundColor: colors.background, 30 | }, 31 | text: { 32 | ...typography.body, 33 | color: colors.textPrimary, 34 | }, 35 | button: { 36 | padding: theme.spacing.md, 37 | borderRadius: theme.borderRadius.md, 38 | }, 39 | }); 40 | ``` 41 | 42 | ## Type Support 43 | 44 | The theme files export TypeScript types to provide autocompletion and type checking: 45 | 46 | ```tsx 47 | import { ColorKey } from './theme/colors'; 48 | import { TypographyStyleKey } from './theme/typography'; 49 | import { SpacingKey, BorderRadiusKey } from './theme/theme'; 50 | 51 | // Type-safe usage 52 | const color: ColorKey = 'primary'; 53 | const textStyle: TypographyStyleKey = 'body'; 54 | const spacing: SpacingKey = 'md'; 55 | ``` 56 | 57 | ## Extending the Theme 58 | 59 | When extending the theme, follow these guidelines: 60 | 61 | 1. Add new values to the appropriate theme file 62 | 2. Ensure all values are properly typed 63 | 3. Use consistent naming conventions 64 | 4. Document any complex values or usage patterns 65 | 5. Update the theme.ts file if necessary to expose new theme elements 66 | 67 | ## Migration from JavaScript 68 | 69 | This theme system has been migrated from JavaScript to TypeScript. All theme files now use the `.ts` extension and include proper type definitions. 70 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "outDir": "./build", 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/CustomerSupport/CustomerSupportScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | import { styles } from './Styles'; 4 | 5 | const CustomerSupportScreen = () => { 6 | return ( 7 | <View style={styles.container}> 8 | <Text style={styles.title}>Customer Support</Text> 9 | <Text style={styles.subtitle}>Coming Soon</Text> 10 | </View> 11 | ); 12 | }; 13 | 14 | export default CustomerSupportScreen; ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/theme/colors.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const colors = { 2 | primary: '#412DA5', 3 | secondary: '#6032FF', 4 | tertiary: '#29243F', 5 | success: '#23AD51', 6 | warning: '#EF7F18', 7 | danger: '#E02D47', 8 | white: '#FFFFFF', 9 | light: '#EEF1F7', 10 | medium: '#747D8B', 11 | dark: '#2D3035', 12 | black: '#070707', 13 | headerBg: '#29243F', 14 | headerFont: '#FFFFFF', 15 | footerBg: '#FFFFFF', 16 | footerFont: '#747D8B', 17 | } as const; ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/config/environment.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const environment = { 2 | server: 'wordpress.betaplanets.com', 3 | production: false, 4 | baseURL: 'https://wordpress.betaplanets.com', 5 | apiURL: 'https://wordpress.betaplanets.com/wp-json' 6 | // server: 'laravelapi2.betaplanets.com/api', 7 | // production: false, 8 | // baseURL: 'https://laravelapi2.betaplanets.com/api', 9 | // apiURL: 'https://laravelapi2.betaplanets.com/api/wp-json' 10 | }; 11 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { View, ActivityIndicator, StyleSheet } from 'react-native'; 3 | 4 | export const LoadingSpinner = () => ( 5 | <View style={styles.container}> 6 | <ActivityIndicator size="large" color="#007AFF" /> 7 | </View> 8 | ); 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | flex: 1, 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | backgroundColor: '#fff' 16 | } 17 | }); 18 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface LoginResponse { 2 | loginInfo: { 3 | token: string; 4 | user_email: string; 5 | user_nicename: string; 6 | user_display_name: string; 7 | [key: string]: any; 8 | }; 9 | } 10 | 11 | export interface LoginCredentials { 12 | email: string; 13 | password: string; 14 | } 15 | 16 | export interface AuthError { 17 | response?: { 18 | data?: { 19 | message?: string; 20 | }; 21 | status?: number; 22 | }; 23 | message?: string; 24 | } 25 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/CustomerSupport/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | 3 | type Styles = { 4 | [key: string]: ViewStyle | TextStyle | ImageStyle; 5 | }; 6 | 7 | export const styles = StyleSheet.create<Styles>({ 8 | container: { 9 | flex: 1, 10 | justifyContent: 'center', 11 | alignItems: 'center', 12 | backgroundColor: '#fff', 13 | }, 14 | title: { 15 | fontSize: 24, 16 | fontWeight: 'bold', 17 | marginBottom: 10, 18 | }, 19 | subtitle: { 20 | fontSize: 16, 21 | color: '#666', 22 | }, 23 | }); 24 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-remote-server", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node build/index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "BluestoneApps Coding Standards and Examples MCP Server", 13 | "dependencies": { 14 | "@modelcontextprotocol/sdk": "^1.1.0", 15 | "glob": "^10.3.10", 16 | "zod": "^3.22.4" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^22.10.5", 20 | "typescript": "^5.7.2" 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "react-native", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "*": ["types/*"] 17 | }, 18 | "typeRoots": ["./types", "./node_modules/@types"] 19 | }, 20 | "include": [ 21 | "**/*.ts", 22 | "**/*.tsx" 23 | ], 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } 28 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/theme/colors_original.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const colors = { 2 | primary: '#007AFF', 3 | secondary: '#50cebb', 4 | tertiary: '#F0A225', 5 | Success: '#2FD36F', 6 | Warning: '#FDC40C', 7 | Danger: '#EC445A', 8 | White: '#FFFFFF', 9 | ultraLight: '#F9F9FC', 10 | light: '#EBEBEB', 11 | mediumLight: '#BEC6C8', 12 | mediumDark: '#707070', 13 | dark: '#222428', 14 | black: '#000000', 15 | text: { 16 | primary: '#2c3e50', 17 | secondary: '#999', 18 | error: '#dc3545', 19 | white: '#fff', 20 | header: '#FFFFFF', 21 | footer: '#96A1A5', 22 | }, 23 | background: { 24 | primary: '#fff', 25 | secondary: '#f8f9fa', 26 | header: '#3B00FF', 27 | footer: '#F9F9FC', 28 | }, 29 | border: { 30 | primary: '#e9ecef', 31 | error: '#dc3545', 32 | }, 33 | } as const; 34 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/AboutUs/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | backgroundColor: colors.primary, 8 | }, 9 | header: { 10 | padding: 16, 11 | borderBottomWidth: 1, 12 | borderBottomColor: colors.light, 13 | backgroundColor: colors.white, 14 | marginBottom: 0 15 | }, 16 | title: { 17 | fontSize: 24, 18 | fontWeight: 'bold', 19 | color: colors.primary, 20 | }, 21 | webViewContainer: { 22 | flex: 1, 23 | backgroundColor: colors.white, 24 | }, 25 | webView: { 26 | flex: 1, 27 | backgroundColor: colors.white, 28 | opacity: 0.99 // Fix for iOS WebView rendering 29 | }, 30 | noContent: { 31 | fontSize: 16, 32 | color: colors.dark, 33 | textAlign: 'center', 34 | marginTop: 20 35 | } 36 | }); 37 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Posts/PostStyles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, Dimensions } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | 6 | export const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | backgroundColor: colors.white, 10 | }, 11 | loadingContainer: { 12 | flex: 1, 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | backgroundColor: colors.white, 16 | }, 17 | errorContainer: { 18 | flex: 1, 19 | justifyContent: 'center', 20 | alignItems: 'center', 21 | padding: 32, 22 | backgroundColor: colors.white, 23 | }, 24 | errorText: { 25 | fontSize: 16, 26 | color: colors.danger, 27 | textAlign: 'center', 28 | }, 29 | featuredImage: { 30 | width: width, 31 | height: width * 0.6, 32 | backgroundColor: colors.secondary, 33 | }, 34 | content: { 35 | padding: 16, 36 | }, 37 | title: { 38 | fontSize: 24, 39 | fontWeight: 'bold', 40 | color: colors.primary, 41 | marginBottom: 8, 42 | }, 43 | date: { 44 | fontSize: 14, 45 | color: colors.dark, 46 | marginBottom: 16, 47 | }, 48 | }); 49 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Calendar/EventDetailsStyles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | export const styles = StyleSheet.create({ 5 | 6 | container: { 7 | flex: 1, 8 | backgroundColor: '#fff', 9 | }, 10 | content: { 11 | flex: 1, 12 | padding: 20, 13 | }, 14 | title: { 15 | fontSize: 24, 16 | fontWeight: 'bold', 17 | color: colors.black, 18 | marginBottom: 10, 19 | }, 20 | date: { 21 | fontSize: 18, 22 | color: colors.secondary, 23 | marginBottom: 5, 24 | }, 25 | time: { 26 | fontSize: 18, 27 | color: colors.secondary, 28 | marginBottom: 20, 29 | }, 30 | section: { 31 | marginBottom: 25, 32 | borderTopWidth: 1, 33 | borderTopColor: colors.light, 34 | paddingTop: 20, 35 | }, 36 | sectionTitle: { 37 | fontSize: 18, 38 | fontWeight: '600', 39 | color: colors.black, 40 | marginBottom: 10, 41 | }, 42 | location: { 43 | fontSize: 16, 44 | color: colors.dark, 45 | lineHeight: 24, 46 | }, 47 | description: { 48 | fontSize: 16, 49 | color: colors.dark, 50 | lineHeight: 24, 51 | }, 52 | price: { 53 | fontSize: 18, 54 | color: colors.secondary, 55 | fontWeight: '500', 56 | marginTop: 5, 57 | }, 58 | }); 59 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/utils/LogSuppressor.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * This file contains utilities to suppress specific console warnings 3 | * that are not relevant to the application's functionality. 4 | */ 5 | 6 | // Original console.warn function 7 | const originalWarn = console.warn; 8 | 9 | // List of warning messages to suppress 10 | const suppressedWarnings = [ 11 | 'TRenderEngineProvider: Support for defaultProps will be removed from function components', 12 | 'MemoizedTNodeRenderer: Support for defaultProps will be removed from memo components', 13 | 'TNodeChildrenRenderer: Support for defaultProps will be removed from function components', 14 | ]; 15 | 16 | // Override console.warn to filter out specific warnings 17 | console.warn = function (message: any, ...optionalParams: any[]) { 18 | // Check if this is a warning we want to suppress 19 | if (typeof message === 'string') { 20 | for (const warningText of suppressedWarnings) { 21 | if (message.includes(warningText)) { 22 | // Skip this warning 23 | return; 24 | } 25 | } 26 | } 27 | 28 | // Pass through to the original console.warn for all other warnings 29 | originalWarn(message, ...optionalParams); 30 | }; 31 | 32 | export {}; // Make this a module 33 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/components/Button.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { 3 | TouchableOpacity, 4 | Text, 5 | StyleSheet, 6 | ViewStyle, 7 | TextStyle, 8 | ActivityIndicator, 9 | } from 'react-native'; 10 | 11 | interface ButtonProps { 12 | title: string; 13 | onPress: () => void; 14 | style?: ViewStyle; 15 | textStyle?: TextStyle; 16 | loading?: boolean; 17 | disabled?: boolean; 18 | } 19 | 20 | export const Button: React.FC<ButtonProps> = ({ 21 | title, 22 | onPress, 23 | style, 24 | textStyle, 25 | loading = false, 26 | disabled = false, 27 | }) => { 28 | return ( 29 | <TouchableOpacity 30 | style={[ 31 | styles.button, 32 | style, 33 | disabled && styles.disabledButton, 34 | ]} 35 | onPress={onPress} 36 | disabled={disabled || loading} 37 | > 38 | {loading ? ( 39 | <ActivityIndicator color="#fff" /> 40 | ) : ( 41 | <Text style={[styles.text, textStyle]}>{title}</Text> 42 | )} 43 | </TouchableOpacity> 44 | ); 45 | }; 46 | 47 | const styles = StyleSheet.create({ 48 | button: { 49 | backgroundColor: '#007AFF', 50 | paddingVertical: 12, 51 | paddingHorizontal: 24, 52 | borderRadius: 8, 53 | alignItems: 'center', 54 | justifyContent: 'center', 55 | }, 56 | text: { 57 | color: '#fff', 58 | fontSize: 16, 59 | fontWeight: '600', 60 | }, 61 | disabledButton: { 62 | opacity: 0.6, 63 | }, 64 | }); 65 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/VerifyEmail/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | 3 | type Styles = { 4 | [key: string]: ViewStyle | TextStyle | ImageStyle; 5 | }; 6 | 7 | export const styles = StyleSheet.create<Styles>({ 8 | container: { 9 | flex: 1, 10 | backgroundColor: '#fff', 11 | padding: 20, 12 | }, 13 | header: { 14 | marginTop: 40, 15 | marginBottom: 30, 16 | }, 17 | title: { 18 | fontSize: 24, 19 | fontWeight: 'bold', 20 | color: '#000', 21 | textAlign: 'center', 22 | marginBottom: 10, 23 | }, 24 | subtitle: { 25 | fontSize: 16, 26 | color: '#666', 27 | textAlign: 'center', 28 | marginBottom: 30, 29 | }, 30 | otpContainer: { 31 | flexDirection: 'row', 32 | justifyContent: 'space-between', 33 | marginBottom: 30, 34 | }, 35 | otpInput: { 36 | width: 50, 37 | height: 50, 38 | borderWidth: 1, 39 | borderColor: '#ddd', 40 | borderRadius: 8, 41 | textAlign: 'center', 42 | fontSize: 20, 43 | color: '#000', 44 | }, 45 | button: { 46 | backgroundColor: '#007AFF', 47 | height: 50, 48 | borderRadius: 8, 49 | justifyContent: 'center', 50 | alignItems: 'center', 51 | marginTop: 20, 52 | }, 53 | buttonText: { 54 | color: '#fff', 55 | fontSize: 16, 56 | fontWeight: 'bold', 57 | }, 58 | backButton: { 59 | marginTop: 20, 60 | alignItems: 'center', 61 | }, 62 | backButtonText: { 63 | color: '#007AFF', 64 | fontSize: 16, 65 | }, 66 | }); 67 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/ForgotPassword/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, Dimensions, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | 6 | type Styles = { 7 | [key: string]: ViewStyle | TextStyle | ImageStyle; 8 | }; 9 | 10 | export const styles = StyleSheet.create({ 11 | container: { 12 | flex: 1, 13 | backgroundColor: colors.white, 14 | padding: 20, 15 | justifyContent: 'center' as const, 16 | }, 17 | logoContainer: { 18 | alignItems: 'center', 19 | marginBottom: 20, 20 | }, 21 | logo: { 22 | width: width * 0.6, 23 | height: 200, 24 | resizeMode: 'contain', 25 | }, 26 | title: { 27 | fontSize: 28, 28 | fontWeight: 'bold', 29 | color: colors.primary, 30 | marginBottom: 10, 31 | textAlign: 'center', 32 | }, 33 | subtitle: { 34 | fontSize: 16, 35 | color: colors.secondary, 36 | marginBottom: 30, 37 | textAlign: 'center', 38 | }, 39 | input: { 40 | backgroundColor: colors.light, 41 | borderRadius: 8, 42 | padding: 15, 43 | marginBottom: 15, 44 | color: colors.black, 45 | }, 46 | button: { 47 | backgroundColor: colors.primary, 48 | borderRadius: 8, 49 | padding: 15, 50 | alignItems: 'center', 51 | marginBottom: 15, 52 | }, 53 | buttonText: { 54 | color: colors.white, 55 | fontSize: 16, 56 | fontWeight: 'bold', 57 | }, 58 | backButton: { 59 | alignItems: 'center', 60 | marginTop: 10, 61 | }, 62 | backButtonText: { 63 | color: colors.primary, 64 | fontSize: 16, 65 | }, 66 | }); 67 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Posts/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | backgroundColor: colors.light, 8 | }, 9 | loadingContainer: { 10 | flex: 1, 11 | justifyContent: 'center', 12 | alignItems: 'center', 13 | backgroundColor: colors.white, 14 | }, 15 | header: { 16 | padding: 16, 17 | backgroundColor: colors.headerBg, 18 | borderBottomWidth: 1, 19 | borderBottomColor: colors.primary, 20 | }, 21 | title: { 22 | fontSize: 24, 23 | fontWeight: 'bold', 24 | color: colors.headerFont, 25 | }, 26 | postsContainer: { 27 | padding: 16, 28 | }, 29 | categorySection: { 30 | marginBottom: 24, 31 | }, 32 | categoryTitle: { 33 | fontSize: 20, 34 | fontWeight: '600', 35 | color: colors.primary, 36 | marginBottom: 12, 37 | }, 38 | postCard: { 39 | backgroundColor: colors.white, 40 | borderRadius: 8, 41 | padding: 16, 42 | marginBottom: 12, 43 | shadowColor: colors.black, 44 | shadowOffset: { 45 | width: 0, 46 | height: 2, 47 | }, 48 | shadowOpacity: 0.1, 49 | shadowRadius: 4, 50 | elevation: 3, 51 | }, 52 | postTitle: { 53 | fontSize: 18, 54 | fontWeight: '500', 55 | color: colors.black, 56 | marginBottom: 8, 57 | }, 58 | postDate: { 59 | fontSize: 14, 60 | color: colors.secondary, 61 | }, 62 | emptyContainer: { 63 | flex: 1, 64 | justifyContent: 'center', 65 | alignItems: 'center', 66 | padding: 32, 67 | }, 68 | emptyText: { 69 | fontSize: 16, 70 | color: colors.secondary, 71 | textAlign: 'center', 72 | }, 73 | }); 74 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/axiosRequest.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios from 'axios'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | import { API } from './config'; 4 | 5 | const axiosRequest = axios.create({ 6 | baseURL: API.BASE_URL, 7 | timeout: 30000, 8 | headers: { 9 | 'Accept': 'application/json' 10 | }, 11 | transformRequest: [(data, headers) => { 12 | // Don't transform FormData 13 | if (data instanceof FormData) { 14 | return data; 15 | } 16 | 17 | // For regular objects, transform to form-urlencoded 18 | if (data && typeof data === 'object') { 19 | headers['Content-Type'] = 'application/x-www-form-urlencoded'; 20 | return Object.entries(data) 21 | .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) 22 | .join('&'); 23 | } 24 | return data; 25 | }] 26 | }); 27 | 28 | // Request interceptor 29 | axiosRequest.interceptors.request.use( 30 | async (config) => { 31 | console.log('Making request to:', config.url); 32 | console.log('Request data:', config.data); 33 | console.log('Request headers:', config.headers); 34 | console.log('Request method:', config.method); 35 | return config; 36 | }, 37 | (error) => { 38 | return Promise.reject(error); 39 | } 40 | ); 41 | 42 | // Response interceptor 43 | axiosRequest.interceptors.response.use( 44 | (response) => { 45 | console.log('API Response:', response.data); 46 | return response; 47 | }, 48 | (error) => { 49 | console.error('API Error:', error.response?.data || error.message); 50 | return Promise.reject(error); 51 | } 52 | ); 53 | 54 | export default axiosRequest; 55 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/utils/axiosUtils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios from 'axios'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | import { API } from '../config/apiConfig'; 4 | 5 | const axiosRequest = axios.create({ 6 | baseURL: API.BASE_URL, 7 | timeout: 30000, 8 | headers: { 9 | 'Accept': 'application/json' 10 | }, 11 | transformRequest: [(data, headers) => { 12 | // Don't transform FormData 13 | if (data instanceof FormData) { 14 | return data; 15 | } 16 | 17 | // For regular objects, transform to form-urlencoded 18 | if (data && typeof data === 'object') { 19 | headers['Content-Type'] = 'application/x-www-form-urlencoded'; 20 | return Object.entries(data) 21 | .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) 22 | .join('&'); 23 | } 24 | return data; 25 | }] 26 | }); 27 | 28 | // Request interceptor 29 | axiosRequest.interceptors.request.use( 30 | async (config) => { 31 | console.log('Making request to:', config.url); 32 | console.log('Request data:', config.data); 33 | console.log('Request headers:', config.headers); 34 | console.log('Request method:', config.method); 35 | return config; 36 | }, 37 | (error) => { 38 | return Promise.reject(error); 39 | } 40 | ); 41 | 42 | // Response interceptor 43 | axiosRequest.interceptors.response.use( 44 | (response) => { 45 | console.log('API Response:', response.data); 46 | return response; 47 | }, 48 | (error) => { 49 | console.error('API Error:', error.response?.data || error.message); 50 | return Promise.reject(error); 51 | } 52 | ); 53 | 54 | export default axiosRequest; 55 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/ChangePassword/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | type Styles = { 5 | [key: string]: ViewStyle | TextStyle | ImageStyle; 6 | }; 7 | 8 | export const styles = StyleSheet.create<Styles>({ 9 | container: { 10 | flex: 1, 11 | backgroundColor: colors.white, 12 | }, 13 | header: { 14 | padding: 20, 15 | paddingTop: 40, 16 | }, 17 | title: { 18 | fontSize: 24, 19 | fontWeight: 'bold', 20 | marginBottom: 10, 21 | }, 22 | backButton: { 23 | color: colors.white, 24 | }, 25 | subtitle: { 26 | fontSize: 16, 27 | color: colors.dark, 28 | marginBottom: 20, 29 | }, 30 | formContainer: { 31 | padding: 20, 32 | }, 33 | inputGroup: { 34 | marginBottom: 20, 35 | }, 36 | label: { 37 | fontSize: 16, 38 | marginBottom: 8, 39 | color: colors.dark, 40 | }, 41 | inputContainer: { 42 | flexDirection: 'row', 43 | alignItems: 'center', 44 | borderWidth: 1, 45 | borderColor: colors.dark, 46 | borderRadius: 8, 47 | paddingHorizontal: 12, 48 | backgroundColor: colors.light, 49 | }, 50 | inputIcon: { 51 | marginRight: 10, 52 | }, 53 | input: { 54 | flex: 1, 55 | height: 48, 56 | fontSize: 16, 57 | color: colors.dark, 58 | }, 59 | errorText: { 60 | color: colors.danger, 61 | fontSize: 14, 62 | marginTop: 5, 63 | }, 64 | button: { 65 | backgroundColor: colors.primary, 66 | borderRadius: 8, 67 | height: 48, 68 | justifyContent: 'center', 69 | alignItems: 'center', 70 | marginTop: 20, 71 | }, 72 | disabledButton: { 73 | opacity: 0.7, 74 | }, 75 | buttonText: { 76 | color: '#fff', 77 | fontSize: 16, 78 | fontWeight: '600', 79 | }, 80 | }); 81 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Contact/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, Dimensions, ViewStyle, TextStyle, ImageStyle, Platform } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | 6 | type Styles = { 7 | [key: string]: ViewStyle | TextStyle | ImageStyle; 8 | }; 9 | 10 | export const styles = StyleSheet.create({ 11 | safeArea: { 12 | flex: 1, 13 | backgroundColor: colors.white, 14 | }, 15 | keyboardAvoidingView: { 16 | flex: 1, 17 | }, 18 | contentContainer: { 19 | flex: 1, 20 | backgroundColor: colors.white, 21 | }, 22 | scrollView: { 23 | flex: 1, 24 | }, 25 | scrollViewContent: { 26 | padding: 20, 27 | }, 28 | formContainer: { 29 | flex: 1, 30 | }, 31 | buttonWrapper: { 32 | backgroundColor: colors.white, 33 | padding: 16, 34 | paddingBottom: Platform.OS === 'ios' ? 0 : 16, 35 | borderTopWidth: 1, 36 | borderTopColor: colors.light, 37 | }, 38 | label: { 39 | fontSize: 16, 40 | fontWeight: '600', 41 | marginBottom: 5, 42 | color: colors.dark, 43 | }, 44 | input: { 45 | height: 50, 46 | borderWidth: 1, 47 | borderColor: colors.dark, 48 | borderRadius: 8, 49 | paddingHorizontal: 15, 50 | marginBottom: 15, 51 | fontSize: 16, 52 | color: colors.dark, 53 | backgroundColor: colors.light, 54 | }, 55 | messageInput: { 56 | height: 120, 57 | textAlignVertical: 'top', 58 | paddingTop: 12, 59 | paddingBottom: 12, 60 | }, 61 | submitButton: { 62 | backgroundColor: colors.primary, 63 | height: 50, 64 | borderRadius: 8, 65 | alignItems: 'center', 66 | justifyContent: 'center', 67 | }, 68 | submitButtonDisabled: { 69 | opacity: 0.7, 70 | }, 71 | submitButtonText: { 72 | color: colors.white, 73 | fontSize: 16, 74 | fontWeight: '600', 75 | }, 76 | }); 77 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/BluestoneAppsAI/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | 3 | type Styles = { 4 | [key: string]: ViewStyle | TextStyle | ImageStyle; 5 | }; 6 | 7 | export const styles = StyleSheet.create<Styles>({ 8 | container: { 9 | flex: 1, 10 | backgroundColor: '#fff', 11 | }, 12 | header: { 13 | padding: 16, 14 | backgroundColor: '#f5f5f5', 15 | }, 16 | title: { 17 | fontSize: 24, 18 | fontWeight: 'bold', 19 | color: '#333', 20 | }, 21 | content: { 22 | flex: 1, 23 | padding: 16, 24 | }, 25 | messageContainer: { 26 | marginBottom: 16, 27 | padding: 16, 28 | backgroundColor: '#f9f9f9', 29 | borderRadius: 8, 30 | borderWidth: 1, 31 | borderColor: '#eee', 32 | }, 33 | inputContainer: { 34 | marginTop: 16, 35 | }, 36 | input: { 37 | borderWidth: 1, 38 | borderColor: '#ddd', 39 | borderRadius: 8, 40 | padding: 12, 41 | marginBottom: 12, 42 | minHeight: 100, 43 | textAlignVertical: 'top', 44 | }, 45 | submitButton: { 46 | backgroundColor: '#007AFF', 47 | padding: 16, 48 | borderRadius: 8, 49 | alignItems: 'center', 50 | }, 51 | submitButtonText: { 52 | color: '#fff', 53 | fontSize: 16, 54 | fontWeight: '600', 55 | }, 56 | loadingText: { 57 | color: '#666', 58 | fontSize: 16, 59 | textAlign: 'center', 60 | padding: 16, 61 | }, 62 | // HTML content styles 63 | htmlContent: { 64 | color: '#333', 65 | fontSize: 16, 66 | lineHeight: 24, 67 | }, 68 | paragraph: { 69 | marginVertical: 8, 70 | color: '#333', 71 | fontSize: 16, 72 | lineHeight: 24, 73 | }, 74 | heading1: { 75 | fontSize: 24, 76 | fontWeight: 'bold', 77 | color: '#333', 78 | marginVertical: 12, 79 | }, 80 | heading2: { 81 | fontSize: 20, 82 | fontWeight: 'bold', 83 | color: '#333', 84 | marginVertical: 10, 85 | }, 86 | }); 87 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Calendar/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | backgroundColor: colors.white, 8 | }, 9 | calendar: { 10 | marginBottom: 0, 11 | }, 12 | contentContainer: { 13 | flex: 1, 14 | backgroundColor: colors.white, 15 | }, 16 | sectionHeader: { 17 | backgroundColor: colors.white, 18 | borderBottomWidth: 1, 19 | borderTopWidth: 1, 20 | borderColor: colors.light, 21 | }, 22 | sectionTitle: { 23 | color: colors.black, 24 | textAlign: 'center', 25 | }, 26 | titleContainer: { 27 | backgroundColor: colors.light, 28 | margin: 15, 29 | borderRadius: 8, 30 | shadowColor: colors.black, 31 | shadowOffset: { 32 | width: 0, 33 | height: 1, 34 | }, 35 | shadowOpacity: 0.1, 36 | shadowRadius: 2, 37 | elevation: 2, 38 | }, 39 | eventsContainer: { 40 | flex: 1, 41 | paddingHorizontal: 15, 42 | }, 43 | eventCard: { 44 | backgroundColor: colors.white, 45 | padding: 15, 46 | marginBottom: 12, 47 | borderRadius: 10, 48 | shadowColor: colors.black, 49 | shadowOffset: { 50 | width: 0, 51 | height: 2, 52 | }, 53 | shadowOpacity: 0.1, 54 | shadowRadius: 3, 55 | elevation: 3, 56 | borderWidth: 1, 57 | borderColor: colors.light, 58 | }, 59 | eventTitle: { 60 | fontSize: 16, 61 | fontWeight: 'bold', 62 | color: colors.black, 63 | marginBottom: 8, 64 | }, 65 | eventTime: { 66 | fontSize: 14, 67 | color: colors.secondary, 68 | marginBottom: 8, 69 | fontWeight: '500', 70 | }, 71 | eventLocation: { 72 | fontSize: 14, 73 | color: colors.dark, 74 | lineHeight: 20, 75 | }, 76 | noEventsText: { 77 | textAlign: 'center', 78 | color: colors.dark, 79 | fontSize: 15, 80 | paddingVertical: 20, 81 | fontStyle: 'italic', 82 | }, 83 | }); 84 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Home/HomeScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import useAppUpdate from '../../../hooks/useAppUpdate'; 4 | import { useFocusEffect } from '@react-navigation/native'; 5 | 6 | const HomeScreen = () => { 7 | // Get the checkForUpdates function from the useAppUpdate hook 8 | // Use a shorter interval for the home screen (30 minutes) 9 | const { checkForUpdates } = useAppUpdate(false, 30 * 60 * 1000); 10 | 11 | // Use a ref to track if we're already checking for updates 12 | const isCheckingRef = useRef(false); 13 | 14 | // Track if this is the first render 15 | const [hasCheckedOnMount, setHasCheckedOnMount] = useState(false); 16 | 17 | // Check for updates once when the component mounts 18 | useEffect(() => { 19 | if (!hasCheckedOnMount && !isCheckingRef.current) { 20 | isCheckingRef.current = true; 21 | 22 | // Run the update check silently and forced 23 | checkForUpdates(true, true).finally(() => { 24 | isCheckingRef.current = false; 25 | setHasCheckedOnMount(true); 26 | }); 27 | } 28 | }, [checkForUpdates, hasCheckedOnMount]); 29 | 30 | // We're not using useFocusEffect anymore to prevent multiple checks 31 | // when the screen is repeatedly focused 32 | 33 | return ( 34 | <View style={styles.container}> 35 | <Text style={styles.welcomeText}>Welcome to the Bluestone Apps Home screen</Text> 36 | </View> 37 | ); 38 | }; 39 | 40 | const styles = StyleSheet.create({ 41 | container: { 42 | flex: 1, 43 | justifyContent: 'center', 44 | alignItems: 'center', 45 | backgroundColor: '#fff', 46 | padding: 20, 47 | }, 48 | welcomeText: { 49 | fontSize: 24, 50 | fontWeight: '600', 51 | color: '#333', 52 | textAlign: 'center', 53 | }, 54 | }); 55 | 56 | export default HomeScreen; 57 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/config/apiConfig.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { environment } from './environment'; 2 | 3 | interface AppConstType { 4 | APP_NAME: string; 5 | VERSION: string; 6 | } 7 | 8 | interface APIEndpoints { 9 | LOGIN: string; 10 | MOBILEAPI: string; 11 | GET_PROFILE: string; 12 | UPDATE_PROFILE: string; 13 | CHANGE_PASSWORD: string; 14 | GET_ABOUTUS: string; 15 | DELETE_USER: string; 16 | GET_EVENTS: string; 17 | GET_AI_API_URL: string; 18 | GET_AI_WELCOME: string; 19 | SAVE_AI_QNA: string; 20 | ASK_BLUESTONEAI: string; 21 | GET_AI_CREDENTIALS: string; 22 | GET_TERMS_DATE: string; 23 | GET_PRIVACY_DATE: string; 24 | GET_TERMS_PAGE: string; 25 | GET_PRIVACY_PAGE: string; 26 | APP_VERSION: string; 27 | } 28 | 29 | interface APIConfig { 30 | BASE_URL: string; 31 | ENDPOINTS: APIEndpoints; 32 | } 33 | 34 | export const AppConst: AppConstType = { 35 | APP_NAME: 'LA React Native', 36 | VERSION: '1.5.0', 37 | }; 38 | 39 | export const API: APIConfig = { 40 | BASE_URL: environment.baseURL, 41 | ENDPOINTS: { 42 | LOGIN: 'wp-json/jwt-auth/v1/token', 43 | MOBILEAPI: 'wp-json/mobileapi/v1', 44 | GET_PROFILE: 'wp-json/mobileapi/v1/getProfile', 45 | UPDATE_PROFILE: 'wp-json/mobileapi/v1/updateProfile', 46 | CHANGE_PASSWORD: 'updatePassword', 47 | GET_ABOUTUS: 'get_aboutus', 48 | DELETE_USER: 'delete_user', 49 | GET_EVENTS: 'wp-json/mobileapi/v1/getEvents', 50 | GET_AI_API_URL: 'get_ai_api_url', 51 | GET_AI_WELCOME: 'get_ai_welcome', 52 | SAVE_AI_QNA: 'save_ai_qna', 53 | ASK_BLUESTONEAI: 'ask_bluestoneai', 54 | GET_AI_CREDENTIALS: 'get_ai_credentials', 55 | GET_TERMS_DATE: 'wp-json/mobileapi/v1/getTermsPublishedDate', 56 | GET_PRIVACY_DATE: 'wp-json/mobileapi/v1/getPrivacyPublishedDate', 57 | GET_TERMS_PAGE: 'wp-json/mobileapi/v1/getTermsPage', 58 | GET_PRIVACY_PAGE: 'wp-json/mobileapi/v1/getPrivacyPage', 59 | APP_VERSION: 'wp-json/mobileapi/v1/app_version' 60 | } 61 | }; 62 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { environment } from '../config/environment'; 2 | 3 | interface AppConstType { 4 | APP_NAME: string; 5 | VERSION: string; 6 | } 7 | 8 | interface APIEndpoints { 9 | LOGIN: string; 10 | MOBILEAPI: string; 11 | GET_PROFILE: string; 12 | UPDATE_PROFILE: string; 13 | CHANGE_PASSWORD: string; 14 | GET_ABOUTUS: string; 15 | DELETE_USER: string; 16 | GET_EVENTS: string; 17 | GET_AI_API_URL: string; 18 | GET_AI_WELCOME: string; 19 | SAVE_AI_QNA: string; 20 | ASK_BLUESTONEAI: string; 21 | GET_AI_CREDENTIALS: string; 22 | GET_TERMS_DATE: string; 23 | GET_PRIVACY_DATE: string; 24 | GET_TERMS_PAGE: string; 25 | GET_PRIVACY_PAGE: string; 26 | APP_VERSION: string; 27 | } 28 | 29 | interface APIConfig { 30 | BASE_URL: string; 31 | ENDPOINTS: APIEndpoints; 32 | } 33 | 34 | export const AppConst: AppConstType = { 35 | APP_NAME: 'LA React Native', 36 | VERSION: '1.5.0', 37 | }; 38 | 39 | export const API: APIConfig = { 40 | BASE_URL: environment.baseURL, 41 | ENDPOINTS: { 42 | LOGIN: 'wp-json/jwt-auth/v1/token', 43 | MOBILEAPI: 'wp-json/mobileapi/v1', 44 | GET_PROFILE: 'wp-json/mobileapi/v1/getProfile', 45 | UPDATE_PROFILE: 'wp-json/mobileapi/v1/updateProfile', 46 | CHANGE_PASSWORD: 'updatePassword', 47 | GET_ABOUTUS: 'get_aboutus', 48 | DELETE_USER: 'delete_user', 49 | GET_EVENTS: 'wp-json/mobileapi/v1/getEvents', 50 | GET_AI_API_URL: 'get_ai_api_url', 51 | GET_AI_WELCOME: 'get_ai_welcome', 52 | SAVE_AI_QNA: 'save_ai_qna', 53 | ASK_BLUESTONEAI: 'ask_bluestoneai', 54 | GET_AI_CREDENTIALS: 'get_ai_credentials', 55 | GET_TERMS_DATE: 'wp-json/mobileapi/v1/getTermsPublishedDate', 56 | GET_PRIVACY_DATE: 'wp-json/mobileapi/v1/getPrivacyPublishedDate', 57 | GET_TERMS_PAGE: 'wp-json/mobileapi/v1/getTermsPage', 58 | GET_PRIVACY_PAGE: 'wp-json/mobileapi/v1/getPrivacyPage', 59 | APP_VERSION: 'wp-json/mobileapi/v1/app_version' 60 | } 61 | }; 62 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/Login/Styles.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, Dimensions } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | 6 | export const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | backgroundColor: colors.white, 10 | padding: 20, 11 | justifyContent: 'center', 12 | }, 13 | logoContainer: { 14 | alignItems: 'center', 15 | marginBottom: 20, 16 | }, 17 | logo: { 18 | width: width * 0.6, 19 | height: 300, 20 | }, 21 | title: { 22 | fontSize: 28, 23 | fontWeight: 'bold', 24 | color: colors.primary, 25 | marginBottom: 30, 26 | textAlign: 'center', 27 | }, 28 | input: { 29 | height: 50, 30 | borderWidth: 1, 31 | borderColor: colors.dark, 32 | borderRadius: 8, 33 | paddingHorizontal: 15, 34 | marginBottom: 15, 35 | fontSize: 16, 36 | color: colors.dark, 37 | backgroundColor: colors.light, 38 | }, 39 | passwordContainer: { 40 | position: 'relative', 41 | marginBottom: 15, 42 | }, 43 | eyeIcon: { 44 | position: 'absolute', 45 | right: 15, 46 | top: 13, 47 | }, 48 | checkboxContainer: { 49 | flexDirection: 'row', 50 | alignItems: 'center', 51 | marginBottom: 20, 52 | }, 53 | customCheckbox: { 54 | width: 24, 55 | height: 24, 56 | borderRadius: 4, 57 | borderWidth: 2, 58 | borderColor: colors.primary, 59 | marginRight: 10, 60 | justifyContent: 'center', 61 | alignItems: 'center', 62 | }, 63 | customCheckboxChecked: { 64 | backgroundColor: colors.primary, 65 | }, 66 | checkboxLabel: { 67 | fontSize: 16, 68 | color: colors.dark, 69 | }, 70 | button: { 71 | backgroundColor: colors.primary, 72 | height: 50, 73 | borderRadius: 8, 74 | justifyContent: 'center', 75 | alignItems: 'center', 76 | }, 77 | buttonText: { 78 | color: colors.white, 79 | fontSize: 18, 80 | fontWeight: '600', 81 | }, 82 | linkContainer: { 83 | flexDirection: 'row', 84 | justifyContent: 'space-between', 85 | marginTop: 20, 86 | }, 87 | link: { 88 | color: colors.secondary, 89 | fontSize: 16, 90 | }, 91 | }); 92 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/storageService.ts: -------------------------------------------------------------------------------- ```typescript 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | class StorageService { 4 | async set(key: string, value: any): Promise<void> { 5 | try { 6 | await AsyncStorage.setItem(key, JSON.stringify(value)); 7 | } catch (error) { 8 | console.error('Error saving to storage:', error); 9 | throw error; 10 | } 11 | } 12 | 13 | async get(key: string): Promise<any> { 14 | try { 15 | const item = await AsyncStorage.getItem(key); 16 | return item ? JSON.parse(item) : null; 17 | } catch (error) { 18 | console.error('Error reading from storage:', error); 19 | throw error; 20 | } 21 | } 22 | 23 | async remove(key: string): Promise<void> { 24 | try { 25 | await AsyncStorage.removeItem(key); 26 | } catch (error) { 27 | console.error('Error removing from storage:', error); 28 | throw error; 29 | } 30 | } 31 | 32 | async clearAll(): Promise<void> { 33 | try { 34 | // Get all keys first 35 | const keys = await AsyncStorage.getAllKeys(); 36 | 37 | if (keys.length === 0) { 38 | return; // Nothing to clear 39 | } 40 | 41 | // Try multiRemove first as it's more efficient 42 | try { 43 | await AsyncStorage.multiRemove(keys); 44 | return; 45 | } catch (multiError) { 46 | console.warn('Error with multiRemove, falling back to individual removal:', multiError); 47 | 48 | // Fall back to removing items one by one 49 | for (const key of keys) { 50 | try { 51 | await AsyncStorage.removeItem(key); 52 | } catch (individualError) { 53 | console.warn(`Failed to remove item with key: ${key}`, individualError); 54 | // Continue with other keys 55 | } 56 | } 57 | } 58 | } catch (error) { 59 | console.error('Error clearing storage:', error); 60 | // Don't throw the error, just log it 61 | } 62 | } 63 | } 64 | 65 | export const storageService = new StorageService(); 66 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/EditProfile/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | type Styles = { 5 | [key: string]: ViewStyle | TextStyle | ImageStyle; 6 | }; 7 | 8 | export const styles = StyleSheet.create<Styles>({ 9 | container: { 10 | flex: 1, 11 | backgroundColor: '#fff', 12 | }, 13 | header: { 14 | alignItems: 'center', 15 | padding: 20, 16 | backgroundColor: colors.light, 17 | }, 18 | avatarContainer: { 19 | width: 120, 20 | height: 120, 21 | borderRadius: 60, 22 | overflow: 'hidden', 23 | backgroundColor: colors.light, 24 | position: 'relative', 25 | }, 26 | profileImage: { 27 | width: '100%', 28 | height: '100%', 29 | }, 30 | cameraIconContainer: { 31 | position: 'absolute', 32 | bottom: 0, 33 | right: 0, 34 | backgroundColor: colors.primary, 35 | width: 36, 36 | height: 36, 37 | borderRadius: 18, 38 | justifyContent: 'center', 39 | alignItems: 'center', 40 | }, 41 | formContainer: { 42 | padding: 20, 43 | }, 44 | inputGroup: { 45 | marginBottom: 20, 46 | }, 47 | label: { 48 | fontSize: 14, 49 | color: '#666', 50 | marginBottom: 8, 51 | }, 52 | inputContainer: { 53 | flexDirection: 'row', 54 | alignItems: 'center', 55 | borderWidth: 1, 56 | borderColor: colors.dark, 57 | borderRadius: 8, 58 | paddingHorizontal: 12, 59 | backgroundColor: colors.light, 60 | }, 61 | inputIcon: { 62 | marginRight: 10, 63 | }, 64 | input: { 65 | flex: 1, 66 | height: 48, 67 | fontSize: 16, 68 | color: colors.black, 69 | backgroundColor: colors.light, 70 | }, 71 | errorText: { 72 | color: colors.danger, 73 | fontSize: 12, 74 | marginTop: 4, 75 | }, 76 | buttonContainer: { 77 | padding: 20, 78 | paddingBottom: 40, 79 | }, 80 | button: { 81 | backgroundColor: colors.primary, 82 | padding: 15, 83 | borderRadius: 8, 84 | alignItems: 'center', 85 | }, 86 | disabledButton: { 87 | opacity: 0.7, 88 | }, 89 | buttonText: { 90 | color: colors.white, 91 | fontSize: 16, 92 | fontWeight: '600', 93 | }, 94 | backButton: { 95 | color: colors.white, 96 | } 97 | }); 98 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/SignUp/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, Dimensions } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | 6 | export const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | backgroundColor: colors.white, 10 | padding: 20, 11 | }, 12 | scrollContainer: { 13 | flexGrow: 1, 14 | justifyContent: 'center', 15 | }, 16 | logoContainer: { 17 | alignItems: 'center', 18 | marginBottom: 0, 19 | }, 20 | logo: { 21 | width: width * 0.6, 22 | height: 200, 23 | resizeMode: 'contain', 24 | }, 25 | title: { 26 | fontSize: 28, 27 | fontWeight: 'bold', 28 | color: colors.primary, 29 | marginBottom: 30, 30 | textAlign: 'center', 31 | }, 32 | input: { 33 | height: 50, 34 | borderWidth: 1, 35 | borderColor: colors.dark, 36 | borderRadius: 8, 37 | paddingHorizontal: 15, 38 | marginBottom: 15, 39 | fontSize: 16, 40 | backgroundColor: colors.light, 41 | color: colors.primary, 42 | }, 43 | errorText: { 44 | color: colors.danger, 45 | fontSize: 14, 46 | marginTop: -10, 47 | marginBottom: 10, 48 | marginLeft: 5, 49 | }, 50 | checkboxContainer: { 51 | flexDirection: 'row', 52 | alignItems: 'center', 53 | marginBottom: 20, 54 | }, 55 | customCheckbox: { 56 | width: 20, 57 | height: 20, 58 | borderRadius: 4, 59 | borderWidth: 2, 60 | borderColor: colors.dark, 61 | marginRight: 8, 62 | justifyContent: 'center', 63 | alignItems: 'center', 64 | }, 65 | customCheckboxChecked: { 66 | backgroundColor: colors.primary, 67 | }, 68 | checkboxLabel: { 69 | flex: 1, 70 | marginLeft: 8, 71 | color: colors.primary, 72 | }, 73 | button: { 74 | backgroundColor: colors.primary, 75 | height: 50, 76 | borderRadius: 8, 77 | justifyContent: 'center', 78 | alignItems: 'center', 79 | marginBottom: 20, 80 | }, 81 | buttonText: { 82 | color: colors.white, 83 | fontSize: 18, 84 | fontWeight: '600', 85 | }, 86 | linkContainer: { 87 | flexDirection: 'row', 88 | justifyContent: 'center', 89 | marginTop: 10, 90 | }, 91 | linkText: { 92 | color: colors.primary, 93 | fontSize: 16, 94 | }, 95 | link: { 96 | color: colors.primary, 97 | textDecorationLine: 'underline', 98 | fontSize: 16, 99 | marginLeft: 5, 100 | }, 101 | }); 102 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/authService.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axiosRequest from './axiosRequest'; 2 | import {API} from './config'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import { LoginResponse, AuthError } from './types'; 5 | 6 | class AuthService { 7 | async login(email: string, password: string): Promise<LoginResponse> { 8 | try { 9 | console.log('Attempting login with:', { email }); 10 | 11 | const data = { 12 | email: email.trim(), 13 | password: password 14 | }; 15 | 16 | console.log('Making request to:', API.ENDPOINTS.LOGIN, 'with data:', data); 17 | 18 | const response = await axiosRequest.post<LoginResponse>( 19 | API.ENDPOINTS.LOGIN, 20 | data 21 | ); 22 | 23 | console.log('Login response:', response); 24 | 25 | if (response?.loginInfo?.token) { 26 | await AsyncStorage.setItem('userToken', response.loginInfo.token); 27 | // Store data in the correct format expected by the profile screen 28 | const userData = { loginInfo: response.loginInfo }; 29 | console.log('Storing userData in AuthService:', userData); 30 | await AsyncStorage.setItem('userData', JSON.stringify(userData)); 31 | } 32 | 33 | return response; 34 | } catch (error) { 35 | console.error('Login error:', error); 36 | throw error as AuthError; 37 | } 38 | } 39 | 40 | async logout(): Promise<void> { 41 | try { 42 | // Try to remove items individually first 43 | const keys = ['userToken', 'userData', 'rememberMe']; 44 | 45 | for (const key of keys) { 46 | try { 47 | await AsyncStorage.removeItem(key); 48 | } catch (err) { 49 | console.warn(`Error removing ${key}:`, err); 50 | // Continue with other keys even if one fails 51 | } 52 | } 53 | 54 | // If we're still here, try to perform any navigation or state resets 55 | return; 56 | } catch (error) { 57 | console.error('Logout error:', error); 58 | // Don't throw the error, just log it and continue 59 | // This prevents the error from bubbling up to the UI 60 | return; 61 | } 62 | } 63 | 64 | // Add other auth-related methods here 65 | } 66 | 67 | export default new AuthService(); 68 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/MyProfile/Styles.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | type Styles = { 5 | [key: string]: ViewStyle | TextStyle | ImageStyle; 6 | }; 7 | 8 | export const styles = StyleSheet.create<Styles>({ 9 | container: { 10 | flex: 1, 11 | backgroundColor: colors.white, 12 | }, 13 | centerContent: { 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | }, 17 | header: { 18 | alignItems: 'center', 19 | padding: 20, 20 | backgroundColor: colors.light, 21 | }, 22 | avatarContainer: { 23 | width: 120, 24 | height: 120, 25 | borderRadius: 60, 26 | overflow: 'hidden', 27 | marginBottom: 15, 28 | backgroundColor: colors.primary, 29 | }, 30 | profileImage: { 31 | width: '100%', 32 | height: '100%', 33 | }, 34 | name: { 35 | fontSize: 24, 36 | fontWeight: '600', 37 | color: '#000', 38 | marginBottom: 5, 39 | }, 40 | location: { 41 | fontSize: 16, 42 | color: colors.dark, 43 | marginTop: 4, 44 | }, 45 | infoSection: { 46 | padding: 20, 47 | backgroundColor: colors.white, 48 | borderRadius: 8, 49 | margin: 16, 50 | shadowColor: colors.black, 51 | shadowOffset: { 52 | width: 0, 53 | height: 2, 54 | }, 55 | shadowOpacity: 0.1, 56 | shadowRadius: 4, 57 | elevation: 3, 58 | }, 59 | infoItem: { 60 | flexDirection: 'row', 61 | alignItems: 'center', 62 | paddingVertical: 12, 63 | borderBottomWidth: 1, 64 | borderBottomColor: colors.light, 65 | }, 66 | icon: { 67 | marginRight: 15, 68 | }, 69 | label: { 70 | fontSize: 14, 71 | color: '#666', 72 | marginBottom: 4, 73 | }, 74 | value: { 75 | fontSize: 16, 76 | color: '#000', 77 | }, 78 | buttonContainer: { 79 | padding: 20, 80 | paddingTop: 0, 81 | }, 82 | button: { 83 | backgroundColor: colors.primary, 84 | borderRadius: 8, 85 | padding: 15, 86 | alignItems: 'center', 87 | marginBottom: 10, 88 | }, 89 | secondaryButton: { 90 | backgroundColor: colors.secondary, 91 | borderWidth: 1, 92 | borderColor: colors.light, 93 | }, 94 | buttonText: { 95 | color: colors.white, 96 | fontSize: 16, 97 | fontWeight: '600', 98 | }, 99 | secondaryButtonText: { 100 | color: colors.white, 101 | }, 102 | errorText: { 103 | fontSize: 16, 104 | color: '#ff3b30', 105 | textAlign: 'center', 106 | }, 107 | }); 108 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/types/react-native.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Type declarations for React Native 3 | * 4 | * This file provides type definitions for React Native components and APIs 5 | * used in the theme examples. This allows TypeScript to understand React Native 6 | * types without requiring the full React Native package to be installed. 7 | */ 8 | 9 | declare module 'react-native' { 10 | export interface TextStyle { 11 | fontFamily?: string; 12 | fontSize?: number; 13 | fontStyle?: 'normal' | 'italic'; 14 | fontWeight?: string | number; 15 | letterSpacing?: number; 16 | lineHeight?: number; 17 | textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify'; 18 | textDecorationLine?: 'none' | 'underline' | 'line-through' | 'underline line-through'; 19 | textDecorationStyle?: 'solid' | 'double' | 'dotted' | 'dashed'; 20 | textDecorationColor?: string; 21 | textTransform?: 'none' | 'capitalize' | 'uppercase' | 'lowercase'; 22 | color?: string; 23 | includeFontPadding?: boolean; 24 | textAlignVertical?: 'auto' | 'top' | 'bottom' | 'center'; 25 | fontVariant?: string[]; 26 | writingDirection?: 'auto' | 'ltr' | 'rtl'; 27 | } 28 | 29 | export interface ViewStyle { 30 | backgroundColor?: string; 31 | borderBottomColor?: string; 32 | borderBottomEndRadius?: number; 33 | borderBottomLeftRadius?: number; 34 | borderBottomRightRadius?: number; 35 | borderBottomStartRadius?: number; 36 | borderBottomWidth?: number; 37 | borderColor?: string; 38 | borderEndColor?: string; 39 | borderEndWidth?: number; 40 | borderLeftColor?: string; 41 | borderLeftWidth?: number; 42 | borderRadius?: number; 43 | borderRightColor?: string; 44 | borderRightWidth?: number; 45 | borderStartColor?: string; 46 | borderStartWidth?: number; 47 | borderStyle?: 'solid' | 'dotted' | 'dashed'; 48 | borderTopColor?: string; 49 | borderTopEndRadius?: number; 50 | borderTopLeftRadius?: number; 51 | borderTopRightRadius?: number; 52 | borderTopStartRadius?: number; 53 | borderTopWidth?: number; 54 | borderWidth?: number; 55 | opacity?: number; 56 | elevation?: number; 57 | shadowColor?: string; 58 | shadowOffset?: { width: number; height: number }; 59 | shadowOpacity?: number; 60 | shadowRadius?: number; 61 | } 62 | 63 | export const Platform: { 64 | OS: 'ios' | 'android' | 'web'; 65 | select: <T extends Record<string, any>>(obj: T) => T[keyof T]; 66 | }; 67 | } 68 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/apiService.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { storageService } from './storageService'; 2 | import { environment } from '../config/environment'; 3 | 4 | const BASE_URL = `https://${environment.server}/`; 5 | const MOBILE_API = `${BASE_URL}wp-json/mobileapi/v1/`; 6 | 7 | interface ApiResponse { 8 | status: string; 9 | errormsg: string; 10 | error_code: string; 11 | categories: Array<{ 12 | category_id: number; 13 | category_name: string; 14 | posts: Array<{ 15 | post_id: number; 16 | post_title: string; 17 | post_content: string; 18 | post_date: string; 19 | featured_image?: string; 20 | }>; 21 | }>; 22 | } 23 | 24 | class ApiService { 25 | async getData(endpoint: string) { 26 | try { 27 | console.log('Fetching from:', MOBILE_API + endpoint); 28 | const response = await fetch(MOBILE_API + endpoint); 29 | if (!response.ok) { 30 | throw new Error(`HTTP error! status: ${response.status}`); 31 | } 32 | const data = await response.json(); 33 | return data; 34 | } catch (error) { 35 | console.error('Error fetching data:', error); 36 | throw error; 37 | } 38 | } 39 | 40 | async sendData(endpoint: string, data: any) { 41 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 42 | const payload = { 43 | ...(typeof data === 'object' ? data : { value: data }), 44 | timezone, 45 | }; 46 | 47 | try { 48 | const response = await fetch(MOBILE_API + endpoint, { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify(payload), 54 | }); 55 | 56 | if (!response.ok) { 57 | throw new Error(`HTTP error! status: ${response.status}`); 58 | } 59 | 60 | const result = await response.json(); 61 | return result; 62 | } catch (error) { 63 | console.error('Error sending data:', error); 64 | throw error; 65 | } 66 | } 67 | 68 | async fetchAndStorePosts() { 69 | try { 70 | const response = await this.getData('getPostsByCategories') as ApiResponse; 71 | console.log('API Response:', response); 72 | 73 | if (response?.status === 'ok' && response?.categories) { 74 | await storageService.set('posts', response.categories); 75 | return response.categories; 76 | } 77 | return null; 78 | } catch (error) { 79 | console.error('Error fetching posts:', error); 80 | throw error; 81 | } 82 | } 83 | } 84 | 85 | export const apiService = new ApiService(); 86 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/navigation/AppNavigator.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { NavigationContainer } from '@react-navigation/native'; 3 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 4 | import LoginScreen from '../screens/PreLogin/Login/LoginScreen'; 5 | 6 | import ForgotPasswordScreen from '../screens/PreLogin/ForgotPassword/ForgotPasswordScreen'; 7 | import SignUpScreen from '../screens/PreLogin/SignUp/SignUpScreen'; 8 | import VerifyEmailScreen from '../screens/PreLogin/VerifyEmail/VerifyEmailScreen'; 9 | import DrawerNavigator from './DrawerNavigator'; 10 | import TermsAndConditionsScreen from '../screens/PreLogin/Legal/TermsAndConditionsScreen'; 11 | import PrivacyPolicyScreen from '../screens/PreLogin/Legal/PrivacyPolicyScreen'; 12 | import { withNavigationWrapper } from './NavigationWrapper'; 13 | 14 | const Stack = createNativeStackNavigator(); 15 | 16 | // Wrap components with navigation wrapper 17 | const WrappedLoginScreen = withNavigationWrapper(LoginScreen); 18 | const WrappedSignUpScreen = withNavigationWrapper(SignUpScreen); 19 | const WrappedForgotPasswordScreen = withNavigationWrapper(ForgotPasswordScreen); 20 | const WrappedVerifyEmailScreen = withNavigationWrapper(VerifyEmailScreen); 21 | const WrappedTermsAndConditionsScreen = withNavigationWrapper(TermsAndConditionsScreen); 22 | const WrappedPrivacyPolicyScreen = withNavigationWrapper(PrivacyPolicyScreen); 23 | 24 | const AppNavigator = () => { 25 | return ( 26 | <NavigationContainer> 27 | <Stack.Navigator 28 | initialRouteName="Login" 29 | screenOptions={{ 30 | headerShown: false, 31 | gestureEnabled: true, 32 | animation: 'default' 33 | }}> 34 | <Stack.Screen 35 | name="Login" 36 | component={WrappedLoginScreen} 37 | /> 38 | <Stack.Screen 39 | name="DrawerNavigator" 40 | component={DrawerNavigator} 41 | /> 42 | <Stack.Screen 43 | name="SignUp" 44 | component={WrappedSignUpScreen} 45 | options={{ 46 | presentation: 'card' 47 | }} 48 | /> 49 | <Stack.Screen 50 | name="ForgotPassword" 51 | component={WrappedForgotPasswordScreen} 52 | options={{ 53 | presentation: 'card' 54 | }} 55 | /> 56 | <Stack.Screen 57 | name="VerifyEmail" 58 | component={WrappedVerifyEmailScreen} 59 | options={{ 60 | presentation: 'card' 61 | }} 62 | /> 63 | <Stack.Screen 64 | name="TermsAndConditions" 65 | component={WrappedTermsAndConditionsScreen} 66 | options={{ 67 | presentation: 'modal' 68 | }} 69 | /> 70 | <Stack.Screen 71 | name="PrivacyPolicy" 72 | component={WrappedPrivacyPolicyScreen} 73 | options={{ 74 | presentation: 'modal' 75 | }} 76 | /> 77 | </Stack.Navigator> 78 | </NavigationContainer> 79 | ); 80 | }; 81 | 82 | export default AppNavigator; 83 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Posts/PostScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { View, Text, ScrollView, Image, ActivityIndicator, useWindowDimensions } from 'react-native'; 3 | import { useNavigation, useRoute } from '@react-navigation/native'; 4 | import RenderHtml from 'react-native-render-html'; 5 | import { storageService } from '../../../helper/storageService'; 6 | import { styles } from './PostStyles'; 7 | import { colors } from '../../../theme/colors'; 8 | 9 | interface Post { 10 | post_id: string; 11 | post_title: string; 12 | post_content: string; 13 | post_date: string; 14 | featured_image?: string; 15 | } 16 | 17 | const PostScreen = () => { 18 | const navigation = useNavigation(); 19 | const route = useRoute(); 20 | const { width } = useWindowDimensions(); 21 | const [loading, setLoading] = useState(true); 22 | const [post, setPost] = useState<Post | null>(null); 23 | const postId = route.params?.id; 24 | 25 | useEffect(() => { 26 | loadPost(); 27 | }, [postId]); 28 | 29 | const loadPost = async () => { 30 | try { 31 | const categories = await storageService.get('posts'); 32 | if (categories && Array.isArray(categories)) { 33 | for (const category of categories) { 34 | const foundPost = category.posts.find(p => p.post_id.toString() === postId?.toString()); 35 | if (foundPost) { 36 | setPost(foundPost); 37 | navigation.setOptions({ 38 | headerTitle: foundPost.post_title, 39 | }); 40 | break; 41 | } 42 | } 43 | } 44 | } catch (error) { 45 | console.error('Error loading post:', error); 46 | } finally { 47 | setLoading(false); 48 | } 49 | }; 50 | 51 | if (loading) { 52 | return ( 53 | <View style={styles.loadingContainer}> 54 | <ActivityIndicator size="large" color={colors.primary} /> 55 | </View> 56 | ); 57 | } 58 | 59 | if (!post) { 60 | return ( 61 | <View style={styles.errorContainer}> 62 | <Text style={styles.errorText}>Post not found</Text> 63 | </View> 64 | ); 65 | } 66 | 67 | const formatDate = (dateString: string) => { 68 | const date = new Date(dateString); 69 | return date.toLocaleDateString('en-US', { 70 | year: 'numeric', 71 | month: 'long', 72 | day: 'numeric', 73 | }); 74 | }; 75 | 76 | return ( 77 | <ScrollView style={styles.container}> 78 | {post.featured_image && ( 79 | <Image 80 | source={{ uri: post.featured_image }} 81 | style={styles.featuredImage} 82 | resizeMode="cover" 83 | /> 84 | )} 85 | <View style={styles.content}> 86 | <Text style={styles.title}>{post.post_title}</Text> 87 | <Text style={styles.date}>{formatDate(post.post_date)}</Text> 88 | <RenderHtml 89 | contentWidth={width - 32} 90 | source={{ html: post.post_content }} 91 | tagsStyles={{ 92 | body: { 93 | color: colors.black, 94 | fontSize: 16, 95 | lineHeight: 24, 96 | }, 97 | a: { 98 | color: colors.primary, 99 | textDecorationLine: 'underline', 100 | }, 101 | }} 102 | /> 103 | </View> 104 | </ScrollView> 105 | ); 106 | }; 107 | 108 | export default PostScreen; 109 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/navigation/NavigationWrapper.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useEffect, useState } from 'react'; 2 | import { Alert, View, Text, StyleSheet } from 'react-native'; 3 | import { NavigationMonitorService } from '../services/NavigationMonitorService'; 4 | 5 | interface NavigationProps { 6 | navigation: any; 7 | route: { 8 | name: string; 9 | }; 10 | } 11 | 12 | export const withNavigationWrapper = (WrappedComponent: React.ComponentType<any>) => { 13 | return function WithNavigationWrapper(props: NavigationProps) { 14 | const { navigation, route } = props; 15 | const [error, setError] = useState<string | null>(null); 16 | 17 | useEffect(() => { 18 | // Only check for updates if not an excluded route 19 | if (!NavigationMonitorService.isExcludedRoute(route.name)) { 20 | // Perform silent update checks without showing loading indicator 21 | try { 22 | checkForUpdates() 23 | .catch(err => { 24 | console.error('Error in checkForUpdates:', err); 25 | setError(`Failed to check for updates: ${err.message}`); 26 | }); 27 | } catch (error) { 28 | console.error('Error in navigation wrapper:', error); 29 | setError(`Navigation error: ${error instanceof Error ? error.message : String(error)}`); 30 | } 31 | } 32 | }, [route]); 33 | 34 | const checkForUpdates = async () => { 35 | try { 36 | // Check for updates with improved error handling 37 | const { termsNeedsUpdate, privacyNeedsUpdate } = await NavigationMonitorService.checkForUpdates(); 38 | 39 | // Only show alert if updates are actually needed 40 | if (termsNeedsUpdate || privacyNeedsUpdate) { 41 | Alert.alert( 42 | 'Update Required', 43 | `Please review and accept the ${termsNeedsUpdate ? 'Terms' : 'Privacy Policy'} updates to continue.`, 44 | [ 45 | { 46 | text: 'Review', 47 | onPress: () => { 48 | navigation.navigate( 49 | termsNeedsUpdate ? 'TermsAndConditions' : 'PrivacyPolicy', 50 | { fromUpdate: true } 51 | ); 52 | }, 53 | }, 54 | ], 55 | { cancelable: false } 56 | ); 57 | } 58 | 59 | return { termsNeedsUpdate, privacyNeedsUpdate }; 60 | } catch (error) { 61 | console.error('Failed to check for terms/privacy updates:', error); 62 | // Don't throw here, just log the error and continue 63 | return { termsNeedsUpdate: false, privacyNeedsUpdate: false }; 64 | } 65 | }; 66 | 67 | // If there's an error, show it but still render the component 68 | if (error) { 69 | console.warn('NavigationWrapper encountered an error:', error); 70 | // We could show an error UI here, but for now we'll just log it and continue 71 | } 72 | 73 | // Render the wrapped component without any loading overlay 74 | return ( 75 | <View style={styles.container}> 76 | <WrappedComponent {...props} /> 77 | </View> 78 | ); 79 | }; 80 | }; 81 | 82 | const styles = StyleSheet.create({ 83 | container: { 84 | flex: 1, 85 | }, 86 | }); 87 | 88 | export default withNavigationWrapper; 89 | ``` -------------------------------------------------------------------------------- /resources/standards/project_structure.md: -------------------------------------------------------------------------------- ```markdown 1 | # React Native Project Structure 2 | 3 | BluestoneApps recommends the following project structure for React Native applications: 4 | 5 | ``` 6 | project-name/ 7 | ├── src/ # Source code 8 | │ ├── assets/ # Static assets (images, fonts, etc.) 9 | │ ├── components/ # Reusable UI components 10 | │ ├── config/ # Configuration files (API endpoints, constants) 11 | │ ├── hooks/ # Custom React hooks 12 | │ ├── navigation/ # Navigation configuration 13 | │ ├── screens/ # Screen components 14 | │ │ ├── PostLogin/ # Screens available after authentication 15 | │ │ └── PreLogin/ # Authentication and onboarding screens 16 | │ ├── services/ # Business logic services 17 | │ ├── theme/ # Theme definitions (colors, spacing, etc.) 18 | │ ├── types/ # TypeScript type definitions 19 | │ └── utils/ # Utility functions 20 | ├── App.tsx # Root component 21 | ├── index.js # Entry point 22 | ├── package.json # Dependencies and scripts 23 | └── README.md # Project documentation 24 | ``` 25 | 26 | ## Key Directories 27 | 28 | ### `src/assets` 29 | 30 | Contains static assets such as images, fonts, and other media files used in the application. 31 | 32 | ### `src/components` 33 | 34 | Reusable UI components that can be used across different screens. Each component is typically defined in its own file with associated styles. 35 | 36 | ### `src/config` 37 | 38 | Configuration files for the application, including API endpoint definitions, environment variables, and other constants. 39 | 40 | ### `src/hooks` 41 | 42 | Custom React hooks for shared logic, state management, and side effects. 43 | 44 | ### `src/navigation` 45 | 46 | Navigation configuration using React Navigation, including drawer navigators, stack navigators, and navigation wrappers. 47 | 48 | ### `src/screens` 49 | 50 | Screen components organized by authentication state: 51 | - `PostLogin`: Screens that are accessible after user authentication 52 | - `PreLogin`: Authentication screens, onboarding, and public screens 53 | 54 | Each screen may have its own subdirectory containing the main screen component and related files (e.g., styles, subcomponents). 55 | 56 | ### `src/services` 57 | 58 | Business logic, API communication, data transformation, and other services. Typically implemented as singleton classes with specific responsibilities. 59 | 60 | ### `src/theme` 61 | 62 | Theme definitions including colors, typography, spacing, and other styling constants. 63 | 64 | ### `src/types` 65 | 66 | TypeScript type definitions, interfaces, and type guards that are shared across the application. This can be a separate directory or included within the config directory depending on the project size. 67 | 68 | ### `src/utils` 69 | 70 | Utility functions and helpers, including API utilities, formatting functions, and other shared logic. 71 | 72 | ## Best Practices 73 | 74 | 1. Use TypeScript for type safety and better developer experience 75 | 2. Implement functional components with React hooks 76 | 3. Keep components small and focused on a single responsibility 77 | 4. Extract business logic into services 78 | 5. Use consistent naming conventions (PascalCase for components, camelCase for functions) 79 | 6. Implement proper error handling in API calls and async operations 80 | 7. Use environment-specific configuration when necessary 81 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Calendar/EventDetails.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { View, Text, ScrollView, StyleSheet, useWindowDimensions } from 'react-native'; 3 | import { useNavigation, useRoute } from '@react-navigation/native'; 4 | import RenderHtml, { defaultSystemFonts } from 'react-native-render-html'; 5 | import { styles } from './EventDetailsStyles'; 6 | import { colors } from '../../../theme/colors'; 7 | 8 | const EventDetails = () => { 9 | const navigation = useNavigation(); 10 | const route = useRoute(); 11 | const event = route.params?.event; 12 | const { width } = useWindowDimensions(); 13 | 14 | const formatTime = (timeStr: string) => { 15 | const [hours, minutes] = timeStr.split(':'); 16 | const date = new Date(); 17 | date.setHours(parseInt(hours, 10)); 18 | date.setMinutes(parseInt(minutes, 10)); 19 | return date.toLocaleTimeString('en-US', { 20 | hour: 'numeric', 21 | minute: '2-digit', 22 | hour12: true 23 | }); 24 | }; 25 | 26 | const formatDate = (dateString: string) => { 27 | const [year, month, day] = dateString.split('-').map(num => parseInt(num, 10)); 28 | const date = new Date(year, month - 1, day); 29 | return date.toLocaleDateString('en-US', { 30 | weekday: 'long', 31 | year: 'numeric', 32 | month: 'long', 33 | day: 'numeric' 34 | }); 35 | }; 36 | 37 | // Define default rendering options 38 | const renderersProps = { 39 | img: { 40 | enableExperimentalPercentWidth: true 41 | } 42 | }; 43 | 44 | const tagsStyles = { 45 | body: { 46 | color: colors.black, 47 | fontSize: 16, 48 | lineHeight: 24, 49 | }, 50 | a: { 51 | color: colors.primary, 52 | textDecorationLine: 'underline', 53 | } 54 | }; 55 | 56 | return ( 57 | <View style={styles.container}> 58 | <ScrollView style={styles.content}> 59 | <Text style={styles.title}>{event.event_title}</Text> 60 | <Text style={styles.date}>{formatDate(event.event_date)}</Text> 61 | <Text style={styles.time}> 62 | {formatTime(event.event_from_time)} - {formatTime(event.event_to_time)} 63 | </Text> 64 | 65 | <View style={styles.section}> 66 | <Text style={styles.sectionTitle}>Location</Text> 67 | <Text style={styles.location}> 68 | {event.event_street_address} 69 | {event.event_apt_suite ? `\n${event.event_apt_suite}` : ''} 70 | {'\n'} 71 | {[ 72 | event.event_city, 73 | event.event_state, 74 | event.event_zip 75 | ].filter(Boolean).join(', ')} 76 | </Text> 77 | </View> 78 | 79 | {event.event_content && ( 80 | <View style={styles.section}> 81 | <Text style={styles.sectionTitle}>Description</Text> 82 | <RenderHtml 83 | contentWidth={width - 40} 84 | source={{ html: event.event_content }} 85 | renderersProps={renderersProps} 86 | tagsStyles={tagsStyles} 87 | systemFonts={defaultSystemFonts} 88 | baseStyle={styles.description} 89 | defaultTextProps={{ 90 | selectable: true 91 | }} 92 | /> 93 | </View> 94 | )} 95 | 96 | {event.event_price && ( 97 | <View style={styles.section}> 98 | <Text style={styles.sectionTitle}>Price</Text> 99 | <Text style={styles.price}>${event.event_price}</Text> 100 | </View> 101 | )} 102 | </ScrollView> 103 | </View> 104 | ); 105 | }; 106 | 107 | export default EventDetails; 108 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/VerifyEmail/VerifyEmailScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | Alert, 8 | KeyboardAvoidingView, 9 | Platform, 10 | } from 'react-native'; 11 | import axios from 'axios'; 12 | import { styles } from './Styles'; 13 | import AsyncStorage from '@react-native-async-storage/async-storage'; 14 | import { API } from '../../../config/apiConfig'; 15 | 16 | const VerifyEmailScreen = ({ route, navigation }: any) => { 17 | const { registerData, otp } = route.params; 18 | const [otpValue, setOtpValue] = useState(''); 19 | const inputRefs = useRef<Array<TextInput | null>>([]); 20 | 21 | const handleOtpChange = (value: string, index: number) => { 22 | const newOtp = otpValue.split(''); 23 | newOtp[index] = value; 24 | const newOtpString = newOtp.join(''); 25 | setOtpValue(newOtpString); 26 | 27 | // Move to next input if value is entered 28 | if (value && index < 3) { 29 | inputRefs.current[index + 1]?.focus(); 30 | } 31 | }; 32 | 33 | const handleSubmit = async () => { 34 | if (!otpValue || otpValue.length !== 4) { 35 | Alert.alert('Error', 'Please enter the complete OTP'); 36 | return; 37 | } 38 | 39 | if (parseInt(otpValue) !== parseInt(otp)) { 40 | Alert.alert('Error', 'OTP does not match. Please try again.'); 41 | return; 42 | } 43 | 44 | try { 45 | const response = await axios.post( 46 | `${API.BASE_URL}${API.ENDPOINTS.MOBILEAPI}/v1/register`, 47 | { 48 | ...registerData, 49 | user_otp: otpValue, 50 | sent_opt: otp, 51 | signup_step1_email_otp: 'done', 52 | } 53 | ); 54 | 55 | if (response.data.loginInfo) { 56 | // Store login info in AsyncStorage 57 | await AsyncStorage.setItem('userToken', response.data.loginInfo.token); 58 | await AsyncStorage.setItem('userData', JSON.stringify(response.data.loginInfo)); 59 | 60 | // Navigate to DrawerNavigator 61 | navigation.reset({ 62 | index: 0, 63 | routes: [{ name: 'DrawerNavigator' }], 64 | }); 65 | } 66 | } catch (error: any) { 67 | Alert.alert( 68 | 'Error', 69 | error.response?.data?.msg || 'Unable to verify OTP. Please try again later.' 70 | ); 71 | } 72 | }; 73 | 74 | return ( 75 | <KeyboardAvoidingView 76 | style={styles.container} 77 | behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 78 | > 79 | <View style={styles.header}> 80 | <Text style={styles.title}>Verify Email</Text> 81 | <Text style={styles.subtitle}> 82 | Please enter the verification code sent to your email 83 | </Text> 84 | </View> 85 | 86 | <View style={styles.otpContainer}> 87 | {[0, 1, 2, 3].map((index) => ( 88 | <TextInput 89 | key={index} 90 | ref={(ref) => (inputRefs.current[index] = ref)} 91 | style={styles.otpInput} 92 | maxLength={1} 93 | keyboardType="number-pad" 94 | onChangeText={(value) => handleOtpChange(value, index)} 95 | value={otpValue[index] || ''} 96 | /> 97 | ))} 98 | </View> 99 | 100 | <TouchableOpacity style={styles.button} onPress={handleSubmit}> 101 | <Text style={styles.buttonText}>Verify</Text> 102 | </TouchableOpacity> 103 | 104 | <TouchableOpacity 105 | style={styles.backButton} 106 | onPress={() => navigation.goBack()} 107 | > 108 | <Text style={styles.backButtonText}>Go Back</Text> 109 | </TouchableOpacity> 110 | </KeyboardAvoidingView> 111 | ); 112 | }; 113 | 114 | export default VerifyEmailScreen; ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/theme/theme.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Main theme configuration file 3 | * 4 | * This file exports the complete theme object that combines all theme elements: 5 | * colors, typography, spacing, etc. 6 | */ 7 | 8 | import { colors } from './colors'; 9 | import { typography, typeScale } from './typography'; 10 | import { ViewStyle } from 'react-native'; 11 | 12 | // Spacing system in pixels 13 | const spacing = { 14 | none: 0, 15 | xs: 4, 16 | sm: 8, 17 | md: 16, 18 | lg: 24, 19 | xl: 32, 20 | xxl: 48, 21 | xxxl: 64, 22 | } as const; 23 | 24 | // Border radius values 25 | const borderRadius = { 26 | none: 0, 27 | xs: 2, 28 | sm: 4, 29 | md: 8, 30 | lg: 12, 31 | xl: 16, 32 | round: 9999, // For circular elements 33 | } as const; 34 | 35 | // Shadow styles 36 | interface ShadowStyle { 37 | shadowColor: string; 38 | shadowOffset: { width: number; height: number }; 39 | shadowOpacity: number; 40 | shadowRadius: number; 41 | elevation: number; 42 | } 43 | 44 | const shadows: Record<string, ShadowStyle> = { 45 | none: { 46 | shadowColor: 'transparent', 47 | shadowOffset: { width: 0, height: 0 }, 48 | shadowOpacity: 0, 49 | shadowRadius: 0, 50 | elevation: 0, 51 | }, 52 | xs: { 53 | shadowColor: colors.black, 54 | shadowOffset: { width: 0, height: 1 }, 55 | shadowOpacity: 0.1, 56 | shadowRadius: 2, 57 | elevation: 1, 58 | }, 59 | sm: { 60 | shadowColor: colors.black, 61 | shadowOffset: { width: 0, height: 2 }, 62 | shadowOpacity: 0.1, 63 | shadowRadius: 4, 64 | elevation: 2, 65 | }, 66 | md: { 67 | shadowColor: colors.black, 68 | shadowOffset: { width: 0, height: 3 }, 69 | shadowOpacity: 0.2, 70 | shadowRadius: 6, 71 | elevation: 3, 72 | }, 73 | lg: { 74 | shadowColor: colors.black, 75 | shadowOffset: { width: 0, height: 4 }, 76 | shadowOpacity: 0.2, 77 | shadowRadius: 8, 78 | elevation: 4, 79 | }, 80 | xl: { 81 | shadowColor: colors.black, 82 | shadowOffset: { width: 0, height: 6 }, 83 | shadowOpacity: 0.3, 84 | shadowRadius: 12, 85 | elevation: 6, 86 | }, 87 | }; 88 | 89 | // Z-index values for stacking elements 90 | const zIndex = { 91 | background: -1, 92 | default: 0, 93 | headerContent: 5, 94 | header: 10, 95 | modal: 20, 96 | toast: 30, 97 | tooltip: 40, 98 | overlay: 50, 99 | } as const; 100 | 101 | // Opacity values 102 | const opacity = { 103 | none: 0, 104 | light: 0.2, 105 | medium: 0.5, 106 | high: 0.8, 107 | full: 1, 108 | } as const; 109 | 110 | // Layout constants 111 | const layout = { 112 | screenPadding: spacing.md, 113 | headerHeight: 56, 114 | tabBarHeight: 64, 115 | maxContentWidth: 680, // Maximum width for content on large screens 116 | } as const; 117 | 118 | // Animation timing constants 119 | const animation = { 120 | durationShort: 150, 121 | durationMedium: 300, 122 | durationLong: 500, 123 | easingDefault: 'ease', 124 | easingAccelerate: 'ease-in', 125 | easingDecelerate: 'ease-out', 126 | } as const; 127 | 128 | // Screen size breakpoints 129 | const breakpoints = { 130 | xs: 0, // Extra small devices 131 | sm: 375, // Small devices (phones) 132 | md: 768, // Medium devices (tablets) 133 | lg: 1024, // Large devices (laptops/desktops) 134 | xl: 1280, // Extra large devices (large desktops) 135 | } as const; 136 | 137 | // Export the complete theme object 138 | const theme = { 139 | colors, 140 | typography, 141 | typeScale, 142 | spacing, 143 | borderRadius, 144 | shadows, 145 | zIndex, 146 | opacity, 147 | layout, 148 | animation, 149 | breakpoints, 150 | } as const; 151 | 152 | export type Theme = typeof theme; 153 | export type SpacingKey = keyof typeof spacing; 154 | export type BorderRadiusKey = keyof typeof borderRadius; 155 | export type ShadowKey = keyof typeof shadows; 156 | export type ZIndexKey = keyof typeof zIndex; 157 | export type OpacityKey = keyof typeof opacity; 158 | export type BreakpointKey = keyof typeof breakpoints; 159 | 160 | export default theme; 161 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Posts/PostsScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, RefreshControl } from 'react-native'; 3 | import { styles } from './Styles'; 4 | import { storageService } from '../../../helper/storageService'; 5 | import { apiService } from '../../../helper/apiService'; 6 | import { colors } from '../../../theme/colors'; 7 | 8 | interface Post { 9 | post_id: string; 10 | post_title: string; 11 | post_content: string; 12 | post_date: string; 13 | featured_image?: string; 14 | } 15 | 16 | interface Category { 17 | category_id: string; 18 | category_name: string; 19 | posts: Post[]; 20 | } 21 | 22 | interface ApiResponse { 23 | status: string; 24 | categories: Category[]; 25 | } 26 | 27 | const PostsScreen = ({ navigation }: any) => { 28 | const [groupedPosts, setGroupedPosts] = useState<Category[]>([]); 29 | const [loading, setLoading] = useState(true); 30 | const [refreshing, setRefreshing] = useState(false); 31 | 32 | const loadPosts = async (fromRefresh = false) => { 33 | try { 34 | // Try to get posts from storage first 35 | let posts = await storageService.get('posts'); 36 | 37 | // If no posts in storage or this is a refresh, fetch from API 38 | if (!posts || fromRefresh) { 39 | const response = await apiService.fetchAndStorePosts(); 40 | if (response) { 41 | posts = response; 42 | } 43 | } 44 | 45 | if (posts && Array.isArray(posts)) { 46 | setGroupedPosts(posts); 47 | } else { 48 | console.log('No posts found or invalid format'); 49 | setGroupedPosts([]); 50 | } 51 | } catch (error) { 52 | console.error('Error getting posts:', error); 53 | setGroupedPosts([]); 54 | } finally { 55 | setLoading(false); 56 | setRefreshing(false); 57 | } 58 | }; 59 | 60 | useEffect(() => { 61 | loadPosts(); 62 | }, []); 63 | 64 | const onRefresh = () => { 65 | setRefreshing(true); 66 | loadPosts(true); 67 | }; 68 | 69 | const handlePostPress = (post: Post) => { 70 | navigation.navigate('Post', { 71 | id: post.post_id, 72 | title: post.post_title 73 | }); 74 | }; 75 | 76 | if (loading) { 77 | return ( 78 | <View style={styles.loadingContainer}> 79 | <ActivityIndicator size="large" color={colors.primary} /> 80 | </View> 81 | ); 82 | } 83 | 84 | return ( 85 | <ScrollView 86 | style={styles.container} 87 | refreshControl={ 88 | <RefreshControl 89 | refreshing={refreshing} 90 | onRefresh={onRefresh} 91 | colors={[colors.dark]} 92 | /> 93 | } 94 | > 95 | 96 | {groupedPosts.length > 0 ? ( 97 | <View style={styles.postsContainer}> 98 | {groupedPosts.map((category) => ( 99 | <View key={category.category_id} style={styles.categorySection}> 100 | <Text style={styles.categoryTitle}>{category.category_name}</Text> 101 | {category.posts.map((post) => ( 102 | <TouchableOpacity 103 | key={post.post_id} 104 | style={styles.postCard} 105 | onPress={() => handlePostPress(post)} 106 | > 107 | <Text style={styles.postTitle}>{post.post_title}</Text> 108 | <Text style={styles.postDate}> 109 | {new Date(post.post_date).toLocaleDateString()} 110 | </Text> 111 | </TouchableOpacity> 112 | ))} 113 | </View> 114 | ))} 115 | </View> 116 | ) : ( 117 | <View style={styles.emptyContainer}> 118 | <Text style={styles.emptyText}>No posts available</Text> 119 | </View> 120 | )} 121 | </ScrollView> 122 | ); 123 | }; 124 | 125 | export default PostsScreen; 126 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/navigation/TabNavigator.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 4 | import Icon from 'react-native-vector-icons/Ionicons'; 5 | import { DrawerActions, useNavigation, useRoute, NavigationProp, ParamListBase } from '@react-navigation/native'; 6 | import { colors } from '../theme/colors'; 7 | 8 | type TabParamList = { 9 | Home: undefined; 10 | ContactTab: undefined; 11 | ProfileTab: undefined; 12 | CalendarTab: undefined; 13 | Menu: undefined; 14 | }; 15 | 16 | const Tab = createBottomTabNavigator<TabParamList>(); 17 | 18 | const TabNavigator = () => { 19 | const navigation = useNavigation<NavigationProp<TabParamList>>(); 20 | const route = useRoute(); 21 | 22 | return ( 23 | <Tab.Navigator 24 | screenOptions={{ 25 | headerShown: false, 26 | tabBarShowLabel: false, 27 | tabBarStyle: { 28 | backgroundColor: colors.footerBg, 29 | borderTopWidth: 1, 30 | borderTopColor: colors.light, 31 | }, 32 | tabBarActiveTintColor: colors.secondary, 33 | tabBarInactiveTintColor: colors.footerBg, 34 | }} 35 | > 36 | <Tab.Screen 37 | name="ProfileTab" 38 | component={View} 39 | listeners={{ 40 | tabPress: (e) => { 41 | e.preventDefault(); 42 | navigation.navigate('MyProfile'); 43 | }, 44 | }} 45 | options={{ 46 | tabBarIcon: ({ focused, color }) => ( 47 | <Icon 48 | name={focused ? 'person-outline' : 'person-outline'} 49 | size={24} 50 | color={route.name === 'MyProfile' ? colors.secondary : colors.footerFont} 51 | /> 52 | ), 53 | }} 54 | /> 55 | <Tab.Screen 56 | name="ContactTab" 57 | component={View} 58 | listeners={{ 59 | tabPress: (e) => { 60 | e.preventDefault(); 61 | navigation.navigate('Contact'); 62 | }, 63 | }} 64 | options={{ 65 | tabBarIcon: ({ focused, color }) => ( 66 | <Icon 67 | name={focused ? 'mail' : 'mail-outline'} 68 | size={24} 69 | color={route.name === 'Contact' ? colors.secondary : colors.footerFont} 70 | /> 71 | ), 72 | }} 73 | /> 74 | <Tab.Screen 75 | name="HomeTab" 76 | component={View} 77 | listeners={{ 78 | tabPress: (e) => { 79 | e.preventDefault(); 80 | navigation.navigate('Home'); 81 | }, 82 | }} 83 | options={{ 84 | tabBarIcon: ({ focused, color }) => ( 85 | <Icon 86 | name={focused ? 'home' : 'home-outline'} 87 | size={24} 88 | color={route.name === 'Home' ? colors.secondary : colors.footerFont} 89 | /> 90 | ), 91 | }} 92 | /> 93 | <Tab.Screen 94 | name="CalendarTab" 95 | component={View} 96 | listeners={{ 97 | tabPress: (e) => { 98 | e.preventDefault(); 99 | navigation.navigate('Calendar'); 100 | }, 101 | }} 102 | options={{ 103 | tabBarIcon: ({ focused, color }) => ( 104 | <Icon 105 | name={focused ? 'calendar' : 'calendar-outline'} 106 | size={24} 107 | color={route.name === 'Calendar' ? colors.secondary : colors.footerFont} 108 | /> 109 | ), 110 | }} 111 | /> 112 | <Tab.Screen 113 | name="Menu" 114 | component={View} 115 | listeners={{ 116 | tabPress: (e) => { 117 | e.preventDefault(); 118 | navigation.dispatch(DrawerActions.openDrawer()); 119 | }, 120 | }} 121 | options={{ 122 | tabBarIcon: ({ focused, color }) => ( 123 | <Icon 124 | name={focused ? 'menu' : 'menu-outline'} 125 | size={24} 126 | color={colors.footerFont} 127 | /> 128 | ), 129 | }} 130 | /> 131 | </Tab.Navigator> 132 | ); 133 | }; 134 | 135 | export default TabNavigator; 136 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { Component, ErrorInfo, ReactNode } from 'react'; 2 | import { 3 | SafeAreaView, 4 | ScrollView, 5 | StyleSheet, 6 | Text, 7 | TouchableOpacity, 8 | View, 9 | } from 'react-native'; 10 | 11 | interface Props { 12 | children: ReactNode; 13 | fallback?: ReactNode; 14 | } 15 | 16 | interface State { 17 | hasError: boolean; 18 | error: Error | null; 19 | errorInfo: ErrorInfo | null; 20 | } 21 | 22 | /** 23 | * ErrorBoundary component to catch JavaScript errors anywhere in the child component tree. 24 | * It logs the errors and displays a fallback UI instead of crashing the whole app. 25 | */ 26 | class ErrorBoundary extends Component<Props, State> { 27 | constructor(props: Props) { 28 | super(props); 29 | this.state = { 30 | hasError: false, 31 | error: null, 32 | errorInfo: null, 33 | }; 34 | } 35 | 36 | static getDerivedStateFromError(error: Error): State { 37 | // Update state so the next render will show the fallback UI 38 | return { 39 | hasError: true, 40 | error, 41 | errorInfo: null, 42 | }; 43 | } 44 | 45 | componentDidCatch(error: Error, errorInfo: ErrorInfo): void { 46 | // Log the error to the console 47 | console.error('ErrorBoundary caught an error:', error, errorInfo); 48 | this.setState({ 49 | errorInfo, 50 | }); 51 | 52 | // You can also log the error to an error reporting service here 53 | // logErrorToService(error, errorInfo); 54 | } 55 | 56 | resetError = (): void => { 57 | this.setState({ 58 | hasError: false, 59 | error: null, 60 | errorInfo: null, 61 | }); 62 | }; 63 | 64 | render(): ReactNode { 65 | if (this.state.hasError) { 66 | // Custom fallback UI 67 | if (this.props.fallback) { 68 | return this.props.fallback; 69 | } 70 | 71 | // Default fallback UI 72 | return ( 73 | <SafeAreaView style={styles.container}> 74 | <ScrollView contentContainerStyle={styles.scrollContainer}> 75 | <View style={styles.errorContainer}> 76 | <Text style={styles.errorTitle}>Something went wrong</Text> 77 | 78 | <View style={styles.errorMessageContainer}> 79 | <Text style={styles.errorMessage}> 80 | {this.state.error?.toString() || 'An unknown error occurred'} 81 | </Text> 82 | </View> 83 | 84 | {this.state.errorInfo && ( 85 | <View style={styles.componentStackContainer}> 86 | <Text style={styles.componentStackTitle}>Component Stack:</Text> 87 | <Text style={styles.componentStack}> 88 | {this.state.errorInfo.componentStack} 89 | </Text> 90 | </View> 91 | )} 92 | 93 | <TouchableOpacity 94 | style={styles.resetButton} 95 | onPress={this.resetError} 96 | > 97 | <Text style={styles.resetButtonText}>Try Again</Text> 98 | </TouchableOpacity> 99 | </View> 100 | </ScrollView> 101 | </SafeAreaView> 102 | ); 103 | } 104 | 105 | return this.props.children; 106 | } 107 | } 108 | 109 | const styles = StyleSheet.create({ 110 | container: { 111 | flex: 1, 112 | backgroundColor: '#f8f9fa', 113 | }, 114 | scrollContainer: { 115 | flexGrow: 1, 116 | padding: 20, 117 | }, 118 | errorContainer: { 119 | flex: 1, 120 | justifyContent: 'center', 121 | alignItems: 'center', 122 | }, 123 | errorTitle: { 124 | fontSize: 24, 125 | fontWeight: 'bold', 126 | color: '#dc3545', 127 | marginBottom: 20, 128 | textAlign: 'center', 129 | }, 130 | errorMessageContainer: { 131 | backgroundColor: '#fff', 132 | borderRadius: 8, 133 | padding: 15, 134 | width: '100%', 135 | marginBottom: 20, 136 | borderWidth: 1, 137 | borderColor: '#dee2e6', 138 | }, 139 | errorMessage: { 140 | fontSize: 16, 141 | color: '#343a40', 142 | lineHeight: 22, 143 | }, 144 | componentStackContainer: { 145 | backgroundColor: '#f1f3f5', 146 | borderRadius: 8, 147 | padding: 15, 148 | width: '100%', 149 | marginBottom: 20, 150 | borderWidth: 1, 151 | borderColor: '#dee2e6', 152 | }, 153 | componentStackTitle: { 154 | fontSize: 16, 155 | fontWeight: 'bold', 156 | color: '#495057', 157 | marginBottom: 10, 158 | }, 159 | componentStack: { 160 | fontSize: 14, 161 | color: '#6c757d', 162 | fontFamily: 'Courier', 163 | }, 164 | resetButton: { 165 | backgroundColor: '#007bff', 166 | paddingVertical: 12, 167 | paddingHorizontal: 24, 168 | borderRadius: 8, 169 | marginTop: 10, 170 | }, 171 | resetButtonText: { 172 | color: '#fff', 173 | fontSize: 16, 174 | fontWeight: 'bold', 175 | }, 176 | }); 177 | 178 | export default ErrorBoundary; 179 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/BluestoneAppsAI/BluestoneAppsAIScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { View, Text, TextInput, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native'; 3 | import { styles } from './Styles'; 4 | import axiosRequest from '../../../helper/axiosRequest'; 5 | import { API } from '../../../helper/config'; 6 | import HTML from 'react-native-render-html'; 7 | import { useWindowDimensions } from 'react-native'; 8 | 9 | const defaultSystemFonts = [ 10 | 'Arial', 11 | 'Helvetica', 12 | 'Helvetica Neue', 13 | 'System', 14 | '-apple-system', 15 | 'sans-serif', 16 | ]; 17 | 18 | const renderersProps = { 19 | img: { 20 | enableExperimentalPercentWidth: true, 21 | }, 22 | }; 23 | 24 | const BluestoneAppsAIScreen = ({ navigation }: any) => { 25 | const [showWelcomeMessage, setShowWelcomeMessage] = useState(true); 26 | const [question, setQuestion] = useState(''); 27 | const [replyContent, setReplyContent] = useState(''); 28 | const [aiWelcomeMessage, setAiWelcomeMessage] = useState(''); 29 | const [isLoading, setIsLoading] = useState(false); 30 | const { width } = useWindowDimensions(); 31 | 32 | useEffect(() => { 33 | getAIWelcome(); 34 | }, []); 35 | 36 | const getAIWelcome = async () => { 37 | try { 38 | console.log('Fetching AI welcome message...'); 39 | const response = await axiosRequest.get( 40 | `${API.ENDPOINTS.MOBILEAPI}/${API.ENDPOINTS.GET_AI_WELCOME}` 41 | ); 42 | 43 | console.log('AI Welcome Response:', response?.data); 44 | if (response?.data?.status === "success") { 45 | console.log('Setting welcome message:', response.data.message); 46 | setAiWelcomeMessage(response.data.message); 47 | } else { 48 | console.log('No success status in response'); 49 | setAiWelcomeMessage('Welcome to Bluestone AI! How can I help you today?'); 50 | } 51 | } catch (error) { 52 | console.error('Error getting AI welcome message:', error); 53 | setAiWelcomeMessage('Welcome to Bluestone AI! How can I help you today?'); 54 | } 55 | }; 56 | 57 | const submitQuestion = async () => { 58 | if (!question.trim()) { 59 | return; 60 | } 61 | 62 | setIsLoading(true); 63 | try { 64 | console.log('Submitting question to ask_bluestoneai endpoint...'); 65 | const response = await axiosRequest.post( 66 | `${API.ENDPOINTS.MOBILEAPI}/${API.ENDPOINTS.ASK_BLUESTONEAI}`, 67 | { question: question.trim() } 68 | ); 69 | 70 | console.log('Question response:', response?.data); 71 | 72 | if (response?.data?.reply) { 73 | setReplyContent(response.data.reply); 74 | setShowWelcomeMessage(false); 75 | setQuestion(''); 76 | } else { 77 | console.error('No reply in response'); 78 | } 79 | } catch (error) { 80 | console.error('Error submitting question:', error); 81 | } finally { 82 | setIsLoading(false); 83 | } 84 | }; 85 | 86 | const renderHtmlContent = (content: string) => { 87 | if (!content) return null; 88 | 89 | return ( 90 | <HTML 91 | source={{ html: content }} 92 | contentWidth={width - 64} 93 | systemFonts={defaultSystemFonts} 94 | renderersProps={renderersProps} 95 | baseStyle={styles.htmlContent} 96 | tagsStyles={{ 97 | p: styles.paragraph, 98 | h1: styles.heading1, 99 | h2: styles.heading2, 100 | body: { color: '#333' }, 101 | }} 102 | enableExperimentalBRCollapsing 103 | enableExperimentalGhostLinesPrevention 104 | /> 105 | ); 106 | }; 107 | 108 | return ( 109 | <ScrollView style={styles.container}> 110 | <View style={styles.content}> 111 | {showWelcomeMessage ? ( 112 | <View style={styles.messageContainer}> 113 | {aiWelcomeMessage ? ( 114 | renderHtmlContent(aiWelcomeMessage) 115 | ) : ( 116 | <Text style={styles.loadingText}>Loading welcome message...</Text> 117 | )} 118 | </View> 119 | ) : null} 120 | 121 | {replyContent ? ( 122 | <View style={styles.messageContainer}> 123 | {renderHtmlContent(replyContent)} 124 | </View> 125 | ) : null} 126 | 127 | <View style={styles.inputContainer}> 128 | <TextInput 129 | style={styles.input} 130 | value={question} 131 | onChangeText={setQuestion} 132 | placeholder="Ask a question" 133 | multiline 134 | /> 135 | <TouchableOpacity 136 | style={styles.submitButton} 137 | onPress={submitQuestion} 138 | disabled={isLoading} 139 | > 140 | {isLoading ? ( 141 | <ActivityIndicator color="#fff" /> 142 | ) : ( 143 | <Text style={styles.submitButtonText}>Submit</Text> 144 | )} 145 | </TouchableOpacity> 146 | </View> 147 | </View> 148 | </ScrollView> 149 | ); 150 | }; 151 | 152 | export default BluestoneAppsAIScreen; 153 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/MyProfile/MyProfileScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useCallback } from 'react'; 2 | import { View, Text, Image, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import { useFocusEffect } from '@react-navigation/native'; 5 | import Icon from 'react-native-vector-icons/Ionicons'; 6 | import { styles } from './Styles'; 7 | 8 | interface UserProfile { 9 | loginInfo: { 10 | first_name: string; 11 | last_name: string; 12 | email: string; 13 | phone: string; 14 | user_avatar?: string; 15 | display_name: string; 16 | city_state?: string; 17 | }; 18 | } 19 | 20 | const MyProfileScreen = ({ navigation }: any) => { 21 | const [userInfo, setUserInfo] = useState<UserProfile | null>(null); 22 | const [isLoading, setIsLoading] = useState(true); 23 | 24 | const loadUserData = async () => { 25 | try { 26 | setIsLoading(true); 27 | console.log('Attempting to load user data...'); 28 | 29 | const userDataString = await AsyncStorage.getItem('userData'); 30 | console.log('Raw userData from AsyncStorage:', userDataString); 31 | 32 | if (!userDataString) { 33 | console.log('No userData found in AsyncStorage'); 34 | setUserInfo(null); 35 | return; 36 | } 37 | 38 | const userData = JSON.parse(userDataString); 39 | console.log('Parsed userData:', userData); 40 | 41 | // Ensure the data has the expected structure 42 | if (!userData?.loginInfo) { 43 | console.error('Invalid user data structure:', userData); 44 | setUserInfo(null); 45 | return; 46 | } 47 | 48 | console.log('Setting valid user data:', userData); 49 | setUserInfo(userData); 50 | } catch (error) { 51 | console.error('Error loading user data:', error); 52 | setUserInfo(null); 53 | } finally { 54 | setIsLoading(false); 55 | } 56 | }; 57 | 58 | useFocusEffect( 59 | useCallback(() => { 60 | loadUserData(); 61 | }, []) 62 | ); 63 | 64 | if (isLoading) { 65 | return ( 66 | <View style={[styles.container, styles.centerContent]}> 67 | <ActivityIndicator size="large" color="#007AFF" /> 68 | </View> 69 | ); 70 | } 71 | 72 | if (!userInfo?.loginInfo) { 73 | return ( 74 | <View style={[styles.container, styles.centerContent]}> 75 | <Text style={styles.errorText}>No user data available</Text> 76 | </View> 77 | ); 78 | } 79 | 80 | const { loginInfo } = userInfo; 81 | 82 | return ( 83 | <ScrollView style={styles.container}> 84 | <View style={styles.header}> 85 | <View style={styles.avatarContainer}> 86 | {loginInfo.user_avatar ? ( 87 | <Image source={{ uri: loginInfo.user_avatar }} style={styles.profileImage} /> 88 | ) : ( 89 | <Image 90 | source={require('../../../assets/images/default_profile_pic.png')} 91 | style={styles.profileImage} 92 | /> 93 | )} 94 | </View> 95 | <Text style={styles.name}>{loginInfo.display_name}</Text> 96 | {loginInfo.city_state && ( 97 | <Text style={styles.location}>{loginInfo.city_state}</Text> 98 | )} 99 | </View> 100 | 101 | <View style={styles.infoSection}> 102 | <View style={styles.infoItem}> 103 | <Icon name="person-outline" size={24} color="#666" style={styles.icon} /> 104 | <View> 105 | <Text style={styles.label}>Full Name</Text> 106 | <Text style={styles.value}> 107 | {`${loginInfo.first_name} ${loginInfo.last_name}`} 108 | </Text> 109 | </View> 110 | </View> 111 | 112 | <View style={styles.infoItem}> 113 | <Icon name="call-outline" size={24} color="#666" style={styles.icon} /> 114 | <View> 115 | <Text style={styles.label}>Mobile Number</Text> 116 | <Text style={styles.value}>{loginInfo.phone || 'Not provided'}</Text> 117 | </View> 118 | </View> 119 | 120 | <View style={styles.infoItem}> 121 | <Icon name="mail-outline" size={24} color="#666" style={styles.icon} /> 122 | <View> 123 | <Text style={styles.label}>Email</Text> 124 | <Text style={styles.value}>{loginInfo.email}</Text> 125 | </View> 126 | </View> 127 | </View> 128 | 129 | <View style={styles.buttonContainer}> 130 | <TouchableOpacity 131 | style={styles.button} 132 | onPress={() => navigation.navigate('EditProfile')} 133 | > 134 | <Text style={styles.buttonText}>Edit Account</Text> 135 | </TouchableOpacity> 136 | 137 | <TouchableOpacity 138 | style={[styles.button, styles.secondaryButton]} 139 | onPress={() => navigation.navigate('ChangePassword')} 140 | > 141 | <Text style={[styles.buttonText, styles.secondaryButtonText]}>Change Password</Text> 142 | </TouchableOpacity> 143 | </View> 144 | </ScrollView> 145 | ); 146 | }; 147 | 148 | export default MyProfileScreen; 149 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/ForgotPassword/ForgotPasswordScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | Alert, 8 | Image, 9 | } from 'react-native'; 10 | import axios from 'axios'; 11 | import { styles } from './Styles'; 12 | import { environment } from '../../../config/environment'; 13 | 14 | const ForgotPasswordScreen = ({ navigation }: any) => { 15 | const [step, setStep] = useState(1); 16 | const [email, setEmail] = useState(''); 17 | const [otp, setOtp] = useState(''); 18 | const [password, setPassword] = useState(''); 19 | const [confirmPassword, setConfirmPassword] = useState(''); 20 | 21 | const handleSendEmail = async () => { 22 | try { 23 | const response = await axios.post(`${environment.apiURL}/custom/v1/forgotPassword`, { 24 | email: email 25 | }); 26 | 27 | if (response.data.status === 'ok') { 28 | Alert.alert('Success', 'OTP sent to your email successfully!'); 29 | setStep(2); 30 | } else { 31 | Alert.alert('Error', response.data.errormsg || 'Failed to send OTP'); 32 | } 33 | } catch (error: any) { 34 | Alert.alert('Error', error.response?.data?.errormsg || 'An error occurred'); 35 | } 36 | }; 37 | 38 | const handleVerifyOTP = async () => { 39 | try { 40 | const response = await axios.post(`${environment.apiURL}/custom/v1/checkOtp`, { 41 | otp: otp, 42 | email: email 43 | }); 44 | 45 | if (response.data.status === 'ok') { 46 | Alert.alert('Success', 'OTP verified successfully!'); 47 | setStep(3); 48 | } else { 49 | Alert.alert('Error', response.data.errormsg || 'Invalid OTP'); 50 | } 51 | } catch (error: any) { 52 | Alert.alert('Error', error.response?.data?.errormsg || 'An error occurred'); 53 | } 54 | }; 55 | 56 | const handleResetPassword = async () => { 57 | if (password !== confirmPassword) { 58 | Alert.alert('Error', 'Passwords do not match'); 59 | return; 60 | } 61 | 62 | try { 63 | const response = await axios.post(`${environment.apiURL}/custom/v1/updatePassword`, { 64 | password: password, 65 | email: email 66 | }); 67 | 68 | if (response.data.status === 'ok') { 69 | Alert.alert('Success', 'Password reset successfully!'); 70 | navigation.navigate('Login'); 71 | } else { 72 | Alert.alert('Error', response.data.errormsg || 'Failed to reset password'); 73 | } 74 | } catch (error: any) { 75 | Alert.alert('Error', error.response?.data?.errormsg || 'An error occurred'); 76 | } 77 | }; 78 | 79 | return ( 80 | <View style={styles.container}> 81 | <View style={styles.logoContainer}> 82 | <Image 83 | source={require('../../../assets/images/logo.png')} 84 | style={styles.logo} 85 | /> 86 | </View> 87 | 88 | <Text style={styles.title}>Forgot Password</Text> 89 | 90 | {step === 1 && ( 91 | <View> 92 | <TextInput 93 | style={styles.input} 94 | placeholder="Email" 95 | placeholderTextColor="#999" 96 | value={email} 97 | onChangeText={setEmail} 98 | keyboardType="email-address" 99 | autoCapitalize="none" 100 | /> 101 | <TouchableOpacity style={styles.button} onPress={handleSendEmail}> 102 | <Text style={styles.buttonText}>Send OTP</Text> 103 | </TouchableOpacity> 104 | </View> 105 | )} 106 | 107 | {step === 2 && ( 108 | <View> 109 | <TextInput 110 | style={styles.input} 111 | placeholder="Enter OTP" 112 | placeholderTextColor="#999" 113 | value={otp} 114 | onChangeText={setOtp} 115 | keyboardType="number-pad" 116 | /> 117 | <TouchableOpacity style={styles.button} onPress={handleVerifyOTP}> 118 | <Text style={styles.buttonText}>Verify OTP</Text> 119 | </TouchableOpacity> 120 | </View> 121 | )} 122 | 123 | {step === 3 && ( 124 | <View> 125 | <TextInput 126 | style={styles.input} 127 | placeholder="New Password" 128 | placeholderTextColor="#999" 129 | value={password} 130 | onChangeText={setPassword} 131 | secureTextEntry 132 | /> 133 | <TextInput 134 | style={styles.input} 135 | placeholder="Confirm Password" 136 | placeholderTextColor="#999" 137 | value={confirmPassword} 138 | onChangeText={setConfirmPassword} 139 | secureTextEntry 140 | /> 141 | <TouchableOpacity style={styles.button} onPress={handleResetPassword}> 142 | <Text style={styles.buttonText}>Reset Password</Text> 143 | </TouchableOpacity> 144 | </View> 145 | )} 146 | 147 | <TouchableOpacity 148 | style={styles.backToLoginContainer} 149 | onPress={() => navigation.navigate('Login')} 150 | > 151 | <Text style={styles.backToLoginText}>Back to Login</Text> 152 | </TouchableOpacity> 153 | </View> 154 | ); 155 | }; 156 | 157 | export default ForgotPasswordScreen; 158 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/NavigationMonitorService.ts: -------------------------------------------------------------------------------- ```typescript 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import axiosRequest from '../helper/axiosRequest'; 3 | import { API } from '../helper/config'; 4 | 5 | const STORAGE_KEYS = { 6 | TERMS_DATE: 'accepted_terms_date', 7 | PRIVACY_DATE: 'accepted_privacy_date', 8 | INITIAL_CHECK_COMPLETED: 'terms_privacy_initial_check_completed' 9 | }; 10 | 11 | const EXCLUDED_ROUTES = [ 12 | 'TermsAndConditions', 13 | 'PrivacyPolicy', 14 | 'Login', 15 | 'SignUp', 16 | 'ForgotPassword', 17 | 'VerifyEmail' 18 | ]; 19 | 20 | export class NavigationMonitorService { 21 | static isExcludedRoute(routeName: string): boolean { 22 | return EXCLUDED_ROUTES.includes(routeName); 23 | } 24 | 25 | /** 26 | * Check if terms and privacy policy need updates 27 | * Improved to handle API errors and prevent constant prompts 28 | */ 29 | static async checkForUpdates() { 30 | try { 31 | // Check if we've already completed the initial check 32 | const initialCheckCompleted = await AsyncStorage.getItem(STORAGE_KEYS.INITIAL_CHECK_COMPLETED); 33 | 34 | // Get stored dates 35 | const [storedTermsDate, storedPrivacyDate] = await Promise.all([ 36 | AsyncStorage.getItem(STORAGE_KEYS.TERMS_DATE), 37 | AsyncStorage.getItem(STORAGE_KEYS.PRIVACY_DATE) 38 | ]); 39 | 40 | // If this is the first time checking and no dates are stored, store current dates 41 | if (!initialCheckCompleted && (!storedTermsDate || !storedPrivacyDate)) { 42 | const currentDate = new Date().toISOString(); 43 | 44 | // Store current date for both terms and privacy 45 | if (!storedTermsDate) { 46 | await AsyncStorage.setItem(STORAGE_KEYS.TERMS_DATE, currentDate); 47 | } 48 | 49 | if (!storedPrivacyDate) { 50 | await AsyncStorage.setItem(STORAGE_KEYS.PRIVACY_DATE, currentDate); 51 | } 52 | 53 | // Mark initial check as completed 54 | await AsyncStorage.setItem(STORAGE_KEYS.INITIAL_CHECK_COMPLETED, 'true'); 55 | 56 | // Return false for both to prevent showing update prompt on first run 57 | return { termsNeedsUpdate: false, privacyNeedsUpdate: false }; 58 | } 59 | 60 | // Initialize with default values 61 | let termsDate = null; 62 | let privacyDate = null; 63 | let termsError = false; 64 | let privacyError = false; 65 | 66 | // Fetch terms date with error handling 67 | try { 68 | const termsResponse = await axiosRequest.get(`${API.BASE_URL}/${API.ENDPOINTS.GET_TERMS_DATE}`); 69 | if (termsResponse && termsResponse.data && termsResponse.data.date) { 70 | termsDate = termsResponse.data.date; 71 | } else { 72 | termsError = true; 73 | } 74 | } catch (error) { 75 | termsError = true; 76 | } 77 | 78 | // Fetch privacy date with error handling 79 | try { 80 | const privacyResponse = await axiosRequest.get(`${API.BASE_URL}/${API.ENDPOINTS.GET_PRIVACY_DATE}`); 81 | if (privacyResponse && privacyResponse.data && privacyResponse.data.date) { 82 | privacyDate = privacyResponse.data.date; 83 | } else { 84 | privacyError = true; 85 | } 86 | } catch (error) { 87 | privacyError = true; 88 | } 89 | 90 | // If both API calls failed, skip update check 91 | if (termsError && privacyError) { 92 | return { termsNeedsUpdate: false, privacyNeedsUpdate: false }; 93 | } 94 | 95 | // Compare dates to determine if updates are needed 96 | const termsNeedsUpdate = termsDate && storedTermsDate 97 | ? new Date(termsDate) > new Date(storedTermsDate) 98 | : false; 99 | 100 | const privacyNeedsUpdate = privacyDate && storedPrivacyDate 101 | ? new Date(privacyDate) > new Date(storedPrivacyDate) 102 | : false; 103 | 104 | return { termsNeedsUpdate, privacyNeedsUpdate }; 105 | } catch (error) { 106 | // In case of any unexpected errors, return false to prevent blocking the user 107 | return { termsNeedsUpdate: false, privacyNeedsUpdate: false }; 108 | } 109 | } 110 | 111 | static async getTermsPublishedDate() { 112 | try { 113 | const response = await axiosRequest.get(`${API.BASE_URL}/${API.ENDPOINTS.GET_TERMS_DATE}`); 114 | return response.data.date; 115 | } catch (error) { 116 | console.error('Error getting terms date:', error); 117 | return null; // Return null instead of current date as fallback 118 | } 119 | } 120 | 121 | static async getPrivacyPublishedDate() { 122 | try { 123 | const response = await axiosRequest.get(`${API.BASE_URL}/${API.ENDPOINTS.GET_PRIVACY_DATE}`); 124 | return response.data.date; 125 | } catch (error) { 126 | console.error('Error getting privacy date:', error); 127 | return null; // Return null instead of current date as fallback 128 | } 129 | } 130 | 131 | static async storeAcceptanceDate(type: 'terms' | 'privacy') { 132 | const key = type === 'terms' ? STORAGE_KEYS.TERMS_DATE : STORAGE_KEYS.PRIVACY_DATE; 133 | await AsyncStorage.setItem(key, new Date().toISOString()); 134 | console.log(`Stored ${type} acceptance date:`, new Date().toISOString()); 135 | } 136 | } ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/AboutUs/AboutUsScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useEffect, useState } from 'react'; 2 | import { View, Text } from 'react-native'; 3 | import axiosRequest from '../../../helper/axiosRequest'; 4 | import { API } from '../../../helper/config'; 5 | import { WebView } from 'react-native-webview'; 6 | import { LoadingSpinner } from '../../../components/LoadingSpinner'; 7 | import { styles } from './Styles'; 8 | 9 | const AboutUsScreen = () => { 10 | const [aboutUsContent, setAboutUsContent] = useState<string>(''); 11 | const [isLoading, setIsLoading] = useState(false); 12 | 13 | useEffect(() => { 14 | getAboutUs(); 15 | }, []); 16 | 17 | const cleanHtml = (html: string) => { 18 | // Extract content between <body> tags if present 19 | const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i); 20 | let content = bodyMatch ? bodyMatch[1] : html; 21 | 22 | // Remove scripts, links, and styles 23 | content = content.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); 24 | content = content.replace(/<link[^>]*>/gi, ''); 25 | content = content.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ''); 26 | 27 | // Remove WordPress specific comments 28 | content = content.replace(/<!--[\s\S]*?-->/g, ''); 29 | 30 | // Clean up image tags 31 | content = content.replace(/loading="lazy"\s/g, ''); 32 | content = content.replace(/decoding="async"\s/g, ''); 33 | content = content.replace(/\sonerror="[^"]*"/g, ''); 34 | 35 | return content; 36 | }; 37 | 38 | const getAboutUs = async () => { 39 | setIsLoading(true); 40 | try { 41 | const response = await axiosRequest.post( 42 | `${API.ENDPOINTS.MOBILEAPI}/${API.ENDPOINTS.GET_ABOUTUS}`, 43 | {}, 44 | { 45 | headers: { 46 | 'Accept': 'application/json', 47 | 'Content-Type': 'application/json' 48 | } 49 | } 50 | ); 51 | 52 | console.log('About Us API Response:', response); 53 | if (response?.data?.aboutus_content) { 54 | const cleanContent = cleanHtml(response.data.aboutus_content); 55 | const content = ` 56 | <!DOCTYPE html> 57 | <html> 58 | <head> 59 | <meta charset="utf-8"> 60 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 61 | <style> 62 | * { 63 | box-sizing: border-box; 64 | } 65 | body { 66 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 67 | padding: 16px; 68 | margin: 0; 69 | color: #333; 70 | font-size: 16px; 71 | line-height: 1.5; 72 | background-color: #ffffff; 73 | } 74 | img { 75 | max-width: 100%; 76 | height: auto; 77 | display: block; 78 | margin: 16px auto; 79 | border-radius: 8px; 80 | } 81 | h2, h3 { 82 | color: #000; 83 | margin: 24px 0 16px 0; 84 | text-align: center; 85 | font-weight: bold; 86 | } 87 | h2 { 88 | font-size: 24px; 89 | } 90 | h3 { 91 | font-size: 20px; 92 | } 93 | h4 { 94 | font-size: 16px; 95 | color: #666; 96 | margin: 8px 0 24px 0; 97 | text-align: center; 98 | font-weight: normal; 99 | } 100 | p { 101 | margin: 16px 0; 102 | color: #333; 103 | } 104 | .team-member { 105 | text-align: center; 106 | margin-bottom: 32px; 107 | } 108 | </style> 109 | </head> 110 | <body> 111 | <div class="content"> 112 | ${cleanContent} 113 | </div> 114 | </body> 115 | </html> 116 | `; 117 | console.log('Final HTML:', content); 118 | setAboutUsContent(content); 119 | } 120 | } catch (error: any) { 121 | console.error('Error getting about us content:', error); 122 | } finally { 123 | setIsLoading(false); 124 | } 125 | }; 126 | 127 | if (isLoading) { 128 | return <LoadingSpinner />; 129 | } 130 | 131 | return ( 132 | <View style={styles.container}> 133 | 134 | <View style={styles.webViewContainer}> 135 | {aboutUsContent ? ( 136 | <WebView 137 | source={{ html: aboutUsContent }} 138 | style={styles.webView} 139 | originWhitelist={['*']} 140 | onError={(syntheticEvent) => { 141 | const { nativeEvent } = syntheticEvent; 142 | console.warn('WebView error: ', nativeEvent); 143 | }} 144 | startInLoadingState={true} 145 | renderLoading={() => <LoadingSpinner />} 146 | showsVerticalScrollIndicator={true} 147 | bounces={true} 148 | automaticallyAdjustContentInsets={true} 149 | injectedJavaScript={` 150 | true; // Required by iOS 151 | `} 152 | /> 153 | ) : ( 154 | <Text style={styles.noContent}>No content available</Text> 155 | )} 156 | </View> 157 | </View> 158 | ); 159 | }; 160 | 161 | export default AboutUsScreen; 162 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/Legal/TermsAndConditionsScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useEffect, useState } from 'react'; 2 | import { View, Text, ScrollView, StyleSheet, TouchableOpacity, ActivityIndicator, Alert } from 'react-native'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | import { colors } from '../../../theme/colors'; 5 | import { API } from '../../../helper/config'; 6 | import { NavigationMonitorService } from '../../../helper/NavigationMonitorService'; 7 | import axiosRequest from '../../../helper/axiosRequest'; 8 | import HTML from 'react-native-render-html'; 9 | 10 | interface TermsResponse { 11 | status: number; 12 | terms_page: Array<{ 13 | post_title: string; 14 | post_content: string; 15 | }>; 16 | } 17 | 18 | const TermsAndConditionsScreen = ({ navigation, route }: any) => { 19 | const [loading, setLoading] = useState(true); 20 | const [error, setError] = useState<string | null>(null); 21 | const [content, setContent] = useState<string>(''); 22 | const [title, setTitle] = useState<string>(''); 23 | const fromUpdate = route.params?.fromUpdate || false; 24 | useEffect(() => { 25 | fetchTermsContent(); 26 | }, []); 27 | 28 | const fetchTermsContent = async () => { 29 | try { 30 | const response = await axiosRequest.get<TermsResponse>(`${API.BASE_URL}/${API.ENDPOINTS.GET_TERMS_PAGE}`); 31 | 32 | if (response.data?.terms_page?.[0]) { 33 | setTitle(response.data.terms_page[0].post_title); 34 | setContent(response.data.terms_page[0].post_content); 35 | } else { 36 | setError('No terms content found'); 37 | } 38 | } catch (err) { 39 | setError('Failed to load terms content'); 40 | console.error('Error fetching terms:', err); 41 | } finally { 42 | setLoading(false); 43 | } 44 | }; 45 | 46 | const handleAccept = async () => { 47 | try { 48 | await NavigationMonitorService.storeAcceptanceDate('terms'); 49 | 50 | if (fromUpdate) { 51 | navigation.goBack(); 52 | } 53 | } catch (err) { 54 | console.error('Error saving acceptance date:', err); 55 | Alert.alert('Error', 'Failed to save your acceptance. Please try again.'); 56 | } 57 | }; 58 | 59 | if (loading) { 60 | return ( 61 | <SafeAreaView style={styles.container}> 62 | <View style={styles.loadingContainer}> 63 | <ActivityIndicator size="large" color={colors.primary} /> 64 | </View> 65 | </SafeAreaView> 66 | ); 67 | } 68 | 69 | if (error) { 70 | return ( 71 | <SafeAreaView style={styles.container}> 72 | <View style={styles.errorContainer}> 73 | <Text style={styles.errorText}>{error}</Text> 74 | <TouchableOpacity style={styles.retryButton} onPress={fetchTermsContent}> 75 | <Text style={styles.retryButtonText}>Retry</Text> 76 | </TouchableOpacity> 77 | </View> 78 | </SafeAreaView> 79 | ); 80 | } 81 | 82 | return ( 83 | <SafeAreaView style={styles.container}> 84 | <View style={styles.header}> 85 | <TouchableOpacity 86 | style={styles.backButton} 87 | onPress={() => navigation.goBack()} 88 | > 89 | <Text style={styles.backButtonText}>←</Text> 90 | </TouchableOpacity> 91 | <Text style={styles.headerTitle}>{title || 'Terms and Conditions'}</Text> 92 | </View> 93 | <ScrollView style={styles.scrollView}> 94 | <View style={styles.content}> 95 | <HTML source={{ html: content }} contentWidth={300} /> 96 | </View> 97 | </ScrollView> 98 | {fromUpdate && ( 99 | <TouchableOpacity style={styles.acceptButton} onPress={handleAccept}> 100 | <Text style={styles.acceptButtonText}>Accept Terms</Text> 101 | </TouchableOpacity> 102 | )} 103 | </SafeAreaView> 104 | ); 105 | }; 106 | 107 | const styles = StyleSheet.create({ 108 | loadingContainer: { 109 | flex: 1, 110 | justifyContent: 'center', 111 | alignItems: 'center', 112 | }, 113 | errorContainer: { 114 | flex: 1, 115 | justifyContent: 'center', 116 | alignItems: 'center', 117 | padding: 20, 118 | }, 119 | errorText: { 120 | fontSize: 16, 121 | color: 'red', 122 | textAlign: 'center', 123 | marginBottom: 20, 124 | }, 125 | retryButton: { 126 | backgroundColor: colors.primary, 127 | padding: 10, 128 | borderRadius: 5, 129 | }, 130 | retryButtonText: { 131 | color: '#FFFFFF', 132 | fontSize: 16, 133 | }, 134 | acceptButton: { 135 | backgroundColor: colors.primary, 136 | padding: 16, 137 | margin: 16, 138 | borderRadius: 8, 139 | alignItems: 'center', 140 | }, 141 | acceptButtonText: { 142 | color: '#FFFFFF', 143 | fontSize: 16, 144 | fontWeight: '600', 145 | }, 146 | container: { 147 | flex: 1, 148 | backgroundColor: '#FFFFFF', 149 | }, 150 | header: { 151 | flexDirection: 'row', 152 | alignItems: 'center', 153 | padding: 16, 154 | borderBottomWidth: 1, 155 | borderBottomColor: '#E5E5E5', 156 | }, 157 | backButton: { 158 | padding: 8, 159 | }, 160 | backButtonText: { 161 | fontSize: 24, 162 | color: colors.primary, 163 | }, 164 | headerTitle: { 165 | fontSize: 18, 166 | fontWeight: '600', 167 | marginLeft: 8, 168 | color: colors.primary, 169 | }, 170 | scrollView: { 171 | flex: 1, 172 | }, 173 | content: { 174 | padding: 20, 175 | }, 176 | title: { 177 | fontSize: 24, 178 | fontWeight: 'bold', 179 | marginBottom: 10, 180 | color: colors.primary, 181 | }, 182 | lastUpdated: { 183 | fontSize: 14, 184 | color: '#666', 185 | marginBottom: 20, 186 | }, 187 | sectionTitle: { 188 | fontSize: 18, 189 | fontWeight: '600', 190 | marginTop: 20, 191 | marginBottom: 10, 192 | color: colors.primary, 193 | }, 194 | text: { 195 | fontSize: 16, 196 | lineHeight: 24, 197 | color: colors.primary, 198 | marginBottom: 15, 199 | }, 200 | }); 201 | 202 | export default TermsAndConditionsScreen; 203 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/Legal/PrivacyPolicyScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useEffect, useState } from 'react'; 2 | import { View, Text, ScrollView, StyleSheet, TouchableOpacity, ActivityIndicator, Alert } from 'react-native'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | import { colors } from '../../../theme/colors'; 5 | import { API } from '../../../helper/config'; 6 | import { NavigationMonitorService } from '../../../helper/NavigationMonitorService'; 7 | import axiosRequest from '../../../helper/axiosRequest'; 8 | import HTML from 'react-native-render-html'; 9 | 10 | interface PrivacyResponse { 11 | status: number; 12 | privacy_page: Array<{ 13 | post_title: string; 14 | post_content: string; 15 | }>; 16 | } 17 | 18 | const PrivacyPolicyScreen = ({ navigation, route }: any) => { 19 | const [loading, setLoading] = useState(true); 20 | const [error, setError] = useState<string | null>(null); 21 | const [content, setContent] = useState<string>(''); 22 | const [title, setTitle] = useState<string>(''); 23 | const fromUpdate = route.params?.fromUpdate || false; 24 | useEffect(() => { 25 | fetchPrivacyContent(); 26 | }, []); 27 | 28 | const fetchPrivacyContent = async () => { 29 | try { 30 | const response = await axiosRequest.get<PrivacyResponse>(`${API.BASE_URL}/${API.ENDPOINTS.GET_PRIVACY_PAGE}`); 31 | 32 | if (response.data?.privacy_page?.[0]) { 33 | setTitle(response.data.privacy_page[0].post_title); 34 | setContent(response.data.privacy_page[0].post_content); 35 | } else { 36 | setError('No privacy policy content found'); 37 | } 38 | } catch (err) { 39 | setError('Failed to load privacy policy content'); 40 | console.error('Error fetching privacy policy:', err); 41 | } finally { 42 | setLoading(false); 43 | } 44 | }; 45 | 46 | const handleAccept = async () => { 47 | try { 48 | await NavigationMonitorService.storeAcceptanceDate('privacy'); 49 | 50 | if (fromUpdate) { 51 | navigation.goBack(); 52 | } 53 | } catch (err) { 54 | console.error('Error saving acceptance date:', err); 55 | Alert.alert('Error', 'Failed to save your acceptance. Please try again.'); 56 | } 57 | }; 58 | 59 | if (loading) { 60 | return ( 61 | <SafeAreaView style={styles.container}> 62 | <View style={styles.loadingContainer}> 63 | <ActivityIndicator size="large" color={colors.primary} /> 64 | </View> 65 | </SafeAreaView> 66 | ); 67 | } 68 | 69 | if (error) { 70 | return ( 71 | <SafeAreaView style={styles.container}> 72 | <View style={styles.errorContainer}> 73 | <Text style={styles.errorText}>{error}</Text> 74 | <TouchableOpacity style={styles.retryButton} onPress={fetchPrivacyContent}> 75 | <Text style={styles.retryButtonText}>Retry</Text> 76 | </TouchableOpacity> 77 | </View> 78 | </SafeAreaView> 79 | ); 80 | } 81 | 82 | return ( 83 | <SafeAreaView style={styles.container}> 84 | <View style={styles.header}> 85 | <TouchableOpacity 86 | style={styles.backButton} 87 | onPress={() => navigation.goBack()} 88 | > 89 | <Text style={styles.backButtonText}>←</Text> 90 | </TouchableOpacity> 91 | <Text style={styles.headerTitle}>{title || 'Privacy Policy'}</Text> 92 | </View> 93 | <ScrollView style={styles.scrollView}> 94 | <View style={styles.content}> 95 | <HTML source={{ html: content }} contentWidth={300} /> 96 | </View> 97 | </ScrollView> 98 | {fromUpdate && ( 99 | <TouchableOpacity style={styles.acceptButton} onPress={handleAccept}> 100 | <Text style={styles.acceptButtonText}>Accept Privacy Policy</Text> 101 | </TouchableOpacity> 102 | )} 103 | </SafeAreaView> 104 | ); 105 | }; 106 | 107 | const styles = StyleSheet.create({ 108 | loadingContainer: { 109 | flex: 1, 110 | justifyContent: 'center', 111 | alignItems: 'center', 112 | }, 113 | errorContainer: { 114 | flex: 1, 115 | justifyContent: 'center', 116 | alignItems: 'center', 117 | padding: 20, 118 | }, 119 | errorText: { 120 | fontSize: 16, 121 | color: 'red', 122 | textAlign: 'center', 123 | marginBottom: 20, 124 | }, 125 | retryButton: { 126 | backgroundColor: colors.primary, 127 | padding: 10, 128 | borderRadius: 5, 129 | }, 130 | retryButtonText: { 131 | color: '#FFFFFF', 132 | fontSize: 16, 133 | }, 134 | acceptButton: { 135 | backgroundColor: colors.primary, 136 | padding: 16, 137 | margin: 16, 138 | borderRadius: 8, 139 | alignItems: 'center', 140 | }, 141 | acceptButtonText: { 142 | color: '#FFFFFF', 143 | fontSize: 16, 144 | fontWeight: '600', 145 | }, 146 | container: { 147 | flex: 1, 148 | backgroundColor: '#FFFFFF', 149 | }, 150 | header: { 151 | flexDirection: 'row', 152 | alignItems: 'center', 153 | padding: 16, 154 | borderBottomWidth: 1, 155 | borderBottomColor: '#E5E5E5', 156 | }, 157 | backButton: { 158 | padding: 8, 159 | }, 160 | backButtonText: { 161 | fontSize: 24, 162 | color: colors.primary, 163 | }, 164 | headerTitle: { 165 | fontSize: 18, 166 | fontWeight: '600', 167 | marginLeft: 8, 168 | color: colors.primary, 169 | }, 170 | scrollView: { 171 | flex: 1, 172 | }, 173 | content: { 174 | padding: 20, 175 | }, 176 | title: { 177 | fontSize: 24, 178 | fontWeight: 'bold', 179 | marginBottom: 10, 180 | color: colors.primary, 181 | }, 182 | lastUpdated: { 183 | fontSize: 14, 184 | color: '#666', 185 | marginBottom: 20, 186 | }, 187 | sectionTitle: { 188 | fontSize: 18, 189 | fontWeight: '600', 190 | marginTop: 20, 191 | marginBottom: 10, 192 | color: colors.primary, 193 | }, 194 | text: { 195 | fontSize: 16, 196 | lineHeight: 24, 197 | color: colors.primary, 198 | marginBottom: 15, 199 | }, 200 | }); 201 | 202 | export default PrivacyPolicyScreen; 203 | ``` -------------------------------------------------------------------------------- /resources/standards/component_design.md: -------------------------------------------------------------------------------- ```markdown 1 | # Component Design Standards 2 | 3 | BluestoneApps follows these standards for designing React Native components. 4 | 5 | ## Component Structure 6 | 7 | ### Flat Component Organization 8 | 9 | We use a flat component organization structure with all reusable components placed directly in the components directory: 10 | 11 | ``` 12 | src/ 13 | └── components/ 14 | ├── Button.tsx 15 | ├── ErrorBoundary.tsx 16 | ├── LoadingSpinner.tsx 17 | └── UpdateModal.tsx 18 | ``` 19 | 20 | Screen-specific components may be placed within their respective screen directories when they are not meant to be reused across the application. 21 | 22 | ## Component Implementation 23 | 24 | ### Functional Components with TypeScript 25 | 26 | We use functional components with TypeScript interfaces for props: 27 | 28 | ```typescript 29 | // src/components/Button.tsx 30 | import React from 'react'; 31 | import { 32 | TouchableOpacity, 33 | Text, 34 | StyleSheet, 35 | ViewStyle, 36 | TextStyle, 37 | ActivityIndicator, 38 | } from 'react-native'; 39 | 40 | interface ButtonProps { 41 | title: string; 42 | onPress: () => void; 43 | style?: ViewStyle; 44 | textStyle?: TextStyle; 45 | loading?: boolean; 46 | disabled?: boolean; 47 | } 48 | 49 | export const Button: React.FC<ButtonProps> = ({ 50 | title, 51 | onPress, 52 | style, 53 | textStyle, 54 | loading = false, 55 | disabled = false, 56 | }) => { 57 | return ( 58 | <TouchableOpacity 59 | style={[ 60 | styles.button, 61 | style, 62 | disabled && styles.disabledButton, 63 | ]} 64 | onPress={onPress} 65 | disabled={disabled || loading} 66 | > 67 | {loading ? ( 68 | <ActivityIndicator color="#fff" /> 69 | ) : ( 70 | <Text style={[styles.text, textStyle]}>{title}</Text> 71 | )} 72 | </TouchableOpacity> 73 | ); 74 | }; 75 | 76 | const styles = StyleSheet.create({ 77 | button: { 78 | backgroundColor: '#007AFF', 79 | paddingVertical: 12, 80 | paddingHorizontal: 24, 81 | borderRadius: 8, 82 | alignItems: 'center', 83 | justifyContent: 'center', 84 | }, 85 | text: { 86 | color: '#FFFFFF', 87 | fontSize: 16, 88 | fontWeight: '600', 89 | }, 90 | disabledButton: { 91 | backgroundColor: '#CCCCCC', 92 | }, 93 | }); 94 | ``` 95 | 96 | ### Inline Styles 97 | 98 | For simpler components, we define styles within the same file using StyleSheet.create(). For more complex components or screens, we may use a separate Styles.ts file in the same directory. 99 | 100 | ```typescript 101 | // Example of styles in a separate file (for a screen component) 102 | // src/screens/PostLogin/BluestoneAppsAI/Styles.ts 103 | import { StyleSheet } from 'react-native'; 104 | 105 | export const styles = StyleSheet.create({ 106 | container: { 107 | flex: 1, 108 | padding: 16, 109 | backgroundColor: '#FFFFFF', 110 | }, 111 | header: { 112 | fontSize: 24, 113 | fontWeight: 'bold', 114 | marginBottom: 16, 115 | }, 116 | // Other styles... 117 | }); 118 | ``` 119 | 120 | ## Component Best Practices 121 | 122 | ### 1. Props and TypeScript Interfaces 123 | 124 | - Use TypeScript interfaces for prop typing 125 | - Provide default values for optional props 126 | - Use optional chaining and nullish coalescing for safer prop access 127 | 128 | ```typescript 129 | // Example of proper TypeScript interface usage 130 | interface LoadingSpinnerProps { 131 | size?: 'small' | 'large'; 132 | color?: string; 133 | text?: string; 134 | } 135 | 136 | export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ 137 | size = 'large', 138 | color = '#007AFF', 139 | text, 140 | }) => { 141 | // Component implementation 142 | }; 143 | ``` 144 | 145 | ### 2. Error Handling 146 | 147 | - Implement error boundaries to catch and handle component errors 148 | - Use try/catch blocks for error handling in async operations 149 | - Provide meaningful feedback to users when errors occur 150 | 151 | ```typescript 152 | // Example of an error boundary component 153 | import React, { Component, ErrorInfo, ReactNode } from 'react'; 154 | import { View, Text, StyleSheet } from 'react-native'; 155 | 156 | interface ErrorBoundaryProps { 157 | children: ReactNode; 158 | fallback?: ReactNode; 159 | } 160 | 161 | interface ErrorBoundaryState { 162 | hasError: boolean; 163 | error: Error | null; 164 | } 165 | 166 | export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { 167 | constructor(props: ErrorBoundaryProps) { 168 | super(props); 169 | this.state = { hasError: false, error: null }; 170 | } 171 | 172 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 173 | return { hasError: true, error }; 174 | } 175 | 176 | componentDidCatch(error: Error, errorInfo: ErrorInfo): void { 177 | console.error('Component error:', error, errorInfo); 178 | } 179 | 180 | render(): ReactNode { 181 | if (this.state.hasError) { 182 | if (this.props.fallback) { 183 | return this.props.fallback; 184 | } 185 | return ( 186 | <View style={styles.errorContainer}> 187 | <Text style={styles.errorText}>Something went wrong</Text> 188 | </View> 189 | ); 190 | } 191 | return this.props.children; 192 | } 193 | } 194 | ``` 195 | 196 | ### 3. State Management 197 | 198 | - Use useState and useEffect hooks for component-level state 199 | - Extract complex state logic into custom hooks 200 | - Use AsyncStorage for persistent data storage 201 | - Consider context API for state that needs to be shared across components 202 | 203 | ### 4. Performance Considerations 204 | 205 | - Memoize expensive calculations with useMemo 206 | - Use useCallback for event handlers passed to child components 207 | - Implement proper list rendering with FlatList or SectionList 208 | - Avoid unnecessary re-renders by using React.memo when appropriate 209 | 210 | ### 5. Accessibility 211 | 212 | - Provide accessible labels for interactive elements 213 | - Ensure sufficient color contrast 214 | - Support dynamic text sizes 215 | - Test with screen readers 216 | - Ensure adequate color contrast 217 | 218 | ### 6. Testing 219 | 220 | - Write unit tests for component logic 221 | - Use snapshot testing for UI consistency 222 | - Test user interactions 223 | 224 | ## Component Documentation 225 | 226 | Document components with JSDoc comments: 227 | 228 | ```typescript 229 | /** 230 | * Primary button component for user actions. 231 | * 232 | * @example 233 | * <Button 234 | * title="Sign Up" 235 | * onPress={handleSignUp} 236 | * variant="primary" 237 | * /> 238 | */ 239 | export const Button: React.FC<ButtonProps> = (/* ... */) => { 240 | // ... 241 | }; 242 | ``` 243 | ``` -------------------------------------------------------------------------------- /resources/standards/api_communication.md: -------------------------------------------------------------------------------- ```markdown 1 | # API Communication Standards 2 | 3 | BluestoneApps follows these standards for API communication in React Native applications. 4 | 5 | ## Structure 6 | 7 | ### API Configuration and Utilities 8 | 9 | We use a structured approach to API communication with the following key files: 10 | 11 | ``` 12 | src/ 13 | ├── config/ 14 | │ ├── apiConfig.ts # API endpoint definitions and configuration 15 | │ └── environment.ts # Environment-specific configuration 16 | ├── services/ 17 | ├── apiService.ts # Core API service with fetch methods 18 | ├── authService.ts # Authentication-related API calls 19 | ├── userService.ts # User-related API calls 20 | └── ... # Other domain-specific services 21 | ``` 22 | 23 | ## API Configuration 24 | 25 | We define API endpoints and configuration in dedicated files: 26 | 27 | ```typescript 28 | // src/config/environment.ts 29 | export const environment = { 30 | server: 'api.example.com', 31 | isProduction: true, 32 | version: '1.0.0', 33 | }; 34 | 35 | // src/config/apiConfig.ts 36 | import { environment } from './environment'; 37 | 38 | export const API = { 39 | BASE_URL: `https://${environment.server}/`, 40 | ENDPOINTS: { 41 | LOGIN: 'auth/login', 42 | REGISTER: 'auth/register', 43 | USER_PROFILE: 'user/profile', 44 | MOBILE_API: 'wp-json/mobileapi/v1/', 45 | // Other endpoints 46 | }, 47 | TIMEOUT: 30000, 48 | }; 49 | 50 | // Optional: Define types for API responses 51 | export interface ApiResponse<T> { 52 | status: string; 53 | message?: string; 54 | data?: T; 55 | } 56 | ``` 57 | 58 | ## API Service Implementation 59 | 60 | We use a centralized API service with fetch-based methods: 61 | 62 | ```typescript 63 | // src/services/apiService.ts 64 | import { storageService } from './storageService'; 65 | import { environment } from '../config/environment'; 66 | 67 | const BASE_URL = `https://${environment.server}/`; 68 | const MOBILE_API = `${BASE_URL}wp-json/mobileapi/v1/`; 69 | 70 | interface ApiResponse { 71 | status: string; 72 | errormsg: string; 73 | error_code: string; 74 | data?: any; 75 | } 76 | 77 | class ApiService { 78 | async getData(endpoint: string) { 79 | try { 80 | console.log('Fetching from:', MOBILE_API + endpoint); 81 | const response = await fetch(MOBILE_API + endpoint); 82 | if (!response.ok) { 83 | throw new Error(`HTTP error! status: ${response.status}`); 84 | } 85 | const data = await response.json(); 86 | return data; 87 | } catch (error) { 88 | console.error('Error fetching data:', error); 89 | throw error; 90 | } 91 | } 92 | 93 | async sendData(endpoint: string, data: any) { 94 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 95 | const payload = { 96 | ...(typeof data === 'object' ? data : { value: data }), 97 | timezone, 98 | }; 99 | 100 | try { 101 | const response = await fetch(MOBILE_API + endpoint, { 102 | method: 'POST', 103 | headers: { 104 | 'Content-Type': 'application/json', 105 | }, 106 | body: JSON.stringify(payload), 107 | }); 108 | 109 | if (!response.ok) { 110 | throw new Error(`HTTP error! status: ${response.status}`); 111 | } 112 | 113 | const result = await response.json(); 114 | return result; 115 | } catch (error) { 116 | console.error('Error sending data:', error); 117 | throw error; 118 | } 119 | } 120 | } 121 | 122 | export const apiService = new ApiService(); 123 | ``` 124 | 125 | ## Domain-Specific Services 126 | 127 | We create separate service files for different domains: 128 | 129 | ```typescript 130 | // src/services/authService.ts 131 | import { apiService } from './apiService'; 132 | import { storageService } from './storageService'; 133 | 134 | class AuthService { 135 | async login(email: string, password: string) { 136 | try { 137 | const response = await apiService.sendData('login', { email, password }); 138 | 139 | if (response.status === 'ok' && response.token) { 140 | await storageService.set('userToken', response.token); 141 | await storageService.set('user', response.user); 142 | return response; 143 | } 144 | 145 | throw new Error(response.errormsg || 'Login failed'); 146 | } catch (error) { 147 | console.error('Login error:', error); 148 | throw error; 149 | } 150 | } 151 | 152 | async logout() { 153 | try { 154 | await storageService.remove('userToken'); 155 | await storageService.remove('user'); 156 | return true; 157 | } catch (error) { 158 | console.error('Logout error:', error); 159 | throw error; 160 | } 161 | } 162 | } 163 | 164 | export const authService = new AuthService(); 165 | ``` 166 | 167 | ## Error Handling 168 | 169 | We implement consistent error handling across all API calls: 170 | 171 | 1. Log errors for debugging 172 | 2. Use try/catch blocks for all async operations 173 | 3. Provide meaningful error messages 174 | 4. Handle specific error types appropriately 175 | 176 | ## Example Usage in Components 177 | 178 | ```typescript 179 | // Example in a screen component 180 | import React, { useState } from 'react'; 181 | import { View, Alert } from 'react-native'; 182 | import { authService } from '../services/authService'; 183 | import { Button } from '../components/Button'; 184 | 185 | const LoginScreen = ({ navigation }) => { 186 | const [loading, setLoading] = useState(false); 187 | 188 | const handleLogin = async (email: string, password: string) => { 189 | try { 190 | setLoading(true); 191 | await authService.login(email, password); 192 | navigation.navigate('Home'); 193 | } catch (error) { 194 | Alert.alert('Login Failed', error instanceof Error ? error.message : 'Unknown error'); 195 | } finally { 196 | setLoading(false); 197 | } 198 | }; 199 | 200 | return ( 201 | <View> 202 | {/* Form fields */} 203 | <Button 204 | title="Login" 205 | onPress={() => handleLogin('[email protected]', 'password')} 206 | loading={loading} 207 | /> 208 | </View> 209 | ); 210 | }; 211 | ``` 212 | 213 | ## Best Practices 214 | 215 | 1. Use TypeScript interfaces for request and response types 216 | 2. Implement proper error handling for all API calls 217 | 3. Store authentication tokens securely 218 | 4. Use environment-specific configuration 219 | 5. Log API calls and errors for debugging 220 | 6. Implement retry logic for important operations 221 | 7. Handle offline scenarios gracefully 222 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/AboutUs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useEffect, useState } from 'react'; 2 | import { View, Text, ScrollView, StyleSheet, Alert } from 'react-native'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import AsyncStorage from '@react-native-async-storage/async-storage'; 5 | import axiosRequest from '../helper/axiosRequest'; 6 | import { API } from '../helper/config'; 7 | import { Button } from '../components/Button'; 8 | import HTML from 'react-native-render-html'; 9 | import { useWindowDimensions } from 'react-native'; 10 | import { LoadingSpinner } from '../components/LoadingSpinner'; 11 | 12 | export const AboutUs = () => { 13 | const [aboutUsContent, setAboutUsContent] = useState<string>(''); 14 | const [userId, setUserId] = useState<string | null>(null); 15 | const [isLoading, setIsLoading] = useState(false); 16 | const navigation = useNavigation(); 17 | const { width } = useWindowDimensions(); 18 | 19 | useEffect(() => { 20 | getAboutUs(); 21 | getUserData(); 22 | }, []); 23 | 24 | const getUserData = async () => { 25 | try { 26 | const userDataString = await AsyncStorage.getItem('userData'); 27 | if (userDataString) { 28 | const userData = JSON.parse(userDataString); 29 | setUserId(userData.user_id); 30 | } 31 | } catch (error) { 32 | console.error('Error getting user data:', error); 33 | } 34 | }; 35 | 36 | const getAboutUs = async () => { 37 | setIsLoading(true); 38 | try { 39 | const response = await axiosRequest.post( 40 | `${API.ENDPOINTS.MOBILEAPI}/${API.ENDPOINTS.GET_ABOUTUS}`, 41 | {}, 42 | { 43 | headers: { 44 | 'Accept': 'application/json', 45 | 'Content-Type': 'application/json' 46 | } 47 | } 48 | ); 49 | setAboutUsContent(response.aboutus_content); 50 | } catch (error: any) { 51 | console.error('Error getting about us content:', error); 52 | Alert.alert('Error', 'Failed to load about us content'); 53 | } finally { 54 | setIsLoading(false); 55 | } 56 | }; 57 | 58 | const confirmDeleteAccount = () => { 59 | Alert.alert( 60 | 'Delete Account', 61 | 'This action cannot be undone. Are you sure you want to delete your account?', 62 | [ 63 | { 64 | text: 'Cancel', 65 | style: 'cancel' 66 | }, 67 | { 68 | text: 'Delete', 69 | onPress: deleteAccount, 70 | style: 'destructive' 71 | } 72 | ] 73 | ); 74 | }; 75 | 76 | const deleteAccount = async () => { 77 | if (!userId) { 78 | Alert.alert('Error', 'User ID not found'); 79 | return; 80 | } 81 | 82 | setIsLoading(true); 83 | try { 84 | const response = await axiosRequest.post( 85 | `${API.ENDPOINTS.MOBILEAPI}/${API.ENDPOINTS.DELETE_USER}`, 86 | { user_id: userId }, 87 | { 88 | headers: { 89 | 'Accept': 'application/json', 90 | 'Content-Type': 'application/json' 91 | } 92 | } 93 | ); 94 | 95 | if (response.status === 'success') { 96 | Alert.alert( 97 | 'Account Deleted', 98 | 'Thank you for using our app.', 99 | [ 100 | { 101 | text: 'OK', 102 | onPress: async () => { 103 | try { 104 | await AsyncStorage.clear(); 105 | } catch (err) { 106 | console.warn('Error clearing storage:', err); 107 | // Continue even if clearing fails 108 | } finally { 109 | navigation.reset({ 110 | index: 0, 111 | routes: [{ name: 'Login' }], 112 | }); 113 | } 114 | } 115 | } 116 | ] 117 | ); 118 | } else { 119 | throw new Error(response.message || 'Failed to delete account'); 120 | } 121 | } catch (error: any) { 122 | console.error('Error deleting account:', error); 123 | Alert.alert('Error', 'Something went wrong. Please try again later.'); 124 | try { 125 | await AsyncStorage.clear(); 126 | } catch (err) { 127 | console.warn('Error clearing storage:', err); 128 | // Continue even if clearing fails 129 | } finally { 130 | navigation.reset({ 131 | index: 0, 132 | routes: [{ name: 'Login' }], 133 | }); 134 | } 135 | } finally { 136 | setIsLoading(false); 137 | } 138 | }; 139 | 140 | if (isLoading) { 141 | return <LoadingSpinner />; 142 | } 143 | 144 | return ( 145 | <View style={styles.container}> 146 | <ScrollView contentContainerStyle={styles.scrollContent}> 147 | {aboutUsContent ? ( 148 | <HTML 149 | source={{ html: aboutUsContent }} 150 | contentWidth={width} 151 | tagsStyles={{ 152 | p: styles.paragraph, 153 | h1: styles.heading, 154 | h2: styles.heading, 155 | h3: styles.heading, 156 | a: styles.link 157 | }} 158 | /> 159 | ) : ( 160 | <Text style={styles.noContent}>No content available</Text> 161 | )} 162 | </ScrollView> 163 | <View style={styles.buttonContainer}> 164 | <Button 165 | title="Delete Account" 166 | onPress={confirmDeleteAccount} 167 | style={styles.deleteButton} 168 | textStyle={styles.deleteButtonText} 169 | /> 170 | </View> 171 | </View> 172 | ); 173 | }; 174 | 175 | const styles = StyleSheet.create({ 176 | container: { 177 | flex: 1, 178 | backgroundColor: '#fff' 179 | }, 180 | scrollContent: { 181 | padding: 16, 182 | paddingBottom: 80 183 | }, 184 | paragraph: { 185 | fontSize: 16, 186 | lineHeight: 24, 187 | color: '#333', 188 | marginBottom: 16 189 | }, 190 | heading: { 191 | fontSize: 20, 192 | fontWeight: 'bold', 193 | color: '#000', 194 | marginBottom: 16, 195 | marginTop: 8 196 | }, 197 | link: { 198 | color: '#007AFF' 199 | }, 200 | noContent: { 201 | fontSize: 16, 202 | color: '#666', 203 | textAlign: 'center', 204 | marginTop: 20 205 | }, 206 | buttonContainer: { 207 | padding: 16, 208 | backgroundColor: '#fff', 209 | borderTopWidth: 1, 210 | borderTopColor: '#eee', 211 | position: 'absolute', 212 | bottom: 0, 213 | left: 0, 214 | right: 0 215 | }, 216 | deleteButton: { 217 | backgroundColor: '#FF3B30' 218 | }, 219 | deleteButtonText: { 220 | color: '#fff' 221 | } 222 | }); 223 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/hooks/useForm.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { useState, useCallback, useMemo } from 'react'; 2 | 3 | /** 4 | * Form field configuration 5 | */ 6 | interface FormField<T> { 7 | value: T; 8 | error?: string; 9 | touched: boolean; 10 | required?: boolean; 11 | validator?: (value: T) => string | undefined; 12 | } 13 | 14 | /** 15 | * Form state containing fields 16 | */ 17 | type FormState<T extends Record<string, any>> = { 18 | [K in keyof T]: FormField<T[K]>; 19 | }; 20 | 21 | /** 22 | * Form values extracted from form state 23 | */ 24 | type FormValues<T extends Record<string, any>> = { 25 | [K in keyof T]: T[K]; 26 | }; 27 | 28 | /** 29 | * Form errors extracted from form state 30 | */ 31 | type FormErrors<T extends Record<string, any>> = { 32 | [K in keyof T]?: string; 33 | }; 34 | 35 | /** 36 | * Custom hook for form state management 37 | * 38 | * @param initialValues Initial values for the form 39 | * @param validators Optional validators for form fields 40 | * @returns Form state and helper functions 41 | * 42 | * @example 43 | * ```tsx 44 | * const { values, errors, touched, handleChange, handleBlur, handleSubmit, isValid } = useForm({ 45 | * email: '', 46 | * password: '', 47 | * }, { 48 | * email: (value) => (!value ? 'Email is required' : !isValidEmail(value) ? 'Invalid email format' : undefined), 49 | * password: (value) => (!value ? 'Password is required' : value.length < 6 ? 'Password too short' : undefined), 50 | * }); 51 | * ``` 52 | */ 53 | function useForm<T extends Record<string, any>>( 54 | initialValues: T, 55 | validators?: Partial<Record<keyof T, (value: any) => string | undefined>>, 56 | requiredFields?: Array<keyof T> 57 | ) { 58 | // Create initial form state 59 | const createInitialState = (): FormState<T> => { 60 | const state: Partial<FormState<T>> = {}; 61 | 62 | for (const key in initialValues) { 63 | if (Object.prototype.hasOwnProperty.call(initialValues, key)) { 64 | state[key] = { 65 | value: initialValues[key], 66 | touched: false, 67 | required: requiredFields?.includes(key) ?? false, 68 | validator: validators?.[key], 69 | }; 70 | } 71 | } 72 | 73 | return state as FormState<T>; 74 | }; 75 | 76 | // State for form fields 77 | const [formState, setFormState] = useState<FormState<T>>(createInitialState()); 78 | 79 | // Extract values from form state 80 | const values = useMemo(() => { 81 | const result: Partial<FormValues<T>> = {}; 82 | 83 | for (const key in formState) { 84 | if (Object.prototype.hasOwnProperty.call(formState, key)) { 85 | result[key] = formState[key].value; 86 | } 87 | } 88 | 89 | return result as FormValues<T>; 90 | }, [formState]); 91 | 92 | // Extract errors from form state 93 | const errors = useMemo(() => { 94 | const result: Partial<FormErrors<T>> = {}; 95 | 96 | for (const key in formState) { 97 | if (Object.prototype.hasOwnProperty.call(formState, key) && formState[key].error) { 98 | result[key] = formState[key].error; 99 | } 100 | } 101 | 102 | return result as FormErrors<T>; 103 | }, [formState]); 104 | 105 | // Extract touched state from form state 106 | const touched = useMemo(() => { 107 | const result: Partial<Record<keyof T, boolean>> = {}; 108 | 109 | for (const key in formState) { 110 | if (Object.prototype.hasOwnProperty.call(formState, key)) { 111 | result[key] = formState[key].touched; 112 | } 113 | } 114 | 115 | return result as Record<keyof T, boolean>; 116 | }, [formState]); 117 | 118 | // Validate a single field 119 | const validateField = useCallback((name: keyof T, value: any): string | undefined => { 120 | const field = formState[name]; 121 | 122 | // Required field validation 123 | if (field.required && (value === '' || value === null || value === undefined)) { 124 | return `${String(name)} is required`; 125 | } 126 | 127 | // Custom validator 128 | if (field.validator) { 129 | return field.validator(value); 130 | } 131 | 132 | return undefined; 133 | }, [formState]); 134 | 135 | // Handle field change 136 | const handleChange = useCallback((name: keyof T, value: any) => { 137 | setFormState(prev => ({ 138 | ...prev, 139 | [name]: { 140 | ...prev[name], 141 | value, 142 | error: validateField(name, value), 143 | }, 144 | })); 145 | }, [validateField]); 146 | 147 | // Handle field blur 148 | const handleBlur = useCallback((name: keyof T) => { 149 | setFormState(prev => ({ 150 | ...prev, 151 | [name]: { 152 | ...prev[name], 153 | touched: true, 154 | error: validateField(name, prev[name].value), 155 | }, 156 | })); 157 | }, [validateField]); 158 | 159 | // Reset form to initial values 160 | const resetForm = useCallback(() => { 161 | setFormState(createInitialState()); 162 | }, []); 163 | 164 | // Validate all fields 165 | const validateForm = useCallback((): boolean => { 166 | let isValid = true; 167 | const newState = { ...formState }; 168 | 169 | for (const key in formState) { 170 | if (Object.prototype.hasOwnProperty.call(formState, key)) { 171 | const error = validateField(key, formState[key].value); 172 | newState[key] = { 173 | ...newState[key], 174 | error, 175 | touched: true, 176 | }; 177 | 178 | if (error) { 179 | isValid = false; 180 | } 181 | } 182 | } 183 | 184 | setFormState(newState); 185 | return isValid; 186 | }, [formState, validateField]); 187 | 188 | // Handle form submission 189 | const handleSubmit = useCallback((onSubmit: (values: FormValues<T>) => void) => { 190 | return (e?: React.FormEvent) => { 191 | if (e) { 192 | e.preventDefault(); 193 | } 194 | 195 | const isValid = validateForm(); 196 | 197 | if (isValid) { 198 | onSubmit(values); 199 | } 200 | }; 201 | }, [validateForm, values]); 202 | 203 | // Check if form is valid 204 | const isValid = useMemo(() => { 205 | for (const key in formState) { 206 | if (Object.prototype.hasOwnProperty.call(formState, key)) { 207 | if (formState[key].error) { 208 | return false; 209 | } 210 | 211 | if (formState[key].required && 212 | (formState[key].value === '' || 213 | formState[key].value === null || 214 | formState[key].value === undefined)) { 215 | return false; 216 | } 217 | } 218 | } 219 | 220 | return true; 221 | }, [formState]); 222 | 223 | return { 224 | values, 225 | errors, 226 | touched, 227 | handleChange, 228 | handleBlur, 229 | handleSubmit, 230 | resetForm, 231 | validateForm, 232 | isValid, 233 | }; 234 | } 235 | 236 | export default useForm; 237 | ```