forked from eatvio/eatvio_app
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12ca205515 | ||
|
|
d8e7464c37 | ||
|
|
e0c90bcb21 | ||
| 4a96390839 | |||
| a3b25270ac | |||
|
|
c72b3e77c6 | ||
| 35f2c0f30a | |||
|
|
9e43be5b4e | ||
| 4db5dda3ce | |||
| 87d5e0bc23 | |||
| fa3cf297a3 | |||
| 7b08560257 | |||
| efd6cca87d | |||
|
|
05bf90a3cf | ||
|
|
feee814bbb | ||
|
|
d871fdc084 | ||
|
|
e865a11d14 | ||
|
|
b242eaa027 | ||
|
|
e073bd3f97 | ||
| 9281213ea1 | |||
|
|
8816b67ff5 | ||
|
|
c88b010cc5 | ||
| 84cdf34859 | |||
| a31f721d81 | |||
| 1b93f5c287 | |||
| 7b8a5d14d7 | |||
| f94fd6f358 | |||
| a9ad6aedd0 | |||
| 83dd53351b | |||
| f269ab59cc | |||
| 0d4ec9390d | |||
| a16e78f804 | |||
| c021cdf2c1 | |||
| 851c300622 |
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
# - name: Build apk
|
||||
# run: flutter build apk
|
||||
# some test
|
||||
# some test 2
|
||||
|
||||
# - name: Rename and move apk
|
||||
# run: |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Build Test Flight"
|
||||
name: 'Build Test Flight'
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Checkout env repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -39,8 +39,8 @@ jobs:
|
||||
run: |
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
|
||||
|
||||
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
|
||||
|
||||
- name: Setup API key
|
||||
run: |
|
||||
echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8
|
||||
@@ -66,9 +66,9 @@ jobs:
|
||||
env:
|
||||
LANG: en_US.UTF-8
|
||||
LC_ALL: en_US.UTF-8
|
||||
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
|
||||
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
|
||||
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
|
||||
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
|
||||
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
|
||||
FASTLANE_USER: eatvio2023@icloud.com
|
||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
||||
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
|
||||
|
||||
167
.kilocode/rules/memory-bank-instructions.md
Normal file
167
.kilocode/rules/memory-bank-instructions.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Memory Bank
|
||||
|
||||
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
|
||||
|
||||
When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization.
|
||||
|
||||
## Memory Bank Structure
|
||||
|
||||
The Memory Bank consists of core files and optional context files, all in Markdown format.
|
||||
|
||||
### Core Files (Required)
|
||||
1. `brief.md`
|
||||
This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved.
|
||||
- Foundation document that shapes all other files
|
||||
- Created at project start if it doesn't exist
|
||||
- Defines core requirements and goals
|
||||
- Source of truth for project scope
|
||||
|
||||
2. `product.md`
|
||||
- Why this project exists
|
||||
- Problems it solves
|
||||
- How it should work
|
||||
- User experience goals
|
||||
|
||||
3. `context.md`
|
||||
This file should be short and factual, not creative or speculative.
|
||||
- Current work focus
|
||||
- Recent changes
|
||||
- Next steps
|
||||
|
||||
4. `architecture.md`
|
||||
- System architecture
|
||||
- Source Code paths
|
||||
- Key technical decisions
|
||||
- Design patterns in use
|
||||
- Component relationships
|
||||
- Critical implementation paths
|
||||
|
||||
5. `tech.md`
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
- Dependencies
|
||||
- Tool usage patterns
|
||||
|
||||
### Additional Files
|
||||
Create additional files/folders within memory-bank/ when they help organize:
|
||||
- `tasks.md` - Documentation of repetitive tasks and their workflows
|
||||
- Complex feature documentation
|
||||
- Integration specifications
|
||||
- API documentation
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
|
||||
## Core workflows
|
||||
|
||||
### Memory Bank Initialization
|
||||
|
||||
The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built.
|
||||
|
||||
When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including:
|
||||
- All source code files and their relationships
|
||||
- Configuration files and build system setup
|
||||
- Project structure and organization patterns
|
||||
- Documentation and comments
|
||||
- Dependencies and external integrations
|
||||
- Testing frameworks and patterns
|
||||
|
||||
I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness.
|
||||
|
||||
After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions.
|
||||
|
||||
### Memory Bank Update
|
||||
|
||||
Memory Bank updates occur when:
|
||||
1. Discovering new project patterns
|
||||
2. After implementing significant changes
|
||||
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
|
||||
4. When context needs clarification
|
||||
|
||||
If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?"
|
||||
|
||||
To execute Memory Bank update, I will:
|
||||
|
||||
1. Review ALL project files
|
||||
2. Document current state
|
||||
3. Document Insights & Patterns
|
||||
4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source
|
||||
|
||||
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state.
|
||||
|
||||
### Add Task
|
||||
|
||||
When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**.
|
||||
|
||||
This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include:
|
||||
- Adding support for new AI model versions
|
||||
- Implementing new API endpoints following established patterns
|
||||
- Adding new features that follow existing architecture
|
||||
|
||||
Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks.
|
||||
|
||||
To execute Add Task workflow:
|
||||
|
||||
1. Create or update `tasks.md` in the memory bank folder
|
||||
2. Document the task with:
|
||||
- Task name and description
|
||||
- Files that need to be modified
|
||||
- Step-by-step workflow followed
|
||||
- Important considerations or gotchas
|
||||
- Example of the completed implementation
|
||||
3. Include any context that was discovered during task execution but wasn't previously documented
|
||||
|
||||
Example task entry:
|
||||
```markdown
|
||||
## Add New Model Support
|
||||
**Last performed:** [date]
|
||||
**Files to modify:**
|
||||
- `/providers/gemini.md` - Add model to documentation
|
||||
- `/src/providers/gemini-config.ts` - Add model configuration
|
||||
- `/src/constants/models.ts` - Add to model list
|
||||
- `/tests/providers/gemini.test.ts` - Add test cases
|
||||
|
||||
**Steps:**
|
||||
1. Add model configuration with proper token limits
|
||||
2. Update documentation with model capabilities
|
||||
3. Add to constants file for UI display
|
||||
4. Write tests for new model configuration
|
||||
|
||||
**Important notes:**
|
||||
- Check Google's documentation for exact token limits
|
||||
- Ensure backward compatibility with existing configurations
|
||||
- Test with actual API calls before committing
|
||||
```
|
||||
|
||||
### Regular Task Execution
|
||||
|
||||
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional.
|
||||
|
||||
The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like:
|
||||
|
||||
"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API."
|
||||
|
||||
When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed.
|
||||
|
||||
If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?"
|
||||
|
||||
In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes.
|
||||
|
||||
## Context Window Management
|
||||
|
||||
When the context window fills up during an extended session:
|
||||
1. I should suggest updating the memory bank to preserve the current state
|
||||
2. Recommend starting a fresh conversation/task
|
||||
3. In the new conversation, I will automatically load the memory bank files to maintain continuity
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access.
|
||||
|
||||
## Important Notes
|
||||
|
||||
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||
|
||||
If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user.
|
||||
|
||||
IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
|
||||
374
.kilocode/rules/memory-bank/architecture.md
Normal file
374
.kilocode/rules/memory-bank/architecture.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# System Architecture
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Core Directory (`lib/core/`)
|
||||
Central infrastructure and shared components:
|
||||
- **Dependency Injection**: GetIt-based service locator in `locator_service.dart`
|
||||
- **Routing**: GoRouter implementation in `app_router.dart` with deep linking
|
||||
- **Theme**: Material theme with Mulish font family in `theme/theme_helper.dart`
|
||||
- **Networking**: Dio HTTP client with token interceptor
|
||||
- **WebSockets**: Pusher Channels for real-time communication
|
||||
- **State Management**: Flutter BLoC/Cubit pattern
|
||||
|
||||
### Features Directory (`lib/features/`)
|
||||
Feature-based modular architecture with 35+ feature modules:
|
||||
|
||||
**Core Features:**
|
||||
- `auth/` - Authentication (email/password, OAuth)
|
||||
- `feed/` - Content discovery feed
|
||||
- `diary/` - Food diary tracking
|
||||
- `chat/` - Real-time messaging
|
||||
- `consultations/` - Consultation management
|
||||
- `profile/` - User profiles and settings
|
||||
|
||||
**Content Creation:**
|
||||
- `create_recipe/` - Recipe creation
|
||||
- `create_diet/` - Diet plan creation
|
||||
- `create_plan/` - Nutrition plan creation
|
||||
- `blog_post_create/` - Blog post creation
|
||||
|
||||
**User Management:**
|
||||
- `settings/` - User preferences
|
||||
- `user_details/` - User information management
|
||||
- `menu/` - App navigation menu
|
||||
- `bottom_navigation_bar/` - Main navigation
|
||||
|
||||
**Consultant Features:**
|
||||
- `consultant/` - Consultant verification
|
||||
- `consultant_rating/` - Rating system
|
||||
- `finances_and_consultations_archive/` - Financial tracking
|
||||
|
||||
**Supporting Features:**
|
||||
- `notifications/` - Push notifications
|
||||
- `glossary/` - Nutrition glossary
|
||||
- `calendar/` - Calendar integration
|
||||
- `support/` - Customer support
|
||||
- `welcome/` - Onboarding
|
||||
- `update_screen/` - App update management
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Clean Architecture Layers
|
||||
|
||||
Each feature follows a layered structure:
|
||||
|
||||
```
|
||||
feature/
|
||||
├── models/ # Data models (freezed + json_serializable)
|
||||
├── services/ # API services (Retrofit)
|
||||
├── repositories/ # Data repositories
|
||||
├── presentation/ # UI layer
|
||||
│ ├── pages/ # Screen widgets
|
||||
│ ├── widgets/ # Feature-specific widgets
|
||||
│ └── bloc/ # State management (Cubit)
|
||||
└── entities/ # Domain entities
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
**Registration Structure** (`lib/core/di/`):
|
||||
- `app_config_registration.dart` - App configuration
|
||||
- `external_registrations.dart` - External dependencies (Dio, etc.)
|
||||
- `service_registrations.dart` - API services
|
||||
- `repository_registrations.dart` - Data repositories
|
||||
- `cubit_registrations.dart` - State management
|
||||
|
||||
**Service Lifecycle:**
|
||||
- Singleton: Services, Repositories (most common)
|
||||
- Factory: Cubits that need fresh instances
|
||||
- Lazy Singleton: WebSocket manager, core services
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
**BLoC/Cubit Implementation:**
|
||||
- Global state: Registered in `AppConfig.globalBlocProviders`
|
||||
- Feature state: Created per screen/feature
|
||||
- State classes: Freezed unions for type-safe states
|
||||
|
||||
**Global Cubits:**
|
||||
- `NewMessagesCubit` - Message notifications
|
||||
- `NotificationsCubit` - Push notifications
|
||||
- `FeedCubit` - Feed content
|
||||
- `ProfileAvatarCubit` - User profile
|
||||
- `ChatsCubit` - Chat rooms
|
||||
- `ViewsCubit` - Content view tracking
|
||||
- `InternetConnectionCubit` - Network status
|
||||
|
||||
## Data Flow
|
||||
|
||||
### API Integration
|
||||
|
||||
**Base Setup:**
|
||||
- Base URL: Configurable via environment (prod/dev)
|
||||
- HTTP Client: Dio with interceptors
|
||||
- API Services: Retrofit for type-safe endpoints
|
||||
- Authentication: Bearer token in headers
|
||||
|
||||
**Interceptors:**
|
||||
1. `TokenInterceptor` - Adds auth token, handles 401 errors
|
||||
2. `PhoneVerifyErrorInterceptor` - Handles phone verification errors
|
||||
|
||||
### Real-time Communication
|
||||
|
||||
**WebSocket Architecture:**
|
||||
- Library: `dart_pusher_channels`
|
||||
- Manager: `WsManager` (singleton)
|
||||
- Connection: Authenticated via backend endpoint
|
||||
|
||||
**Channels:**
|
||||
1. Private Notifications: `App\Events\BroadcastNotificationCreated`
|
||||
2. Private Messages: `App\Events\ChatMessageUpdated`
|
||||
3. Direct Chat: Room-specific message updates
|
||||
4. Client Typing: Real-time typing indicators
|
||||
|
||||
**Socket ID Storage:**
|
||||
- Persisted via SharedPreferences
|
||||
- Used for authentication and channel subscription
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
**Methods:**
|
||||
1. Email/Password - Traditional credentials
|
||||
2. OAuth Providers:
|
||||
- VK ID (Russian social network)
|
||||
- Yandex ID
|
||||
- OK (Odnoklassniki)
|
||||
- Mail.ru
|
||||
|
||||
**Token Management:**
|
||||
- Storage: SharedPreferences via `TokenStorage`
|
||||
- Refresh: Automatic on 401 response
|
||||
- Logout: Token removal + WebSocket cleanup
|
||||
|
||||
**Post-Authentication:**
|
||||
1. WebSocket initialization
|
||||
2. Profile loading
|
||||
3. Chat room loading
|
||||
4. Notification loading
|
||||
5. Firebase push token registration
|
||||
|
||||
## Navigation Architecture
|
||||
|
||||
### GoRouter Setup
|
||||
|
||||
**Route Structure:**
|
||||
- Shell routes for persistent bottom navigation
|
||||
- Named routes for easy navigation
|
||||
- Deep linking support for content (recipe/blog_post/plan/diet)
|
||||
|
||||
**Route Groups:**
|
||||
- `AuthRoutes` - Login, registration, password reset
|
||||
- `MainNavigationRoutes` - Shell route with bottom nav
|
||||
- `ContentRoutes` - Content viewing screens
|
||||
- `SettingsRoutes` - User settings
|
||||
- `OtherRoutes` - Miscellaneous screens
|
||||
|
||||
**Deep Linking Pattern:**
|
||||
```
|
||||
/content/{type}/{identifier}
|
||||
- /content/recipe/{slug}
|
||||
- /content/blog_post/{id}
|
||||
- /content/plan/{slug}
|
||||
- /content/diet/{slug}
|
||||
```
|
||||
|
||||
### Bottom Navigation
|
||||
|
||||
**Main Sections:**
|
||||
1. My Profile - User profile and content
|
||||
2. Feed - Content discovery
|
||||
3. Diary - Food tracking
|
||||
4. Consultations - Consultation management
|
||||
5. Chat - Messaging
|
||||
6. Notifications - Notification center
|
||||
7. Menu - Settings and more
|
||||
|
||||
**Implementation:**
|
||||
- `BottomNavigationBarWidget` wraps content
|
||||
- No transition animations for tab switches
|
||||
- WebSocket initialization on feed screen
|
||||
|
||||
## Data Persistence
|
||||
|
||||
### Storage Solutions
|
||||
|
||||
**SharedPreferences:**
|
||||
- Authentication token
|
||||
- Socket ID
|
||||
- User preferences
|
||||
|
||||
**Cached Network Images:**
|
||||
- Library: `cached_network_image`
|
||||
- Automatic caching of user avatars, content images
|
||||
|
||||
**Firebase:**
|
||||
- Push notification tokens
|
||||
- Remote configuration
|
||||
- Analytics events
|
||||
|
||||
## Critical Implementation Details
|
||||
|
||||
### Content View Tracking
|
||||
|
||||
**Pattern:**
|
||||
- `ViewsCubit` handles visibility detection
|
||||
- `VisibilityDetector` widget tracks on-screen time
|
||||
- Views sent to backend when 50%+ visible for 2+ seconds
|
||||
- Debounced to prevent excessive API calls
|
||||
|
||||
**Tracked Content:**
|
||||
- Blog posts
|
||||
- Recipes
|
||||
- Diets
|
||||
- Nutrition plans
|
||||
|
||||
### Image Handling
|
||||
|
||||
**Upload Flow:**
|
||||
1. `ImagePicker` for selection
|
||||
2. `FlutterImageCompress` for compression
|
||||
3. MultiPart form data upload via Dio
|
||||
4. Preview via `CachedNetworkImage`
|
||||
|
||||
**Supported Formats:**
|
||||
- User photos (profile, diary)
|
||||
- Content images (recipes, diets, plans)
|
||||
- Chat attachments
|
||||
|
||||
### Search Implementation
|
||||
|
||||
**Debounced Search:**
|
||||
- Library: `easy_debounce`
|
||||
- Delay: 500ms
|
||||
- Min query length: 3 characters
|
||||
- Applied to: Feed, Recipes, Diets, Plans, Consultants
|
||||
|
||||
**Tag-based Filtering:**
|
||||
- Recipe tags for filtering
|
||||
- Modal bottom sheet UI
|
||||
- Persistent selection across sessions
|
||||
|
||||
### Pagination Pattern
|
||||
|
||||
**Implementation:**
|
||||
- Page-based pagination
|
||||
- Scroll detection for next page load
|
||||
- Loading state during fetch
|
||||
- Meta data tracking (current page, last page)
|
||||
|
||||
**Example (FeedCubit):**
|
||||
```dart
|
||||
currentPage = 1
|
||||
lastPage = meta.lastPage
|
||||
loadNextPage() when currentPage < lastPage
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Network Errors
|
||||
|
||||
**401 Unauthorized:**
|
||||
- Auto logout
|
||||
- Token removal
|
||||
- Navigate to auth screen
|
||||
|
||||
**Connection Errors:**
|
||||
- `InternetConnectionCubit` monitors status
|
||||
- Retry mechanisms in repositories
|
||||
- User-friendly error messages
|
||||
|
||||
### State Error Handling
|
||||
|
||||
**Pattern:**
|
||||
```dart
|
||||
sealed class State {
|
||||
const State.loading();
|
||||
const State.loaded({required data});
|
||||
const State.error({required String errorMessage});
|
||||
}
|
||||
```
|
||||
|
||||
**UI Response:**
|
||||
- Loading: Show loaders/skeletons
|
||||
- Error: Show `DataErrorWidget` with retry
|
||||
- Empty: Show empty state messages
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Lazy Loading
|
||||
- Lazy singleton services
|
||||
- On-demand cubit creation
|
||||
- Deferred widget loading
|
||||
|
||||
### Image Optimization
|
||||
- Compression before upload
|
||||
- Cached network images
|
||||
- Thumbnail generation
|
||||
|
||||
### WebSocket Management
|
||||
- Connection reuse
|
||||
- Automatic reconnection
|
||||
- Proper cleanup on logout
|
||||
|
||||
### List Rendering
|
||||
- Pagination for large lists
|
||||
- ListView.builder for efficiency
|
||||
- Visibility detection for tracking
|
||||
|
||||
## Platform-Specific Features
|
||||
|
||||
### Android
|
||||
- Huawei HMS support for Huawei devices
|
||||
- Google FCM for push notifications
|
||||
- ProGuard rules for release builds
|
||||
- QR code scanning via camera
|
||||
|
||||
### iOS
|
||||
- APNs for push notifications
|
||||
- Deep linking configuration
|
||||
- App Store compliance
|
||||
|
||||
### Firebase Integration
|
||||
- Separate configs for Android/iOS
|
||||
- Environment-based configuration
|
||||
- Push notification handling
|
||||
- Analytics tracking
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
- Bearer token authentication
|
||||
- Secure token storage
|
||||
- Automatic session management
|
||||
- OAuth 2.0 for social login
|
||||
|
||||
### Data Protection
|
||||
- HTTPS only communication
|
||||
- Token encryption in storage
|
||||
- Secure WebSocket connections
|
||||
- Privacy policy compliance
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
**Entry Point:**
|
||||
- `lib/main.dart` - App initialization
|
||||
|
||||
**Core Infrastructure:**
|
||||
- `lib/core/app_initializer.dart` - Startup sequence
|
||||
- `lib/core/locator_service.dart` - DI container
|
||||
- `lib/core/app_config.dart` - App configuration
|
||||
- `lib/core/app_router.dart` - Routing setup
|
||||
|
||||
**WebSocket:**
|
||||
- `lib/core/websockets/ws_manager.dart` - Connection management
|
||||
- `lib/core/websockets/ws_service.dart` - API service
|
||||
|
||||
**Authentication:**
|
||||
- `lib/features/auth/sdk/social_auth_facade.dart` - Social auth
|
||||
- `lib/core/token_storage.dart` - Token persistence
|
||||
- `lib/core/interceptors/token_interceptor.dart` - Auth interceptor
|
||||
|
||||
**Environment:**
|
||||
- `lib/core/env_config.dart` - Environment variables
|
||||
- `.env` - Environment file (not in repo)
|
||||
35
.kilocode/rules/memory-bank/brief.md
Normal file
35
.kilocode/rules/memory-bank/brief.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Project Brief
|
||||
|
||||
## Project Name
|
||||
Eatvio - Nutrition & Diet Management Application
|
||||
|
||||
## Project Description
|
||||
A comprehensive mobile application connecting users with nutrition consultants, providing diet planning tools, recipes, food diary tracking, and professional consultation services.
|
||||
|
||||
## Core Features
|
||||
- **Blog Posts**: Educational nutrition content and articles
|
||||
- **Nutrition Consultation**: Professional consultation services with certified nutritionists
|
||||
- **Recipes**: User-generated and professional recipes with nutritional information
|
||||
- **Diets**: Structured diet plans with daily schedules
|
||||
- **Nutrition Plans**: Long-term nutrition planning and tracking
|
||||
|
||||
## Target Audience
|
||||
- Primary: Russian-speaking users interested in nutrition and healthy eating
|
||||
- Secondary: Certified nutrition consultants offering professional services
|
||||
- Market: Russian Federation and Russian-speaking countries
|
||||
|
||||
## Platform
|
||||
- Mobile: iOS and Android (including Huawei devices)
|
||||
- Language: Russian (ru)
|
||||
- Current Version: 1.3.0+79
|
||||
|
||||
## Business Model
|
||||
- Paid consultations with nutritionists
|
||||
- Content marketplace for premium diets and plans
|
||||
- Subscription-based features for consultants
|
||||
|
||||
## Key Success Metrics
|
||||
- User engagement with food diary
|
||||
- Consultation bookings and completion rates
|
||||
- Content creation by consultants
|
||||
- User retention and progress tracking
|
||||
29
.kilocode/rules/memory-bank/context.md
Normal file
29
.kilocode/rules/memory-bank/context.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Current Context
|
||||
|
||||
## Project Status
|
||||
Memory Bank initialized on 2025-10-16. This is a mature Flutter application (v1.3.0+79) currently in production.
|
||||
|
||||
## Recent Activity
|
||||
- Memory Bank initialization completed
|
||||
- All core files documented
|
||||
- Architecture and technology stack catalogued
|
||||
|
||||
## Current Focus
|
||||
Ready for development tasks. No active work in progress.
|
||||
|
||||
## Known Issues
|
||||
None documented at initialization.
|
||||
|
||||
## Next Steps
|
||||
Awaiting user direction for:
|
||||
- Feature development
|
||||
- Bug fixes
|
||||
- Architecture improvements
|
||||
- Performance optimizations
|
||||
|
||||
## Important Notes
|
||||
- Primary language: Russian (ru)
|
||||
- Target market: Russian users
|
||||
- Multi-platform: Android (including Huawei) and iOS
|
||||
- Environment: Uses `.env` file for configuration (not in repository)
|
||||
- Build system: Requires code generation for models/services
|
||||
73
.kilocode/rules/memory-bank/product.md
Normal file
73
.kilocode/rules/memory-bank/product.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Product Overview
|
||||
|
||||
## Purpose
|
||||
Eatvio is a comprehensive nutrition and diet management mobile application that connects users with nutrition consultants, provides diet planning tools, recipes, and personal food diary tracking.
|
||||
|
||||
## Core Problems It Solves
|
||||
|
||||
### For Regular Users:
|
||||
1. **Diet Management**: Track daily food intake with detailed nutritional information
|
||||
2. **Meal Planning**: Access to recipes and structured diet plans
|
||||
3. **Professional Guidance**: Connect with certified nutrition consultants
|
||||
4. **Progress Tracking**: Monitor weight, body measurements, and dietary habits
|
||||
5. **Knowledge Access**: Learn about nutrition through blog posts and glossary
|
||||
|
||||
### For Nutrition Consultants:
|
||||
1. **Client Management**: Communicate with clients via integrated chat
|
||||
2. **Content Creation**: Create and publish diets, meal plans, recipes, and blog posts
|
||||
3. **Consultation Services**: Offer paid nutrition consultation services
|
||||
4. **Income Tracking**: Monitor earnings and client purchases
|
||||
5. **Professional Presence**: Build profile and gain subscribers
|
||||
|
||||
## How It Works
|
||||
|
||||
### User Journey:
|
||||
1. **Authentication**: Sign up via email/password or social networks (VK, Yandex, OK, Mail.ru)
|
||||
2. **Feed Exploration**: Browse recipes, diets, nutrition plans, blog posts, and consultants
|
||||
3. **Content Interaction**: Like, bookmark, comment on content
|
||||
4. **Diary Tracking**: Log daily meals with photos and nutritional data
|
||||
5. **Consultations**: Book and pay for nutrition consultant services
|
||||
6. **Real-time Chat**: Communicate with consultants through integrated messaging
|
||||
7. **Progress Monitoring**: Track weight and body measurements over time
|
||||
|
||||
### Consultant Journey:
|
||||
1. **Become Consultant**: Apply and get verified as nutrition consultant
|
||||
2. **Content Creation**: Publish professional diets, meal plans, recipes
|
||||
3. **Client Interaction**: Respond to consultations and provide guidance
|
||||
4. **Diary Review**: Review client food diaries and provide feedback
|
||||
5. **Earnings Management**: Track income from consultations and content
|
||||
|
||||
## User Experience Goals
|
||||
|
||||
### Seamless Navigation:
|
||||
- Bottom navigation bar for main sections (Profile, Feed, Diary, Consultations, Chat)
|
||||
- Tab-based feed filtering (Blog posts, Recipes, Diets, Plans, Consultants)
|
||||
- Deep linking support for direct content access
|
||||
|
||||
### Real-time Engagement:
|
||||
- WebSocket-powered chat for instant messaging
|
||||
- Push notifications for messages and updates
|
||||
- Live content updates without app restart
|
||||
|
||||
### Professional Interface:
|
||||
- Clean, modern UI with Mulish font family
|
||||
- Intuitive search and filtering across all content types
|
||||
- Visual progress tracking with charts and gauges
|
||||
|
||||
### Content Discovery:
|
||||
- Tag-based recipe filtering
|
||||
- Search functionality across all content types
|
||||
- Personalized feed based on user interests
|
||||
|
||||
### Offline Capability:
|
||||
- Token persistence for seamless re-authentication
|
||||
- Cached content for better performance
|
||||
- Queue system for offline actions
|
||||
|
||||
## Key Differentiators
|
||||
|
||||
1. **Integrated Platform**: Combines diet tracking, content, and professional consultations
|
||||
2. **Social Features**: Follow consultants, like/bookmark content, engage with community
|
||||
3. **Professional Tools**: Full toolkit for consultants to manage practice
|
||||
4. **Russian Market Focus**: Optimized for Russian users with local payment and auth integrations
|
||||
5. **Multi-platform Support**: Works on Android (including Huawei) and iOS
|
||||
152
.kilocode/rules/memory-bank/tech.md
Normal file
152
.kilocode/rules/memory-bank/tech.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Technology Stack
|
||||
|
||||
## Core Framework
|
||||
- **Flutter**: SDK 3.8.0+
|
||||
- **Dart**: Language version >=3.8.0 <4.0.0
|
||||
- **Version**: 1.3.0+79 (build 79)
|
||||
|
||||
## State Management
|
||||
- **flutter_bloc**: ^9.1.1 - BLoC pattern implementation
|
||||
- **equatable**: ^2.0.7 - Value equality for state objects
|
||||
|
||||
## Data Management
|
||||
- **freezed**: ^3.2.0 - Immutable data classes
|
||||
- **freezed_annotation**: ^3.1.0 - Code generation annotations
|
||||
- **json_annotation**: ^4.9.0 - JSON serialization
|
||||
- **json_serializable**: ^6.10.0 - JSON code generation
|
||||
|
||||
## Networking
|
||||
- **dio**: ^5.9.0 - HTTP client
|
||||
- **retrofit**: ^4.7.1 - Type-safe REST client
|
||||
- **retrofit_generator**: ^10.0.2 - Retrofit code generation
|
||||
- **http**: ^1.5.0 - Additional HTTP support
|
||||
- **http_parser**: ^4.1.2 - HTTP parsing utilities
|
||||
|
||||
## Real-time Communication
|
||||
- **dart_pusher_channels**: ^1.2.3 - WebSocket support via Pusher
|
||||
- **connectivity_plus**: ^6.1.5 - Network connectivity monitoring
|
||||
|
||||
## Navigation & Routing
|
||||
- **go_router**: ^16.1.0 - Declarative routing
|
||||
- Deep linking support for content
|
||||
|
||||
## UI Components
|
||||
- **cached_network_image**: ^3.4.1 - Image caching
|
||||
- **flutter_svg**: ^2.2.0 - SVG rendering
|
||||
- **google_fonts**: ^6.3.0 - Mulish font family
|
||||
- **carousel_slider_plus**: ^7.1.1 - Carousel widgets
|
||||
- **photo_view**: ^0.15.0 - Image viewer
|
||||
- **dismissible_page**: ^1.0.2 - Swipe to dismiss
|
||||
- **badges**: ^3.1.2 - Notification badges
|
||||
- **lottie**: ^3.3.1 - Animation support
|
||||
- **flutter_portal**: ^1.1.4 - Overlay widgets
|
||||
- **dotted_border**: ^3.1.0 - Dotted borders
|
||||
- **flutter_rating_bar**: ^4.0.1 - Rating widgets
|
||||
|
||||
## Forms & Input
|
||||
- **flutter_form_builder**: ^10.2.0 - Form management
|
||||
- **validators**: ^3.0.0 - Input validation
|
||||
- **flutter_typeahead**: ^5.2.0 - Autocomplete
|
||||
- **keyboard_dismisser**: ^3.0.0 - Keyboard management
|
||||
- **flutter_keyboard_visibility**: ^6.0.0 - Keyboard state
|
||||
- **mask_text_input_formatter**: ^2.9.0 - Input masking
|
||||
|
||||
## Media & Files
|
||||
- **image_picker**: ^1.2.0 - Image selection
|
||||
- **flutter_image_compress**: ^2.4.0 - Image compression
|
||||
- **image**: ^4.5.4 - Image manipulation
|
||||
- **file_picker**: ^10.3.1 - File selection
|
||||
- **audioplayers**: ^6.5.0 - Audio playback
|
||||
- **qr_code_scanner_plus**: ^2.0.10+1 - QR code scanning
|
||||
|
||||
## Data Visualization
|
||||
- **syncfusion_flutter_charts**: ^30.2.5 - Chart widgets
|
||||
- **syncfusion_flutter_gauges**: ^30.2.5 - Gauge widgets
|
||||
- **table_calendar**: ^3.2.0 - Calendar widget
|
||||
|
||||
## Storage & Persistence
|
||||
- **shared_preferences**: ^2.5.3 - Local key-value storage
|
||||
- **path_provider**: ^2.1.5 - File system paths
|
||||
- **path**: ^1.9.1 - Path manipulation
|
||||
|
||||
## Firebase
|
||||
- **firebase_core**: ^4.0.0 - Firebase initialization
|
||||
- **firebase_messaging**: ^16.0.0 - Push notifications
|
||||
|
||||
## Analytics & Monitoring
|
||||
- **sentry_flutter**: ^9.6.0 - Error tracking
|
||||
- **appmetrica_plugin**: ^3.3.0 - Analytics (Russian market)
|
||||
|
||||
## Authentication
|
||||
- **vk_id**: ^1.0.3 - VK social login
|
||||
- **flutter_login_yandex**: ^0.0.5 - Yandex ID login
|
||||
- OAuth support for OK and Mail.ru
|
||||
|
||||
## Utilities
|
||||
- **url_launcher**: ^6.3.2 - URL handling
|
||||
- **easy_debounce**: ^2.0.3 - Debouncing
|
||||
- **share_plus**: ^11.1.0 - Content sharing
|
||||
- **flutter_html**: ^3.0.0 - HTML rendering
|
||||
- **webview_flutter**: ^4.13.0 - WebView
|
||||
- **intl**: ^0.20.2 - Internationalization
|
||||
- **collection**: ^1.19.1 - Collection utilities
|
||||
- **uuid**: ^4.5.1 - UUID generation
|
||||
- **visibility_detector**: ^0.4.0+2 - Visibility tracking
|
||||
- **fluttertoast**: ^8.2.12 - Toast messages
|
||||
- **version**: ^3.0.2 - Version management
|
||||
- **package_info_plus**: ^8.3.1 - Package info
|
||||
- **device_info_plus**: ^11.5.0 - Device information
|
||||
- **permission_handler**: ^12.0.1 - Permissions
|
||||
|
||||
## Platform-Specific
|
||||
|
||||
### Android
|
||||
- Huawei HMS Push (6.12.0.300)
|
||||
- Huawei Analytics (6.12.0.300)
|
||||
- Google Play Services
|
||||
- ProGuard for code obfuscation
|
||||
- NDK version 27.0.12077973
|
||||
- Compile SDK: Flutter default
|
||||
- Min SDK: Flutter default
|
||||
- Target SDK: Flutter default
|
||||
|
||||
### iOS
|
||||
- APNs for push notifications
|
||||
- Deep linking support
|
||||
|
||||
## Development Tools
|
||||
- **build_runner**: ^2.7.0 - Code generation
|
||||
- **flutter_native_splash**: ^2.4.6 - Splash screen
|
||||
- **flutter_lints**: ^6.0.0 - Linting
|
||||
- **mockito**: ^5.5.0 - Mocking for tests
|
||||
- **flutter_test**: SDK - Testing framework
|
||||
- **integration_test**: SDK - Integration testing
|
||||
- **flutter_dotenv**: ^5.2.1 - Environment variables
|
||||
|
||||
## Localization
|
||||
- **flutter_localizations**: SDK - Localization
|
||||
- **flutter_intl**: Enabled - Internationalization
|
||||
- Primary language: Russian (ru)
|
||||
|
||||
## Build Configuration
|
||||
- **Environment**: Production/Development/Test modes
|
||||
- **Build types**: Debug, Release, Profile
|
||||
- **Code generation**: Freezed, JSON, Retrofit
|
||||
- **Obfuscation**: ProGuard (Android release)
|
||||
- **Resource optimization**: Shrink resources enabled
|
||||
|
||||
## API Integration Patterns
|
||||
- RESTful API via Retrofit
|
||||
- Bearer token authentication
|
||||
- Interceptor-based auth injection
|
||||
- Automatic 401 handling
|
||||
- Environment-based base URLs
|
||||
- Type-safe API clients
|
||||
|
||||
## Development Workflow
|
||||
1. Code generation: `flutter pub run build_runner build`
|
||||
2. Clean generation: `flutter pub run build_runner build --delete-conflicting-outputs`
|
||||
3. Watch mode: `flutter pub run build_runner watch`
|
||||
4. Environment setup: `.env` file configuration
|
||||
5. Firebase setup: Separate configs per platform
|
||||
6. Testing: Unit tests + Integration tests
|
||||
450
README.md
450
README.md
@@ -1 +1,449 @@
|
||||
# eatvio_app
|
||||
# Eatvio - Nutrition & Diet Management Application
|
||||
|
||||
## Project Overview
|
||||
|
||||
Eatvio is a comprehensive mobile application connecting users with nutrition consultants, providing diet planning tools, recipes, food diary tracking, and professional consultation services. The application is specifically designed for Russian-speaking users interested in nutrition and healthy eating.
|
||||
|
||||
### Core Features
|
||||
- **Blog Posts**: Educational nutrition content and articles
|
||||
- **Nutrition Consultation**: Professional consultation services with certified nutritionists
|
||||
- **Recipes**: User-generated and professional recipes with nutritional information
|
||||
- **Diets**: Structured diet plans with daily schedules
|
||||
- **Nutrition Plans**: Long-term nutrition planning and tracking
|
||||
|
||||
### Target Audience
|
||||
- **Primary**: Russian-speaking users interested in nutrition and healthy eating
|
||||
- **Secondary**: Certified nutrition consultants offering professional services
|
||||
- **Market**: Russian Federation and Russian-speaking countries
|
||||
|
||||
### Business Model
|
||||
- Paid consultations with nutritionists
|
||||
- Content marketplace for premium diets and plans
|
||||
- Subscription-based features for consultants
|
||||
|
||||
### Platform Information
|
||||
- **Mobile**: iOS and Android (including Huawei devices)
|
||||
- **Language**: Russian (ru)
|
||||
- **Current Version**: 1.3.0+79 (build 79)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Flutter & Dart Versions
|
||||
- **Flutter**: SDK 3.8.0+
|
||||
- **Dart**: Language version >=3.8.0 <4.0.0
|
||||
|
||||
### Platform-Specific Requirements
|
||||
|
||||
#### Android
|
||||
- **NDK Version**: 27.0.12077973
|
||||
- **Huawei HMS Dependencies**:
|
||||
- Huawei Push (6.12.0.300)
|
||||
- Huawei Analytics (6.12.0.300)
|
||||
- **Google Play Services**
|
||||
- **ProGuard** for code obfuscation
|
||||
- **Compile SDK**: Flutter default
|
||||
- **Min SDK**: Flutter default
|
||||
- **Target SDK**: Flutter default
|
||||
|
||||
#### iOS
|
||||
- **APNs** for push notifications
|
||||
- **Deep linking** configuration
|
||||
- **Xcode** version compatible with Flutter requirements
|
||||
|
||||
#### General IDE Requirements
|
||||
- **VS Code** with Flutter/Dart extensions
|
||||
- **Android Studio** with Flutter plugin
|
||||
- **Visual Studio Code** recommended for development
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### .env Setup Process
|
||||
|
||||
1. Copy the environment template:
|
||||
```bash
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
2. Edit the `.env` file with your configuration values
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
#### API URLs
|
||||
- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru)
|
||||
- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com)
|
||||
|
||||
#### Sentry Configuration
|
||||
- `SENTRY_DSN` - DSN для Sentry мониторинга
|
||||
|
||||
#### Firebase Configuration
|
||||
- `FIREBASE_PROJECT_ID` - ID проекта Firebase
|
||||
- `FIREBASE_DATABASE_URL` - URL базы данных Firebase
|
||||
- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов
|
||||
- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений
|
||||
|
||||
#### Firebase Android Configuration
|
||||
- `FIREBASE_ANDROID_API_KEY` - API ключ для Android
|
||||
- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android
|
||||
|
||||
#### Firebase iOS Configuration
|
||||
- `FIREBASE_IOS_API_KEY` - API ключ для iOS
|
||||
- `FIREBASE_IOS_APP_ID` - ID приложения для iOS
|
||||
- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS
|
||||
|
||||
#### App Store URLs
|
||||
- `APP_STORE_URL` - URL App Store
|
||||
- `RUSTORE_URL` - URL RuStore
|
||||
|
||||
#### Support URLs
|
||||
- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram
|
||||
- `SUPPORT_EMAIL` - email для поддержки
|
||||
|
||||
#### Test Credentials (только для разработки)
|
||||
- `TEST_EMAIL` - тестовый email
|
||||
- `TEST_PASSWORD` - тестовый пароль
|
||||
|
||||
#### Email Validation API
|
||||
- `EMAIL_VALIDATION_API_URL` - URL API для валидации email
|
||||
|
||||
#### Connection Check URLs
|
||||
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
|
||||
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
|
||||
|
||||
#### VK ID Configuration
|
||||
- `VK_CLIENT_ID` - ID приложения VK (числовой)
|
||||
- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа"
|
||||
|
||||
#### Yandex ID Configuration
|
||||
- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth
|
||||
- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth
|
||||
- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении)
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Framework
|
||||
- **Flutter**: SDK 3.8.0+
|
||||
- **Dart**: Language version >=3.8.0 <4.0.0
|
||||
- **Version**: 1.3.0+79 (build 79)
|
||||
|
||||
### State Management
|
||||
- **flutter_bloc**: ^9.1.1 - BLoC pattern implementation
|
||||
- **equatable**: ^2.0.7 - Value equality for state objects
|
||||
|
||||
### Data Management
|
||||
- **freezed**: ^3.2.0 - Immutable data classes
|
||||
- **freezed_annotation**: ^3.1.0 - Code generation annotations
|
||||
- **json_annotation**: ^4.9.0 - JSON serialization
|
||||
- **json_serializable**: ^6.10.0 - JSON code generation
|
||||
|
||||
### Networking
|
||||
- **dio**: ^5.9.0 - HTTP client
|
||||
- **retrofit**: ^4.7.1 - Type-safe REST client
|
||||
- **retrofit_generator**: ^10.0.2 - Retrofit code generation
|
||||
- **http**: ^1.5.0 - Additional HTTP support
|
||||
- **http_parser**: ^4.1.2 - HTTP parsing utilities
|
||||
|
||||
### Real-time Communication
|
||||
- **dart_pusher_channels**: ^1.2.3 - WebSocket support via Pusher
|
||||
- **connectivity_plus**: ^6.1.5 - Network connectivity monitoring
|
||||
|
||||
### Navigation & Routing
|
||||
- **go_router**: ^16.1.0 - Declarative routing
|
||||
- Deep linking support for content
|
||||
|
||||
### UI Components
|
||||
- **cached_network_image**: ^3.4.1 - Image caching
|
||||
- **flutter_svg**: ^2.2.0 - SVG rendering
|
||||
- **google_fonts**: ^6.3.0 - Mulish font family
|
||||
- **carousel_slider_plus**: ^7.1.1 - Carousel widgets
|
||||
- **photo_view**: ^0.15.0 - Image viewer
|
||||
- **dismissible_page**: ^1.0.2 - Swipe to dismiss
|
||||
- **badges**: ^3.1.2 - Notification badges
|
||||
- **lottie**: ^3.3.1 - Animation support
|
||||
- **flutter_portal**: ^1.1.4 - Overlay widgets
|
||||
- **dotted_border**: ^3.1.0 - Dotted borders
|
||||
- **flutter_rating_bar**: ^4.0.1 - Rating widgets
|
||||
|
||||
### Forms & Input
|
||||
- **flutter_form_builder**: ^10.2.0 - Form management
|
||||
- **validators**: ^3.0.0 - Input validation
|
||||
- **flutter_typeahead**: ^5.2.0 - Autocomplete
|
||||
- **keyboard_dismisser**: ^3.0.0 - Keyboard management
|
||||
- **flutter_keyboard_visibility**: ^6.0.0 - Keyboard state
|
||||
- **mask_text_input_formatter**: ^2.9.0 - Input masking
|
||||
|
||||
### Media & Files
|
||||
- **image_picker**: ^1.2.0 - Image selection
|
||||
- **flutter_image_compress**: ^2.4.0 - Image compression
|
||||
- **image**: ^4.5.4 - Image manipulation
|
||||
- **file_picker**: ^10.3.1 - File selection
|
||||
- **audioplayers**: ^6.5.0 - Audio playback
|
||||
- **qr_code_scanner_plus**: ^2.0.10+1 - QR code scanning
|
||||
|
||||
### Data Visualization
|
||||
- **syncfusion_flutter_charts**: ^30.2.5 - Chart widgets
|
||||
- **syncfusion_flutter_gauges**: ^30.2.5 - Gauge widgets
|
||||
- **table_calendar**: ^3.2.0 - Calendar widget
|
||||
|
||||
### Storage & Persistence
|
||||
- **shared_preferences**: ^2.5.3 - Local key-value storage
|
||||
- **path_provider**: ^2.1.5 - File system paths
|
||||
- **path**: ^1.9.1 - Path manipulation
|
||||
|
||||
### Firebase
|
||||
- **firebase_core**: ^4.0.0 - Firebase initialization
|
||||
- **firebase_messaging**: ^16.0.0 - Push notifications
|
||||
|
||||
### Analytics & Monitoring
|
||||
- **sentry_flutter**: ^9.6.0 - Error tracking
|
||||
- **appmetrica_plugin**: ^3.3.0 - Analytics (Russian market)
|
||||
|
||||
### Authentication
|
||||
- **vk_id**: ^1.0.3 - VK social login
|
||||
- **flutter_login_yandex**: ^0.0.5 - Yandex ID login
|
||||
- OAuth support for OK and Mail.ru
|
||||
|
||||
### Utilities
|
||||
- **url_launcher**: ^6.3.2 - URL handling
|
||||
- **easy_debounce**: ^2.0.3 - Debouncing
|
||||
- **share_plus**: ^11.1.0 - Content sharing
|
||||
- **flutter_html**: ^3.0.0 - HTML rendering
|
||||
- **webview_flutter**: ^4.13.0 - WebView
|
||||
- **intl**: ^0.20.2 - Internationalization
|
||||
- **collection**: ^1.19.1 - Collection utilities
|
||||
- **uuid**: ^4.5.1 - UUID generation
|
||||
- **visibility_detector**: ^0.4.0+2 - Visibility tracking
|
||||
- **fluttertoast**: ^8.2.12 - Toast messages
|
||||
- **version**: ^3.0.2 - Version management
|
||||
- **package_info_plus**: ^8.3.1 - Package info
|
||||
- **device_info_plus**: ^11.5.0 - Device information
|
||||
- **permission_handler**: ^12.0.1 - Permissions
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Generation
|
||||
- Run `flutter pub run build_runner build` for code generation
|
||||
- Run `flutter pub run build_runner build --delete-conflicting-outputs` for clean generation
|
||||
- Use `flutter pub run build_runner watch` for watch mode
|
||||
|
||||
### Environment Setup
|
||||
- Configure `.env` file with all required variables
|
||||
- Set up Firebase for both Android and iOS platforms
|
||||
- Configure social authentication providers (VK, Yandex, etc.)
|
||||
|
||||
### Testing
|
||||
- Unit tests with `flutter_test`
|
||||
- Integration tests with `integration_test`
|
||||
- Mocking with `mockito`
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Development Builds
|
||||
```bash
|
||||
# Run on connected device
|
||||
flutter run
|
||||
|
||||
# Run on specific Android device
|
||||
flutter run -d emulator-5554
|
||||
|
||||
# Run on specific iOS simulator
|
||||
flutter run -d iPhone\ 15
|
||||
```
|
||||
|
||||
### Production Builds
|
||||
|
||||
#### Android
|
||||
```bash
|
||||
# Build APK (split by ABI)
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
# Build App Bundle
|
||||
flutter build appbundle --release
|
||||
|
||||
# Huawei-specific build (with HMS)
|
||||
flutter build apk --release --flavor production --target-platform android-arm64
|
||||
```
|
||||
|
||||
#### iOS
|
||||
```bash
|
||||
# Build IPA for TestFlight
|
||||
flutter build ipa --export-options-plist=ExportOptions.plist
|
||||
|
||||
# Build for specific device architecture
|
||||
flutter build ipa --export-options-plist=ExportOptions.plist --no-codesign
|
||||
```
|
||||
|
||||
## Optimization Techniques
|
||||
|
||||
### Android Build Optimization
|
||||
1. **ProGuard Rules**:
|
||||
- Configure rules in [`android/app/proguard-rules.pro`](android/app/proguard-rules.pro:1)
|
||||
- Essential rules for Flutter:
|
||||
```
|
||||
-keep class io.flutter.app.** { *; }
|
||||
-keep class io.flutter.plugin.** { *; }
|
||||
-keep class io.flutter.util.** { *; }
|
||||
-keep class io.flutter.view.** { *; }
|
||||
```
|
||||
|
||||
2. **Resource Shrinking**:
|
||||
- Enabled in `android/app/build.gradle`:
|
||||
```gradle
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Code Obfuscation**:
|
||||
- Use `--obfuscate` flag with build commands
|
||||
- Verify with `flutter build apk --release --obfuscate --split-debug-info=debug-info/`
|
||||
|
||||
4. **Image Compression Workflow**:
|
||||
- Automatic compression via [`flutter_image_compress`](lib/features/create_diet/models/create_diet_model.dart:1)
|
||||
- Configuration in image upload flows:
|
||||
```dart
|
||||
final compressedFile = await FlutterImageCompress.compressWithFile(
|
||||
file.path,
|
||||
quality: 85,
|
||||
minWidth: 1024,
|
||||
minHeight: 1024,
|
||||
);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Missing .env Variables
|
||||
- **Symptom**: Application crashes on startup with "Missing environment variable" error
|
||||
- **Solution**:
|
||||
1. Verify `.env` file exists in project root
|
||||
2. Check [`ENV_SETUP.md`](ENV_SETUP.md) for required variables
|
||||
3. Regenerate configuration with `flutter clean && flutter pub get`
|
||||
|
||||
#### Firebase Configuration Errors
|
||||
- **Android**:
|
||||
- Missing `google-services.json` in `android/app/`
|
||||
- Missing `agconnect-services.json` for Huawei devices
|
||||
- Verify SHA-1 fingerprints match Firebase console
|
||||
- **iOS**:
|
||||
- Missing `GoogleService-Info.plist` in `ios/Runner`
|
||||
- Check bundle identifier matches Firebase project
|
||||
|
||||
#### WebSocket Connection Failures
|
||||
- **Symptoms**: Chat messages not delivering, real-time updates not working
|
||||
- **Solutions**:
|
||||
1. Verify Pusher configuration in [`lib/core/websockets/ws_manager.dart`](lib/core/websockets/ws_manager.dart:1)
|
||||
2. Check network connectivity with `connectivity_plus` package
|
||||
3. Ensure proper authentication token is sent with WebSocket connection
|
||||
|
||||
#### Platform-Specific Build Errors
|
||||
- **Huawei vs Google Services**:
|
||||
- For Huawei devices, ensure `agconnect-services.json` is present
|
||||
- Use `--flavor` flag to specify build variant:
|
||||
```bash
|
||||
flutter build apk --release --flavor hms
|
||||
```
|
||||
- Check for conflicting dependencies between HMS and GMS
|
||||
|
||||
- **iOS Code Signing Issues**:
|
||||
- Verify `ExportOptions.plist` contains correct provisioning profiles
|
||||
- Update signing certificates in Xcode
|
||||
- Clean build folder: `flutter clean && rm -rf ios/Pods ios/Podfile.lock`
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Content View Tracking
|
||||
- `ViewsCubit` handles visibility detection
|
||||
- `VisibilityDetector` widget tracks on-screen time
|
||||
- Views sent to backend when 50%+ visible for 2+ seconds
|
||||
- Debounced to prevent excessive API calls
|
||||
|
||||
### Image Handling
|
||||
- `ImagePicker` for selection
|
||||
- `FlutterImageCompress` for compression
|
||||
- MultiPart form data upload via Dio
|
||||
- Preview via `CachedNetworkImage`
|
||||
|
||||
### Search Implementation
|
||||
- Debounced search with 500ms delay
|
||||
- Min query length: 3 characters
|
||||
- Applied to: Feed, Recipes, Diets, Plans, Consultants
|
||||
- Tag-based filtering for recipes
|
||||
|
||||
### Pagination Pattern
|
||||
- Page-based pagination
|
||||
- Scroll detection for next page load
|
||||
- Loading state during fetch
|
||||
- Meta data tracking (current page, last page)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
- Bearer token authentication
|
||||
- Secure token storage
|
||||
- Automatic session management
|
||||
- OAuth 2.0 for social login
|
||||
|
||||
### Data Protection
|
||||
- HTTPS only communication
|
||||
- Token encryption in storage
|
||||
- Secure WebSocket connections
|
||||
- Privacy policy compliance
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
### Entry Point
|
||||
- `lib/main.dart` - App initialization
|
||||
|
||||
### Core Infrastructure
|
||||
- `lib/core/app_initializer.dart` - Startup sequence
|
||||
- `lib/core/locator_service.dart` - DI container
|
||||
- `lib/core/app_config.dart` - App configuration
|
||||
- `lib/core/app_router.dart` - Routing setup
|
||||
|
||||
### WebSocket
|
||||
- `lib/core/websockets/ws_manager.dart` - Connection management
|
||||
- `lib/core/websockets/ws_service.dart` - API service
|
||||
|
||||
### Authentication
|
||||
- `lib/features/auth/sdk/social_auth_facade.dart` - Social auth
|
||||
- `lib/core/token_storage.dart` - Token persistence
|
||||
- `lib/core/interceptors/token_interceptor.dart` - Auth interceptor
|
||||
|
||||
### Environment
|
||||
- `lib/core/env_config.dart` - Environment variables
|
||||
- `.env` - Environment file (not in repo)
|
||||
|
||||
## Key Success Metrics
|
||||
|
||||
- User engagement with food diary
|
||||
- Consultation bookings and completion rates
|
||||
- Content creation by consultants
|
||||
- User retention and progress tracking
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
- **Last Updated**: 2025-10-16
|
||||
- All paths are relative to `/home/apoloz/dev/eatvio_app`
|
||||
- Russian market focus maintained throughout documentation
|
||||
- Version compatibility matrix:
|
||||
| Component | Version | Notes |
|
||||
|-----------|---------|-------|
|
||||
| Flutter | 3.8.0+ | Required |
|
||||
| Dart | 3.8.0-3.9.9 | Required |
|
||||
| Android SDK | 34 | Target SDK |
|
||||
| iOS Deployment | 14.0 | Minimum |
|
||||
|
||||
## Ubuntu Development PC Optimization
|
||||
```bash
|
||||
-# apt install linux-oem-24.04
|
||||
-# sudo apt-get install --install-recommends linux-lowlatency-hwe-24.04
|
||||
```
|
||||
----
|
||||
- ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel)
|
||||
----
|
||||
- ubuntu instal intel iRIS gpu [drivers](https://dgpu-docs.intel.com/driver/client/overview.html#ubuntu-latest)
|
||||
```
|
||||
|
||||
@@ -388,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -539,7 +539,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -578,7 +578,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>74</string>
|
||||
<string>80</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -15,22 +15,22 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
/// Конфигурация приложения
|
||||
class AppConfig {
|
||||
AppConfig({required this.buildType}){
|
||||
AppConfig({required this.buildType}) {
|
||||
_baseUrl = switch (buildType) {
|
||||
AppBuildType.prod => EnvConfig.baseUrl,
|
||||
AppBuildType.tests => EnvConfig.devBaseUrl,
|
||||
AppBuildType.tests => EnvConfig.devBaseUrl,
|
||||
AppBuildType.dev => EnvConfig.devBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
final AppBuildType buildType;
|
||||
late final String _baseUrl;
|
||||
late String _baseUrl;
|
||||
String get baseUrl => _baseUrl;
|
||||
|
||||
void setBaseUrl(String baseUrls) {
|
||||
_baseUrl = baseUrls;
|
||||
}
|
||||
|
||||
|
||||
/// Список глобальных BlocProvider'ов
|
||||
List<BlocProvider> get globalBlocProviders => [
|
||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
|
||||
@@ -141,5 +142,6 @@ class CubitRegistrations {
|
||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
|
||||
final data = err.response?.data;
|
||||
|
||||
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
|
||||
final String phone = await sl<ProfileRepository>().getMyPhone();
|
||||
final String passKey = await sl<ProfileRepository>().getPassKey(phone);
|
||||
final String phone = await sl<ProfileRepository>().getMyPhone();
|
||||
final String passKey = await sl<ProfileRepository>().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone});
|
||||
await showErrorDialog(AppRouter.navigatorKey.currentContext!,
|
||||
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона');
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
|
||||
|
||||
@@ -180,7 +180,7 @@ class OtherRoutes {
|
||||
),
|
||||
|
||||
// Other routes
|
||||
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
|
||||
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String)),
|
||||
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.notificationsSettingsScreen,
|
||||
|
||||
@@ -8,6 +8,7 @@ abstract class AuthResultEntity with _$AuthResultEntity {
|
||||
const factory AuthResultEntity({
|
||||
required String accessToken,
|
||||
required String userId,
|
||||
required String clientId,
|
||||
String? refreshToken,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
|
||||
@@ -6,9 +6,10 @@ part 'oauth_login_request.g.dart';
|
||||
@freezed
|
||||
abstract class OAuthLoginRequest with _$OAuthLoginRequest {
|
||||
const factory OAuthLoginRequest({
|
||||
required String provider,
|
||||
@JsonKey(name: 'client_id') required String clientId,
|
||||
@JsonKey(name: 'access_token') required String accessToken,
|
||||
@JsonKey(name: 'refresh_token') required String refreshToken,
|
||||
required String provider,
|
||||
}) = _OAuthLoginRequest;
|
||||
|
||||
factory OAuthLoginRequest.fromJson(Map<String, dynamic> json) => _$OAuthLoginRequestFromJson(json);
|
||||
|
||||
@@ -6,15 +6,16 @@ part 'oauth_model.g.dart';
|
||||
@freezed
|
||||
abstract class OAuthModel with _$OAuthModel {
|
||||
const factory OAuthModel({
|
||||
required String provider,
|
||||
required String id,
|
||||
@JsonKey(name: 'access_token') required String accessToken,
|
||||
@JsonKey(name: 'refresh_token') required String refreshToken,
|
||||
@JsonKey(name: 'first_name') required String firstName,
|
||||
@JsonKey(name: 'last_name') required String lastName,
|
||||
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
|
||||
@JsonKey(name: 'client_id') required String clientId,
|
||||
required String provider,
|
||||
required String id,
|
||||
required String email,
|
||||
required String phone,
|
||||
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
|
||||
required String gender,
|
||||
required int weight,
|
||||
required int height,
|
||||
|
||||
@@ -25,19 +25,4 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getTokenBySocial({required String provider, required String accessToken}) async {
|
||||
emit(const AuthState.loading());
|
||||
try {
|
||||
final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token;
|
||||
emit(AuthState.loaded(token: token));
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
showErrorToast('Не удалось войти через $provider');
|
||||
emit(AuthState.error(errorMessage: '$e'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:developer';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -8,24 +10,27 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
|
||||
OAuthCubit({required this.authRepository}) : super(const OAuthState());
|
||||
|
||||
Future<void> getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async {
|
||||
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
|
||||
try {
|
||||
log('OAuthCubit: Starting OAuth login for provider: $provider');
|
||||
emit(const OAuthState.loading());
|
||||
log('OAuthCubit: Emitted loading state');
|
||||
|
||||
final tokenModel = await authRepository.getOAuthLogin(provider: provider, accessToken: accessToken, refreshToken: refreshToken);
|
||||
final tokenModel = await authRepository.signInViaSocialMedia(
|
||||
provider: provider,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
clientId: clientId,
|
||||
);
|
||||
|
||||
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
||||
TokenStorage.saveTokenInStorage(tokenModel.token);
|
||||
emit(OAuthState.loaded(token: tokenModel.token));
|
||||
log('OAuthCubit: Emitted loaded state');
|
||||
} catch (e) {
|
||||
log('OAuthCubit: Error occurred: $e');
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
log('OAuthCubit: Emitted error state');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getOAuthToken({
|
||||
Future<List<RegistrationValidationErrors>?> signUpViaSocialMedia({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
required String id,
|
||||
@@ -38,10 +43,11 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
required String gender,
|
||||
required int weight,
|
||||
required int height,
|
||||
required String clientId,
|
||||
}) async {
|
||||
try {
|
||||
emit(const OAuthState.loading());
|
||||
final token = await authRepository.getOAuthToken(
|
||||
final token = await authRepository.signUpViaSocialMedia(
|
||||
provider: provider,
|
||||
accessToken: accessToken,
|
||||
id: id,
|
||||
@@ -54,10 +60,42 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
gender: gender,
|
||||
weight: weight,
|
||||
height: height,
|
||||
clientId: clientId,
|
||||
);
|
||||
TokenStorage.saveTokenInStorage(token);
|
||||
emit(OAuthState.loaded(token: token));
|
||||
} catch (e) {
|
||||
} on DioException catch (e) {
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
|
||||
final emailErrors = errors?['email'] as List<dynamic>?;
|
||||
final phoneErrors = errors?['phone'] as List<dynamic>?;
|
||||
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
||||
final errorsList = <RegistrationValidationErrors>[];
|
||||
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||
if (emailErrors.contains('Некорректный формат email')) {
|
||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||
}
|
||||
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
|
||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||
}
|
||||
}
|
||||
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||
if (phoneErrors.contains('Такой phone уже существует.')) {
|
||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||
}
|
||||
}
|
||||
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
||||
if (dateOfBirth.contains('Некорректный формат даты рождения')) {
|
||||
errorsList.add(RegistrationValidationErrors.incorrectDateOfBirthFormat);
|
||||
}
|
||||
}
|
||||
if (errorsList.isNotEmpty) {
|
||||
return errorsList;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// ignore_for_file: unused_element, duplicate_ignore
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
@@ -95,6 +93,138 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
void _onVkPressed() async {
|
||||
try {
|
||||
log('VK button pressed');
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) {
|
||||
log('Context is null');
|
||||
return;
|
||||
}
|
||||
|
||||
log('Getting VK auth result');
|
||||
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
|
||||
if (authResult == null) {
|
||||
log('VK auth result is null');
|
||||
return;
|
||||
}
|
||||
|
||||
log('VK auth result received, trying OAuth login');
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'vk');
|
||||
} else {
|
||||
log('Context is not mounted');
|
||||
}
|
||||
} catch (e) {
|
||||
log('VK button error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _onOkPressed() async {
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'vk');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onMailRuPressed() async {
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'vk');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onYandexPressed() async {
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithYandex();
|
||||
if (authResult == null) return;
|
||||
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'yandex');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async {
|
||||
log('Starting OAuth login for provider: $provider');
|
||||
|
||||
try {
|
||||
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
|
||||
StreamSubscription? subscription;
|
||||
|
||||
subscription = oauthCubit.stream.listen((state) {
|
||||
log('OAuth state changed: ${state.runtimeType}');
|
||||
|
||||
if (state is OAuthStateLoaded) {
|
||||
log('OAuth login successful, navigating to main screen');
|
||||
subscription?.cancel();
|
||||
_reconnectAfterAuth();
|
||||
} else if (state is OAuthStateError) {
|
||||
log('OAuth login failed: ${state.errorMessage}');
|
||||
subscription?.cancel();
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
|
||||
}
|
||||
});
|
||||
|
||||
log('Calling getOAuthLogin with provider: $provider');
|
||||
await oauthCubit.signInViaSocialMedia(
|
||||
provider: provider,
|
||||
accessToken: authResult.accessToken,
|
||||
refreshToken: authResult.refreshToken ?? '',
|
||||
clientId: authResult.clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
log('OAuth login error in catch block: $e');
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reconnectAfterAuth() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
try {
|
||||
await sl<WsManager>().clear();
|
||||
await sl<WsManager>().initialize();
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var shortestSide = MediaQuery.sizeOf(context).shortestSide;
|
||||
@@ -129,7 +259,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Social auth buttons at the top (outlined)
|
||||
const Center(child: Text('Авторизация через')),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
@@ -405,143 +534,3 @@ class _SocialButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onVkPressed() async {
|
||||
try {
|
||||
log('VK button pressed');
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) {
|
||||
log('Context is null');
|
||||
return;
|
||||
}
|
||||
|
||||
log('Getting VK auth result');
|
||||
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
|
||||
if (authResult == null) {
|
||||
log('VK auth result is null');
|
||||
return;
|
||||
}
|
||||
|
||||
log('VK auth result received, trying OAuth login');
|
||||
// Пробуем сначала авторизоваться через новый API
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'vk');
|
||||
} else {
|
||||
log('Context is not mounted');
|
||||
}
|
||||
} catch (e) {
|
||||
log('VK button error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _onOkPressed() async {
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Пробуем сначала авторизоваться через новый API
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'ok');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onMailRuPressed() async {
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Пробуем сначала авторизоваться через новый API
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'mailru');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onYandexPressed() async {
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithYandex();
|
||||
if (authResult == null) return;
|
||||
|
||||
// Пробуем сначала авторизоваться через новый API
|
||||
if (context.mounted) {
|
||||
await _tryOAuthLogin(context, authResult, 'yandex');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Пробует авторизоваться через новый OAuth login API
|
||||
/// Если получает ошибку (включая 422 - пользователь не найден), переходит к регистрации
|
||||
Future<void> _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async {
|
||||
log('Starting OAuth login for provider: $provider');
|
||||
|
||||
try {
|
||||
// Создаем OAuth cubit
|
||||
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
|
||||
StreamSubscription? subscription;
|
||||
|
||||
// Слушаем изменения состояния
|
||||
subscription = oauthCubit.stream.listen((state) {
|
||||
log('OAuth state changed: ${state.runtimeType}');
|
||||
|
||||
if (state is OAuthStateLoaded) {
|
||||
// Успешная авторизация - переходим на главный экран
|
||||
log('OAuth login successful, navigating to main screen');
|
||||
subscription?.cancel();
|
||||
_reconnectAfterAuth();
|
||||
} else if (state is OAuthStateError) {
|
||||
// Ошибка авторизации (включая 422 - пользователь не найден) - переходим к регистрации
|
||||
log('OAuth login failed: ${state.errorMessage}');
|
||||
subscription?.cancel();
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
|
||||
}
|
||||
});
|
||||
|
||||
// Пробуем авторизоваться
|
||||
log('Calling getOAuthLogin with provider: $provider');
|
||||
await oauthCubit.getOAuthLogin(provider: provider, accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? '');
|
||||
|
||||
// Ждем немного, чтобы состояние успело обновиться
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
} catch (e) {
|
||||
// В случае любой ошибки (включая 422 - пользователь не найден) переходим к регистрации
|
||||
log('OAuth login error in catch block: $e');
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reconnectAfterAuth() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
try {
|
||||
await sl<WsManager>().clear();
|
||||
await sl<WsManager>().initialize();
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (_) {}
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
@@ -42,6 +43,12 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
final dateOfBirthController = TextEditingController();
|
||||
final weightController = TextEditingController();
|
||||
final heightController = TextEditingController();
|
||||
bool emailExistError = false;
|
||||
bool phoneExistError = false;
|
||||
bool dateOfBirthError = false;
|
||||
bool weightError = false;
|
||||
bool heightError = false;
|
||||
bool incorrectEmailFormatError = false;
|
||||
|
||||
// Состояние для выбора пола
|
||||
bool isMaleSelected = true;
|
||||
@@ -211,8 +218,11 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
hintText: 'Введите email',
|
||||
textInputAction: TextInputAction.next,
|
||||
hasError: emailExistError || incorrectEmailFormatError,
|
||||
onChanged: (newValue) {},
|
||||
),
|
||||
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
|
||||
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
|
||||
const SizedBox(height: 12),
|
||||
const Text('Номер телефона'),
|
||||
const SizedBox(height: 4),
|
||||
@@ -222,13 +232,13 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [maskFormatter],
|
||||
onChanged: (newValue) {
|
||||
// Дополнительная обработка изменений номера телефона
|
||||
// Это поможет маске корректно работать при редактировании
|
||||
},
|
||||
hasError: phoneExistError,
|
||||
onChanged: (newValue) {},
|
||||
),
|
||||
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
@@ -243,9 +253,11 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
hintText: '01.01.2000',
|
||||
controller: dateOfBirthController,
|
||||
keyboardType: TextInputType.number,
|
||||
hasError: dateOfBirthError,
|
||||
inputFormatters: [maskFormatterForBirthday],
|
||||
onChanged: (newValue) {},
|
||||
),
|
||||
if (dateOfBirthError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат даты рождения')],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -271,6 +283,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
@@ -280,7 +293,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
children: [
|
||||
const Text('Вес'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}),
|
||||
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
|
||||
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -292,7 +306,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
children: [
|
||||
const Text('Рост'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}),
|
||||
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
|
||||
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -361,23 +376,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Показываем ошибки если есть
|
||||
if (state is OAuthStateError) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Text(
|
||||
state.errorMessage,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Обработка успешной регистрации
|
||||
if (state is OAuthStateLoaded) ...[
|
||||
const SizedBox(height: 16),
|
||||
@@ -438,7 +437,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleRegistration(BuildContext context) {
|
||||
Future<void> _handleRegistration(BuildContext context) async {
|
||||
final clientID = widget.authResult?.clientId;
|
||||
// Валидация полей
|
||||
if (firstNameController.text.isEmpty ||
|
||||
lastNameController.text.isEmpty ||
|
||||
@@ -447,26 +447,45 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
dateOfBirthController.text.isEmpty ||
|
||||
weightController.text.isEmpty ||
|
||||
heightController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Пожалуйста, заполните все поля')));
|
||||
showErrorToast('Пожалуйста, заполните все поля');
|
||||
return;
|
||||
}
|
||||
|
||||
// Парсим числовые значения
|
||||
int? weight;
|
||||
int? height;
|
||||
try {
|
||||
weight = int.parse(weightController.text);
|
||||
height = int.parse(heightController.text);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Вес и рост должны быть числами')));
|
||||
return;
|
||||
|
||||
weight = int.tryParse(weightController.text);
|
||||
height = int.tryParse(heightController.text);
|
||||
bool hasErrorWeightOrHeightError = false;
|
||||
if (weight == null || height == null) {
|
||||
setState(() {
|
||||
weightError = true;
|
||||
heightError = true;
|
||||
});
|
||||
hasErrorWeightOrHeightError = true;
|
||||
}
|
||||
if (weight! < 30 || weight > 300) {
|
||||
setState(() {
|
||||
weightError = true;
|
||||
});
|
||||
hasErrorWeightOrHeightError = true;
|
||||
}
|
||||
if (height! < 100 || height > 250) {
|
||||
setState(() {
|
||||
heightError = true;
|
||||
});
|
||||
hasErrorWeightOrHeightError = true;
|
||||
}
|
||||
|
||||
if (hasErrorWeightOrHeightError) return;
|
||||
|
||||
// Очищаем номер телефона от маски для отправки
|
||||
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
|
||||
|
||||
if (clientID == null) return;
|
||||
// Вызываем OAuth регистрацию
|
||||
context.read<OAuthCubit>().getOAuthToken(
|
||||
final errors = await context.read<OAuthCubit>().signUpViaSocialMedia(
|
||||
provider: widget.provider ?? 'social',
|
||||
accessToken: widget.authResult?.accessToken ?? 'dummy_token',
|
||||
id: widget.authResult?.userId ?? 'dummy_id',
|
||||
@@ -479,6 +498,35 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
gender: isMaleSelected ? 'М' : 'Ж',
|
||||
weight: weight,
|
||||
height: height,
|
||||
clientId: clientID,
|
||||
);
|
||||
if (errors != null) {
|
||||
for (final error in errors) {
|
||||
switch (error) {
|
||||
case RegistrationValidationErrors.incorrectEmailFormat:
|
||||
setState(() {
|
||||
incorrectEmailFormatError = true;
|
||||
});
|
||||
break;
|
||||
case RegistrationValidationErrors.emailExist:
|
||||
setState(() {
|
||||
emailExistError = true;
|
||||
});
|
||||
break;
|
||||
case RegistrationValidationErrors.phoneExist:
|
||||
setState(() {
|
||||
phoneExistError = true;
|
||||
});
|
||||
break;
|
||||
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
||||
setState(() {
|
||||
dateOfBirthError = true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
showSuccessToast('Регистрация успешна');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
|
||||
|
||||
class AuthRepository {
|
||||
final AuthService authService;
|
||||
@@ -20,27 +19,23 @@ class AuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<TokenModel> getTokenBySocial({required String provider, required String accessToken}) async {
|
||||
|
||||
Future<TokenModel> signInViaSocialMedia({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
required String refreshToken,
|
||||
required String clientId,
|
||||
}) async {
|
||||
try {
|
||||
return await authService.getTokenBySocial(
|
||||
body: SocialExchangeRequest(provider: provider, accessToken: accessToken),
|
||||
return await authService.signInViaSocialMedia(
|
||||
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TokenModel> getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async {
|
||||
try {
|
||||
return await authService.getOAuthLogin(
|
||||
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken),
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getOAuthToken({
|
||||
Future<String> signUpViaSocialMedia({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
required String id,
|
||||
@@ -53,9 +48,10 @@ class AuthRepository {
|
||||
required String gender,
|
||||
required int weight,
|
||||
required int height,
|
||||
required String clientId,
|
||||
}) async {
|
||||
try {
|
||||
return (await authService.getOAuthToken(
|
||||
return (await authService.signUpViaSocialMedia(
|
||||
body: OAuthModel(
|
||||
provider: provider,
|
||||
accessToken: accessToken,
|
||||
@@ -69,6 +65,7 @@ class AuthRepository {
|
||||
gender: gender == 'М' ? 'male' : 'female',
|
||||
weight: weight,
|
||||
height: height,
|
||||
clientId: clientId,
|
||||
),
|
||||
))['token']!
|
||||
as String;
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:vk_id/vk_id.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
@@ -38,7 +39,7 @@ class SocialAuthFacade {
|
||||
if (redirectParams == null) {
|
||||
return null;
|
||||
}
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier);
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
@@ -48,6 +49,7 @@ class SocialAuthFacade {
|
||||
required Map<String, String> redirectParams,
|
||||
required VkIDController controller,
|
||||
required String codeVerifier,
|
||||
required String clientId,
|
||||
}) async {
|
||||
final code = redirectParams['code'];
|
||||
final deviceId = redirectParams['device_id'];
|
||||
@@ -88,6 +90,7 @@ class SocialAuthFacade {
|
||||
phone: profile.phone,
|
||||
gender: GenderEnum.fromVkGender(profile.parsedSex),
|
||||
birthDate: profile.bDayDt?.toString(),
|
||||
clientId: clientId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,6 +154,7 @@ class SocialAuthFacade {
|
||||
phone: yandexProfileData.defaultPhone.number,
|
||||
gender: GenderEnum.fromString(yandexProfileData.sex),
|
||||
birthDate: yandexProfileData.birthday,
|
||||
clientId: clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
@@ -188,7 +192,7 @@ class SocialAuthFacade {
|
||||
try {
|
||||
final controller = VkIDController(clID: int.parse(clientIdStr));
|
||||
|
||||
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email,]);
|
||||
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email]);
|
||||
final codeVerifier = authUri.key;
|
||||
final authUrl = authUri.value.toString();
|
||||
final redirectScheme = 'vk$clientIdStr://';
|
||||
@@ -199,7 +203,7 @@ class SocialAuthFacade {
|
||||
final deviceId = redirectParams['device_id'];
|
||||
if (code == null || deviceId == null) return null;
|
||||
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier);
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
@@ -230,7 +234,9 @@ class SocialAuthFacade {
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigator.of(context, rootNavigator: true).pop(params);
|
||||
if (GoRouter.of(context).canPop()) {
|
||||
GoRouter.of(context).pop(params);
|
||||
}
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'auth_service.g.dart';
|
||||
@@ -15,12 +14,9 @@ abstract class AuthService {
|
||||
@POST('/api/login')
|
||||
Future<TokenModel> getTokenByCredentials({@Body() required CredentialsModel credentials});
|
||||
|
||||
@POST('/api/social/login')
|
||||
Future<TokenModel> getTokenBySocial({@Body() required SocialExchangeRequest body});
|
||||
|
||||
@POST('/api/oauth/callback')
|
||||
Future<Map<String, Object?>> getOAuthToken({@Body() required OAuthModel body});
|
||||
Future<Map<String, Object?>> signUpViaSocialMedia({@Body() required OAuthModel body});
|
||||
|
||||
@POST('/api/oauth/login')
|
||||
Future<TokenModel> getOAuthLogin({@Body() required OAuthLoginRequest body});
|
||||
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class GlossaryScreen extends StatefulWidget {
|
||||
const GlossaryScreen({super.key});
|
||||
|
||||
const GlossaryScreen({super.key, required this.passKey});
|
||||
final String passKey;
|
||||
@override
|
||||
State<GlossaryScreen> createState() => _GlossaryScreenState();
|
||||
}
|
||||
@@ -19,15 +19,13 @@ class _GlossaryScreenState extends State<GlossaryScreen> {
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..loadRequest(Uri.parse('${sl<AppConfig>().baseUrl}/product_nutritional_value'));
|
||||
..loadRequest(Uri.parse('${sl<AppConfig>().baseUrl}/login_with_pass_key?id=${widget.passKey}'));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Глоссарий'),
|
||||
),
|
||||
appBar: AppBar(title: const Text('Глоссарий')),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MenuCubit extends Cubit<MenuState> {
|
||||
final ProfileRepository profileRepository;
|
||||
MenuCubit(this.profileRepository) : super(MenuState());
|
||||
|
||||
Future<String?> makePassKey() async {
|
||||
try {
|
||||
final passKey = await profileRepository.getPassKey(routeName: 'product_nutritional_value.index', routeParams: {});
|
||||
return passKey;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
class MenuState {}
|
||||
@@ -10,12 +10,14 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -27,66 +29,86 @@ class MenuScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: const Text('Меню')),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
|
||||
if (!hidePlans) ...[
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: 'assets/icons/finance.svg',
|
||||
title: 'Мои финансы',
|
||||
onTap: () {
|
||||
if (profileModel.roles.contains('consultant')) {
|
||||
GoRouter.of(context).push(ConsultantFinancesScreen.path);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserPurchasesScreen.path);
|
||||
},
|
||||
return BlocProvider(
|
||||
create: (context) => sl<MenuCubit>(),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: const Text('Меню')),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
|
||||
if (!hidePlans) ...[
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: 'assets/icons/finance.svg',
|
||||
title: 'Мои финансы',
|
||||
onTap: () {
|
||||
if (profileModel.roles.contains('consultant')) {
|
||||
GoRouter.of(context).push(ConsultantFinancesScreen.path);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserPurchasesScreen.path);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(
|
||||
leading: 'assets/icons/consultant_rating_icon.svg',
|
||||
title: 'Мой рейтинг',
|
||||
onTap: () {
|
||||
GoRouter.of(context).push(ConsultantRatingScreen.path);
|
||||
},
|
||||
),
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: ImageConstant.icnGlossary,
|
||||
title: 'Глоссарий',
|
||||
onTap: () async {
|
||||
final passKey = await context.read<MenuCubit>().makePassKey();
|
||||
if (passKey != null && context.mounted) {
|
||||
GoRouter.of(context).push('/glossary-screen', extra: passKey);
|
||||
}
|
||||
},
|
||||
),
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: 'assets/icons/notifications.svg',
|
||||
title: 'Уведомления',
|
||||
onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen'),
|
||||
),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
|
||||
_divider(context),
|
||||
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
|
||||
// _divider(context),
|
||||
// _tile(
|
||||
// leading: ImageConstant.icnDevTools,
|
||||
// title: 'Для разработчиков',
|
||||
// onTap: () {
|
||||
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
|
||||
// },
|
||||
// ),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
|
||||
const SizedBox(height: 10),
|
||||
const AppInfo(),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(
|
||||
leading: 'assets/icons/consultant_rating_icon.svg',
|
||||
title: 'Мой рейтинг',
|
||||
onTap: () {
|
||||
GoRouter.of(context).push(ConsultantRatingScreen.path);
|
||||
},
|
||||
),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
|
||||
_divider(context),
|
||||
_tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
|
||||
_divider(context),
|
||||
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
|
||||
// _divider(context),
|
||||
// _tile(
|
||||
// leading: ImageConstant.icnDevTools,
|
||||
// title: 'Для разработчиков',
|
||||
// onTap: () {
|
||||
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
|
||||
// },
|
||||
// ),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
|
||||
const SizedBox(height: 10),
|
||||
const AppInfo(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
15
lib/features/profile/models/pass_key_params.dart
Normal file
15
lib/features/profile/models/pass_key_params.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'pass_key_params.freezed.dart';
|
||||
part 'pass_key_params.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PassKeyParams with _$PassKeyParams {
|
||||
|
||||
factory PassKeyParams({
|
||||
@JsonKey(name: 'route_name') required String routeName,
|
||||
@JsonKey(name: 'route_params') Map<String, dynamic>? routeParams,
|
||||
}) = _PassKeyParams;
|
||||
|
||||
factory PassKeyParams.fromJson(Map<String, dynamic> json) => _$PassKeyParamsFromJson(json);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/pass_key_params.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/sub_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/weight_model.dart';
|
||||
@@ -154,10 +155,10 @@ class ProfileRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getPassKey(String phone) async {
|
||||
return (jsonDecode(
|
||||
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}')),
|
||||
))['key_is']!;
|
||||
Future<String> getPassKey({required String routeName, Map<String, dynamic>? routeParams}) async {
|
||||
return (await profileService.getPassKey(
|
||||
json: PassKeyParams(routeName: routeName, routeParams: routeParams),
|
||||
)).data['key_is']!;
|
||||
}
|
||||
|
||||
Future<String> getMyPhone() async {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/pass_key_params.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/sub_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/weight_model.dart';
|
||||
@@ -75,7 +76,7 @@ abstract class ProfileService {
|
||||
Future<ChosenDietsPaginationModel> getChosenDiets();
|
||||
|
||||
@POST('/api/profile/make_pass_key')
|
||||
Future<String> getPassKey({@Body() required String json});
|
||||
Future<HttpResponse> getPassKey({@Body() required PassKeyParams json});
|
||||
|
||||
@GET('/api/profile/my_phone')
|
||||
Future<String> getMyPhone();
|
||||
|
||||
@@ -28,9 +28,10 @@ class S {
|
||||
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
|
||||
|
||||
static Future<S> load(Locale locale) {
|
||||
final name = (locale.countryCode?.isEmpty ?? false)
|
||||
? locale.languageCode
|
||||
: locale.toString();
|
||||
final name =
|
||||
(locale.countryCode?.isEmpty ?? false)
|
||||
? locale.languageCode
|
||||
: locale.toString();
|
||||
final localeName = Intl.canonicalizedLocale(name);
|
||||
return initializeMessages(localeName).then((_) {
|
||||
Intl.defaultLocale = localeName;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.2.14+74
|
||||
version: 1.3.1+80
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user