From 2323f195e6e11a2c5f998e438701406b421f820f Mon Sep 17 00:00:00 2001 From: urvashikharecha Date: Tue, 17 Jun 2025 13:56:40 +0530 Subject: [PATCH 01/31] - Cursor rules --- .cursor/rules/assets-rules.mdc | 227 +++++++++ .cursor/rules/atomic-design-rule.mdc | 714 +++++++++++++++++++++++++++ .cursor/rules/auto-route.mdc | 147 ++++++ .cursor/rules/bloc.mdc | 311 ++++++++++++ .cursor/rules/color-text-style.mdc | 354 +++++++++++++ .cursor/rules/flutter-dart.mdc | 120 +++++ .cursor/rules/rest-api-client.mdc | 88 ++++ .cursor/rules/structure.mdc | 183 +++++++ 8 files changed, 2144 insertions(+) create mode 100644 .cursor/rules/assets-rules.mdc create mode 100644 .cursor/rules/atomic-design-rule.mdc create mode 100644 .cursor/rules/auto-route.mdc create mode 100644 .cursor/rules/bloc.mdc create mode 100644 .cursor/rules/color-text-style.mdc create mode 100644 .cursor/rules/flutter-dart.mdc create mode 100644 .cursor/rules/rest-api-client.mdc create mode 100644 .cursor/rules/structure.mdc diff --git a/.cursor/rules/assets-rules.mdc b/.cursor/rules/assets-rules.mdc new file mode 100644 index 0000000..185cb2c --- /dev/null +++ b/.cursor/rules/assets-rules.mdc @@ -0,0 +1,227 @@ +--- +description: +globs: +alwaysApply: true +--- +description: "Flutter Assets Management Rule (flutter_gen)" +alwaysApply: true + +## Overview +This rule enforces the use of `flutter_gen` package for type-safe asset management in Flutter projects, replacing raw string asset paths with generated code. + +## Rule Application + +### ❌ NEVER Use Raw String Paths +**Avoid this pattern:** +```dart +// DON'T DO THIS +Image.asset("assets/demo.png") +Image.asset("assets/icons/home.svg") +Image.asset("assets/images/profile.jpg") +``` + +### ✅ ALWAYS Use Generated Asset Classes +**Use this pattern instead:** +```dart +// DO THIS +Assets.images.demo.image() +Assets.icons.home.svg() +Assets.images.profile.image() +``` + +## Implementation Steps + +### 1. Asset Placement +- **ALWAYS** add assets to the `assets` folder in the **app_ui** package +- Organize assets by type (images, icons, fonts, etc.) +- Use descriptive, snake_case naming for asset files + +### 2. Directory Structure +``` +app_ui/ +├── assets/ +│ ├── images/ +│ │ ├── demo.png +│ │ ├── profile.jpg +│ │ └── background.png +│ ├── icons/ +│ │ ├── home.svg +│ │ ├── search.svg +│ │ └── settings.svg +│ └── fonts/ +│ └── custom_font.ttf +``` + +### 3. Code Generation +After adding new assets, **ALWAYS** run: +```bash +melos run asset-gen +``` + +### 4. Usage Patterns + +#### Images +```dart +// For PNG/JPG images +Assets.images.demo.image() +Assets.images.profile.image() +Assets.images.background.image() + +// With additional properties +Assets.images.demo.image( + width: 100, + height: 100, + fit: BoxFit.cover, +) +``` + +#### SVG Icons +```dart +// For SVG assets +Assets.icons.home.svg() +Assets.icons.search.svg() +Assets.icons.settings.svg() + +// With color and size +Assets.icons.home.svg( + color: Colors.blue, + width: 24, + height: 24, +) +``` + +#### Raw Asset Paths (when needed) +```dart +// If you need the path string +Assets.images.demo.path +Assets.icons.home.path +``` + +## Asset Type Mappings + +### Common Asset Extensions and Usage +| Extension | Usage Pattern | Example | +|-----------|---------------|---------| +| `.png`, `.jpg`, `.jpeg` | `.image()` | `Assets.images.photo.image()` | +| `.svg` | `.svg()` | `Assets.icons.star.svg()` | +| `.json` | `.path` | `Assets.animations.loading.path` | +| `.ttf`, `.otf` | Reference in theme | Font family name | + +## Implementation Checklist + +### Adding New Assets: +- [ ] Place asset in appropriate folder within `app_ui/assets/` +- [ ] Use descriptive, snake_case naming +- [ ] Run `melos run asset-gen` command +- [ ] Verify asset appears in generated `Assets` class +- [ ] Update existing raw string references to use generated code + +### Code Review Checklist: +- [ ] No raw string asset paths (`"assets/..."`) +- [ ] All assets use `Assets.category.name.method()` pattern +- [ ] Asset generation command run after adding new assets +- [ ] Unused assets removed from assets folder + +## Common Patterns + +### Image Widget +```dart +// Basic image +Assets.images.logo.image() + +// Image with properties +Assets.images.banner.image( + width: double.infinity, + height: 200, + fit: BoxFit.cover, +) + +// Image in Container +Container( + decoration: BoxDecoration( + image: DecorationImage( + image: Assets.images.background.provider(), + fit: BoxFit.cover, + ), + ), +) +``` + +### SVG Usage +```dart +// Basic SVG +Assets.icons.menu.svg() + +// Styled SVG +Assets.icons.heart.svg( + color: theme.primaryColor, + width: 20, + height: 20, +) + +// SVG in IconButton +IconButton( + onPressed: () {}, + icon: Assets.icons.settings.svg(), +) +``` + +### Asset Provider (for advanced usage) +```dart +// For use with other widgets that need ImageProvider +CircleAvatar( + backgroundImage: Assets.images.avatar.provider(), +) + +// For precaching +precacheImage(Assets.images.splash.provider(), context); +``` + +## Best Practices + +### Naming Conventions +- Use `snake_case` for asset file names +- Be descriptive: `user_profile.png` instead of `img1.png` +- Group related assets: `icon_home.svg`, `icon_search.svg` + +### Organization +- **images/**: Photos, illustrations, backgrounds +- **icons/**: SVG icons, small graphics +- **animations/**: Lottie files, GIFs +- **fonts/**: Custom font files + +### Performance +- Use appropriate image formats (SVG for icons, PNG/JPG for photos) +- Optimize image sizes before adding to assets +- Consider using `precacheImage()` for critical images + +## Migration from Raw Strings + +### Find and Replace Pattern +1. Search for: `Image.asset("assets/` +2. Replace with appropriate `Assets.` pattern +3. Run asset generation if needed +4. Test all asset references + +### Example Migration +```dart +// Before +Image.asset("assets/images/logo.png", width: 100) + +// After +Assets.images.logo.image(width: 100) +``` + +## Troubleshooting + +### Asset Not Found +1. Verify asset exists in `app_ui/assets/` folder +2. Check file naming (no spaces, special characters) +3. Run `melos run asset-gen` command +4. Restart IDE/hot restart app + +### Generated Code Not Updated +1. Run `melos run asset-gen` command +2. Check for build errors in terminal +3. Verify `flutter_gen` is properly configured in `pubspec.yaml` +4. Clean and rebuild project if necessary \ No newline at end of file diff --git a/.cursor/rules/atomic-design-rule.mdc b/.cursor/rules/atomic-design-rule.mdc new file mode 100644 index 0000000..23f6c64 --- /dev/null +++ b/.cursor/rules/atomic-design-rule.mdc @@ -0,0 +1,714 @@ +--- +description: +globs: +alwaysApply: true +--- + +description: "Flutter app_ui Package Rule (Atomic Design Pattern)" +alwaysApply: true + +## Overview +This rule enforces the use of the `app_ui` package components following the Atomic Design Pattern. The package provides consistent theming, spacing, and reusable components across the Flutter Launchpad project. + +## app_translations Package 📦 + +### Overview +The `app_translations` package manages localization in the application using the **slang** package. It provides type-safe, auto-generated translations for consistent internationalization across the Flutter Launchpad project. + +### Implementation Rules + +#### ✅ ALWAYS Use context.t for Text +**Correct Pattern:** +```dart +// Use generated translations +AppText.medium(text: context.t.welcome) +AppButton(text: context.t.submit, onPressed: () {}) +``` + +#### ❌ NEVER Use Hardcoded Strings +**Avoid these patterns:** +```dart +// DON'T DO THIS +AppText.medium(text: "Welcome") +AppButton(text: "Submit", onPressed: () {}) +``` + +### Adding New Translations + +#### Step 1: Add Key-Value Pairs +Add translations to JSON files in the `i18n` folder within `app_translations` package: + +**English (`en.json`):** +```json +{ + "login": "Login Screen", + "welcome": "Welcome to the app", + "submit": "Submit", + "cancel": "Cancel", + "loading": "Loading...", + "error": { + "network": "Network error occurred", + "validation": "Please check your input" + } +} +``` + +**Other languages (e.g., `es.json`):** +```json +{ + "login": "Pantalla de Inicio de Sesión", + "welcome": "Bienvenido a la aplicación", + "submit": "Enviar", + "cancel": "Cancelar", + "loading": "Cargando...", + "error": { + "network": "Ocurrió un error de red", + "validation": "Por favor verifica tu entrada" + } +} +``` + +#### Step 2: Generate Code +After adding key-value pairs, run the generation command: +```bash +melos run locale-gen +``` + +#### Step 3: Use in Code +```dart + +// In AppText widget +AppText.medium(text: context.t.welcome) + +// In buttons +AppButton( + text: context.t.submit, + onPressed: () {}, +) + + +AppText.small(text: context.t.error.validation) +``` + +### Translation Patterns + +#### Basic Usage +```dart +// Page titles +AppText.large(text: context.t.loginTitle) + +// Form labels +AppTextField( + label: context.t.email, + hint: context.t.emailHint, +) + +// Button text +AppButton(text: context.t.signIn, onPressed: () {}) +AppButton.secondary(text: context.t.cancel, onPressed: () {}) +``` + +#### Complex Translations +```dart +// Nested objects +AppText.small(text: context.t.errors.validation.required) +AppText.small(text: context.t.messages.success.saved) + +// Pluralization (if supported) +AppText.small(text: context.t.itemCount(count: items.length)) + +// Interpolation (if supported) +AppText.small(text: context.t.welcomeUser(name: user.name)) +``` + +### JSON Structure Best Practices + +#### Organize by Feature +```json +{ + "auth": { + "login": "Login", + "register": "Register", + "forgotPassword": "Forgot Password", + "errors": { + "invalidEmail": "Invalid email address", + "weakPassword": "Password too weak" + } + }, + "home": { + "welcome": "Welcome", + "recentActivity": "Recent Activity" + }, + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "loading": "Loading..." + } +} +``` + +#### Usage with Organized Structure +```dart +// Auth-related texts +AppText.large(text: context.t.auth.login) +AppText.small(text: context.t.auth.errors.invalidEmail) + +// Home screen texts +AppText.medium(text: context.t.home.welcome) + +// Common actions +AppButton(text: context.t.common.save, onPressed: () {}) +AppButton.secondary(text: context.t.common.cancel, onPressed: () {}) +``` + +### Implementation Checklist + +#### For New Features: +- [ ] Add all text strings to appropriate JSON files +- [ ] Organize keys by feature/screen +- [ ] Run `melos run locale-gen` after adding keys +- [ ] Use `context.t.*` throughout the code +- [ ] Test with different locales + +#### Code Review Checklist: +- [ ] No hardcoded strings in UI components +- [ ] All user-facing text uses `context.t.*` +- [ ] Translation keys are descriptive and organized +- [ ] Generated code is up-to-date +- [ ] Multiple language files updated consistently + +### Common Translation Patterns + +#### Form Validation +```json +{ + "validation": { + "required": "This field is required", + "invalidEmail": "Please enter a valid email", + "passwordTooShort": "Password must be at least 8 characters", + "passwordMismatch": "Passwords do not match" + } +} +``` + +```dart +// Usage in validation +if (email.isEmpty) { + return context.t.validation.required; +} +if (!isValidEmail(email)) { + return context.t.validation.invalidEmail; +} +``` + +#### Navigation and Actions +```json +{ + "navigation": { + "back": "Back", + "next": "Next", + "done": "Done", + "close": "Close" + }, + "actions": { + "edit": "Edit", + "delete": "Delete", + "share": "Share", + "copy": "Copy" + } +} +``` + +```dart +// Usage in UI +AppButton(text: context.t.navigation.next, onPressed: () {}) +IconButton( + tooltip: context.t.actions.share, + onPressed: () {}, + icon: Icon(Icons.share), +) +``` + +### Troubleshooting + +#### Translation Not Found +1. Verify key exists in JSON files +2. Check spelling and nested structure +3. Run `melos run locale-gen` to regenerate +4. Restart IDE/hot restart app + +#### Generated Code Issues +1. Ensure JSON syntax is valid +2. Check for duplicate keys +3. Verify slang package configuration +4. Clean and rebuild project + +## Package Structure +The `app_ui` package is organized using **Atomic Design Pattern**: +- 🎨 **App Themes** - Color schemes and typography +- 🔤 **Fonts** - Custom font configurations +- 📁 **Assets Storage** - Images, icons, and other assets +- 🧩 **Common Widgets** - Reusable UI components +- 🛠️ **Generated Files** - Auto-generated asset and theme files + +## Atomic Design Levels + +### 🛰️ Atoms (Basic Building Blocks) + +#### Spacing Rules +**❌ NEVER Use Raw SizedBox for Spacing** +```dart +// DON'T DO THIS +const SizedBox(height: 8) +const SizedBox(width: 16) +const SizedBox(height: 24, width: 32) +``` + +**✅ ALWAYS Use VSpace and HSpace** +```dart +// DO THIS - Vertical spacing +VSpace.xsmall() // Extra small vertical space +VSpace.small() // Small vertical space +VSpace.medium() // Medium vertical space +VSpace.large() // Large vertical space +VSpace.xlarge() // Extra large vertical space + +// Horizontal spacing +HSpace.xsmall() // Extra small horizontal space +HSpace.small() // Small horizontal space +HSpace.medium() // Medium horizontal space +HSpace.large() // Large horizontal space +HSpace.xlarge() // Extra large horizontal space +``` + +#### Other Atom-Level Components +```dart +// Border radius +AppBorderRadius.small +AppBorderRadius.medium +AppBorderRadius.large + +// Padding/margins +Insets.small +Insets.medium +Insets.large + +// Text components +AppText.small(text: "Content") +AppText.medium(text: "Content") +AppText.large(text: "Content") + +// Loading indicators +AppLoadingIndicator() +AppLoadingIndicator.small() +AppLoadingIndicator.large() +``` + +### 🔵 Molecules (Component Combinations) + +#### Button Usage Rules +**❌ NEVER Use Raw Material Buttons** +```dart +// DON'T DO THIS +ElevatedButton( + onPressed: () {}, + child: Text("Login"), +) + +TextButton( + onPressed: () {}, + child: Text("Cancel"), +) +``` + +**✅ ALWAYS Use AppButton** +```dart +// DO THIS - Basic button +AppButton( + text: context.t.login, + onPressed: () {}, +) + +// Expanded button +AppButton( + text: context.t.submit, + onPressed: () {}, + isExpanded: true, +) + +// Disabled button +AppButton( + text: context.t.save, + onPressed: () {}, + isEnabled: false, +) + +// Button variants +AppButton.secondary( + text: context.t.cancel, + onPressed: () {}, +) + +AppButton.outline( + text: context.t.edit, + onPressed: () {}, +) + +AppButton.text( + text: context.t.skip, + onPressed: () {}, +) +``` + +## Spacing Implementation Patterns + +### Column/Row Spacing +```dart +// Instead of multiple SizedBox widgets +Column( + children: [ + Widget1(), + VSpace.medium(), + Widget2(), + VSpace.small(), + Widget3(), + ], +) + +Row( + children: [ + Widget1(), + HSpace.large(), + Widget2(), + HSpace.medium(), + Widget3(), + ], +) +``` + +### Complex Layout Spacing +```dart +// Combining vertical and horizontal spacing +Container( + padding: Insets.medium, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.large(text: context.t.title), + VSpace.small(), + AppText.medium(text: context.t.description), + VSpace.large(), + Row( + children: [ + AppButton( + text: context.t.confirm, + onPressed: () {}, + isExpanded: true, + ), + HSpace.medium(), + AppButton.secondary( + text: context.t.cancel, + onPressed: () {}, + isExpanded: true, + ), + ], + ), + ], + ), +) +``` + +## Button Configuration Patterns + +### Basic Button Usage +```dart +// Standard button +AppButton( + text: context.t.login, + onPressed: () => _handleLogin(), +) + +// Button with loading state +AppButton( + text: context.t.submit, + onPressed: isLoading ? null : () => _handleSubmit(), + isEnabled: !isLoading, + child: isLoading ? AppLoadingIndicator.small() : null, +) +``` + +### Button Variants +```dart +// Primary button (default) +AppButton( + text: context.t.save, + onPressed: () {}, +) + +// Secondary button +AppButton.secondary( + text: context.t.cancel, + onPressed: () {}, +) + +// Outline button +AppButton.outline( + text: context.t.edit, + onPressed: () {}, +) + +// Text button +AppButton.text( + text: context.t.skip, + onPressed: () {}, +) + +// Destructive button +AppButton.destructive( + text: context.t.delete, + onPressed: () {}, +) +``` + +### Button Properties +```dart +AppButton( + text: context.t.action, + onPressed: () {}, + isExpanded: true, // Full width button + isEnabled: true, // Enable/disable state + isLoading: false, // Loading state + icon: Icons.save, // Leading icon + suffixIcon: Icons.arrow_forward, // Trailing icon + backgroundColor: context.colorScheme.primary, + textColor: context.colorScheme.onPrimary, +) +``` + +## App UI Component Categories + +### Atoms +```dart +// Spacing +VSpace.small() +HSpace.medium() + +// Text +AppText.medium(text: "Content") + +// Border radius +AppBorderRadius.large + +// Padding +Insets.all16 + +// Loading +AppLoadingIndicator() +``` + +### Molecules +```dart +// Buttons +AppButton(text: "Action", onPressed: () {}) + +// Input fields +AppTextField( + label: context.t.email, + controller: emailController, +) + +// Cards +AppCard( + child: Column(children: [...]), +) +``` + +### Organisms +```dart +// Forms +AppForm( + children: [ + AppTextField(...), + VSpace.medium(), + AppButton(...), + ], +) + +// Navigation +AppBottomNavigationBar( + items: [...], +) +``` + +## Customization Guidelines + +### Modifying Spacing +**Edit `spacing.dart`:** +```dart +class VSpace extends StatelessWidget { + static Widget xsmall() => const SizedBox(height: 4); + static Widget small() => const SizedBox(height: 8); + static Widget medium() => const SizedBox(height: 16); + static Widget large() => const SizedBox(height: 24); + static Widget xlarge() => const SizedBox(height: 32); +} +``` + +### Modifying Buttons +**Edit `app_button.dart`:** +```dart +class AppButton extends StatelessWidget { + const AppButton({ + required this.text, + required this.onPressed, + this.isExpanded = false, + this.isEnabled = true, + // Add more customization options + }); +} +``` + +## Implementation Checklist + +### For New UI Components: +- [ ] Use `VSpace`/`HSpace` instead of `SizedBox` +- [ ] Use `AppButton` instead of raw Material buttons +- [ ] Use `AppText` for all text rendering +- [ ] Use `Insets` for consistent padding/margins +- [ ] Use `AppBorderRadius` for consistent corner radius +- [ ] Follow atomic design hierarchy (atoms → molecules → organisms) + +### Code Review Checklist: +- [ ] No raw `SizedBox` for spacing +- [ ] No raw Material buttons (`ElevatedButton`, `TextButton`, etc.) +- [ ] No hardcoded spacing values +- [ ] Proper use of app_ui components +- [ ] Consistent spacing throughout the UI +- [ ] Localized button text using `context.t` + +## Common Usage Patterns + +### Form Layout +```dart +Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppText.large(text: context.t.loginTitle), + VSpace.large(), + AppTextField( + label: context.t.email, + controller: emailController, + ), + VSpace.medium(), + AppTextField( + label: context.t.password, + controller: passwordController, + obscureText: true, + ), + VSpace.large(), + AppButton( + text: context.t.login, + onPressed: () => _handleLogin(), + isExpanded: true, + ), + VSpace.small(), + AppButton.text( + text: context.t.forgotPassword, + onPressed: () => _navigateToForgotPassword(), + ), + ], +) +``` + +### Card Layout +```dart +AppCard( + padding: Insets.medium, + borderRadius: AppBorderRadius.medium, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.large(text: context.t.cardTitle), + VSpace.small(), + AppText.medium(text: context.t.cardDescription), + VSpace.medium(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AppButton.text( + text: context.t.cancel, + onPressed: () {}, + ), + HSpace.small(), + AppButton( + text: context.t.confirm, + onPressed: () {}, + ), + ], + ), + ], + ), +) +``` + +### List Item Spacing +```dart +ListView.separated( + itemCount: items.length, + separatorBuilder: (context, index) => VSpace.small(), + itemBuilder: (context, index) => ListTile( + title: AppText.medium(text: items[index].title), + subtitle: AppText.small(text: items[index].subtitle), + trailing: AppButton.text( + text: context.t.view, + onPressed: () => _viewItem(items[index]), + ), + ), +) +``` + +## Best Practices + +### Spacing Consistency +- Use predefined spacing values from `VSpace`/`HSpace` +- Maintain consistent spacing ratios throughout the app +- Group related elements with smaller spacing +- Separate different sections with larger spacing + +### Component Reusability +- Extend app_ui components rather than creating new ones +- Follow atomic design principles +- Keep components configurable but opinionated +- Maintain consistent API patterns across components + +### Performance +- Use `const` constructors where possible +- Avoid rebuilding spacing widgets unnecessarily +- Cache complex spacing calculations + +## Migration Guide + +### From Raw Spacing +```dart +// Before +const SizedBox(height: 16) + +// After +VSpace.medium() +``` + +### From Raw Buttons +```dart +// Before +ElevatedButton( + onPressed: () {}, + child: Text("Submit"), +) + +// After +AppButton( + text: context.t.submit, + onPressed: () {}, +) +``` \ No newline at end of file diff --git a/.cursor/rules/auto-route.mdc b/.cursor/rules/auto-route.mdc new file mode 100644 index 0000000..f1fcdfc --- /dev/null +++ b/.cursor/rules/auto-route.mdc @@ -0,0 +1,147 @@ +--- +description: +globs: +alwaysApply: true +--- +description: "Flutter Auto Route Implementation Rule" +alwaysApply: true + +## Overview +This rule ensures consistent implementation of Auto Route navigation in Flutter applications with proper annotations, route configurations, and BLoC integration. + +## Rule Application + +### 1. Screen Widget Annotation +- **ALWAYS** annotate screen widgets with `@RoutePage()` decorator +- Place the annotation directly above the class declaration +- No additional parameters needed for basic routes + +### 2. StatelessWidget Pattern +For simple screens without state management: +```dart +@RoutePage() +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return // Your widget implementation + } +} +``` + +### 3. StatefulWidget with BLoC Pattern +For screens requiring state management with BLoC/Cubit: +```dart +@RoutePage() +class HomeScreen extends StatefulWidget implements AutoRouteWrapper { + const HomeScreen({super.key}); + + @override + Widget wrappedRoute(BuildContext context) { + return MultiRepositoryProvider( + providers: [ + RepositoryProvider(create: (context) => HomeRepository()), + RepositoryProvider(create: (context) => ProfileRepository()), + RepositoryProvider(create: (context) => const AuthRepository()), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + lazy: false, + create: (context) => HomeBloc( + repository: context.read() + )..safeAdd(const FetchPostsEvent()), + ), + BlocProvider( + create: (context) => ProfileCubit( + context.read(), + context.read(), + )..fetchProfileDetail(), + ), + ], + child: this, + ), + ); + } + + @override + State createState() => _HomeScreenState(); +} +``` + +### 4. Route Configuration in app_router.dart +```dart +@AutoRouterConfig(replaceInRouteName: 'Page|Screen,Route') +class AppRouter extends RootStackRouter { + @override + List get routes => [ + AutoRoute( + initial: true, + page: SplashRoute.page, + guards: [AuthGuard()], + ), + AutoRoute(page: HomeRoute.page), + // Add new routes here + ]; +} +``` + +### 5. Code Generation Command +After adding new routes, **ALWAYS** run: +```bash +melos run build-runner +``` + +## Implementation Checklist + +### For New Screens: +- [ ] Add `@RoutePage()` annotation above class declaration +- [ ] Choose appropriate pattern (StatelessWidget vs StatefulWidget with AutoRouteWrapper) +- [ ] If using BLoC, implement `AutoRouteWrapper` interface +- [ ] Add route configuration in `app_router.dart` +- [ ] Run build runner command +- [ ] Verify route generation in generated files + +### For BLoC Integration: +- [ ] Implement `AutoRouteWrapper` interface +- [ ] Use `MultiRepositoryProvider` for dependency injection +- [ ] Use `MultiBlocProvider` for state management +- [ ] Initialize BLoCs with required repositories +- [ ] Return `this` as child in wrapper + +### Route Configuration: +- [ ] Add route to `routes` list in `AppRouter` +- [ ] Use `RouteNameHere.page` format +- [ ] Add guards if authentication required +- [ ] Set `initial: true` for entry point routes + +## Common Patterns + +### Basic Navigation Route +```dart +AutoRoute(page: ScreenNameRoute.page) +``` + +### Protected Route with Guard +```dart +AutoRoute( + page: ScreenNameRoute.page, + guards: [AuthGuard()], +) +``` + +### Initial/Entry Route +```dart +AutoRoute( + initial: true, + page: SplashRoute.page, + guards: [AuthGuard()], +) +``` + +## Notes +- Route names are automatically generated based on screen class names +- The `replaceInRouteName` parameter converts 'Page' or 'Screen' suffixes to 'Route' +- Always run code generation after route changes +- Use lazy loading for BLoCs when appropriate (set `lazy: false` for immediate initialization) \ No newline at end of file diff --git a/.cursor/rules/bloc.mdc b/.cursor/rules/bloc.mdc new file mode 100644 index 0000000..4daf337 --- /dev/null +++ b/.cursor/rules/bloc.mdc @@ -0,0 +1,311 @@ +--- +description: +globs: +alwaysApply: true +--- + +description: "Bloc Rules" +alwaysApply: true +# Bloc Rules +## Overview +The BLoC layer serves as the bridge between UI and data layers, managing application state through events and state emissions. This layer follows a strict architectural pattern with three core components. + +## BLoC Architecture Components + +| Component | Purpose | Description | +|-----------|---------|-------------| +| **State file 💽** | Data Holder | Contains reference to data displayed in UI | +| **Event file ▶️** | UI Triggers | Holds events triggered from the UI layer | +| **BLoC file 🔗** | Logic Controller | Connects State and Event, performs business logic | + +## 1. Event File Implementation ⏭️ + +### Event Class Structure +- **Use sealed classes** instead of abstract classes for events +- **Implement with final classes** for concrete event types +- **Name in past tense** - events represent actions that have already occurred + +```dart +part of '[feature]_bloc.dart'; + +sealed class [Feature]Event extends Equatable { + const [Feature]Event(); + + @override + List get props => []; +} + +final class [Feature]GetDataEvent extends [Feature]Event { + const [Feature]GetDataEvent(); +} +``` + +### Event Naming Conventions +- **Base Event Class**: `[BlocSubject]Event` +- **Initial Load Events**: `[BlocSubject]Started` +- **Action Events**: `[BlocSubject][Action]Event` +- **Past Tense**: Events represent completed user actions + +### Event Examples +```dart +// Good examples +final class HomeGetPostEvent extends HomeEvent {...} +final class ProfileUpdateEvent extends ProfileEvent {...} +final class AuthLoginEvent extends AuthEvent {...} + +// Initial load events +final class HomeStarted extends HomeEvent {...} +final class ProfileStarted extends ProfileEvent {...} +``` + +## 2. State File Implementation 📌 + +### State Class Structure +- **Hybrid approach**: Combines Named Constructors and copyWith methods +- **Equatable implementation**: For proper state comparison +- **Private constructor**: Main constructor should be private +- **ApiStatus integration**: Use standardized status enum + +```dart +part of '[feature]_bloc.dart'; + +class [Feature]State extends Equatable { + final List<[Feature]Model> dataList; + final bool hasReachedMax; + final ApiStatus status; + + const [Feature]State._({ + this.dataList = const <[Feature]Model>[], + this.hasReachedMax = false, + this.status = ApiStatus.initial, + }); + + // Named constructors for common states + const [Feature]State.initial() : this._(status: ApiStatus.initial); + const [Feature]State.loading() : this._(status: ApiStatus.loading); + const [Feature]State.loaded(List<[Feature]Model> dataList, bool hasReachedMax) + : this._( + status: ApiStatus.loaded, + dataList: dataList, + hasReachedMax: hasReachedMax, + ); + const [Feature]State.error() : this._(status: ApiStatus.error); + + [Feature]State copyWith({ + ApiStatus? status, + List<[Feature]Model>? dataList, + bool? hasReachedMax, + }) { + return [Feature]State._( + status: status ?? this.status, + dataList: dataList ?? this.dataList, + hasReachedMax: hasReachedMax ?? this.hasReachedMax, + ); + } + + @override + List get props => [dataList, hasReachedMax, status]; + + @override + bool get stringify => true; +} +``` + +### State Design Patterns +- **Private Main Constructor**: Use `._()` pattern +- **Named Constructors**: For common state scenarios +- **CopyWith Method**: For incremental state updates +- **Proper Props**: Include all relevant fields in props list +- **Stringify**: Enable for better debugging + +### ApiStatus Enum Usage +```dart +enum ApiStatus { + initial, // Before any operation + loading, // During API call + loaded, // Successful data fetch + error, // API call failed +} +``` + +## 3. BLoC File Implementation 🟦 + +### BLoC Class Structure +```dart +class [Feature]Bloc extends Bloc<[Feature]Event, [Feature]State> { + [Feature]Bloc({required this.repository}) : super(const [Feature]State.initial()) { + on<[Feature]GetDataEvent>(_on[Feature]GetDataEvent, transformer: droppable()); + } + + final I[Feature]Repository repository; + int _pageCount = 1; + + FutureOr _on[Feature]GetDataEvent( + [Feature]GetDataEvent event, + Emitter<[Feature]State> emit, + ) async { + if (state.hasReachedMax) return; + + // Show loader only on initial load + state.status == ApiStatus.initial + ? emit(const [Feature]State.loading()) + : emit([Feature]State.loaded(state.dataList, false)); + + final dataEither = await repository.fetchData(page: _pageCount).run(); + + dataEither.fold( + (error) => emit(const [Feature]State.error()), + (result) { + emit([Feature]State.loaded( + state.dataList.followedBy(result).toList(), + false, + )); + _pageCount++; + }, + ); + } +} +``` + +### BLoC Implementation Patterns +- **Repository Injection**: Always inject repository through constructor +- **Event Transformers**: Use appropriate transformers (droppable, concurrent, sequential) +- **State Management**: Check current state before emitting new states +- **Error Handling**: Use TaskEither fold method for error handling +- **Pagination Logic**: Implement proper pagination tracking + +### Event Transformers +```dart +// Use droppable for operations that shouldn't be queued +on(_handler, transformer: droppable()); + +// Use concurrent for independent operations +on(_handler, transformer: concurrent()); + +// Use sequential for ordered operations (default) +on(_handler, transformer: sequential()); +``` + +## 4. UI Integration with AutoRoute 🎁 + +### Screen Implementation with Providers +```dart +class [Feature]Screen extends StatefulWidget implements AutoRouteWrapper { + const [Feature]Screen({super.key}); + + @override + Widget wrappedRoute(BuildContext context) { + return RepositoryProvider<[Feature]Repository>( + create: (context) => [Feature]Repository(), + child: BlocProvider( + lazy: false, + create: (context) => [Feature]Bloc( + repository: RepositoryProvider.of<[Feature]Repository>(context), + )..add(const [Feature]GetDataEvent()), + child: this, + ), + ); + } + + @override + State<[Feature]Screen> createState() => _[Feature]ScreenState(); +} +``` + +### Provider Pattern Guidelines +- **AutoRouteWrapper**: Implement for scoped provider injection +- **RepositoryProvider**: Provide repository instances +- **BlocProvider**: Provide BLoC instances with repository injection +- **Lazy Loading**: Set `lazy: false` for immediate initialization +- **Initial Events**: Add initial events in BLoC creation + +## Development Guidelines + +### File Organization +```dart +// Event file structure +part of '[feature]_bloc.dart'; +sealed class [Feature]Event extends Equatable {...} + +// State file structure +part of '[feature]_bloc.dart'; +class [Feature]State extends Equatable {...} + +// BLoC file structure +import 'package:bloc/bloc.dart'; +part '[feature]_event.dart'; +part '[feature]_state.dart'; +``` + +### Naming Conventions +- **BLoC Class**: `[Feature]Bloc` +- **State Class**: `[Feature]State` +- **Event Base Class**: `[Feature]Event` +- **Event Handlers**: `_on[Feature][Action]Event` +- **Private Fields**: Use underscore prefix for internal state + +### Error Handling Patterns +```dart +// Standard error handling with fold +final resultEither = await repository.operation().run(); +resultEither.fold( + (failure) => emit(const FeatureState.error()), + (success) => emit(FeatureState.loaded(success)), +); +``` + +### State Emission Best Practices +- **Check Current State**: Prevent unnecessary emissions +- **Loading States**: Show loader only when appropriate +- **Error Recovery**: Provide ways to retry failed operations +- **Pagination**: Handle has-reached-max scenarios + +## Testing Considerations + +### BLoC Testing Structure +```dart +group('[Feature]Bloc', () { + late [Feature]Bloc bloc; + late Mock[Feature]Repository mockRepository; + + setUp(() { + mockRepository = Mock[Feature]Repository(); + bloc = [Feature]Bloc(repository: mockRepository); + }); + + blocTest<[Feature]Bloc, [Feature]State>( + 'emits loaded state when event succeeds', + build: () => bloc, + act: (bloc) => bloc.add(const [Feature]GetDataEvent()), + expect: () => [ + const [Feature]State.loading(), + isA<[Feature]State>().having((s) => s.status, 'status', ApiStatus.loaded), + ], + ); +}); +``` + +### Build Runner Commands +```bash +# Generate necessary files +flutter packages pub run build_runner build + +# Watch for changes +flutter packages pub run build_runner watch + +# Clean and rebuild +flutter packages pub run build_runner clean +flutter packages pub run build_runner build --delete-conflicting-outputs +``` + +## Performance Optimizations + +### Memory Management +- **Proper Disposal**: BLoC automatically handles disposal +- **Stream Subscriptions**: Cancel in BLoC close method if manually created +- **Repository Scoping**: Scope repositories to feature level + +### Event Handling Efficiency +- **Debouncing**: Use appropriate transformers for user input +- **Caching**: Implement at repository level, not BLoC level +- **Pagination**: Implement proper pagination logic to avoid memory issues \ No newline at end of file diff --git a/.cursor/rules/color-text-style.mdc b/.cursor/rules/color-text-style.mdc new file mode 100644 index 0000000..f078ac3 --- /dev/null +++ b/.cursor/rules/color-text-style.mdc @@ -0,0 +1,354 @@ +--- +description: +globs: +alwaysApply: true +--- +description: "Flutter Colors and TextStyles Rule (Material Design)" +alwaysApply: true + +## Overview +This rule enforces consistent usage of Material Design colors and typography through context extensions and custom widgets in Flutter Launchpad projects. + +## Rule Application + +### 🎨 Color Usage Rules + +#### ✅ ALWAYS Use Context ColorScheme +**Correct Pattern:** +```dart +// Use Material Design color scheme +Container(color: context.colorScheme.primary) +Container(color: context.colorScheme.secondary) +Container(color: context.colorScheme.surface) +Container(color: context.colorScheme.onPrimary) +``` + +#### ❌ NEVER Use Hardcoded Colors +**Avoid these patterns:** +```dart +// DON'T DO THIS +Container(color: Colors.blue) +Container(color: Color(0xFF123456)) +Container(color: const Color.fromRGb(255, 0, 0)) +``` + +### 📝 Typography Rules + +#### ✅ ALWAYS Use AppText Widget +**Correct Pattern:** +```dart +// Use AppText for consistent typography +AppText.medium( + text: context.t.login, + color: context.colorScheme.primary, +) + +AppText.large( + text: "Welcome", + color: context.colorScheme.onSurface, +) + +AppText.small( + text: "Description", + color: context.colorScheme.onSecondary, +) +``` + +#### ❌ NEVER Use Raw Text Widget +**Avoid this pattern:** +```dart +// DON'T DO THIS +Text( + "Login", + style: TextStyle(fontSize: 16, color: Colors.black), +) +``` + +#### ✅ Use Context TextTheme for Advanced Cases +**For RichText and complex styling:** +```dart +RichText( + text: TextSpan( + text: context.t.login, + style: context.textTheme.medium, + ), +) + +// Multiple text spans +RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Welcome ", + style: context.textTheme.large, + ), + TextSpan( + text: "User", + style: context.textTheme.medium.copyWith( + color: context.colorScheme.primary, + ), + ), + ], + ), +) +``` + +## Material ColorScheme Reference + +### Primary Colors +```dart +context.colorScheme.primary // Main brand color +context.colorScheme.onPrimary // Text/icons on primary +context.colorScheme.primaryContainer // Tinted primary surface +context.colorScheme.onPrimaryContainer // Text on primary container +``` + +### Secondary Colors +```dart +context.colorScheme.secondary // Accent color +context.colorScheme.onSecondary // Text/icons on secondary +context.colorScheme.secondaryContainer // Tinted secondary surface +context.colorScheme.onSecondaryContainer // Text on secondary container +``` + +### Surface Colors +```dart +context.colorScheme.surface // Component backgrounds +context.colorScheme.onSurface // Text/icons on surface +context.colorScheme.surfaceVariant // Alternative surface +context.colorScheme.onSurfaceVariant // Text on surface variant +``` + +### Utility Colors +```dart +context.colorScheme.background // App background +context.colorScheme.onBackground // Text/icons on background +context.colorScheme.error // Error states +context.colorScheme.onError // Text/icons on error +context.colorScheme.outline // Borders and dividers +context.colorScheme.shadow // Shadows and elevation +``` + +## AppText Widget Variants + +### Text Size Variants +```dart +AppText.small(text: "Small text") // Caption/helper text +AppText.medium(text: "Medium text") // Body text +AppText.large(text: "Large text") // Headings +AppText.extraLarge(text: "XL text") // Main titles +``` + +### Common AppText Patterns +```dart +// Basic usage +AppText.medium( + text: context.t.welcome, + color: context.colorScheme.onSurface, +) + +// With custom styling +AppText.large( + text: "Title", + color: context.colorScheme.primary, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, +) + +// Localized text +AppText.medium( + text: context.t.login, + color: context.colorScheme.onPrimary, +) +``` + +## Context TextTheme Reference + +### Available TextTheme Styles +```dart +context.textTheme.small // Small body text +context.textTheme.medium // Regular body text +context.textTheme.large // Large body text +context.textTheme.headlineSmall // Small headlines +context.textTheme.headlineMedium // Medium headlines +context.textTheme.headlineLarge // Large headlines +context.textTheme.displaySmall // Small display text +context.textTheme.displayMedium // Medium display text +context.textTheme.displayLarge // Large display text +context.textTheme.labelSmall // Small labels +context.textTheme.labelMedium // Medium labels +context.textTheme.labelLarge // Large labels +``` + +## Implementation Patterns + +### Container with Color +```dart +Container( + decoration: BoxDecoration( + color: context.colorScheme.surface, + border: Border.all( + color: context.colorScheme.outline, + ), + borderRadius: BorderRadius.circular(8), + ), + child: AppText.medium( + text: context.t.content, + color: context.colorScheme.onSurface, + ), +) +``` + +### Card with Proper Colors +```dart +Card( + color: context.colorScheme.surfaceVariant, + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + AppText.large( + text: context.t.title, + color: context.colorScheme.onSurfaceVariant, + ), + AppText.medium( + text: context.t.description, + color: context.colorScheme.onSurfaceVariant, + ), + ], + ), + ), +) +``` + +### Button Styling +```dart +ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: context.colorScheme.primary, + foregroundColor: context.colorScheme.onPrimary, + ), + onPressed: () {}, + child: AppText.medium( + text: context.t.submit, + color: context.colorScheme.onPrimary, + ), +) +``` + +## Customization Guidelines + +### Adding New Colors +**Edit `colors.dart`:** +```dart +// Add custom colors to color scheme +ColorScheme customColorScheme = ColorScheme.fromSeed( + seedColor: Colors.blue, + // Add custom color mappings +); +``` + +### Adding New TextStyles +**Edit `typography.dart`:** +```dart +// Define custom text styles +TextTheme customTextTheme = TextTheme( + headlineLarge: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + ), + // Add more custom styles +); +``` + +## Implementation Checklist + +### For UI Components: +- [ ] Use `context.colorScheme.*` for all colors +- [ ] Use `AppText.*` instead of `Text` widget +- [ ] Use `context.textTheme.*` for complex text styling +- [ ] Follow Material Design color semantics +- [ ] Use localized text with `context.t.*` + +### Code Review Checklist: +- [ ] No hardcoded colors (`Colors.blue`, `Color(0xFF...)`) +- [ ] No raw `Text` widgets +- [ ] Proper color contrast (primary/onPrimary pairs) +- [ ] Consistent typography hierarchy +- [ ] Localization used where appropriate + +## Common Anti-Patterns to Avoid + +### ❌ Wrong Color Usage +```dart +// DON'T DO THIS +Container(color: Colors.red) +Text("Error", style: TextStyle(color: Colors.red)) +``` + +### ❌ Wrong Text Usage +```dart +// DON'T DO THIS +Text( + "Login", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), +) +``` + +### ❌ Inconsistent Theming +```dart +// DON'T DO THIS - mixing theme and hardcoded values +Container( + color: context.colorScheme.surface, + child: Text( + "Mixed theming", + style: TextStyle(color: Colors.black), // Should use theme + ), +) +``` + +## Best Practices + +### Semantic Color Usage +- Use `primary` for main actions and branding +- Use `secondary` for accent elements +- Use `surface` for component backgrounds +- Use `error` for error states only +- Always pair colors with their `on*` counterparts + +### Typography Hierarchy +- Use `AppText.extraLarge` for main titles +- Use `AppText.large` for section headers +- Use `AppText.medium` for body text +- Use `AppText.small` for captions and helper text + +### Accessibility +- Ensure proper color contrast ratios +- Test with dark/light theme variations +- Use semantic colors for better screen reader support + +## Migration Guide + +### From Hardcoded Colors +```dart +// Before +Container(color: Colors.blue) + +// After +Container(color: context.colorScheme.primary) +``` + +### From Text Widget +```dart +// Before +Text("Login", style: TextStyle(fontSize: 16)) + +// After +AppText.medium(text: context.t.login) +``` \ No newline at end of file diff --git a/.cursor/rules/flutter-dart.mdc b/.cursor/rules/flutter-dart.mdc new file mode 100644 index 0000000..bfea988 --- /dev/null +++ b/.cursor/rules/flutter-dart.mdc @@ -0,0 +1,120 @@ +--- +description: +globs: +alwaysApply: true +--- +description: "Effective Dart Rules" +alwaysApply: true +# Effective Dart Rules + +### Naming Conventions +1. Use terms consistently throughout your code. +2. Name types using `UpperCamelCase` (classes, enums, typedefs, type parameters). +3. Name extensions using `UpperCamelCase`. +4. Name packages, directories, and source files using `lowercase_with_underscores`. +5. Name import prefixes using `lowercase_with_underscores`. +6. Name other identifiers using `lowerCamelCase` (variables, parameters, named parameters). +7. Capitalize acronyms and abbreviations longer than two letters like words. +8. Avoid abbreviations unless the abbreviation is more common than the unabbreviated term. +9. Prefer putting the most descriptive noun last in names. +10. Prefer a noun phrase for non-boolean properties or variables. + +### Architecture +1. Separate your features into three layers: Presentation, Business Logic, and Data. +2. The Data Layer is responsible for retrieving and manipulating data from sources such as databases or network requests. +3. Structure the Data Layer into repositories (wrappers around data providers) and data providers (perform CRUD operations). +4. The Business Logic Layer responds to input from the presentation layer and communicates with repositories to build new states. +5. The Presentation Layer renders UI based on bloc states and handles user input and lifecycle events. +6. Inject repositories into blocs via constructors; blocs should not directly access data providers. +7. Avoid direct bloc-to-bloc communication to prevent tight coupling. +8. To coordinate between blocs, use BlocListener in the presentation layer to listen to one bloc and add events to another. +9. For shared data, inject the same repository into multiple blocs; let each bloc listen to repository streams independently. +10. Always strive for loose coupling between architectural layers and components. +11. Structure your project consistently and intentionally; there is no single right way. +12. Follow repository pattern with abstract interfaces (IAuthRepository) and concrete implementations +13. Use TaskEither from fpdart for functional error handling instead of try-catch blocks +14. Implement mapping functions that separate API calls from response processing +15. Chain operations using .chainEither() and .flatMap() for clean functional composition +16. Always use RepositoryUtils.checkStatusCode for status validation and RepositoryUtils.mapToModel for JSON parsing + +### Types and Functions +1. Use class modifiers to control if your class can be extended or used as an interface. +2. Type annotate fields and top-level variables if the type isn't obvious. +3. Annotate return types on function declarations. +4. Annotate parameter types on function declarations. +5. Use `Future` as the return type of asynchronous members that do not produce values. +6. Use getters for operations that conceptually access properties. +7. Use setters for operations that conceptually change properties. +8. Use inclusive start and exclusive end parameters to accept a range. + +### Style and Structure +1. Prefer `final` over `var` when variable values won't change. +2. Use `const` for compile-time constants. +3. Keep files focused on a single responsibility. +4. Limit file length to maintain readability. +5. Group related functionality together. +6. Prefer making declarations private. + +### Imports & Files +1. Don't import libraries inside the `src` directory of another package. +2. Prefer relative import paths within a package. +3. Don't use `/lib/` or `../` in import paths. +4. Consider writing a library-level doc comment for library files. + +### Usage +1. Use strings in `part of` directives. +2. Use adjacent strings to concatenate string literals. +3. Use collection literals when possible. +4. Use `whereType()` to filter a collection by type. +5. Test for `Future` when disambiguating a `FutureOr` whose type argument could be `Object`. +6. Initialize fields at their declaration when possible. +7. Use initializing formals when possible. +8. Use `;` instead of `{}` for empty constructor bodies. +9. Use `rethrow` to rethrow a caught exception. +10. Override `hashCode` if you override `==`. +11. Make your `==` operator obey the mathematical rules of equality. + +### Documentation +1. Use `///` doc comments to document members and types; don't use block comments for documentation. +2. Prefer writing doc comments for public APIs. +3. Start doc comments with a single-sentence summary. +4. Use square brackets in doc comments to refer to in-scope identifiers. +### Flutter Best Practices +1. Extract reusable widgets into separate components. +2. Use `StatelessWidget` when possible. +3. Keep build methods simple and focused. +4. Avoid unnecessary `StatefulWidget`s. +5. Keep state as local as possible. +6. Use `const` constructors when possible. +7. Avoid expensive operations in build methods. +8. Implement pagination for large lists. + +### Dart 3: Records +1. Records are anonymous, immutable, aggregate types that bundle multiple objects into a single value. +2. Records are fixed-sized, heterogeneous, and strongly typed. Each field can have a different type. +3. Record expressions use parentheses with comma-delimited positional and/or named fields, e.g. `('first', a: 2, b: true, 'last')`. +4. Record fields are accessed via built-in getters: positional fields as `$1`, `$2`, etc., and named fields by their name (e.g., `.a`). +5. Records are immutable: fields do not have setters. +6. Use records for functions that return multiple values; destructure with pattern matching: `var (name, age) = userInfo(json);` +7. Use type aliases (`typedef`) for record types to improve readability and maintainability. +8. Records are best for simple, immutable data aggregation; use classes for abstraction, encapsulation, and behavior. + +### Dart 3: Patterns +1. Patterns represent the shape of values for matching and destructuring. +2. Pattern matching checks if a value has a certain shape, constant, equality, or type. +3. Pattern destructuring allows extracting parts of a matched value and binding them to variables. +4. Use wildcard patterns (`_`) to ignore parts of a matched value. +5. Use rest elements (`...`, `...rest`) in list patterns to match arbitrary-length lists. +6. Use logical-or patterns (e.g., `case a || b`) to match multiple alternatives in a single case. +7. Add guard clauses (`when`) to further constrain when a case matches. +8. Use the `sealed` modifier on a class to enable exhaustiveness checking when switching over its subtypes. + +### Common Flutter Errors +1. If you get a "RenderFlex overflowed" error, check if a `Row` or `Column` contains unconstrained widgets. Fix by wrapping children in `Flexible`, `Expanded`, or by setting constraints. +2. If you get "Vertical viewport was given unbounded height", ensure `ListView` or similar scrollable widgets inside a `Column` have a bounded height. +3. If you get "An InputDecorator...cannot have an unbounded width", constrain the width of widgets like `TextField`. +4. If you get a "setState called during build" error, do not call `setState` or `showDialog` directly inside the build method. +5. If you get "The ScrollController is attached to multiple scroll views", make sure each `ScrollController` is only attached to a single scrollable widget. +6. If you get a "RenderBox was not laid out" error, check for missing or unbounded constraints in your widget tree. +7. Use the Flutter Inspector and review widget constraints to debug layout issues. + diff --git a/.cursor/rules/rest-api-client.mdc b/.cursor/rules/rest-api-client.mdc new file mode 100644 index 0000000..9b518c6 --- /dev/null +++ b/.cursor/rules/rest-api-client.mdc @@ -0,0 +1,88 @@ +--- +description: +globs: +alwaysApply: true +--- + +description: "Rest API Client - Cursor Rules" +alwaysApply: true + +## Overview +The `app_client` package provides comprehensive API call functionality with the `RestApiClient` class as the core component for remote data source communication. + +## Core Architecture + +### RestApiClient Class +- **Primary Purpose**: Connect with remote data sources using Dio as the underlying HTTP client +- **Responsibility**: Execute API requests and handle success/error responses +- **Foundation**: Built on top of Dio for robust HTTP communication + +### Supported HTTP Methods +- `GET` - Retrieve data from server +- `POST` - Create new resources +- `PUT` - Update existing resources +- `DELETE` - Remove resources +- `dynamic` - Flexible method selection based on runtime requirements + +## Key Features + +### 1. Caching System +- **Implementation**: Optional caching via `DioCacheInterceptor` +- **Storage**: Uses `HiveCacheStore` for local cache persistence +- **Configuration**: Enable/disable through initialization parameters +- **Benefits**: Reduces network calls and improves performance + +### 2. Token Management +- **Automatic Handling**: Seamless access and refresh token management +- **Interceptor**: `ApiTokensInterceptor` handles token lifecycle +- **Refresh Logic**: Automatic token refresh when expired +- **Endpoint Exclusion**: Configure endpoints that don't require tokens + +### 3. Logging & Debugging +- **Logger**: `PrettyDioLogger` for enhanced API request/response visibility +- **Formatting**: Beautified output for easier debugging +- **Development Aid**: Comprehensive request/response information + +### 4. Centralized Error Handling +- **Unified Approach**: Single point for API error management +- **Consistent Response**: Standardized error handling across the application +- **Token Expiry**: Automatic logout trigger on authentication failures + +## Initialization Pattern + +```dart +await restApiClient.init( + isApiCacheEnabled: true, // Enable caching + baseURL: 'https://your-api.com', // API base URL + refreshTokenEndpoint: '/auth/refresh', // Token refresh endpoint + endPointsToEscapeHeaderToken: ['/auth/login'], // Endpoints without token + onForceLogout: () => handleLogout(), // Logout callback +); +``` + +## Development Guidelines + +### Best Practices +1. **Initialize Once**: Call `init()` during app startup +2. **Error Handling**: Implement proper error handling in consuming code +3. **Token Management**: Configure token-free endpoints appropriately +4. **Caching Strategy**: Enable caching for appropriate endpoints +5. **Logout Handling**: Provide proper logout callback implementation + +### Configuration Considerations +- Set appropriate base URL for environment (dev/staging/prod) +- Configure refresh token endpoint correctly +- Identify endpoints that should bypass token authentication +- Implement secure logout mechanism + +### Usage Patterns +- Use appropriate HTTP methods for RESTful operations +- Leverage caching for frequently accessed, relatively static data +- Monitor logs during development for request/response debugging +- Handle token refresh scenarios gracefully in UI + +## Integration Notes +- Designed to work seamlessly with Flutter/Dart applications +- Integrates with Hive for local storage requirements +- Compatible with standard REST API architectures +- Supports both authenticated and public API endpoints \ No newline at end of file diff --git a/.cursor/rules/structure.mdc b/.cursor/rules/structure.mdc new file mode 100644 index 0000000..3b1f930 --- /dev/null +++ b/.cursor/rules/structure.mdc @@ -0,0 +1,183 @@ +--- +description: +globs: +alwaysApply: true +--- + +description: "Project Structure Standards" +alwaysApply: true + +### Base Module Location +All features must be created within the `lib/modules` directory of the `app_core` package: + +``` +lib +└── modules + └── [feature_name] +``` + +### Feature Folder Structure +Each feature follows a consistent 4-folder architecture: + +``` +lib +└── modules + ├── [feature_name] + │ ├── bloc/ + │ │ ├── [feature]_event.dart + │ │ ├── [feature]_state.dart + │ │ └── [feature]_bloc.dart + │ ├── model/ + │ │ └── [feature]_model.dart + │ ├── repository/ + │ │ └── [feature]_repository.dart + │ └── screen/ + │ └── [feature]_screen.dart +``` + +## Folder Responsibilities + +| Folder | Purpose | Contains | +|--------|---------|----------| +| **bloc** 🧱 | State Management | BLoC, Event, and State classes for the feature | +| **model** 🏪 | Data Models | Dart model classes for JSON serialization/deserialization | +| **repository** 🪣 | API Integration | Functions for API calls and data manipulation | +| **screen** 📲 | User Interface | UI components and screens for the feature | + +## Repository Layer Implementation + +### Core Pattern: TaskEither Approach +All API integrations use `TaskEither` pattern from `fp_dart` for functional error handling. + +### Abstract Interface Structure +```dart +abstract interface class I[Feature]Repository { + /// Returns TaskEither where: + /// - Task: Indicates Future operation + /// - Either: Success (T) or Failure handling + TaskEither> fetch[Data](); +} +``` + +### Implementation Steps + +#### 1. HTTP Request Layer 🎁 +```dart +class [Feature]Repository implements I[Feature]Repository { + @override + TaskEither> fetch[Data]() => + mappingRequest('[endpoint]'); + + TaskEither make[Operation]Request(String url) { + return ApiClient.request( + path: url, + queryParameters: {'_limit': 10}, + requestType: RequestType.get, + ); + } +} +``` + +#### 2. Response Validation ✔️ +```dart +TaskEither> mappingRequest(String url) => + make[Operation]Request(url) + .chainEither(checkStatusCode); + +Either checkStatusCode(Response response) => + Either.fromPredicate( + response, + (response) => response.statusCode == 200 || response.statusCode == 304, + (error) => APIFailure(error: error), + ); +``` + +#### 3. JSON Decoding 🔁 +```dart +TaskEither> mappingRequest(String url) => + make[Operation]Request(url) + .chainEither(checkStatusCode) + .chainEither(mapToList); + +Either>> mapToList(Response response) { + return Either>>.safeCast( + response.data, + (error) => ModelConversionFailure(error: error), + ); +} +``` + +#### 4. Model Conversion ✅ +```dart +TaskEither> mappingRequest(String url) => + make[Operation]Request(url) + .chainEither(checkStatusCode) + .chainEither(mapToList) + .flatMap(mapToModel); + +TaskEither> mapToModel( + List> responseList +) => TaskEither>.tryCatch( + () async => responseList.map([Feature]Model.fromJson).toList(), + (error, stackTrace) => ModelConversionFailure( + error: error, + stackTrace: stackTrace, + ), +); +``` + +## Development Guidelines + +### Naming Conventions +- **Feature Names**: Use descriptive, lowercase names (auth, home, profile, settings) +- **File Names**: Follow pattern `[feature]_[type].dart` +- **Class Names**: Use PascalCase with feature prefix (`HomeRepository`, `HomeBloc`) +- **Method Names**: Use camelCase with descriptive verbs (`fetchPosts`, `updateProfile`) + +### Error Handling Strategy +- **Consistent Failures**: Use standardized `Failure` classes + - `APIFailure`: For HTTP/network errors + - `ModelConversionFailure`: For JSON parsing errors +- **Functional Approach**: Chain operations using `TaskEither` +- **No Exceptions**: Handle all errors through `Either` types + +### API Integration Patterns +1. **Abstract Interface**: Define contract with abstract interface class +2. **Implementation**: Implement interface in concrete repository class +3. **Function Chaining**: Use `.chainEither()` and `.flatMap()` for sequential operations +4. **Error Propagation**: Let `TaskEither` handle error propagation automatically + +### BLoC Integration +- Repository layer feeds directly into BLoC layer +- BLoC handles UI state management +- Repository focuses purely on data operations +- Maintain separation of concerns between layers + +## Best Practices + +### Code Organization +- Keep abstract interface and implementation in same file for discoverability +- Create separate functions for each operation step +- Use descriptive function names that indicate their purpose +- Maintain consistent error handling patterns across all repositories + +### Performance Considerations +- Leverage `TaskEither` for lazy evaluation +- Chain operations efficiently to avoid nested callbacks +- Use appropriate query parameters for data limiting +- Implement proper caching strategies in API client layer + +### Testing Strategy +- Mock abstract interfaces for unit testing +- Test each step of the repository chain individually +- Verify error handling for all failure scenarios +- Ensure proper model conversion testing + +## Example Feature Names +- `auth` - Authentication and authorization +- `home` - Home screen and dashboard +- `profile` - User profile management +- `settings` - Application settings +- `notifications` - Push notifications +- `search` - Search functionality +- `chat` - Messaging features \ No newline at end of file From 898cef77a59c97ca5ea1154ba2c10932df7d67a6 Mon Sep 17 00:00:00 2001 From: avni prajapati Date: Wed, 18 Jun 2025 15:17:53 +0530 Subject: [PATCH 02/31] Imp: Update auto-route rules to use BackButtonListener instead of PopScope to avoid conflicts with auto_route --- .cursor/rules/auto-route.mdc | 18 ++++++++++++++---- packages/widgetbook/pubspec.lock | 22 +++++++++++----------- packages/widgetbook/pubspec_overrides.yaml | 3 +-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.cursor/rules/auto-route.mdc b/.cursor/rules/auto-route.mdc index f1fcdfc..7b02ff1 100644 --- a/.cursor/rules/auto-route.mdc +++ b/.cursor/rules/auto-route.mdc @@ -16,8 +16,7 @@ This rule ensures consistent implementation of Auto Route navigation in Flutter - Place the annotation directly above the class declaration - No additional parameters needed for basic routes -### 2. StatelessWidget Pattern -For simple screens without state management: +### 2. For simple screens without state management: ```dart @RoutePage() class HomeScreen extends StatelessWidget { @@ -30,8 +29,7 @@ class HomeScreen extends StatelessWidget { } ``` -### 3. StatefulWidget with BLoC Pattern -For screens requiring state management with BLoC/Cubit: +### 3. For screens requiring state management with BLoC/Cubit: ```dart @RoutePage() class HomeScreen extends StatefulWidget implements AutoRouteWrapper { @@ -93,6 +91,18 @@ After adding new routes, **ALWAYS** run: melos run build-runner ``` +### 6. Use BackButtonListener instead of PopScope while project contains AutoRoute to avoid conflicts because of auto_route. +For this you can wrap the AppScafflold with BackButtonListener like this, +```dart +@override + Widget build(BuildContext context) { + return BackButtonListener( + onBackButtonPressed: (){}, + child: AppScafflold(), + ); + } +``` + ## Implementation Checklist ### For New Screens: diff --git a/packages/widgetbook/pubspec.lock b/packages/widgetbook/pubspec.lock index 1ba8abf..bc858ca 100644 --- a/packages/widgetbook/pubspec.lock +++ b/packages/widgetbook/pubspec.lock @@ -52,10 +52,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -419,10 +419,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -659,10 +659,10 @@ packages: dependency: transitive description: name: skeletonizer - sha256: "0dcacc51c144af4edaf37672072156f49e47036becbc394d7c51850c5c1e884b" + sha256: a9ddf63900947f4c0648372b6e9987bc2b028db9db843376db6767224d166c31 url: "https://pub.dev" source: hosted - version: "1.4.3" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -920,10 +920,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: @@ -1006,4 +1006,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.31.0-0.0.pre" diff --git a/packages/widgetbook/pubspec_overrides.yaml b/packages/widgetbook/pubspec_overrides.yaml index 32cda75..1cf53a3 100644 --- a/packages/widgetbook/pubspec_overrides.yaml +++ b/packages/widgetbook/pubspec_overrides.yaml @@ -1,5 +1,4 @@ -# melos_managed_dependency_overrides: app_ui,skeletonizer +# melos_managed_dependency_overrides: app_ui dependency_overrides: app_ui: path: ../app_ui - skeletonizer: ^2.0.0-pre From 70dc67db9b070a93686e48f70957d42762b4955f Mon Sep 17 00:00:00 2001 From: avni prajapati Date: Wed, 18 Jun 2025 15:31:38 +0530 Subject: [PATCH 03/31] fix: Replaced AppBar with CustomAppBar in multiple screens for consistency --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .../auth/sign_up/screens/sign_up_screen.dart | 2 +- .../screen/change_password_screen.dart | 48 +++++-------------- .../lib/modules/home/screen/home_screen.dart | 24 +++------- .../profile/screen/edit_profile_screen.dart | 25 +++------- .../screen/subscription_screen.dart | 2 +- .../lib/widgets/atoms/app_border_radius.dart | 15 ++---- .../lib/widgets/atoms/app_text.dart | 10 ++-- .../widgetbook/lib/widgets/atoms/padding.dart | 5 +- .../widgetbook/lib/widgets/atoms/spacing.dart | 27 +++-------- .../lib/widgets/organisms/app_scaffold.dart | 48 ++++--------------- 11 files changed, 55 insertions(+), 153 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4897de0..0f375ab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,7 +21,7 @@ void main() => runApp(MaterialApp(home: Home())); class Home extends StatelessWidget { final count = 0; @override - Widget build(context) => AppScaffold( + Widget build(context) => Scaffold( appBar: AppBar(title: Text("Demo")), body: Center( child: Text("$count"), diff --git a/apps/app_core/lib/modules/auth/sign_up/screens/sign_up_screen.dart b/apps/app_core/lib/modules/auth/sign_up/screens/sign_up_screen.dart index 721e935..b820f22 100644 --- a/apps/app_core/lib/modules/auth/sign_up/screens/sign_up_screen.dart +++ b/apps/app_core/lib/modules/auth/sign_up/screens/sign_up_screen.dart @@ -37,7 +37,7 @@ class SignUpPage extends StatelessWidget implements AutoRouteWrapper { @override Widget build(BuildContext context) { return AppScaffold( - appBar: AppBar(), + appBar: const CustomAppBar(), body: BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) async { diff --git a/apps/app_core/lib/modules/change_password/screen/change_password_screen.dart b/apps/app_core/lib/modules/change_password/screen/change_password_screen.dart index 39bf066..ef53a23 100644 --- a/apps/app_core/lib/modules/change_password/screen/change_password_screen.dart +++ b/apps/app_core/lib/modules/change_password/screen/change_password_screen.dart @@ -17,16 +17,12 @@ class ChangePasswordScreen extends StatelessWidget implements AutoRouteWrapper { @override Widget build(BuildContext context) { return AppScaffold( - appBar: AppBar(), + appBar: const CustomAppBar(), body: BlocListener( listenWhen: (prev, current) => prev.apiStatus != current.apiStatus, listener: (_, state) async { if (state.apiStatus == ApiStatus.error) { - showAppSnackbar( - context, - context.t.failed_to_update, - type: SnackbarType.failed, - ); + showAppSnackbar(context, context.t.failed_to_update, type: SnackbarType.failed); } else if (state.apiStatus == ApiStatus.loaded) { showAppSnackbar(context, context.t.update_successful); } @@ -48,14 +44,8 @@ class ChangePasswordScreen extends StatelessWidget implements AutoRouteWrapper { child: AppText.XL(text: context.t.change_password), ), SlideAndFadeAnimationWrapper(delay: 400, child: _PasswordInput()), - SlideAndFadeAnimationWrapper( - delay: 400, - child: _ConfirmPasswordInput(), - ), - const SlideAndFadeAnimationWrapper( - delay: 600, - child: _CreateAccountButton(), - ), + SlideAndFadeAnimationWrapper(delay: 400, child: _ConfirmPasswordInput()), + const SlideAndFadeAnimationWrapper(delay: 600, child: _CreateAccountButton()), ], ), ), @@ -69,10 +59,7 @@ class ChangePasswordScreen extends StatelessWidget implements AutoRouteWrapper { create: (_) => ProfileRepository(), child: BlocProvider( lazy: false, - create: - (context) => ChangePasswordCubit( - RepositoryProvider.of(context), - ), + create: (context) => ChangePasswordCubit(RepositoryProvider.of(context)), child: this, ), ); @@ -83,24 +70,20 @@ class _ConfirmPasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: - (previous, current) => - previous.confirmPassword != current.confirmPassword, + buildWhen: (previous, current) => previous.confirmPassword != current.confirmPassword, builder: (context, state) { return AppTextField.password( initialValue: state.confirmPassword.value, label: context.t.confirm_password, textInputAction: TextInputAction.done, onChanged: - (password) => - context.read().onConfirmPasswordChange( - confirmPassword: password, - password: state.password.value, - ), + (password) => context.read().onConfirmPasswordChange( + confirmPassword: password, + password: state.password.value, + ), errorText: - state.confirmPassword.error == - ConfirmPasswordValidationError.invalid + state.confirmPassword.error == ConfirmPasswordValidationError.invalid ? context.t.common_validation_confirm_password : null, autofillHints: const [AutofillHints.password], @@ -120,15 +103,10 @@ class _PasswordInput extends StatelessWidget { initialValue: state.password.value, label: context.t.password, textInputAction: TextInputAction.done, - onChanged: - (password) => context - .read() - .onPasswordChange(password), + onChanged: (password) => context.read().onPasswordChange(password), errorText: - state.password.displayError != null - ? context.t.common_validation_password - : null, + state.password.displayError != null ? context.t.common_validation_password : null, autofillHints: const [AutofillHints.password], ); }, diff --git a/apps/app_core/lib/modules/home/screen/home_screen.dart b/apps/app_core/lib/modules/home/screen/home_screen.dart index 62b5c1f..76137f5 100644 --- a/apps/app_core/lib/modules/home/screen/home_screen.dart +++ b/apps/app_core/lib/modules/home/screen/home_screen.dart @@ -37,10 +37,9 @@ class HomeScreen extends StatefulWidget implements AutoRouteWrapper { ), BlocProvider( create: - (context) => ProfileCubit( - context.read(), - context.read(), - )..fetchProfileDetail(), + (context) => + ProfileCubit(context.read(), context.read()) + ..fetchProfileDetail(), ), ], @@ -71,10 +70,7 @@ class _HomeScreenState extends State with PaginationService { @override Widget build(BuildContext context) { return AppScaffold( - appBar: AppBar( - title: Text(context.t.homepage_title), - actions: const [ProfileImage()], - ), + appBar: CustomAppBar(title: context.t.homepage_title, actions: const [ProfileImage()]), body: Column( children: [ Expanded( @@ -89,10 +85,7 @@ class _HomeScreenState extends State with PaginationService { case ApiStatus.loading: return const Center(child: AppCircularProgressIndicator()); case ApiStatus.loaded: - return _ListWidget( - hasReachedMax: state.hasReachedMax, - post: state.postList, - ); + return _ListWidget(hasReachedMax: state.hasReachedMax, post: state.postList); case ApiStatus.error: return AppText.L(text: context.t.post_error); case ApiStatus.empty: @@ -144,8 +137,7 @@ class _ListWidgetState extends State<_ListWidget> with PaginationService { @override Widget build(BuildContext context) { return AppRefreshIndicator( - onRefresh: - () async => context.read().add(const FetchPostsEvent()), + onRefresh: () async => context.read().add(const FetchPostsEvent()), child: ListView.builder( controller: scrollController, itemCount: widget.post.length + (widget.hasReachedMax ? 0 : 1), @@ -155,9 +147,7 @@ class _ListWidgetState extends State<_ListWidget> with PaginationService { } return Container( padding: const EdgeInsets.symmetric(vertical: Insets.xxxxlarge80), - child: Text( - "${widget.post[index].title ?? ''} ${widget.post[index].body ?? ''}", - ), + child: Text("${widget.post[index].title ?? ''} ${widget.post[index].body ?? ''}"), ); }, ), diff --git a/apps/app_core/lib/modules/profile/screen/edit_profile_screen.dart b/apps/app_core/lib/modules/profile/screen/edit_profile_screen.dart index 9014e64..d54e660 100644 --- a/apps/app_core/lib/modules/profile/screen/edit_profile_screen.dart +++ b/apps/app_core/lib/modules/profile/screen/edit_profile_screen.dart @@ -19,12 +19,8 @@ class EditProfileScreen extends StatelessWidget implements AutoRouteWrapper { Widget wrappedRoute(BuildContext context) { return MultiRepositoryProvider( providers: [ - RepositoryProvider( - create: (context) => const AuthRepository(), - ), - RepositoryProvider( - create: (context) => ProfileRepository(), - ), + RepositoryProvider(create: (context) => const AuthRepository()), + RepositoryProvider(create: (context) => ProfileRepository()), ], child: BlocProvider( create: @@ -42,13 +38,8 @@ class EditProfileScreen extends StatelessWidget implements AutoRouteWrapper { return BlocConsumer( listener: (context, state) { if (state.apiStatus == ApiStatus.error) { - showAppSnackbar( - context, - state.errorMessage, - type: SnackbarType.failed, - ); - } else if (state.profileActionStatus == - ProfileActionStatus.profileEdited) { + showAppSnackbar(context, state.errorMessage, type: SnackbarType.failed); + } else if (state.profileActionStatus == ProfileActionStatus.profileEdited) { showAppSnackbar(context, context.t.profile_edit_success); } else if ((state.isPermissionDenied ?? false) == true) { showAppSnackbar( @@ -60,7 +51,7 @@ class EditProfileScreen extends StatelessWidget implements AutoRouteWrapper { }, builder: (context, state) { return Scaffold( - appBar: AppBar(title: AppText.L(text: context.t.edit_profile)), + appBar: CustomAppBar(title: context.t.edit_profile), body: state.apiStatus == ApiStatus.loading ? const Center(child: AppLoadingIndicator()) @@ -68,11 +59,7 @@ class EditProfileScreen extends StatelessWidget implements AutoRouteWrapper { padding: EdgeInsets.symmetric(horizontal: Insets.medium16), child: Column( spacing: Insets.medium16, - children: [ - _ProfileImage(), - _NameTextFiled(), - _EditButton(), - ], + children: [_ProfileImage(), _NameTextFiled(), _EditButton()], ), ), ); diff --git a/apps/app_core/lib/modules/subscription/screen/subscription_screen.dart b/apps/app_core/lib/modules/subscription/screen/subscription_screen.dart index d108469..9e0a77b 100644 --- a/apps/app_core/lib/modules/subscription/screen/subscription_screen.dart +++ b/apps/app_core/lib/modules/subscription/screen/subscription_screen.dart @@ -48,7 +48,7 @@ class SubscriptionScreen extends StatelessWidget implements AutoRouteWrapper { flog('status in listen of subscription: ${state.status}'); }, child: AppScaffold( - appBar: AppBar(centerTitle: true, title: const Text('Purchase Plans')), + appBar: const CustomAppBar(title: 'Purchase Plans', centerTitle: true), body: SingleChildScrollView( child: Column( spacing: Insets.xsmall8, diff --git a/packages/widgetbook/lib/widgets/atoms/app_border_radius.dart b/packages/widgetbook/lib/widgets/atoms/app_border_radius.dart index 90eea73..1bd5823 100644 --- a/packages/widgetbook/lib/widgets/atoms/app_border_radius.dart +++ b/packages/widgetbook/lib/widgets/atoms/app_border_radius.dart @@ -1,13 +1,9 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' - as widgetbook; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; -@widgetbook.UseCase( - name: 'Interactive Border Radius', - type: AppBorderRadius, -) +@widgetbook.UseCase(name: 'Interactive Border Radius', type: AppBorderRadius) Widget interactiveAppBorderRadius(BuildContext context) { final knobs = context.knobs; @@ -42,7 +38,7 @@ Widget interactiveAppBorderRadius(BuildContext context) { // Sample widget to apply the border radius return AppScaffold( - appBar: AppBar(title: const Text('Interactive Border Radius')), + appBar: const CustomAppBar(title: 'Interactive Border Radius'), body: Padding( padding: const EdgeInsets.all(16.0), child: Container( @@ -53,10 +49,7 @@ Widget interactiveAppBorderRadius(BuildContext context) { borderRadius: BorderRadius.circular(borderRadiusValue), ), child: Center( - child: Text( - 'Border Radius: $borderRadiusValue', - style: TextStyle(color: Colors.white), - ), + child: Text('Border Radius: $borderRadiusValue', style: TextStyle(color: Colors.white)), ), ), ), diff --git a/packages/widgetbook/lib/widgets/atoms/app_text.dart b/packages/widgetbook/lib/widgets/atoms/app_text.dart index b2a72ad..c148d3c 100644 --- a/packages/widgetbook/lib/widgets/atoms/app_text.dart +++ b/packages/widgetbook/lib/widgets/atoms/app_text.dart @@ -1,8 +1,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' - as widgetbook; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase(name: 'Interactive AppText', type: AppText) Widget interactiveAppText(BuildContext context) { @@ -40,10 +39,7 @@ Widget interactiveAppText(BuildContext context) { }, ); - final color = knobs.color( - label: 'Text Color', - initialValue: Colors.black, - ); + final color = knobs.color(label: 'Text Color', initialValue: Colors.black); final textAlign = knobs.list( label: 'Text Align', @@ -67,7 +63,7 @@ Widget interactiveAppText(BuildContext context) { ); return AppScaffold( - appBar: AppBar(title: const Text('Interactive AppText')), + appBar: const CustomAppBar(title: 'Interactive AppText'), body: Container( color: Colors.white, width: double.infinity, diff --git a/packages/widgetbook/lib/widgets/atoms/padding.dart b/packages/widgetbook/lib/widgets/atoms/padding.dart index f443550..c0aa6fa 100644 --- a/packages/widgetbook/lib/widgets/atoms/padding.dart +++ b/packages/widgetbook/lib/widgets/atoms/padding.dart @@ -1,8 +1,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' - as widgetbook; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase(name: 'Interactive Padding', type: AppPadding) Widget interactiveAppPadding(BuildContext context) { @@ -47,7 +46,7 @@ Widget interactiveAppPadding(BuildContext context) { ); return AppScaffold( - appBar: AppBar(title: const Text('Interactive AppPadding')), + appBar: CustomAppBar(title: 'Interactive AppPadding'), body: Center( child: Container( color: Colors.red, diff --git a/packages/widgetbook/lib/widgets/atoms/spacing.dart b/packages/widgetbook/lib/widgets/atoms/spacing.dart index 6d18afc..95a163c 100644 --- a/packages/widgetbook/lib/widgets/atoms/spacing.dart +++ b/packages/widgetbook/lib/widgets/atoms/spacing.dart @@ -1,8 +1,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' - as widgetbook; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase(name: 'Interactive VSpace', type: VSpace) Widget interactiveVSpace(BuildContext context) { @@ -36,16 +35,12 @@ Widget interactiveVSpace(BuildContext context) { ); return AppScaffold( - appBar: AppBar(title: const Text('Interactive VSpace')), + appBar: const CustomAppBar(title: 'Interactive VSpace'), body: Column( children: [ - Flexible( - child: const Text('This text is above the VSpace widget'), - ), + Flexible(child: const Text('This text is above the VSpace widget')), VSpace(size), // Dynamically set vertical spacing - Flexible( - child: const Text('This text is below the VSpace widget'), - ), + Flexible(child: const Text('This text is below the VSpace widget')), ], ), ); @@ -79,20 +74,12 @@ Widget interactiveHSpace(BuildContext context) { ); return AppScaffold( - appBar: AppBar(title: const Text('Interactive HSpace')), + appBar: const CustomAppBar(title: 'Interactive HSpace'), body: Row( children: [ - Flexible( - child: const Text( - 'This text is to the left of the HSpace widget', - ), - ), + Flexible(child: const Text('This text is to the left of the HSpace widget')), HSpace(size), // Dynamically set horizontal spacing - Flexible( - child: const Text( - 'This text is to the right of the HSpace widget', - ), - ), + Flexible(child: const Text('This text is to the right of the HSpace widget')), ], ), ); diff --git a/packages/widgetbook/lib/widgets/organisms/app_scaffold.dart b/packages/widgetbook/lib/widgets/organisms/app_scaffold.dart index 18681ed..b7e8e73 100644 --- a/packages/widgetbook/lib/widgets/organisms/app_scaffold.dart +++ b/packages/widgetbook/lib/widgets/organisms/app_scaffold.dart @@ -1,67 +1,39 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' - as widgetbook; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; -@widgetbook.UseCase( - name: 'Interactive AppScaffold', - type: AppScaffold, -) +@widgetbook.UseCase(name: 'Interactive AppScaffold', type: AppScaffold) Widget interactiveAppScaffold(BuildContext context) { final knobs = context.knobs; // Knobs to dynamically adjust the properties of AppScaffold final appBar = knobs.boolean(label: 'Has AppBar', initialValue: true) - ? AppBar(title: const Text('App Scaffold')) + ? CustomAppBar(title: 'App Scaffold') : null; final backgroundColor = knobs.list( label: 'Background Color', - options: [ - Colors.blueGrey, - Colors.teal, - Colors.green, - Colors.orange, - Colors.pink, - ], + options: [Colors.blueGrey, Colors.teal, Colors.green, Colors.orange, Colors.pink], initialOption: Colors.blueGrey, labelBuilder: (color) => color.toString(), ); final bottomNavigationBar = - knobs.boolean( - label: 'Has Bottom Navigation', - initialValue: true, - ) + knobs.boolean(label: 'Has Bottom Navigation', initialValue: true) ? BottomNavigationBar( items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.search), - label: 'Search', - ), - BottomNavigationBarItem( - icon: Icon(Icons.settings), - label: 'Settings', - ), + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'), + BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'), ], ) : null; final floatingActionButton = - knobs.boolean( - label: 'Has Floating Action Button', - initialValue: true, - ) - ? FloatingActionButton( - onPressed: () {}, - child: const Icon(Icons.add), - ) + knobs.boolean(label: 'Has Floating Action Button', initialValue: true) + ? FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)) : null; final body = From 001bcb6093f27fc92bb119664ae5033ca31dd52b Mon Sep 17 00:00:00 2001 From: avni prajapati Date: Wed, 18 Jun 2025 16:32:32 +0530 Subject: [PATCH 04/31] fix: Updated atomic design rule documentation for app_translations package and remove unused translation patterns --- .cursor/rules/atomic-design-rule.mdc | 169 +-------------------------- 1 file changed, 2 insertions(+), 167 deletions(-) diff --git a/.cursor/rules/atomic-design-rule.mdc b/.cursor/rules/atomic-design-rule.mdc index 23f6c64..9d89067 100644 --- a/.cursor/rules/atomic-design-rule.mdc +++ b/.cursor/rules/atomic-design-rule.mdc @@ -10,7 +10,7 @@ alwaysApply: true ## Overview This rule enforces the use of the `app_ui` package components following the Atomic Design Pattern. The package provides consistent theming, spacing, and reusable components across the Flutter Launchpad project. -## app_translations Package 📦 +## 1. app_translations Package 📦 ### Overview The `app_translations` package manages localization in the application using the **slang** package. It provides type-safe, auto-generated translations for consistent internationalization across the Flutter Launchpad project. @@ -46,10 +46,6 @@ Add translations to JSON files in the `i18n` folder within `app_translations` pa "submit": "Submit", "cancel": "Cancel", "loading": "Loading...", - "error": { - "network": "Network error occurred", - "validation": "Please check your input" - } } ``` @@ -61,10 +57,6 @@ Add translations to JSON files in the `i18n` folder within `app_translations` pa "submit": "Enviar", "cancel": "Cancelar", "loading": "Cargando...", - "error": { - "network": "Ocurrió un error de red", - "validation": "Por favor verifica tu entrada" - } } ``` @@ -86,149 +78,9 @@ AppButton( onPressed: () {}, ) - AppText.small(text: context.t.error.validation) ``` -### Translation Patterns - -#### Basic Usage -```dart -// Page titles -AppText.large(text: context.t.loginTitle) - -// Form labels -AppTextField( - label: context.t.email, - hint: context.t.emailHint, -) - -// Button text -AppButton(text: context.t.signIn, onPressed: () {}) -AppButton.secondary(text: context.t.cancel, onPressed: () {}) -``` - -#### Complex Translations -```dart -// Nested objects -AppText.small(text: context.t.errors.validation.required) -AppText.small(text: context.t.messages.success.saved) - -// Pluralization (if supported) -AppText.small(text: context.t.itemCount(count: items.length)) - -// Interpolation (if supported) -AppText.small(text: context.t.welcomeUser(name: user.name)) -``` - -### JSON Structure Best Practices - -#### Organize by Feature -```json -{ - "auth": { - "login": "Login", - "register": "Register", - "forgotPassword": "Forgot Password", - "errors": { - "invalidEmail": "Invalid email address", - "weakPassword": "Password too weak" - } - }, - "home": { - "welcome": "Welcome", - "recentActivity": "Recent Activity" - }, - "common": { - "save": "Save", - "cancel": "Cancel", - "delete": "Delete", - "loading": "Loading..." - } -} -``` - -#### Usage with Organized Structure -```dart -// Auth-related texts -AppText.large(text: context.t.auth.login) -AppText.small(text: context.t.auth.errors.invalidEmail) - -// Home screen texts -AppText.medium(text: context.t.home.welcome) - -// Common actions -AppButton(text: context.t.common.save, onPressed: () {}) -AppButton.secondary(text: context.t.common.cancel, onPressed: () {}) -``` - -### Implementation Checklist - -#### For New Features: -- [ ] Add all text strings to appropriate JSON files -- [ ] Organize keys by feature/screen -- [ ] Run `melos run locale-gen` after adding keys -- [ ] Use `context.t.*` throughout the code -- [ ] Test with different locales - -#### Code Review Checklist: -- [ ] No hardcoded strings in UI components -- [ ] All user-facing text uses `context.t.*` -- [ ] Translation keys are descriptive and organized -- [ ] Generated code is up-to-date -- [ ] Multiple language files updated consistently - -### Common Translation Patterns - -#### Form Validation -```json -{ - "validation": { - "required": "This field is required", - "invalidEmail": "Please enter a valid email", - "passwordTooShort": "Password must be at least 8 characters", - "passwordMismatch": "Passwords do not match" - } -} -``` - -```dart -// Usage in validation -if (email.isEmpty) { - return context.t.validation.required; -} -if (!isValidEmail(email)) { - return context.t.validation.invalidEmail; -} -``` - -#### Navigation and Actions -```json -{ - "navigation": { - "back": "Back", - "next": "Next", - "done": "Done", - "close": "Close" - }, - "actions": { - "edit": "Edit", - "delete": "Delete", - "share": "Share", - "copy": "Copy" - } -} -``` - -```dart -// Usage in UI -AppButton(text: context.t.navigation.next, onPressed: () {}) -IconButton( - tooltip: context.t.actions.share, - onPressed: () {}, - icon: Icon(Icons.share), -) -``` ### Troubleshooting @@ -393,6 +245,7 @@ Row( Container( padding: Insets.medium, child: Column( + spacing : EdgeInsets.all(Insets.small8), crossAxisAlignment: CrossAxisAlignment.start, children: [ AppText.large(text: context.t.title), @@ -569,24 +422,6 @@ class AppButton extends StatelessWidget { } ``` -## Implementation Checklist - -### For New UI Components: -- [ ] Use `VSpace`/`HSpace` instead of `SizedBox` -- [ ] Use `AppButton` instead of raw Material buttons -- [ ] Use `AppText` for all text rendering -- [ ] Use `Insets` for consistent padding/margins -- [ ] Use `AppBorderRadius` for consistent corner radius -- [ ] Follow atomic design hierarchy (atoms → molecules → organisms) - -### Code Review Checklist: -- [ ] No raw `SizedBox` for spacing -- [ ] No raw Material buttons (`ElevatedButton`, `TextButton`, etc.) -- [ ] No hardcoded spacing values -- [ ] Proper use of app_ui components -- [ ] Consistent spacing throughout the UI -- [ ] Localized button text using `context.t` - ## Common Usage Patterns ### Form Layout From 9075f12712fe81ac4ba36b4f0c9c16cb5eba9478 Mon Sep 17 00:00:00 2001 From: avni prajapati Date: Wed, 18 Jun 2025 16:45:43 +0530 Subject: [PATCH 05/31] imp: Updated color usage guidelines and removed unnecessary guidelines. --- .cursor/rules/color-text-style.mdc | 354 ----------------------------- .cursor/rules/color.mdc | 36 +++ 2 files changed, 36 insertions(+), 354 deletions(-) delete mode 100644 .cursor/rules/color-text-style.mdc create mode 100644 .cursor/rules/color.mdc diff --git a/.cursor/rules/color-text-style.mdc b/.cursor/rules/color-text-style.mdc deleted file mode 100644 index f078ac3..0000000 --- a/.cursor/rules/color-text-style.mdc +++ /dev/null @@ -1,354 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -description: "Flutter Colors and TextStyles Rule (Material Design)" -alwaysApply: true - -## Overview -This rule enforces consistent usage of Material Design colors and typography through context extensions and custom widgets in Flutter Launchpad projects. - -## Rule Application - -### 🎨 Color Usage Rules - -#### ✅ ALWAYS Use Context ColorScheme -**Correct Pattern:** -```dart -// Use Material Design color scheme -Container(color: context.colorScheme.primary) -Container(color: context.colorScheme.secondary) -Container(color: context.colorScheme.surface) -Container(color: context.colorScheme.onPrimary) -``` - -#### ❌ NEVER Use Hardcoded Colors -**Avoid these patterns:** -```dart -// DON'T DO THIS -Container(color: Colors.blue) -Container(color: Color(0xFF123456)) -Container(color: const Color.fromRGb(255, 0, 0)) -``` - -### 📝 Typography Rules - -#### ✅ ALWAYS Use AppText Widget -**Correct Pattern:** -```dart -// Use AppText for consistent typography -AppText.medium( - text: context.t.login, - color: context.colorScheme.primary, -) - -AppText.large( - text: "Welcome", - color: context.colorScheme.onSurface, -) - -AppText.small( - text: "Description", - color: context.colorScheme.onSecondary, -) -``` - -#### ❌ NEVER Use Raw Text Widget -**Avoid this pattern:** -```dart -// DON'T DO THIS -Text( - "Login", - style: TextStyle(fontSize: 16, color: Colors.black), -) -``` - -#### ✅ Use Context TextTheme for Advanced Cases -**For RichText and complex styling:** -```dart -RichText( - text: TextSpan( - text: context.t.login, - style: context.textTheme.medium, - ), -) - -// Multiple text spans -RichText( - text: TextSpan( - children: [ - TextSpan( - text: "Welcome ", - style: context.textTheme.large, - ), - TextSpan( - text: "User", - style: context.textTheme.medium.copyWith( - color: context.colorScheme.primary, - ), - ), - ], - ), -) -``` - -## Material ColorScheme Reference - -### Primary Colors -```dart -context.colorScheme.primary // Main brand color -context.colorScheme.onPrimary // Text/icons on primary -context.colorScheme.primaryContainer // Tinted primary surface -context.colorScheme.onPrimaryContainer // Text on primary container -``` - -### Secondary Colors -```dart -context.colorScheme.secondary // Accent color -context.colorScheme.onSecondary // Text/icons on secondary -context.colorScheme.secondaryContainer // Tinted secondary surface -context.colorScheme.onSecondaryContainer // Text on secondary container -``` - -### Surface Colors -```dart -context.colorScheme.surface // Component backgrounds -context.colorScheme.onSurface // Text/icons on surface -context.colorScheme.surfaceVariant // Alternative surface -context.colorScheme.onSurfaceVariant // Text on surface variant -``` - -### Utility Colors -```dart -context.colorScheme.background // App background -context.colorScheme.onBackground // Text/icons on background -context.colorScheme.error // Error states -context.colorScheme.onError // Text/icons on error -context.colorScheme.outline // Borders and dividers -context.colorScheme.shadow // Shadows and elevation -``` - -## AppText Widget Variants - -### Text Size Variants -```dart -AppText.small(text: "Small text") // Caption/helper text -AppText.medium(text: "Medium text") // Body text -AppText.large(text: "Large text") // Headings -AppText.extraLarge(text: "XL text") // Main titles -``` - -### Common AppText Patterns -```dart -// Basic usage -AppText.medium( - text: context.t.welcome, - color: context.colorScheme.onSurface, -) - -// With custom styling -AppText.large( - text: "Title", - color: context.colorScheme.primary, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, -) - -// Localized text -AppText.medium( - text: context.t.login, - color: context.colorScheme.onPrimary, -) -``` - -## Context TextTheme Reference - -### Available TextTheme Styles -```dart -context.textTheme.small // Small body text -context.textTheme.medium // Regular body text -context.textTheme.large // Large body text -context.textTheme.headlineSmall // Small headlines -context.textTheme.headlineMedium // Medium headlines -context.textTheme.headlineLarge // Large headlines -context.textTheme.displaySmall // Small display text -context.textTheme.displayMedium // Medium display text -context.textTheme.displayLarge // Large display text -context.textTheme.labelSmall // Small labels -context.textTheme.labelMedium // Medium labels -context.textTheme.labelLarge // Large labels -``` - -## Implementation Patterns - -### Container with Color -```dart -Container( - decoration: BoxDecoration( - color: context.colorScheme.surface, - border: Border.all( - color: context.colorScheme.outline, - ), - borderRadius: BorderRadius.circular(8), - ), - child: AppText.medium( - text: context.t.content, - color: context.colorScheme.onSurface, - ), -) -``` - -### Card with Proper Colors -```dart -Card( - color: context.colorScheme.surfaceVariant, - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - AppText.large( - text: context.t.title, - color: context.colorScheme.onSurfaceVariant, - ), - AppText.medium( - text: context.t.description, - color: context.colorScheme.onSurfaceVariant, - ), - ], - ), - ), -) -``` - -### Button Styling -```dart -ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: context.colorScheme.primary, - foregroundColor: context.colorScheme.onPrimary, - ), - onPressed: () {}, - child: AppText.medium( - text: context.t.submit, - color: context.colorScheme.onPrimary, - ), -) -``` - -## Customization Guidelines - -### Adding New Colors -**Edit `colors.dart`:** -```dart -// Add custom colors to color scheme -ColorScheme customColorScheme = ColorScheme.fromSeed( - seedColor: Colors.blue, - // Add custom color mappings -); -``` - -### Adding New TextStyles -**Edit `typography.dart`:** -```dart -// Define custom text styles -TextTheme customTextTheme = TextTheme( - headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - ), - // Add more custom styles -); -``` - -## Implementation Checklist - -### For UI Components: -- [ ] Use `context.colorScheme.*` for all colors -- [ ] Use `AppText.*` instead of `Text` widget -- [ ] Use `context.textTheme.*` for complex text styling -- [ ] Follow Material Design color semantics -- [ ] Use localized text with `context.t.*` - -### Code Review Checklist: -- [ ] No hardcoded colors (`Colors.blue`, `Color(0xFF...)`) -- [ ] No raw `Text` widgets -- [ ] Proper color contrast (primary/onPrimary pairs) -- [ ] Consistent typography hierarchy -- [ ] Localization used where appropriate - -## Common Anti-Patterns to Avoid - -### ❌ Wrong Color Usage -```dart -// DON'T DO THIS -Container(color: Colors.red) -Text("Error", style: TextStyle(color: Colors.red)) -``` - -### ❌ Wrong Text Usage -```dart -// DON'T DO THIS -Text( - "Login", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), -) -``` - -### ❌ Inconsistent Theming -```dart -// DON'T DO THIS - mixing theme and hardcoded values -Container( - color: context.colorScheme.surface, - child: Text( - "Mixed theming", - style: TextStyle(color: Colors.black), // Should use theme - ), -) -``` - -## Best Practices - -### Semantic Color Usage -- Use `primary` for main actions and branding -- Use `secondary` for accent elements -- Use `surface` for component backgrounds -- Use `error` for error states only -- Always pair colors with their `on*` counterparts - -### Typography Hierarchy -- Use `AppText.extraLarge` for main titles -- Use `AppText.large` for section headers -- Use `AppText.medium` for body text -- Use `AppText.small` for captions and helper text - -### Accessibility -- Ensure proper color contrast ratios -- Test with dark/light theme variations -- Use semantic colors for better screen reader support - -## Migration Guide - -### From Hardcoded Colors -```dart -// Before -Container(color: Colors.blue) - -// After -Container(color: context.colorScheme.primary) -``` - -### From Text Widget -```dart -// Before -Text("Login", style: TextStyle(fontSize: 16)) - -// After -AppText.medium(text: context.t.login) -``` \ No newline at end of file diff --git a/.cursor/rules/color.mdc b/.cursor/rules/color.mdc new file mode 100644 index 0000000..76cc48c --- /dev/null +++ b/.cursor/rules/color.mdc @@ -0,0 +1,36 @@ +--- +description: +globs: +alwaysApply: true +--- +description: "Usage Colors" +alwaysApply: true + +## Overview +This rule enforces consistent usage of colors in project. + +If you look at the `extensions.dart` file, you will be able to see extensions related to accessing colors and textstyles. +we follow material conventions. So to use any color, you can use context.colorscheme like this: + +```dart +Container(color:context.colorScheme.primary) +``` + +Use AppText instead of Text widget to utilise typography. +```dart +AppText.medium( + text:context.t.login, + color: context.colorScheme.primary, +), +``` + +Same way, You can use TextStyles using context.textTheme. + +```dart +RichText( + text: TextSpan( + text: context.t.login, + Style: context.textTheme.medium, + ) +) +``` \ No newline at end of file From 3065f130b6b1987d067aef56b5241d9117e28849 Mon Sep 17 00:00:00 2001 From: avni prajapati Date: Wed, 18 Jun 2025 16:49:44 +0530 Subject: [PATCH 06/31] imp: Removd unnecessary rules for Rest API Client documentation --- .cursor/rules/rest-api-client.mdc | 88 ------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 .cursor/rules/rest-api-client.mdc diff --git a/.cursor/rules/rest-api-client.mdc b/.cursor/rules/rest-api-client.mdc deleted file mode 100644 index 9b518c6..0000000 --- a/.cursor/rules/rest-api-client.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- - -description: "Rest API Client - Cursor Rules" -alwaysApply: true - -## Overview -The `app_client` package provides comprehensive API call functionality with the `RestApiClient` class as the core component for remote data source communication. - -## Core Architecture - -### RestApiClient Class -- **Primary Purpose**: Connect with remote data sources using Dio as the underlying HTTP client -- **Responsibility**: Execute API requests and handle success/error responses -- **Foundation**: Built on top of Dio for robust HTTP communication - -### Supported HTTP Methods -- `GET` - Retrieve data from server -- `POST` - Create new resources -- `PUT` - Update existing resources -- `DELETE` - Remove resources -- `dynamic` - Flexible method selection based on runtime requirements - -## Key Features - -### 1. Caching System -- **Implementation**: Optional caching via `DioCacheInterceptor` -- **Storage**: Uses `HiveCacheStore` for local cache persistence -- **Configuration**: Enable/disable through initialization parameters -- **Benefits**: Reduces network calls and improves performance - -### 2. Token Management -- **Automatic Handling**: Seamless access and refresh token management -- **Interceptor**: `ApiTokensInterceptor` handles token lifecycle -- **Refresh Logic**: Automatic token refresh when expired -- **Endpoint Exclusion**: Configure endpoints that don't require tokens - -### 3. Logging & Debugging -- **Logger**: `PrettyDioLogger` for enhanced API request/response visibility -- **Formatting**: Beautified output for easier debugging -- **Development Aid**: Comprehensive request/response information - -### 4. Centralized Error Handling -- **Unified Approach**: Single point for API error management -- **Consistent Response**: Standardized error handling across the application -- **Token Expiry**: Automatic logout trigger on authentication failures - -## Initialization Pattern - -```dart -await restApiClient.init( - isApiCacheEnabled: true, // Enable caching - baseURL: 'https://your-api.com', // API base URL - refreshTokenEndpoint: '/auth/refresh', // Token refresh endpoint - endPointsToEscapeHeaderToken: ['/auth/login'], // Endpoints without token - onForceLogout: () => handleLogout(), // Logout callback -); -``` - -## Development Guidelines - -### Best Practices -1. **Initialize Once**: Call `init()` during app startup -2. **Error Handling**: Implement proper error handling in consuming code -3. **Token Management**: Configure token-free endpoints appropriately -4. **Caching Strategy**: Enable caching for appropriate endpoints -5. **Logout Handling**: Provide proper logout callback implementation - -### Configuration Considerations -- Set appropriate base URL for environment (dev/staging/prod) -- Configure refresh token endpoint correctly -- Identify endpoints that should bypass token authentication -- Implement secure logout mechanism - -### Usage Patterns -- Use appropriate HTTP methods for RESTful operations -- Leverage caching for frequently accessed, relatively static data -- Monitor logs during development for request/response debugging -- Handle token refresh scenarios gracefully in UI - -## Integration Notes -- Designed to work seamlessly with Flutter/Dart applications -- Integrates with Hive for local storage requirements -- Compatible with standard REST API architectures -- Supports both authenticated and public API endpoints \ No newline at end of file From 7ad897788403690ceff1a8002a05a9b5a4ac78ac Mon Sep 17 00:00:00 2001 From: avni prajapati Date: Wed, 18 Jun 2025 17:14:14 +0530 Subject: [PATCH 07/31] imp: added project rules for trae --- .cursor/rules/atomic-design-rule.mdc | 2 - .trae/rules/project_rules.md | 1527 ++++++++++++++++++++++++++ 2 files changed, 1527 insertions(+), 2 deletions(-) create mode 100644 .trae/rules/project_rules.md diff --git a/.cursor/rules/atomic-design-rule.mdc b/.cursor/rules/atomic-design-rule.mdc index 9d89067..e3287e6 100644 --- a/.cursor/rules/atomic-design-rule.mdc +++ b/.cursor/rules/atomic-design-rule.mdc @@ -153,8 +153,6 @@ AppText.large(text: "Content") // Loading indicators AppLoadingIndicator() -AppLoadingIndicator.small() -AppLoadingIndicator.large() ``` ### 🔵 Molecules (Component Combinations) diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..bc578a2 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,1527 @@ +## Overview +This rule enforces the use of `flutter_gen` package for type-safe asset management in Flutter projects, replacing raw string asset paths with generated code. + +## Rule Application + +### ❌ NEVER Use Raw String Paths +**Avoid this pattern:** +```dart +// DON'T DO THIS +Image.asset("assets/demo.png") +Image.asset("assets/icons/home.svg") +Image.asset("assets/images/profile.jpg") +``` + +### ✅ ALWAYS Use Generated Asset Classes +**Use this pattern instead:** +```dart +// DO THIS +Assets.images.demo.image() +Assets.icons.home.svg() +Assets.images.profile.image() +``` + +## Implementation Steps + +### 1. Asset Placement +- **ALWAYS** add assets to the `assets` folder in the **app_ui** package +- Organize assets by type (images, icons, fonts, etc.) +- Use descriptive, snake_case naming for asset files + +### 2. Directory Structure +``` +app_ui/ +├── assets/ +│ ├── images/ +│ │ ├── demo.png +│ │ ├── profile.jpg +│ │ └── background.png +│ ├── icons/ +│ │ ├── home.svg +│ │ ├── search.svg +│ │ └── settings.svg +│ └── fonts/ +│ └── custom_font.ttf +``` + +### 3. Code Generation +After adding new assets, **ALWAYS** run: +```bash +melos run asset-gen +``` + +### 4. Usage Patterns + +#### Images +```dart +// For PNG/JPG images +Assets.images.demo.image() +Assets.images.profile.image() +Assets.images.background.image() + +// With additional properties +Assets.images.demo.image( + width: 100, + height: 100, + fit: BoxFit.cover, +) +``` + +#### SVG Icons +```dart +// For SVG assets +Assets.icons.home.svg() +Assets.icons.search.svg() +Assets.icons.settings.svg() + +// With color and size +Assets.icons.home.svg( + color: Colors.blue, + width: 24, + height: 24, +) +``` + +#### Raw Asset Paths (when needed) +```dart +// If you need the path string +Assets.images.demo.path +Assets.icons.home.path +``` + +## Asset Type Mappings + +### Common Asset Extensions and Usage +| Extension | Usage Pattern | Example | +|-----------|---------------|---------| +| `.png`, `.jpg`, `.jpeg` | `.image()` | `Assets.images.photo.image()` | +| `.svg` | `.svg()` | `Assets.icons.star.svg()` | +| `.json` | `.path` | `Assets.animations.loading.path` | +| `.ttf`, `.otf` | Reference in theme | Font family name | + +## Implementation Checklist + +### Adding New Assets: +- [ ] Place asset in appropriate folder within `app_ui/assets/` +- [ ] Use descriptive, snake_case naming +- [ ] Run `melos run asset-gen` command +- [ ] Verify asset appears in generated `Assets` class +- [ ] Update existing raw string references to use generated code + +### Code Review Checklist: +- [ ] No raw string asset paths (`"assets/..."`) +- [ ] All assets use `Assets.category.name.method()` pattern +- [ ] Asset generation command run after adding new assets +- [ ] Unused assets removed from assets folder + +## Common Patterns + +### Image Widget +```dart +// Basic image +Assets.images.logo.image() + +// Image with properties +Assets.images.banner.image( + width: double.infinity, + height: 200, + fit: BoxFit.cover, +) + +// Image in Container +Container( + decoration: BoxDecoration( + image: DecorationImage( + image: Assets.images.background.provider(), + fit: BoxFit.cover, + ), + ), +) +``` + +### SVG Usage +```dart +// Basic SVG +Assets.icons.menu.svg() + +// Styled SVG +Assets.icons.heart.svg( + color: theme.primaryColor, + width: 20, + height: 20, +) + +// SVG in IconButton +IconButton( + onPressed: () {}, + icon: Assets.icons.settings.svg(), +) +``` + +### Asset Provider (for advanced usage) +```dart +// For use with other widgets that need ImageProvider +CircleAvatar( + backgroundImage: Assets.images.avatar.provider(), +) + +// For precaching +precacheImage(Assets.images.splash.provider(), context); +``` + +## Best Practices + +### Naming Conventions +- Use `snake_case` for asset file names +- Be descriptive: `user_profile.png` instead of `img1.png` +- Group related assets: `icon_home.svg`, `icon_search.svg` + +### Organization +- **images/**: Photos, illustrations, backgrounds +- **icons/**: SVG icons, small graphics +- **animations/**: Lottie files, GIFs +- **fonts/**: Custom font files + +### Performance +- Use appropriate image formats (SVG for icons, PNG/JPG for photos) +- Optimize image sizes before adding to assets +- Consider using `precacheImage()` for critical images + +## Migration from Raw Strings + +### Find and Replace Pattern +1. Search for: `Image.asset("assets/` +2. Replace with appropriate `Assets.` pattern +3. Run asset generation if needed +4. Test all asset references + +### Example Migration +```dart +// Before +Image.asset("assets/images/logo.png", width: 100) + +// After +Assets.images.logo.image(width: 100) +``` + +## Troubleshooting + +### Asset Not Found +1. Verify asset exists in `app_ui/assets/` folder +2. Check file naming (no spaces, special characters) +3. Run `melos run asset-gen` command +4. Restart IDE/hot restart app + +### Generated Code Not Updated +1. Run `melos run asset-gen` command +2. Check for build errors in terminal +3. Verify `flutter_gen` is properly configured in `pubspec.yaml` +4. Clean and rebuild project if necessary + +## Overview +This rule enforces the use of the `app_ui` package components following the Atomic Design Pattern. The package provides consistent theming, spacing, and reusable components across the Flutter Launchpad project. + +## 1. app_translations Package 📦 + +### Overview +The `app_translations` package manages localization in the application using the **slang** package. It provides type-safe, auto-generated translations for consistent internationalization across the Flutter Launchpad project. + +### Implementation Rules + +#### ✅ ALWAYS Use context.t for Text +**Correct Pattern:** +```dart +// Use generated translations +AppText.medium(text: context.t.welcome) +AppButton(text: context.t.submit, onPressed: () {}) +``` + +#### ❌ NEVER Use Hardcoded Strings +**Avoid these patterns:** +```dart +// DON'T DO THIS +AppText.medium(text: "Welcome") +AppButton(text: "Submit", onPressed: () {}) +``` + +### Adding New Translations + +#### Step 1: Add Key-Value Pairs +Add translations to JSON files in the `i18n` folder within `app_translations` package: + +**English (`en.json`):** +```json +{ + "login": "Login Screen", + "welcome": "Welcome to the app", + "submit": "Submit", + "cancel": "Cancel", + "loading": "Loading...", +} +``` + +**Other languages (e.g., `es.json`):** +```json +{ + "login": "Pantalla de Inicio de Sesión", + "welcome": "Bienvenido a la aplicación", + "submit": "Enviar", + "cancel": "Cancelar", + "loading": "Cargando...", +} +``` + +#### Step 2: Generate Code +After adding key-value pairs, run the generation command: +```bash +melos run locale-gen +``` + +#### Step 3: Use in Code +```dart + +// In AppText widget +AppText.medium(text: context.t.welcome) + +// In buttons +AppButton( + text: context.t.submit, + onPressed: () {}, +) + +AppText.small(text: context.t.error.validation) +``` + +### Troubleshooting + +#### Translation Not Found +1. Verify key exists in JSON files +2. Check spelling and nested structure +3. Run `melos run locale-gen` to regenerate +4. Restart IDE/hot restart app + +#### Generated Code Issues +1. Ensure JSON syntax is valid +2. Check for duplicate keys +3. Verify slang package configuration +4. Clean and rebuild project + +## Package Structure +The `app_ui` package is organized using **Atomic Design Pattern**: +- 🎨 **App Themes** - Color schemes and typography +- 🔤 **Fonts** - Custom font configurations +- 📁 **Assets Storage** - Images, icons, and other assets +- 🧩 **Common Widgets** - Reusable UI components +- 🛠️ **Generated Files** - Auto-generated asset and theme files + +## Atomic Design Levels + +### 🛰️ Atoms (Basic Building Blocks) + +#### Spacing Rules +**❌ NEVER Use Raw SizedBox for Spacing** +```dart +// DON'T DO THIS +const SizedBox(height: 8) +const SizedBox(width: 16) +const SizedBox(height: 24, width: 32) +``` + +**✅ ALWAYS Use VSpace and HSpace** +```dart +// DO THIS - Vertical spacing +VSpace.xsmall() // Extra small vertical space +VSpace.small() // Small vertical space +VSpace.medium() // Medium vertical space +VSpace.large() // Large vertical space +VSpace.xlarge() // Extra large vertical space + +// Horizontal spacing +HSpace.xsmall() // Extra small horizontal space +HSpace.small() // Small horizontal space +HSpace.medium() // Medium horizontal space +HSpace.large() // Large horizontal space +HSpace.xlarge() // Extra large horizontal space +``` + +#### Other Atom-Level Components +```dart +// Border radius +AppBorderRadius.small +AppBorderRadius.medium +AppBorderRadius.large + +// Padding/margins +Insets.small +Insets.medium +Insets.large + +// Text components +AppText.small(text: "Content") +AppText.medium(text: "Content") +AppText.large(text: "Content") + +// Loading indicators +AppLoadingIndicator() +``` + +### 🔵 Molecules (Component Combinations) + +#### Button Usage Rules +**❌ NEVER Use Raw Material Buttons** +```dart +// DON'T DO THIS +ElevatedButton( + onPressed: () {}, + child: Text("Login"), +) + +TextButton( + onPressed: () {}, + child: Text("Cancel"), +) +``` + +**✅ ALWAYS Use AppButton** +```dart +// DO THIS - Basic button +AppButton( + text: context.t.login, + onPressed: () {}, +) + +// Expanded button +AppButton( + text: context.t.submit, + onPressed: () {}, + isExpanded: true, +) + +// Disabled button +AppButton( + text: context.t.save, + onPressed: () {}, + isEnabled: false, +) + +// Button variants +AppButton.secondary( + text: context.t.cancel, + onPressed: () {}, +) + +AppButton.outline( + text: context.t.edit, + onPressed: () {}, +) + +AppButton.text( + text: context.t.skip, + onPressed: () {}, +) +``` + +## Spacing Implementation Patterns + +### Column/Row Spacing +```dart +// Instead of multiple SizedBox widgets +Column( + children: [ + Widget1(), + VSpace.medium(), + Widget2(), + VSpace.small(), + Widget3(), + ], +) + +Row( + children: [ + Widget1(), + HSpace.large(), + Widget2(), + HSpace.medium(), + Widget3(), + ], +) +``` + +### Complex Layout Spacing +```dart +// Combining vertical and horizontal spacing +Container( + padding: Insets.medium, + child: Column( + spacing : EdgeInsets.all(Insets.small8), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.large(text: context.t.title), + VSpace.small(), + AppText.medium(text: context.t.description), + VSpace.large(), + Row( + children: [ + AppButton( + text: context.t.confirm, + onPressed: () {}, + isExpanded: true, + ), + HSpace.medium(), + AppButton.secondary( + text: context.t.cancel, + onPressed: () {}, + isExpanded: true, + ), + ], + ), + ], + ), +) +``` + +## Button Configuration Patterns + +### Basic Button Usage +```dart +// Standard button +AppButton( + text: context.t.login, + onPressed: () => _handleLogin(), +) + +// Button with loading state +AppButton( + text: context.t.submit, + onPressed: isLoading ? null : () => _handleSubmit(), + isEnabled: !isLoading, + child: isLoading ? AppLoadingIndicator.small() : null, +) +``` + +### Button Variants +```dart +// Primary button (default) +AppButton( + text: context.t.save, + onPressed: () {}, +) + +// Secondary button +AppButton.secondary( + text: context.t.cancel, + onPressed: () {}, +) + +// Outline button +AppButton.outline( + text: context.t.edit, + onPressed: () {}, +) + +// Text button +AppButton.text( + text: context.t.skip, + onPressed: () {}, +) + +// Destructive button +AppButton.destructive( + text: context.t.delete, + onPressed: () {}, +) +``` + +### Button Properties +```dart +AppButton( + text: context.t.action, + onPressed: () {}, + isExpanded: true, // Full width button + isEnabled: true, // Enable/disable state + isLoading: false, // Loading state + icon: Icons.save, // Leading icon + suffixIcon: Icons.arrow_forward, // Trailing icon + backgroundColor: context.colorScheme.primary, + textColor: context.colorScheme.onPrimary, +) +``` + +## App UI Component Categories + +### Atoms +```dart +// Spacing +VSpace.small() +HSpace.medium() + +// Text +AppText.medium(text: "Content") + +// Border radius +AppBorderRadius.large + +// Padding +Insets.all16 + +// Loading +AppLoadingIndicator() +``` + +### Molecules +```dart +// Buttons +AppButton(text: "Action", onPressed: () {}) + +// Input fields +AppTextField( + label: context.t.email, + controller: emailController, +) + +// Cards +AppCard( + child: Column(children: [...]), +) +``` + +### Organisms +```dart +// Forms +AppForm( + children: [ + AppTextField(...), + VSpace.medium(), + AppButton(...), + ], +) + +// Navigation +AppBottomNavigationBar( + items: [...], +) +``` + +## Customization Guidelines + +### Modifying Spacing +**Edit `spacing.dart`:** +```dart +class VSpace extends StatelessWidget { + static Widget xsmall() => const SizedBox(height: 4); + static Widget small() => const SizedBox(height: 8); + static Widget medium() => const SizedBox(height: 16); + static Widget large() => const SizedBox(height: 24); + static Widget xlarge() => const SizedBox(height: 32); +} +``` + +### Modifying Buttons +**Edit `app_button.dart`:** +```dart +class AppButton extends StatelessWidget { + const AppButton({ + required this.text, + required this.onPressed, + this.isExpanded = false, + this.isEnabled = true, + // Add more customization options + }); +} +``` + +## Common Usage Patterns + +### Form Layout +```dart +Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppText.large(text: context.t.loginTitle), + VSpace.large(), + AppTextField( + label: context.t.email, + controller: emailController, + ), + VSpace.medium(), + AppTextField( + label: context.t.password, + controller: passwordController, + obscureText: true, + ), + VSpace.large(), + AppButton( + text: context.t.login, + onPressed: () => _handleLogin(), + isExpanded: true, + ), + VSpace.small(), + AppButton.text( + text: context.t.forgotPassword, + onPressed: () => _navigateToForgotPassword(), + ), + ], +) +``` + +### Card Layout +```dart +AppCard( + padding: Insets.medium, + borderRadius: AppBorderRadius.medium, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.large(text: context.t.cardTitle), + VSpace.small(), + AppText.medium(text: context.t.cardDescription), + VSpace.medium(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AppButton.text( + text: context.t.cancel, + onPressed: () {}, + ), + HSpace.small(), + AppButton( + text: context.t.confirm, + onPressed: () {}, + ), + ], + ), + ], + ), +) +``` + +### List Item Spacing +```dart +ListView.separated( + itemCount: items.length, + separatorBuilder: (context, index) => VSpace.small(), + itemBuilder: (context, index) => ListTile( + title: AppText.medium(text: items[index].title), + subtitle: AppText.small(text: items[index].subtitle), + trailing: AppButton.text( + text: context.t.view, + onPressed: () => _viewItem(items[index]), + ), + ), +) +``` + +## Best Practices + +### Spacing Consistency +- Use predefined spacing values from `VSpace`/`HSpace` +- Maintain consistent spacing ratios throughout the app +- Group related elements with smaller spacing +- Separate different sections with larger spacing + +### Component Reusability +- Extend app_ui components rather than creating new ones +- Follow atomic design principles +- Keep components configurable but opinionated +- Maintain consistent API patterns across components + +### Performance +- Use `const` constructors where possible +- Avoid rebuilding spacing widgets unnecessarily +- Cache complex spacing calculations + +## Migration Guide + +### From Raw Spacing +```dart +// Before +const SizedBox(height: 16) + +// After +VSpace.medium() +``` + +### From Raw Buttons +```dart +// Before +ElevatedButton( + onPressed: () {}, + child: Text("Submit"), +) + +// After +AppButton( + text: context.t.submit, + onPressed: () {}, +) +``` + +## Overview +This rule ensures consistent implementation of Auto Route navigation in Flutter applications with proper annotations, route configurations, and BLoC integration. + +## Rule Application + +### 1. Screen Widget Annotation +- **ALWAYS** annotate screen widgets with `@RoutePage()` decorator +- Place the annotation directly above the class declaration +- No additional parameters needed for basic routes + +### 2. For simple screens without state management: +```dart +@RoutePage() +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return // Your widget implementation + } +} +``` + +### 3. For screens requiring state management with BLoC/Cubit: +```dart +@RoutePage() +class HomeScreen extends StatefulWidget implements AutoRouteWrapper { + const HomeScreen({super.key}); + + @override + Widget wrappedRoute(BuildContext context) { + return MultiRepositoryProvider( + providers: [ + RepositoryProvider(create: (context) => HomeRepository()), + RepositoryProvider(create: (context) => ProfileRepository()), + RepositoryProvider(create: (context) => const AuthRepository()), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + lazy: false, + create: (context) => HomeBloc( + repository: context.read() + )..safeAdd(const FetchPostsEvent()), + ), + BlocProvider( + create: (context) => ProfileCubit( + context.read(), + context.read(), + )..fetchProfileDetail(), + ), + ], + child: this, + ), + ); + } + + @override + State createState() => _HomeScreenState(); +} +``` + +### 4. Route Configuration in app_router.dart +```dart +@AutoRouterConfig(replaceInRouteName: 'Page|Screen,Route') +class AppRouter extends RootStackRouter { + @override + List get routes => [ + AutoRoute( + initial: true, + page: SplashRoute.page, + guards: [AuthGuard()], + ), + AutoRoute(page: HomeRoute.page), + // Add new routes here + ]; +} +``` + +### 5. Code Generation Command +After adding new routes, **ALWAYS** run: +```bash +melos run build-runner +``` + +### 6. Use BackButtonListener instead of PopScope while project contains AutoRoute to avoid conflicts because of auto_route. +For this you can wrap the AppScafflold with BackButtonListener like this, +```dart +@override + Widget build(BuildContext context) { + return BackButtonListener( + onBackButtonPressed: (){}, + child: AppScafflold(), + ); + } +``` + +## Implementation Checklist + +### For New Screens: +- [ ] Add `@RoutePage()` annotation above class declaration +- [ ] Choose appropriate pattern (StatelessWidget vs StatefulWidget with AutoRouteWrapper) +- [ ] If using BLoC, implement `AutoRouteWrapper` interface +- [ ] Add route configuration in `app_router.dart` +- [ ] Run build runner command +- [ ] Verify route generation in generated files + +### For BLoC Integration: +- [ ] Implement `AutoRouteWrapper` interface +- [ ] Use `MultiRepositoryProvider` for dependency injection +- [ ] Use `MultiBlocProvider` for state management +- [ ] Initialize BLoCs with required repositories +- [ ] Return `this` as child in wrapper + +### Route Configuration: +- [ ] Add route to `routes` list in `AppRouter` +- [ ] Use `RouteNameHere.page` format +- [ ] Add guards if authentication required +- [ ] Set `initial: true` for entry point routes + +## Common Patterns + +### Basic Navigation Route +```dart +AutoRoute(page: ScreenNameRoute.page) +``` + +### Protected Route with Guard +```dart +AutoRoute( + page: ScreenNameRoute.page, + guards: [AuthGuard()], +) +``` + +### Initial/Entry Route +```dart +AutoRoute( + initial: true, + page: SplashRoute.page, + guards: [AuthGuard()], +) +``` + +## Notes +- Route names are automatically generated based on screen class names +- The `replaceInRouteName` parameter converts 'Page' or 'Screen' suffixes to 'Route' +- Always run code generation after route changes +- Use lazy loading for BLoCs when appropriate (set `lazy: false` for immediate initialization) + +# Bloc Rules +## Overview +The BLoC layer serves as the bridge between UI and data layers, managing application state through events and state emissions. This layer follows a strict architectural pattern with three core components. + +## BLoC Architecture Components + +| Component | Purpose | Description | +|-----------|---------|-------------| +| **State file 💽** | Data Holder | Contains reference to data displayed in UI | +| **Event file ▶️** | UI Triggers | Holds events triggered from the UI layer | +| **BLoC file 🔗** | Logic Controller | Connects State and Event, performs business logic | + +## 1. Event File Implementation ⏭️ + +### Event Class Structure +- **Use sealed classes** instead of abstract classes for events +- **Implement with final classes** for concrete event types +- **Name in past tense** - events represent actions that have already occurred + +```dart +part of '[feature]_bloc.dart'; + +sealed class [Feature]Event extends Equatable { + const [Feature]Event(); + + @override + List get props => []; +} + +final class [Feature]GetDataEvent extends [Feature]Event { + const [Feature]GetDataEvent(); +} +``` + +### Event Naming Conventions +- **Base Event Class**: `[BlocSubject]Event` +- **Initial Load Events**: `[BlocSubject]Started` +- **Action Events**: `[BlocSubject][Action]Event` +- **Past Tense**: Events represent completed user actions + +### Event Examples +```dart +// Good examples +final class HomeGetPostEvent extends HomeEvent {...} +final class ProfileUpdateEvent extends ProfileEvent {...} +final class AuthLoginEvent extends AuthEvent {...} + +// Initial load events +final class HomeStarted extends HomeEvent {...} +final class ProfileStarted extends ProfileEvent {...} +``` + +## 2. State File Implementation 📌 + +### State Class Structure +- **Hybrid approach**: Combines Named Constructors and copyWith methods +- **Equatable implementation**: For proper state comparison +- **Private constructor**: Main constructor should be private +- **ApiStatus integration**: Use standardized status enum + +```dart +part of '[feature]_bloc.dart'; + +class [Feature]State extends Equatable { + final List<[Feature]Model> dataList; + final bool hasReachedMax; + final ApiStatus status; + + const [Feature]State._({ + this.dataList = const <[Feature]Model>[], + this.hasReachedMax = false, + this.status = ApiStatus.initial, + }); + + // Named constructors for common states + const [Feature]State.initial() : this._(status: ApiStatus.initial); + const [Feature]State.loading() : this._(status: ApiStatus.loading); + const [Feature]State.loaded(List<[Feature]Model> dataList, bool hasReachedMax) + : this._( + status: ApiStatus.loaded, + dataList: dataList, + hasReachedMax: hasReachedMax, + ); + const [Feature]State.error() : this._(status: ApiStatus.error); + + [Feature]State copyWith({ + ApiStatus? status, + List<[Feature]Model>? dataList, + bool? hasReachedMax, + }) { + return [Feature]State._( + status: status ?? this.status, + dataList: dataList ?? this.dataList, + hasReachedMax: hasReachedMax ?? this.hasReachedMax, + ); + } + + @override + List get props => [dataList, hasReachedMax, status]; + + @override + bool get stringify => true; +} +``` + +### State Design Patterns +- **Private Main Constructor**: Use `._()` pattern +- **Named Constructors**: For common state scenarios +- **CopyWith Method**: For incremental state updates +- **Proper Props**: Include all relevant fields in props list +- **Stringify**: Enable for better debugging + +### ApiStatus Enum Usage +```dart +enum ApiStatus { + initial, // Before any operation + loading, // During API call + loaded, // Successful data fetch + error, // API call failed +} +``` + +## 3. BLoC File Implementation 🟦 + +### BLoC Class Structure +```dart +class [Feature]Bloc extends Bloc<[Feature]Event, [Feature]State> { + [Feature]Bloc({required this.repository}) : super(const [Feature]State.initial()) { + on<[Feature]GetDataEvent>(_on[Feature]GetDataEvent, transformer: droppable()); + } + + final I[Feature]Repository repository; + int _pageCount = 1; + + FutureOr _on[Feature]GetDataEvent( + [Feature]GetDataEvent event, + Emitter<[Feature]State> emit, + ) async { + if (state.hasReachedMax) return; + + // Show loader only on initial load + state.status == ApiStatus.initial + ? emit(const [Feature]State.loading()) + : emit([Feature]State.loaded(state.dataList, false)); + + final dataEither = await repository.fetchData(page: _pageCount).run(); + + dataEither.fold( + (error) => emit(const [Feature]State.error()), + (result) { + emit([Feature]State.loaded( + state.dataList.followedBy(result).toList(), + false, + )); + _pageCount++; + }, + ); + } +} +``` + +### BLoC Implementation Patterns +- **Repository Injection**: Always inject repository through constructor +- **Event Transformers**: Use appropriate transformers (droppable, concurrent, sequential) +- **State Management**: Check current state before emitting new states +- **Error Handling**: Use TaskEither fold method for error handling +- **Pagination Logic**: Implement proper pagination tracking + +### Event Transformers +```dart +// Use droppable for operations that shouldn't be queued +on(_handler, transformer: droppable()); + +// Use concurrent for independent operations +on(_handler, transformer: concurrent()); + +// Use sequential for ordered operations (default) +on(_handler, transformer: sequential()); +``` + +## 4. UI Integration with AutoRoute 🎁 + +### Screen Implementation with Providers +```dart +class [Feature]Screen extends StatefulWidget implements AutoRouteWrapper { + const [Feature]Screen({super.key}); + + @override + Widget wrappedRoute(BuildContext context) { + return RepositoryProvider<[Feature]Repository>( + create: (context) => [Feature]Repository(), + child: BlocProvider( + lazy: false, + create: (context) => [Feature]Bloc( + repository: RepositoryProvider.of<[Feature]Repository>(context), + )..add(const [Feature]GetDataEvent()), + child: this, + ), + ); + } + + @override + State<[Feature]Screen> createState() => _[Feature]ScreenState(); +} +``` + +### Provider Pattern Guidelines +- **AutoRouteWrapper**: Implement for scoped provider injection +- **RepositoryProvider**: Provide repository instances +- **BlocProvider**: Provide BLoC instances with repository injection +- **Lazy Loading**: Set `lazy: false` for immediate initialization +- **Initial Events**: Add initial events in BLoC creation + +## Development Guidelines + +### File Organization +```dart +// Event file structure +part of '[feature]_bloc.dart'; +sealed class [Feature]Event extends Equatable {...} + +// State file structure +part of '[feature]_bloc.dart'; +class [Feature]State extends Equatable {...} + +// BLoC file structure +import 'package:bloc/bloc.dart'; +part '[feature]_event.dart'; +part '[feature]_state.dart'; +``` + +### Naming Conventions +- **BLoC Class**: `[Feature]Bloc` +- **State Class**: `[Feature]State` +- **Event Base Class**: `[Feature]Event` +- **Event Handlers**: `_on[Feature][Action]Event` +- **Private Fields**: Use underscore prefix for internal state + +### Error Handling Patterns +```dart +// Standard error handling with fold +final resultEither = await repository.operation().run(); +resultEither.fold( + (failure) => emit(const FeatureState.error()), + (success) => emit(FeatureState.loaded(success)), +); +``` + +### State Emission Best Practices +- **Check Current State**: Prevent unnecessary emissions +- **Loading States**: Show loader only when appropriate +- **Error Recovery**: Provide ways to retry failed operations +- **Pagination**: Handle has-reached-max scenarios + +## Testing Considerations + +### BLoC Testing Structure +```dart +group('[Feature]Bloc', () { + late [Feature]Bloc bloc; + late Mock[Feature]Repository mockRepository; + + setUp(() { + mockRepository = Mock[Feature]Repository(); + bloc = [Feature]Bloc(repository: mockRepository); + }); + + blocTest<[Feature]Bloc, [Feature]State>( + 'emits loaded state when event succeeds', + build: () => bloc, + act: (bloc) => bloc.add(const [Feature]GetDataEvent()), + expect: () => [ + const [Feature]State.loading(), + isA<[Feature]State>().having((s) => s.status, 'status', ApiStatus.loaded), + ], + ); +}); +``` + +### Build Runner Commands +```bash +# Generate necessary files +flutter packages pub run build_runner build + +# Watch for changes +flutter packages pub run build_runner watch + +# Clean and rebuild +flutter packages pub run build_runner clean +flutter packages pub run build_runner build --delete-conflicting-outputs +``` + +## Performance Optimizations + +### Memory Management +- **Proper Disposal**: BLoC automatically handles disposal +- **Stream Subscriptions**: Cancel in BLoC close method if manually created +- **Repository Scoping**: Scope repositories to feature level + +### Event Handling Efficiency +- **Debouncing**: Use appropriate transformers for user input +- **Caching**: Implement at repository level, not BLoC level +- **Pagination**: Implement proper pagination logic to avoid memory issues + +## Overview +This rule enforces consistent usage of colors in project. + +If you look at the `extensions.dart` file, you will be able to see extensions related to accessing colors and textstyles. +we follow material conventions. So to use any color, you can use context.colorscheme like this: + +```dart +Container(color:context.colorScheme.primary) +``` + +Use AppText instead of Text widget to utilise typography. +```dart +AppText.medium( + text:context.t.login, + color: context.colorScheme.primary, +), +``` + +Same way, You can use TextStyles using context.textTheme. + +```dart +RichText( + text: TextSpan( + text: context.t.login, + Style: context.textTheme.medium, + ) +) +``` +# Effective Dart Rules + +### Naming Conventions +1. Use terms consistently throughout your code. +2. Name types using `UpperCamelCase` (classes, enums, typedefs, type parameters). +3. Name extensions using `UpperCamelCase`. +4. Name packages, directories, and source files using `lowercase_with_underscores`. +5. Name import prefixes using `lowercase_with_underscores`. +6. Name other identifiers using `lowerCamelCase` (variables, parameters, named parameters). +7. Capitalize acronyms and abbreviations longer than two letters like words. +8. Avoid abbreviations unless the abbreviation is more common than the unabbreviated term. +9. Prefer putting the most descriptive noun last in names. +10. Prefer a noun phrase for non-boolean properties or variables. + +### Architecture +1. Separate your features into three layers: Presentation, Business Logic, and Data. +2. The Data Layer is responsible for retrieving and manipulating data from sources such as databases or network requests. +3. Structure the Data Layer into repositories (wrappers around data providers) and data providers (perform CRUD operations). +4. The Business Logic Layer responds to input from the presentation layer and communicates with repositories to build new states. +5. The Presentation Layer renders UI based on bloc states and handles user input and lifecycle events. +6. Inject repositories into blocs via constructors; blocs should not directly access data providers. +7. Avoid direct bloc-to-bloc communication to prevent tight coupling. +8. To coordinate between blocs, use BlocListener in the presentation layer to listen to one bloc and add events to another. +9. For shared data, inject the same repository into multiple blocs; let each bloc listen to repository streams independently. +10. Always strive for loose coupling between architectural layers and components. +11. Structure your project consistently and intentionally; there is no single right way. +12. Follow repository pattern with abstract interfaces (IAuthRepository) and concrete implementations +13. Use TaskEither from fpdart for functional error handling instead of try-catch blocks +14. Implement mapping functions that separate API calls from response processing +15. Chain operations using .chainEither() and .flatMap() for clean functional composition +16. Always use RepositoryUtils.checkStatusCode for status validation and RepositoryUtils.mapToModel for JSON parsing + +### Types and Functions +1. Use class modifiers to control if your class can be extended or used as an interface. +2. Type annotate fields and top-level variables if the type isn't obvious. +3. Annotate return types on function declarations. +4. Annotate parameter types on function declarations. +5. Use `Future` as the return type of asynchronous members that do not produce values. +6. Use getters for operations that conceptually access properties. +7. Use setters for operations that conceptually change properties. +8. Use inclusive start and exclusive end parameters to accept a range. + +### Style and Structure +1. Prefer `final` over `var` when variable values won't change. +2. Use `const` for compile-time constants. +3. Keep files focused on a single responsibility. +4. Limit file length to maintain readability. +5. Group related functionality together. +6. Prefer making declarations private. + +### Imports & Files +1. Don't import libraries inside the `src` directory of another package. +2. Prefer relative import paths within a package. +3. Don't use `/lib/` or `../` in import paths. +4. Consider writing a library-level doc comment for library files. + +### Usage +1. Use strings in `part of` directives. +2. Use adjacent strings to concatenate string literals. +3. Use collection literals when possible. +4. Use `whereType()` to filter a collection by type. +5. Test for `Future` when disambiguating a `FutureOr` whose type argument could be `Object`. +6. Initialize fields at their declaration when possible. +7. Use initializing formals when possible. +8. Use `;` instead of `{}` for empty constructor bodies. +9. Use `rethrow` to rethrow a caught exception. +10. Override `hashCode` if you override `==`. +11. Make your `==` operator obey the mathematical rules of equality. + +### Documentation +1. Use `///` doc comments to document members and types; don't use block comments for documentation. +2. Prefer writing doc comments for public APIs. +3. Start doc comments with a single-sentence summary. +4. Use square brackets in doc comments to refer to in-scope identifiers. +### Flutter Best Practices +1. Extract reusable widgets into separate components. +2. Use `StatelessWidget` when possible. +3. Keep build methods simple and focused. +4. Avoid unnecessary `StatefulWidget`s. +5. Keep state as local as possible. +6. Use `const` constructors when possible. +7. Avoid expensive operations in build methods. +8. Implement pagination for large lists. + +### Dart 3: Records +1. Records are anonymous, immutable, aggregate types that bundle multiple objects into a single value. +2. Records are fixed-sized, heterogeneous, and strongly typed. Each field can have a different type. +3. Record expressions use parentheses with comma-delimited positional and/or named fields, e.g. `('first', a: 2, b: true, 'last')`. +4. Record fields are accessed via built-in getters: positional fields as `$1`, `$2`, etc., and named fields by their name (e.g., `.a`). +5. Records are immutable: fields do not have setters. +6. Use records for functions that return multiple values; destructure with pattern matching: `var (name, age) = userInfo(json);` +7. Use type aliases (`typedef`) for record types to improve readability and maintainability. +8. Records are best for simple, immutable data aggregation; use classes for abstraction, encapsulation, and behavior. + +### Dart 3: Patterns +1. Patterns represent the shape of values for matching and destructuring. +2. Pattern matching checks if a value has a certain shape, constant, equality, or type. +3. Pattern destructuring allows extracting parts of a matched value and binding them to variables. +4. Use wildcard patterns (`_`) to ignore parts of a matched value. +5. Use rest elements (`...`, `...rest`) in list patterns to match arbitrary-length lists. +6. Use logical-or patterns (e.g., `case a || b`) to match multiple alternatives in a single case. +7. Add guard clauses (`when`) to further constrain when a case matches. +8. Use the `sealed` modifier on a class to enable exhaustiveness checking when switching over its subtypes. + +### Common Flutter Errors +1. If you get a "RenderFlex overflowed" error, check if a `Row` or `Column` contains unconstrained widgets. Fix by wrapping children in `Flexible`, `Expanded`, or by setting constraints. +2. If you get "Vertical viewport was given unbounded height", ensure `ListView` or similar scrollable widgets inside a `Column` have a bounded height. +3. If you get "An InputDecorator...cannot have an unbounded width", constrain the width of widgets like `TextField`. +4. If you get a "setState called during build" error, do not call `setState` or `showDialog` directly inside the build method. +5. If you get "The ScrollController is attached to multiple scroll views", make sure each `ScrollController` is only attached to a single scrollable widget. +6. If you get a "RenderBox was not laid out" error, check for missing or unbounded constraints in your widget tree. +7. Use the Flutter Inspector and review widget constraints to debug layout issues. + +### Base Module Location +All features must be created within the `lib/modules` directory of the `app_core` package: + +``` +lib +└── modules + └── [feature_name] +``` + +### Feature Folder Structure +Each feature follows a consistent 4-folder architecture: + +``` +lib +└── modules + ├── [feature_name] + │ ├── bloc/ + │ │ ├── [feature]_event.dart + │ │ ├── [feature]_state.dart + │ │ └── [feature]_bloc.dart + │ ├── model/ + │ │ └── [feature]_model.dart + │ ├── repository/ + │ │ └── [feature]_repository.dart + │ └── screen/ + │ └── [feature]_screen.dart +``` + +## Folder Responsibilities + +| Folder | Purpose | Contains | +|--------|---------|----------| +| **bloc** 🧱 | State Management | BLoC, Event, and State classes for the feature | +| **model** 🏪 | Data Models | Dart model classes for JSON serialization/deserialization | +| **repository** 🪣 | API Integration | Functions for API calls and data manipulation | +| **screen** 📲 | User Interface | UI components and screens for the feature | + +## Repository Layer Implementation + +### Core Pattern: TaskEither Approach +All API integrations use `TaskEither` pattern from `fp_dart` for functional error handling. + +### Abstract Interface Structure +```dart +abstract interface class I[Feature]Repository { + /// Returns TaskEither where: + /// - Task: Indicates Future operation + /// - Either: Success (T) or Failure handling + TaskEither> fetch[Data](); +} +``` + +### Implementation Steps + +#### 1. HTTP Request Layer 🎁 +```dart +class [Feature]Repository implements I[Feature]Repository { + @override + TaskEither> fetch[Data]() => + mappingRequest('[endpoint]'); + + TaskEither make[Operation]Request(String url) { + return ApiClient.request( + path: url, + queryParameters: {'_limit': 10}, + requestType: RequestType.get, + ); + } +} +``` + +#### 2. Response Validation ✔️ +```dart +TaskEither> mappingRequest(String url) => + make[Operation]Request(url) + .chainEither(checkStatusCode); + +Either checkStatusCode(Response response) => + Either.fromPredicate( + response, + (response) => response.statusCode == 200 || response.statusCode == 304, + (error) => APIFailure(error: error), + ); +``` + +#### 3. JSON Decoding 🔁 +```dart +TaskEither> mappingRequest(String url) => + make[Operation]Request(url) + .chainEither(checkStatusCode) + .chainEither(mapToList); + +Either>> mapToList(Response response) { + return Either>>.safeCast( + response.data, + (error) => ModelConversionFailure(error: error), + ); +} +``` + +#### 4. Model Conversion ✅ +```dart +TaskEither> mappingRequest(String url) => + make[Operation]Request(url) + .chainEither(checkStatusCode) + .chainEither(mapToList) + .flatMap(mapToModel); + +TaskEither> mapToModel( + List> responseList +) => TaskEither>.tryCatch( + () async => responseList.map([Feature]Model.fromJson).toList(), + (error, stackTrace) => ModelConversionFailure( + error: error, + stackTrace: stackTrace, + ), +); +``` + +## Development Guidelines + +### Naming Conventions +- **Feature Names**: Use descriptive, lowercase names (auth, home, profile, settings) +- **File Names**: Follow pattern `[feature]_[type].dart` +- **Class Names**: Use PascalCase with feature prefix (`HomeRepository`, `HomeBloc`) +- **Method Names**: Use camelCase with descriptive verbs (`fetchPosts`, `updateProfile`) + +### Error Handling Strategy +- **Consistent Failures**: Use standardized `Failure` classes + - `APIFailure`: For HTTP/network errors + - `ModelConversionFailure`: For JSON parsing errors +- **Functional Approach**: Chain operations using `TaskEither` +- **No Exceptions**: Handle all errors through `Either` types + +### API Integration Patterns +1. **Abstract Interface**: Define contract with abstract interface class +2. **Implementation**: Implement interface in concrete repository class +3. **Function Chaining**: Use `.chainEither()` and `.flatMap()` for sequential operations +4. **Error Propagation**: Let `TaskEither` handle error propagation automatically + +### BLoC Integration +- Repository layer feeds directly into BLoC layer +- BLoC handles UI state management +- Repository focuses purely on data operations +- Maintain separation of concerns between layers + +## Best Practices + +### Code Organization +- Keep abstract interface and implementation in same file for discoverability +- Create separate functions for each operation step +- Use descriptive function names that indicate their purpose +- Maintain consistent error handling patterns across all repositories + +### Performance Considerations +- Leverage `TaskEither` for lazy evaluation +- Chain operations efficiently to avoid nested callbacks +- Use appropriate query parameters for data limiting +- Implement proper caching strategies in API client layer + +### Testing Strategy +- Mock abstract interfaces for unit testing +- Test each step of the repository chain individually +- Verify error handling for all failure scenarios +- Ensure proper model conversion testing + +## Example Feature Names +- `auth` - Authentication and authorization +- `home` - Home screen and dashboard +- `profile` - User profile management +- `settings` - Application settings +- `notifications` - Push notifications +- `search` - Search functionality +- `chat` - Messaging features From a00086931123dedd28240faeff86814998d7a916 Mon Sep 17 00:00:00 2001 From: Avni Prajapati Date: Tue, 24 Jun 2025 11:56:18 +0530 Subject: [PATCH 08/31] Updated firebase_remote_config_service.dart - Added try-catch to handle error in Remote Config initialisation. - Added 'force_update' key and assigned JSON value to it while set up initial default values, to match Firebase's variable configuration. --- .../firebase_remote_config_service.dart | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/app_core/lib/core/data/services/firebase_remote_config_service.dart b/apps/app_core/lib/core/data/services/firebase_remote_config_service.dart index ca739a0..d825b53 100644 --- a/apps/app_core/lib/core/data/services/firebase_remote_config_service.dart +++ b/apps/app_core/lib/core/data/services/firebase_remote_config_service.dart @@ -1,4 +1,5 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:app_core/app/helpers/logger_helper.dart'; ///In your firebase console, you can set the default values for your app. ///The json will looks like this: @@ -24,25 +25,29 @@ class FirebaseRemoteConfigService { Future _setDefaults() async { await _remoteConfig.setDefaults(const { - 'android': ''' - { - "version": "1.0.0", - "allow_cancel": true - } - ''', - 'ios': ''' - { - "version": "1.0.0", - "allow_cancel": true - } - ''', + 'force_update': ''' + { + "android": { + "version": "1.0.0", + "allow_cancel": true + }, + "ios": { + "version": "1.0.0", + "allow_cancel": true + } + } + ''', }); } Future initialize() async { - await _setConfigSettings(); - await _setDefaults(); - await fetchAndActivate(); + try { + await _setConfigSettings(); + await _setDefaults(); + await fetchAndActivate(); + } catch (e) { + flog('Error initializing Firebase Remote Config: $e'); + } } Future fetchAndActivate() async { From 99158bd84823dc6c0f3909f0236f138e3dbce964 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 16:49:37 +0530 Subject: [PATCH 09/31] feat: Implemented forgot password and verify OTP functionality This commit introduces the following changes: - Added a forgot password feature, allowing users to reset their passwords. - Implemented a verify OTP (One-Time Password) screen for enhanced security. - Updated the authentication repository to include methods for forgot password and OTP verification. - Added relevant translations for the new features. - Integrated the forgot password flow into the sign-in screen. - Updated API endpoints for forgot password and OTP verification. - Added necessary dependencies, including `pinput` for OTP input. --- .../lib/app/config/api_endpoints.dart | 2 + apps/app_core/lib/app/routes/app_router.dart | 10 +- .../auth/model/auth_request_model.dart | 18 ++ .../auth/repository/auth_repository.dart | 118 ++++----- .../auth/sign_in/screens/sign_in_screen.dart | 44 ++-- .../bloc/forgot_password_bloc.dart | 39 +++ .../bloc/forgot_password_event.dart | 21 ++ .../bloc/forgot_password_state.dart | 32 +++ .../screens/forgot_password_screen.dart | 107 ++++++++ .../verify_otp/bloc/verify_otp_bloc.dart | 91 +++++++ .../verify_otp/bloc/verify_otp_event.dart | 17 ++ .../verify_otp/bloc/verify_otp_state.dart | 52 ++++ .../verify_otp/screens/verify_otp_screen.dart | 239 ++++++++++++++++++ apps/app_core/pubspec.yaml | 2 + .../app_translations/assets/i18n/en.i18n.json | 10 +- 15 files changed, 716 insertions(+), 86 deletions(-) create mode 100644 apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart create mode 100644 apps/app_core/lib/modules/forgot_password/bloc/forgot_password_event.dart create mode 100644 apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart create mode 100644 apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart create mode 100644 apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart create mode 100644 apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart create mode 100644 apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart create mode 100644 apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart diff --git a/apps/app_core/lib/app/config/api_endpoints.dart b/apps/app_core/lib/app/config/api_endpoints.dart index c6fa26c..08bba8e 100644 --- a/apps/app_core/lib/app/config/api_endpoints.dart +++ b/apps/app_core/lib/app/config/api_endpoints.dart @@ -1,6 +1,8 @@ class ApiEndpoints { static const login = '/api/v1/login'; static const signup = '/api/register'; + static const forgotPassword = '/api/forgotPassword'; + static const verifyOTP = '/api/verifyOTP'; static const profile = '/api/users'; static const logout = '/api/users'; static const socialLogin = '/auth/socialLogin/'; diff --git a/apps/app_core/lib/app/routes/app_router.dart b/apps/app_core/lib/app/routes/app_router.dart index 5a4a0b2..98b487e 100644 --- a/apps/app_core/lib/app/routes/app_router.dart +++ b/apps/app_core/lib/app/routes/app_router.dart @@ -4,11 +4,13 @@ import 'package:app_core/modules/auth/sign_in/screens/sign_in_screen.dart'; import 'package:app_core/modules/auth/sign_up/screens/sign_up_screen.dart'; import 'package:app_core/modules/bottom_navigation_bar.dart'; import 'package:app_core/modules/change_password/screen/change_password_screen.dart'; +import 'package:app_core/modules/forgot_password/screens/forgot_password_screen.dart'; import 'package:app_core/modules/home/screen/home_screen.dart'; import 'package:app_core/modules/profile/screen/edit_profile_screen.dart'; import 'package:app_core/modules/profile/screen/profile_screen.dart'; import 'package:app_core/modules/splash/splash_screen.dart'; import 'package:app_core/modules/subscription/screen/subscription_screen.dart'; +import 'package:app_core/modules/verify_otp/screens/verify_otp_screen.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/cupertino.dart'; @@ -22,6 +24,8 @@ class AppRouter extends RootStackRouter { AutoRoute(page: SubscriptionRoute.page), AutoRoute(initial: true, page: SplashRoute.page, path: '/'), AutoRoute(page: SignInRoute.page), + AutoRoute(page: ForgotPasswordRoute.page), + AutoRoute(page: VerifyOTPRoute.page), AutoRoute( page: BottomNavigationBarRoute.page, guards: [AuthGuard()], @@ -32,11 +36,7 @@ class AppRouter extends RootStackRouter { path: 'account', children: [ AutoRoute(page: ProfileRoute.page), - AutoRoute( - page: ChangePasswordRoute.page, - path: 'change-password', - meta: const {'hideNavBar': true}, - ), + AutoRoute(page: ChangePasswordRoute.page, path: 'change-password', meta: const {'hideNavBar': true}), ], ), ], diff --git a/apps/app_core/lib/modules/auth/model/auth_request_model.dart b/apps/app_core/lib/modules/auth/model/auth_request_model.dart index 5044351..c82895b 100644 --- a/apps/app_core/lib/modules/auth/model/auth_request_model.dart +++ b/apps/app_core/lib/modules/auth/model/auth_request_model.dart @@ -10,6 +10,10 @@ class AuthRequestModel { this.oneSignalPlayerId, }); + AuthRequestModel.verifyOTP({required this.email, required this.token}); + + AuthRequestModel.forgotPassword({required this.email}); + String? email; String? name; String? password; @@ -18,6 +22,7 @@ class AuthRequestModel { String? providerId; String? providerToken; String? oneSignalPlayerId; + String? token; Map toMap() { final map = {}; @@ -28,6 +33,13 @@ class AuthRequestModel { return map; } + Map toVerifyOTPMap() { + final map = {}; + map['email'] = email; + map['token'] = token; + return map; + } + Map toSocialSignInMap() { final map = {}; map['name'] = name; @@ -40,4 +52,10 @@ class AuthRequestModel { map['oneSignalPlayerId'] = oneSignalPlayerId; return map; } + + Map toForgotPasswordMap() { + final map = {}; + map['email'] = email; + return map; + } } diff --git a/apps/app_core/lib/modules/auth/repository/auth_repository.dart b/apps/app_core/lib/modules/auth/repository/auth_repository.dart index f4b792d..70ed223 100644 --- a/apps/app_core/lib/modules/auth/repository/auth_repository.dart +++ b/apps/app_core/lib/modules/auth/repository/auth_repository.dart @@ -19,9 +19,11 @@ abstract interface class IAuthRepository { TaskEither logout(); - TaskEither socialLogin({ - required AuthRequestModel requestModel, - }); + TaskEither forgotPassword(AuthRequestModel authRequestModel); + + TaskEither socialLogin({required AuthRequestModel requestModel}); + + TaskEither verifyOTP(AuthRequestModel authRequestModel); } // ignore: comment_references @@ -31,48 +33,28 @@ class AuthRepository implements IAuthRepository { const AuthRepository(); @override - TaskEither login( - AuthRequestModel authRequestModel, - ) => makeLoginRequest(authRequestModel) + TaskEither login(AuthRequestModel authRequestModel) => makeLoginRequest(authRequestModel) .chainEither(RepositoryUtils.checkStatusCode) .chainEither( (response) => RepositoryUtils.mapToModel(() { - return AuthResponseModel.fromMap( - response.data as Map, - ); + return AuthResponseModel.fromMap(response.data as Map); }), ) .flatMap(saveUserToLocal); - TaskEither makeLoginRequest( - AuthRequestModel authRequestModel, - ) => userApiClient.request( + TaskEither makeLoginRequest(AuthRequestModel authRequestModel) => userApiClient.request( requestType: RequestType.post, path: ApiEndpoints.login, body: authRequestModel.toMap(), - options: Options( - headers: { - 'x-api-key': 'reqres-free-v1', - 'Content-Type': 'application/json', - }, - ), + options: Options(headers: {'x-api-key': 'reqres-free-v1', 'Content-Type': 'application/json'}), ); - TaskEither saveUserToLocal( - AuthResponseModel authResponseModel, - ) => getIt().setUserData( - UserModel( - name: 'user name', - email: 'user email', - profilePicUrl: '', - id: int.parse(authResponseModel.id), - ), + TaskEither saveUserToLocal(AuthResponseModel authResponseModel) => getIt().setUserData( + UserModel(name: 'user name', email: 'user email', profilePicUrl: '', id: int.parse(authResponseModel.id)), ); @override - TaskEither signup( - AuthRequestModel authRequestModel, - ) => makeSignUpRequest(authRequestModel) + TaskEither signup(AuthRequestModel authRequestModel) => makeSignUpRequest(authRequestModel) .chainEither(RepositoryUtils.checkStatusCode) .chainEither( (r) => RepositoryUtils.mapToModel(() { @@ -82,27 +64,20 @@ class AuthRepository implements IAuthRepository { // return AuthResponseModel.fromMap( // r.data as Map, // ); - return AuthResponseModel( - email: 'eve.holt@reqres.in', - id: (r.data as Map)['id'].toString(), - ); + return AuthResponseModel(email: 'eve.holt@reqres.in', id: (r.data as Map)['id'].toString()); }), ) .flatMap(saveUserToLocal); - TaskEither makeSignUpRequest( - AuthRequestModel authRequestModel, - ) => userApiClient.request( + TaskEither makeSignUpRequest(AuthRequestModel authRequestModel) => userApiClient.request( requestType: RequestType.post, path: ApiEndpoints.signup, body: authRequestModel.toMap(), options: Options(headers: {'Content-Type': 'application/json'}), ); - TaskEither _clearHiveData() => TaskEither.tryCatch( - () => getIt().logout().run(), - (error, stackTrace) => APIFailure(), - ); + TaskEither _clearHiveData() => + TaskEither.tryCatch(() => getIt().logout().run(), (error, stackTrace) => APIFailure()); @override TaskEither logout() => makeLogoutRequest().flatMap( @@ -111,14 +86,11 @@ class AuthRepository implements IAuthRepository { }), ); - TaskEither _getNotificationId() => - TaskEither.tryCatch(() { - return getIt() - .getNotificationSubscriptionId(); - }, APIFailure.new); + TaskEither _getNotificationId() => TaskEither.tryCatch(() { + return getIt().getNotificationSubscriptionId(); + }, APIFailure.new); - TaskEither - makeLogoutRequest() => _getNotificationId().flatMap( + TaskEither makeLogoutRequest() => _getNotificationId().flatMap( (playerID) => userApiClient.request( requestType: RequestType.delete, @@ -130,26 +102,54 @@ class AuthRepository implements IAuthRepository { ); @override - TaskEither socialLogin({ - required AuthRequestModel requestModel, - }) => makeSocialLoginRequest(requestModel: requestModel) + TaskEither socialLogin({required AuthRequestModel requestModel}) => makeSocialLoginRequest( + requestModel: requestModel, + ) .chainEither(RepositoryUtils.checkStatusCode) .chainEither( - (response) => RepositoryUtils.mapToModel( - () => AuthResponseModel.fromMap( - response.data as Map, - ), - ), + (response) => + RepositoryUtils.mapToModel(() => AuthResponseModel.fromMap(response.data as Map)), ) .flatMap(saveUserToLocal); - TaskEither makeSocialLoginRequest({ - required AuthRequestModel requestModel, - }) { + TaskEither makeSocialLoginRequest({required AuthRequestModel requestModel}) { return userApiClient.request( requestType: RequestType.post, path: ApiEndpoints.socialLogin, body: requestModel.toSocialSignInMap(), ); } + + @override + TaskEither forgotPassword(AuthRequestModel authRequestModel) => makeForgotPasswordRequest(authRequestModel) + .chainEither(RepositoryUtils.checkStatusCode) + .chainEither( + (response) => RepositoryUtils.mapToModel(() { + return response.data; + }), + ) + .map((_) {}); + + TaskEither makeForgotPasswordRequest(AuthRequestModel authRequestModel) => userApiClient.request( + requestType: RequestType.post, + path: ApiEndpoints.forgotPassword, + body: authRequestModel.toForgotPasswordMap(), + options: Options(headers: {'x-api-key': 'reqres-free-v1', 'Content-Type': 'application/json'}), + ); + + @override + TaskEither verifyOTP(AuthRequestModel authRequestModel) => makeVerifyOTPRequest(authRequestModel) + .chainEither(RepositoryUtils.checkStatusCode) + .chainEither( + (response) => RepositoryUtils.mapToModel(() { + return AuthResponseModel.fromMap(response.data as Map); + }), + ); + + TaskEither makeVerifyOTPRequest(AuthRequestModel authRequestModel) => userApiClient.request( + requestType: RequestType.post, + path: ApiEndpoints.verifyOTP, + body: authRequestModel.toVerifyOTPMap(), + options: Options(headers: {'Content-Type': 'application/json'}), + ); } diff --git a/apps/app_core/lib/modules/auth/sign_in/screens/sign_in_screen.dart b/apps/app_core/lib/modules/auth/sign_in/screens/sign_in_screen.dart index 94dd557..2fd5c22 100644 --- a/apps/app_core/lib/modules/auth/sign_in/screens/sign_in_screen.dart +++ b/apps/app_core/lib/modules/auth/sign_in/screens/sign_in_screen.dart @@ -27,12 +27,7 @@ class SignInPage extends StatelessWidget implements AutoRouteWrapper { providers: [RepositoryProvider(create: (context) => const AuthRepository())], child: MultiBlocProvider( providers: [ - BlocProvider( - create: - (context) => SignInBloc( - authenticationRepository: RepositoryProvider.of(context), - ), - ), + BlocProvider(create: (context) => SignInBloc(authenticationRepository: RepositoryProvider.of(context))), ], child: this, ), @@ -61,21 +56,32 @@ class SignInPage extends StatelessWidget implements AutoRouteWrapper { children: [ VSpace.xxxxlarge80(), VSpace.large24(), - const SlideAndFadeAnimationWrapper( - delay: 100, - child: Center(child: FlutterLogo(size: 100)), - ), + const SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: FlutterLogo(size: 100))), VSpace.xxlarge40(), VSpace.large24(), - SlideAndFadeAnimationWrapper( - delay: 200, - child: AppText.XL(text: context.t.sign_in), - ), + SlideAndFadeAnimationWrapper(delay: 200, child: AppText.XL(text: context.t.sign_in)), VSpace.large24(), SlideAndFadeAnimationWrapper(delay: 300, child: _EmailInput()), VSpace.large24(), SlideAndFadeAnimationWrapper(delay: 400, child: _PasswordInput()), VSpace.large24(), + AnimatedGestureDetector( + onTap: () { + context.pushRoute(const ForgotPasswordRoute()); + }, + child: SlideAndFadeAnimationWrapper( + delay: 200, + child: Align( + alignment: Alignment.topRight, + child: AppText.regular10( + fontSize: 14, + text: context.t.forgot_password, + color: context.colorScheme.primary400, + ), + ), + ), + ), + VSpace.large24(), SlideAndFadeAnimationWrapper(delay: 400, child: _UserConsentWidget()), VSpace.xxlarge40(), const SlideAndFadeAnimationWrapper(delay: 500, child: _LoginButton()), @@ -84,8 +90,7 @@ class SignInPage extends StatelessWidget implements AutoRouteWrapper { VSpace.large24(), const SlideAndFadeAnimationWrapper(delay: 600, child: _ContinueWithGoogleButton()), VSpace.large24(), - if (Platform.isIOS) - const SlideAndFadeAnimationWrapper(delay: 600, child: _ContinueWithAppleButton()), + if (Platform.isIOS) const SlideAndFadeAnimationWrapper(delay: 600, child: _ContinueWithAppleButton()), ], ), ), @@ -125,8 +130,7 @@ class _PasswordInput extends StatelessWidget { label: context.t.password, textInputAction: TextInputAction.done, onChanged: (password) => context.read().add(SignInPasswordChanged(password)), - errorText: - state.password.displayError != null ? context.t.common_validation_password : null, + errorText: state.password.displayError != null ? context.t.common_validation_password : null, autofillHints: const [AutofillHints.password], ); }, @@ -143,9 +147,7 @@ class _UserConsentWidget extends StatelessWidget { return UserConsentWidget( value: isUserConsent, onCheckBoxValueChanged: (userConsent) { - context.read().add( - SignInUserConsentChangedEvent(userConsent: userConsent ?? false), - ); + context.read().add(SignInUserConsentChangedEvent(userConsent: userConsent ?? false)); }, onTermsAndConditionTap: () => launchUrl( diff --git a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart new file mode 100644 index 0000000..8885c21 --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:app_core/core/domain/validators/login_validators.dart'; +import 'package:app_core/modules/auth/model/auth_request_model.dart'; +import 'package:app_core/modules/auth/repository/auth_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; +import 'package:fpdart/fpdart.dart'; + +part 'forgot_password_event.dart'; +part 'forgot_password_state.dart'; + +class ForgotPasswordBloc extends Bloc { + ForgotPasswordBloc({required IAuthRepository authenticationRepository}) + : _authenticationRepository = authenticationRepository, + super(const ForgotPasswordState()) { + on(_onEmailChanged); + on(_onSubmitted); + } + + final IAuthRepository _authenticationRepository; + + void _onEmailChanged(ForgotPasswordEmailChanged event, Emitter emit) { + final email = EmailValidator.dirty(event.email); + emit(state.copyWith(email: email, isValid: Formz.validate([email]))); + } + + Future _onSubmitted(ForgotPasswordSubmitted event, Emitter emit) async { + final email = EmailValidator.dirty(state.email.value); + emit(state.copyWith(email: email, isValid: Formz.validate([email]))); + if (state.isValid) { + emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); + await _authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value.trim())).run(); + emit(state.copyWith(status: FormzSubmissionStatus.success)); + } + return unit; + } +} diff --git a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_event.dart b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_event.dart new file mode 100644 index 0000000..8242159 --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_event.dart @@ -0,0 +1,21 @@ +part of 'forgot_password_bloc.dart'; + +sealed class ForgotPasswordEvent extends Equatable { + const ForgotPasswordEvent(); + + @override + List get props => []; +} + +final class ForgotPasswordEmailChanged extends ForgotPasswordEvent { + const ForgotPasswordEmailChanged(this.email); + + final String email; + + @override + List get props => [email]; +} + +final class ForgotPasswordSubmitted extends ForgotPasswordEvent { + const ForgotPasswordSubmitted(); +} diff --git a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart new file mode 100644 index 0000000..705d78c --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart @@ -0,0 +1,32 @@ +part of 'forgot_password_bloc.dart'; + +final class ForgotPasswordState extends Equatable { + const ForgotPasswordState({ + this.status = FormzSubmissionStatus.initial, + this.email = const EmailValidator.pure(), + this.isValid = false, + this.errorMessage = '', + }); + + ForgotPasswordState copyWith({ + EmailValidator? email, + bool? isValid, + FormzSubmissionStatus? status, + String? errorMessage, + }) { + return ForgotPasswordState( + email: email ?? this.email, + isValid: isValid ?? this.isValid, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + final FormzSubmissionStatus status; + final EmailValidator email; + final bool isValid; + final String errorMessage; + + @override + List get props => [status, email, isValid]; +} diff --git a/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart new file mode 100644 index 0000000..d891668 --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart @@ -0,0 +1,107 @@ +import 'package:app_core/app/routes/app_router.dart'; +import 'package:app_core/core/presentation/widgets/app_snackbar.dart'; +import 'package:app_core/modules/auth/repository/auth_repository.dart'; +import 'package:app_core/modules/forgot_password/bloc/forgot_password_bloc.dart'; +import 'package:app_translations/app_translations.dart'; +import 'package:app_ui/app_ui.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; + +@RoutePage() +class ForgotPasswordPage extends StatelessWidget implements AutoRouteWrapper { + const ForgotPasswordPage({super.key}); + + @override + Widget wrappedRoute(BuildContext context) { + return RepositoryProvider( + create: (context) => const AuthRepository(), + child: BlocProvider( + create: (context) => ForgotPasswordBloc(authenticationRepository: RepositoryProvider.of(context)), + child: this, + ), + ); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + body: BlocListener( + listenWhen: (previous, current) => previous.status != current.status, + listener: (context, state) async { + if (state.status.isFailure) { + showAppSnackbar(context, state.errorMessage); + } else if (state.status.isSuccess) { + showAppSnackbar(context, context.t.reset_password_mail_sent); + await context.pushRoute(VerifyOTPRoute(emailAddress: state.email.value)); + } + }, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: Insets.large24), + child: AutofillGroup( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + VSpace.xxxlarge66(), + SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), + VSpace.large24(), + SlideAndFadeAnimationWrapper( + delay: 200, + child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + ), + VSpace.large24(), + SlideAndFadeAnimationWrapper(delay: 300, child: _EmailInput()), + VSpace.xxlarge40(), + const SlideAndFadeAnimationWrapper(delay: 500, child: _ForgotPasswordButton()), + VSpace.large24(), + ], + ), + ), + ), + ), + ); + } +} + +class _EmailInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.email != current.email, + builder: (context, state) { + return AppTextField( + textInputAction: TextInputAction.done, + initialValue: state.email.value, + label: context.t.email, + keyboardType: TextInputType.emailAddress, + onChanged: (email) => context.read().add(ForgotPasswordEmailChanged(email)), + errorText: state.email.displayError != null ? context.t.common_validation_email : null, + autofillHints: const [AutofillHints.email], + ); + }, + ); + } +} + +class _ForgotPasswordButton extends StatelessWidget { + const _ForgotPasswordButton(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AppButton( + isLoading: state.status.isInProgress, + text: context.t.reset_password, + onPressed: () { + TextInput.finishAutofillContext(); + context.read().add(const ForgotPasswordSubmitted()); + }, + isExpanded: true, + ); + }, + ); + } +} diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart new file mode 100644 index 0000000..c8c6726 --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:api_client/api_client.dart'; +import 'package:app_core/core/domain/validators/login_validators.dart'; +import 'package:app_core/modules/auth/model/auth_request_model.dart'; +import 'package:app_core/modules/auth/repository/auth_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fpdart/fpdart.dart'; + +part 'verify_otp_event.dart'; +part 'verify_otp_state.dart'; + +class VerifyOTPBloc extends Bloc { + VerifyOTPBloc(this.authenticationRepository, this.emailAddress) : super(const VerifyOTPState()) { + on(_onVerifyButtonPressed); + on(_onVerifyOTPChanged); + on(_onResendEmail); + } + + final AuthRepository authenticationRepository; + final String emailAddress; + + Future _onVerifyButtonPressed(VerifyButtonPressed event, Emitter emit) async { + emit(state.copyWith(statusForVerifyOTP: ApiStatus.loading, statusForResendOTP: ApiStatus.initial)); + final verifyOTPEither = + await authenticationRepository.verifyOTP(AuthRequestModel.verifyOTP(email: emailAddress, token: state.otp)).run(); + + verifyOTPEither.fold( + (failure) { + emit( + state.copyWith( + statusForVerifyOTP: ApiStatus.error, + statusForResendOTP: ApiStatus.initial, + errorMessage: failure.message, + ), + ); + }, + (success) { + emit(state.copyWith(statusForVerifyOTP: ApiStatus.loaded, statusForResendOTP: ApiStatus.initial)); + }, + ); + return unit; + } + + Future _onVerifyOTPChanged(VerifyOTPChanged event, Emitter emit) async { + if (event.otp.length == 6) { + emit( + state.copyWith( + otpIsValid: true, + otp: event.otp, + statusForVerifyOTP: ApiStatus.initial, + statusForResendOTP: ApiStatus.initial, + ), + ); + } else { + emit( + state.copyWith( + otpIsValid: false, + otp: event.otp, + statusForVerifyOTP: ApiStatus.initial, + statusForResendOTP: ApiStatus.initial, + ), + ); + } + return unit; + } + + Future _onResendEmail(ResendEmailEvent event, Emitter emit) async { + emit( + state.copyWith(statusForVerifyOTP: ApiStatus.initial, statusForResendOTP: ApiStatus.loading, otp: '', otpIsValid: false), + ); + final response = await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: emailAddress)).run(); + + response.fold( + (failure) { + emit( + state.copyWith( + statusForResendOTP: ApiStatus.error, + statusForVerifyOTP: ApiStatus.initial, + errorMessage: failure.message, + ), + ); + }, + (success) { + emit(state.copyWith(statusForVerifyOTP: ApiStatus.initial, statusForResendOTP: ApiStatus.loaded)); + }, + ); + return unit; + } +} diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart new file mode 100644 index 0000000..3d9329c --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -0,0 +1,17 @@ +part of 'verify_otp_bloc.dart'; + +abstract class VerifyOTPEvent {} + +class VerifyOTPChanged extends VerifyOTPEvent { + VerifyOTPChanged(this.otp); + final String otp; +} + +class EmailAddressChanged extends VerifyOTPEvent { + EmailAddressChanged(this.email); + final String email; +} + +class VerifyButtonPressed extends VerifyOTPEvent {} + +class ResendEmailEvent extends VerifyOTPEvent {} diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart new file mode 100644 index 0000000..b9913a9 --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -0,0 +1,52 @@ +part of 'verify_otp_bloc.dart'; + +final class VerifyOTPState extends Equatable { + const VerifyOTPState({ + this.statusForResendOTP = ApiStatus.initial, + this.statusForVerifyOTP = ApiStatus.initial, + this.email = const EmailValidator.pure(), + this.isValid = false, + this.errorMessage = '', + this.otp = '', + this.otpIsValid = false, + }); + + VerifyOTPState copyWith({ + EmailValidator? email, + bool? otpIsValid, + bool? isValid, + ApiStatus? statusForResendOTP, + ApiStatus? statusForVerifyOTP, + String? errorMessage, + String? otp, + }) { + return VerifyOTPState( + email: email ?? this.email, + otpIsValid: otpIsValid ?? this.otpIsValid, + isValid: isValid ?? this.isValid, + statusForResendOTP: statusForResendOTP ?? this.statusForResendOTP, + statusForVerifyOTP: statusForVerifyOTP ?? this.statusForVerifyOTP, + errorMessage: errorMessage ?? '', + otp: otp ?? this.otp, + ); + } + + final ApiStatus statusForResendOTP; + final ApiStatus statusForVerifyOTP; + final EmailValidator email; + final bool otpIsValid; + final bool isValid; + final String errorMessage; + final String otp; + + @override + List get props => [ + statusForResendOTP, + email, + otp, + otpIsValid, + isValid, + errorMessage, + statusForVerifyOTP, + ]; +} diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart new file mode 100644 index 0000000..1dd7f78 --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -0,0 +1,239 @@ +import 'dart:async'; + +import 'package:api_client/api_client.dart'; +import 'package:app_core/app/routes/app_router.dart'; +import 'package:app_core/core/presentation/widgets/app_snackbar.dart'; +import 'package:app_core/modules/auth/repository/auth_repository.dart'; +import 'package:app_core/modules/verify_otp/bloc/verify_otp_bloc.dart'; +import 'package:app_translations/app_translations.dart'; +import 'package:app_ui/app_ui.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinput/pinput.dart'; + +@RoutePage() +class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { + const VerifyOTPScreen({super.key, this.emailAddress}); + + final String? emailAddress; + + @override + State createState() => _VerifyOTPScreenState(); + + @override + Widget wrappedRoute(BuildContext context) { + return RepositoryProvider( + create: (context) => const AuthRepository(), + child: BlocProvider( + lazy: false, + create: (context) => VerifyOTPBloc(RepositoryProvider.of(context), emailAddress ?? ''), + child: this, + ), + ); + } +} + +class _VerifyOTPScreenState extends State with TickerProviderStateMixin { + late final TextEditingController pinController; + late final FocusNode focusNode; + late final GlobalKey formKey; + + Timer? _timer; + int _secondsRemaining = 30; + bool _isTimerRunning = true; + + @override + void dispose() { + _timer?.cancel(); + pinController.dispose(); + focusNode.dispose(); + super.dispose(); + } + + @override + void initState() { + formKey = GlobalKey(); + pinController = TextEditingController(); + focusNode = FocusNode(); + _startTimer(); + super.initState(); + } + + void _startTimer() { + setState(() { + _secondsRemaining = 30; + _isTimerRunning = true; + }); + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_secondsRemaining > 0) { + setState(() { + _secondsRemaining--; + }); + } else { + setState(() { + _isTimerRunning = false; + }); + _timer?.cancel(); + } + }); + } + + void _onResendOTP(BuildContext context) { + final email = widget.emailAddress; + if (email == null || email.isEmpty) { + showAppSnackbar(context, 'Email address is missing. Cannot resend OTP.'); + return; + } + debugPrint('Resending OTP to email: $email'); + + pinController.clear(); + FocusScope.of(context).unfocus(); + context.read().add(ResendEmailEvent()); + _startTimer(); + } + + void _onVerifyOTP(BuildContext contextBuild, VerifyOTPState state) { + TextInput.finishAutofillContext(); + FocusScope.of(context).unfocus(); + // Static check for OTP + if (state.otp == '222222') { + pinController.clear(); + showAppSnackbar(contextBuild, 'OTP verified successfully!'); + contextBuild.maybePop(); + if (mounted) { + contextBuild.pushRoute(const ChangePasswordRoute()); + } + } else { + showAppSnackbar(contextBuild, 'Invalid OTP'); + } + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: CustomAppBar( + backgroundColor: context.colorScheme.white, + automaticallyImplyLeading: true, + title: context.t.verify_otp, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(Insets.small12), + child: BlocConsumer( + listener: (BuildContext context, VerifyOTPState state) { + if (state.statusForResendOTP == ApiStatus.error || state.statusForVerifyOTP == ApiStatus.error) { + final errorMessage = state.errorMessage; + pinController.clear(); + showAppSnackbar(context, errorMessage); + } + if (state.statusForResendOTP == ApiStatus.loaded) { + showAppSnackbar(context, context.t.otp_send_to_email); + } + // Remove API success navigation, handled in static check + }, + builder: (context, state) { + return SingleChildScrollView( + child: Column( + children: [ + VSpace.large24(), + SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), + VSpace.large24(), + SlideAndFadeAnimationWrapper( + delay: 200, + child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + ), + VSpace.large24(), + AppTextField( + initialValue: widget.emailAddress, + label: context.t.email, + textInputAction: TextInputAction.done, + keyboardType: TextInputType.emailAddress, + autofillHints: const [AutofillHints.email], + ), + VSpace.medium16(), + Padding(padding: const EdgeInsets.all(Insets.small12), child: AppText.sSemiBold(text: context.t.enter_otp)), + VSpace.small12(), + BlocBuilder( + builder: + (context, state) => Pinput( + length: 6, + controller: pinController, + focusNode: focusNode, + separatorBuilder: (index) => HSpace.xxsmall4(), + validator: (value) { + return value == '222222' ? null : 'Pin is incorrect'; + }, + onCompleted: (pin) { + debugPrint('onCompleted: $pin'); + }, + onChanged: (value) { + context.read().add(VerifyOTPChanged(value)); + }, + ), + ), + VSpace.xsmall8(), + if (_isTimerRunning) + AppText( + text: '00:${_secondsRemaining.toString().padLeft(2, '0')}', + style: context.textTheme?.sSemiBold.copyWith(color: context.colorScheme.primary400), + ), + VSpace.small12(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppText( + text: "Didn't receive the verification OTP?", + style: context.textTheme?.xsRegular.copyWith(color: context.colorScheme.black), + ), + AppButton( + text: context.t.resend_otp, + buttonType: ButtonType.text, + textColor: context.colorScheme.primary400, + onPressed: _isTimerRunning ? null : () => _onResendOTP(context), + ), + const SizedBox(width: 8), + ], + ), + VSpace.large24(), + BlocBuilder( + builder: + (contextBuild, state) => Visibility( + visible: state.otpIsValid, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Insets.large24), + child: AppButton( + isExpanded: true, + text: context.t.verify_otp, + isLoading: state.statusForVerifyOTP == ApiStatus.loading, + onPressed: () => _onVerifyOTP(contextBuild, state), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ), + ); + } +} + +class Countdown extends AnimatedWidget { + Countdown({super.key, this.animation}) : super(listenable: animation!); + final Animation? animation; + + @override + Widget build(BuildContext context) { + final clockTimer = Duration(seconds: animation!.value); + + final timerText = '${clockTimer.inMinutes.remainder(60)}:${clockTimer.inSeconds.remainder(60).toString().padLeft(2, '0')}'; + + return AppText(text: timerText); + } +} diff --git a/apps/app_core/pubspec.yaml b/apps/app_core/pubspec.yaml index 622508f..436777a 100644 --- a/apps/app_core/pubspec.yaml +++ b/apps/app_core/pubspec.yaml @@ -121,6 +121,8 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 + pinput: ^5.0.1 + flutter_gen: output: lib/gen/ line_length: 80 diff --git a/packages/app_translations/assets/i18n/en.i18n.json b/packages/app_translations/assets/i18n/en.i18n.json index 83a146e..5d60b87 100644 --- a/packages/app_translations/assets/i18n/en.i18n.json +++ b/packages/app_translations/assets/i18n/en.i18n.json @@ -54,5 +54,13 @@ "terms_and_condition" : "Terms and Condition", "privacy_policy" : "Privacy Policy", "and": "and", - "please_accept_terms" : "Please accept the Terms & Conditions and Privacy Policy to continue." + "please_accept_terms" : "Please accept the Terms & Conditions and Privacy Policy to continue.", + "reset_password_mail_sent": "Reset password mail sent", + "welcome": "Welcome", + "reset_password": "Reset Password", + "go_back": "Go Back", + "enter_otp": "Enter OTP", + "verify_otp": "Verify OTP", + "resend_otp": "Resend OTP", + "otp_send_to_email": "OTP sent to your email" } \ No newline at end of file From ce06d46e1aff80a95617b439f4adf919959fc8c1 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 17:21:33 +0530 Subject: [PATCH 10/31] Fix: Moved pinput dependency from dev_dependencies to dependencies - Updated ForgotPasswordBloc to handle success and failure cases. - Included errorMessage in ForgotPasswordState props. - Added static OTP validation in VerifyOTPScreen. - Updated VerifyOTPState and VerifyOTPEvent to include props for better state management and event handling. - Removed skeletonizer dependency override from widgetbook. --- .../bloc/forgot_password_bloc.dart | 8 ++++-- .../bloc/forgot_password_state.dart | 9 ++----- .../verify_otp/bloc/verify_otp_event.dart | 26 ++++++++++++++----- .../verify_otp/bloc/verify_otp_state.dart | 12 ++------- .../verify_otp/screens/verify_otp_screen.dart | 1 + apps/app_core/pubspec.yaml | 3 ++- 6 files changed, 33 insertions(+), 26 deletions(-) diff --git a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart index 8885c21..5e119e3 100644 --- a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart @@ -31,8 +31,12 @@ class ForgotPasswordBloc extends Bloc emit(state.copyWith(email: email, isValid: Formz.validate([email]))); if (state.isValid) { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); - await _authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value.trim())).run(); - emit(state.copyWith(status: FormzSubmissionStatus.success)); + final result = + await _authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value.trim())).run(); + result.fold( + (failure) => emit(state.copyWith(status: FormzSubmissionStatus.failure)), + (success) => emit(state.copyWith(status: FormzSubmissionStatus.success)), + ); } return unit; } diff --git a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart index 705d78c..b9d336c 100644 --- a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart @@ -8,12 +8,7 @@ final class ForgotPasswordState extends Equatable { this.errorMessage = '', }); - ForgotPasswordState copyWith({ - EmailValidator? email, - bool? isValid, - FormzSubmissionStatus? status, - String? errorMessage, - }) { + ForgotPasswordState copyWith({EmailValidator? email, bool? isValid, FormzSubmissionStatus? status, String? errorMessage}) { return ForgotPasswordState( email: email ?? this.email, isValid: isValid ?? this.isValid, @@ -28,5 +23,5 @@ final class ForgotPasswordState extends Equatable { final String errorMessage; @override - List get props => [status, email, isValid]; + List get props => [status, email, isValid, errorMessage]; } diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart index 3d9329c..77447ac 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -1,17 +1,31 @@ part of 'verify_otp_bloc.dart'; -abstract class VerifyOTPEvent {} +sealed class VerifyOTPEvent extends Equatable { + const VerifyOTPEvent(); -class VerifyOTPChanged extends VerifyOTPEvent { - VerifyOTPChanged(this.otp); + @override + List get props => []; +} + +final class VerifyOTPChanged extends VerifyOTPEvent { + const VerifyOTPChanged(this.otp); final String otp; + + @override + List get props => [otp]; } class EmailAddressChanged extends VerifyOTPEvent { - EmailAddressChanged(this.email); + const EmailAddressChanged(this.email); final String email; + @override + List get props => [email]; } -class VerifyButtonPressed extends VerifyOTPEvent {} +class VerifyButtonPressed extends VerifyOTPEvent { + const VerifyButtonPressed(); +} -class ResendEmailEvent extends VerifyOTPEvent {} +class ResendEmailEvent extends VerifyOTPEvent { + const ResendEmailEvent(); +} diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart index b9913a9..29a0b10 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -26,7 +26,7 @@ final class VerifyOTPState extends Equatable { isValid: isValid ?? this.isValid, statusForResendOTP: statusForResendOTP ?? this.statusForResendOTP, statusForVerifyOTP: statusForVerifyOTP ?? this.statusForVerifyOTP, - errorMessage: errorMessage ?? '', + errorMessage: errorMessage ?? this.errorMessage, otp: otp ?? this.otp, ); } @@ -40,13 +40,5 @@ final class VerifyOTPState extends Equatable { final String otp; @override - List get props => [ - statusForResendOTP, - email, - otp, - otpIsValid, - isValid, - errorMessage, - statusForVerifyOTP, - ]; + List get props => [statusForResendOTP, email, otp, otpIsValid, isValid, errorMessage, statusForVerifyOTP]; } diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 1dd7f78..0b98453 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -164,6 +164,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt focusNode: focusNode, separatorBuilder: (index) => HSpace.xxsmall4(), validator: (value) { + //For now we added static validation return value == '222222' ? null : 'Pin is incorrect'; }, onCompleted: (pin) { diff --git a/apps/app_core/pubspec.yaml b/apps/app_core/pubspec.yaml index 436777a..469cf1e 100644 --- a/apps/app_core/pubspec.yaml +++ b/apps/app_core/pubspec.yaml @@ -89,6 +89,7 @@ dependencies: # Launch URL url_launcher: ^6.3.1 + pinput: ^5.0.1 dependency_overrides: web: ^1.0.0 @@ -121,7 +122,7 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 - pinput: ^5.0.1 + flutter_gen: output: lib/gen/ From d67d5974b5269eb563c3e2938eb93a913521059e Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 17:21:55 +0530 Subject: [PATCH 11/31] Add const keyword for ResendEmailEvent in VerifyOTPBloc --- .../lib/modules/verify_otp/screens/verify_otp_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 0b98453..30caae5 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -91,7 +91,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt pinController.clear(); FocusScope.of(context).unfocus(); - context.read().add(ResendEmailEvent()); + context.read().add(const ResendEmailEvent()); _startTimer(); } From 3c6cfc89cdd397e775fa55cbd1104f85412827a2 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 17:56:43 +0530 Subject: [PATCH 12/31] - Added `did_not_receive_otp` key to `en.i18n.json`. --- .../lib/modules/verify_otp/screens/verify_otp_screen.dart | 2 +- packages/app_translations/assets/i18n/en.i18n.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 30caae5..b37380b 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -186,7 +186,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt mainAxisAlignment: MainAxisAlignment.center, children: [ AppText( - text: "Didn't receive the verification OTP?", + text: context.t.did_not_receive_otp, style: context.textTheme?.xsRegular.copyWith(color: context.colorScheme.black), ), AppButton( diff --git a/packages/app_translations/assets/i18n/en.i18n.json b/packages/app_translations/assets/i18n/en.i18n.json index 5d60b87..4191bcb 100644 --- a/packages/app_translations/assets/i18n/en.i18n.json +++ b/packages/app_translations/assets/i18n/en.i18n.json @@ -62,5 +62,6 @@ "enter_otp": "Enter OTP", "verify_otp": "Verify OTP", "resend_otp": "Resend OTP", - "otp_send_to_email": "OTP sent to your email" + "otp_send_to_email": "OTP sent to your email", + "did_not_receive_otp": "Didn't receive the verification OTP?" } \ No newline at end of file From 0919b04d08f76191e63ff54a8980d69bb41f9fc6 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 18:06:42 +0530 Subject: [PATCH 13/31] Refactor: Improve OTP verification and UI elements - Updated API endpoints for forgot password and verify OTP. - Set email field to read-only on the verify OTP screen. - Removed unused `Countdown` widget. - Standardized OTP length to 6 digits in `VerifyOTPBloc`. - Added `readOnly` property to `AppTextField` widget. --- .../lib/app/config/api_endpoints.dart | 4 +- .../verify_otp/bloc/verify_otp_bloc.dart | 3 +- .../verify_otp/screens/verify_otp_screen.dart | 22 +-------- .../src/widgets/molecules/app_textfield.dart | 45 ++++++++----------- 4 files changed, 24 insertions(+), 50 deletions(-) diff --git a/apps/app_core/lib/app/config/api_endpoints.dart b/apps/app_core/lib/app/config/api_endpoints.dart index 08bba8e..65305fb 100644 --- a/apps/app_core/lib/app/config/api_endpoints.dart +++ b/apps/app_core/lib/app/config/api_endpoints.dart @@ -1,8 +1,8 @@ class ApiEndpoints { static const login = '/api/v1/login'; static const signup = '/api/register'; - static const forgotPassword = '/api/forgotPassword'; - static const verifyOTP = '/api/verifyOTP'; + static const forgotPassword = '/api/v1/forgot-password'; + static const verifyOTP = '/api/v1/verify-otp'; static const profile = '/api/users'; static const logout = '/api/users'; static const socialLogin = '/auth/socialLogin/'; diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart index c8c6726..95313f2 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart @@ -43,8 +43,9 @@ class VerifyOTPBloc extends Bloc { return unit; } + static const int _otpLength = 6; Future _onVerifyOTPChanged(VerifyOTPChanged event, Emitter emit) async { - if (event.otp.length == 6) { + if (event.otp.length == _otpLength) { emit( state.copyWith( otpIsValid: true, diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index b37380b..e3ad73c 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -146,13 +146,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), ), VSpace.large24(), - AppTextField( - initialValue: widget.emailAddress, - label: context.t.email, - textInputAction: TextInputAction.done, - keyboardType: TextInputType.emailAddress, - autofillHints: const [AutofillHints.email], - ), + AppTextField(initialValue: widget.emailAddress, label: context.t.email, readOnly: true), VSpace.medium16(), Padding(padding: const EdgeInsets.all(Insets.small12), child: AppText.sSemiBold(text: context.t.enter_otp)), VSpace.small12(), @@ -224,17 +218,3 @@ class _VerifyOTPScreenState extends State with TickerProviderSt ); } } - -class Countdown extends AnimatedWidget { - Countdown({super.key, this.animation}) : super(listenable: animation!); - final Animation? animation; - - @override - Widget build(BuildContext context) { - final clockTimer = Duration(seconds: animation!.value); - - final timerText = '${clockTimer.inMinutes.remainder(60)}:${clockTimer.inSeconds.remainder(60).toString().padLeft(2, '0')}'; - - return AppText(text: timerText); - } -} diff --git a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart index f6a39ae..2516965 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart @@ -9,6 +9,7 @@ class AppTextField extends StatefulWidget { this.textInputAction = TextInputAction.next, this.showLabel = true, this.hintText, + this.readOnly, this.keyboardType, this.initialValue, this.onChanged, @@ -20,8 +21,8 @@ class AppTextField extends StatefulWidget { this.contentPadding, this.autofillHints, this.hintTextBelowTextField, - }) : isPasswordField = false, - isObscureText = false; + }) : isPasswordField = false, + isObscureText = false; const AppTextField.password({ required this.label, @@ -37,15 +38,17 @@ class AppTextField extends StatefulWidget { this.backgroundColor, this.minLines, this.focusNode, + this.readOnly, this.autofillHints, this.hintTextBelowTextField, this.contentPadding, - }) : isPasswordField = true, - isObscureText = true; + }) : isPasswordField = true, + isObscureText = true; final String label; final String? initialValue; final String? hintText; + final bool? readOnly; final String? errorText; final String? hintTextBelowTextField; final TextInputAction? textInputAction; @@ -85,10 +88,7 @@ class _AppTextFieldState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.showLabel) ...[ - AppText.xsSemiBold(text: widget.label), - VSpace.xsmall8(), - ], + if (widget.showLabel) ...[AppText.xsSemiBold(text: widget.label), VSpace.xsmall8()], TextFormField( initialValue: widget.initialValue, cursorColor: context.colorScheme.black, @@ -98,6 +98,7 @@ class _AppTextFieldState extends State { validator: widget.validator, obscureText: isObscureText, onChanged: widget.onChanged, + readOnly: widget.readOnly ?? false, autofillHints: widget.autofillHints, focusNode: widget.focusNode, decoration: InputDecoration( @@ -110,29 +111,21 @@ class _AppTextFieldState extends State { borderRadius: BorderRadius.circular(Insets.xsmall8), borderSide: BorderSide(color: context.colorScheme.primary400), ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(Insets.xsmall8), - borderSide: BorderSide.none, - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(Insets.xsmall8), borderSide: BorderSide.none), errorText: widget.errorText, - suffixIcon: widget.isPasswordField - ? IconButton( - splashColor: context.colorScheme.primary50, - onPressed: toggleObscureText, - icon: Icon( - isObscureText ? Icons.visibility_off : Icons.visibility, - color: context.colorScheme.grey700, - ), - ) - : null, + suffixIcon: + widget.isPasswordField + ? IconButton( + splashColor: context.colorScheme.primary50, + onPressed: toggleObscureText, + icon: Icon(isObscureText ? Icons.visibility_off : Icons.visibility, color: context.colorScheme.grey700), + ) + : null, ), minLines: widget.minLines, maxLines: widget.minLines ?? 0 + 1, ), - if (widget.hintTextBelowTextField != null) ...[ - VSpace.xsmall8(), - AppText.xsRegular(text: widget.hintTextBelowTextField), - ], + if (widget.hintTextBelowTextField != null) ...[VSpace.xsmall8(), AppText.xsRegular(text: widget.hintTextBelowTextField)], ], ); } From 5c7c47a3e3eab5b20672f2f6e880125635029a44 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 18:12:34 +0530 Subject: [PATCH 14/31] Fix: Added mounted checks in VerifyOTPScreen timer callbacks --- .../modules/verify_otp/screens/verify_otp_screen.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index e3ad73c..8ee6b25 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -62,6 +62,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt } void _startTimer() { + if (!mounted) return; setState(() { _secondsRemaining = 30; _isTimerRunning = true; @@ -69,10 +70,18 @@ class _VerifyOTPScreenState extends State with TickerProviderSt _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_secondsRemaining > 0) { + if (!mounted) { + timer.cancel(); + return; + } setState(() { _secondsRemaining--; }); } else { + if (!mounted) { + timer.cancel(); + return; + } setState(() { _isTimerRunning = false; }); From e155fb7d1f8b72d87fde8d89f61997c19622bad2 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 18:34:30 +0530 Subject: [PATCH 15/31] Update COUNTER Incremented the counter to 2. --- BOILERPLATE/COUNTER | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BOILERPLATE/COUNTER b/BOILERPLATE/COUNTER index a3be6c6..e72e2db 100644 --- a/BOILERPLATE/COUNTER +++ b/BOILERPLATE/COUNTER @@ -1,2 +1,2 @@ # Increment this counter to push your code again -1 \ No newline at end of file +2 \ No newline at end of file From dece4459afd161cc5ae0bf5b585ab02468af6cf2 Mon Sep 17 00:00:00 2001 From: Avni Prajapati Date: Thu, 26 Jun 2025 14:58:05 +0530 Subject: [PATCH 16/31] Updated app_textfield.dart --- .../src/widgets/molecules/app_textfield.dart | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart index 2516965..cc0bf20 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart @@ -21,6 +21,7 @@ class AppTextField extends StatefulWidget { this.contentPadding, this.autofillHints, this.hintTextBelowTextField, + this.maxLength, }) : isPasswordField = false, isObscureText = false; @@ -42,6 +43,7 @@ class AppTextField extends StatefulWidget { this.autofillHints, this.hintTextBelowTextField, this.contentPadding, + this.maxLength, }) : isPasswordField = true, isObscureText = true; @@ -63,6 +65,7 @@ class AppTextField extends StatefulWidget { final FocusNode? focusNode; final int? minLines; final EdgeInsetsGeometry? contentPadding; + final int? maxLength; @override State createState() => _AppTextFieldState(); @@ -101,31 +104,43 @@ class _AppTextFieldState extends State { readOnly: widget.readOnly ?? false, autofillHints: widget.autofillHints, focusNode: widget.focusNode, + maxLength: widget.maxLength, decoration: InputDecoration( filled: true, fillColor: widget.backgroundColor ?? context.colorScheme.grey100, hintText: widget.hintText, - contentPadding: widget.contentPadding ?? const EdgeInsets.only(left: Insets.small12), + contentPadding: + widget.contentPadding ?? + const EdgeInsets.only(left: Insets.small12, right: Insets.small12), errorMaxLines: 2, focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(Insets.xsmall8), borderSide: BorderSide(color: context.colorScheme.primary400), ), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(Insets.xsmall8), borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(Insets.xsmall8), + borderSide: BorderSide.none, + ), errorText: widget.errorText, suffixIcon: widget.isPasswordField ? IconButton( splashColor: context.colorScheme.primary50, onPressed: toggleObscureText, - icon: Icon(isObscureText ? Icons.visibility_off : Icons.visibility, color: context.colorScheme.grey700), + icon: Icon( + isObscureText ? Icons.visibility_off : Icons.visibility, + color: context.colorScheme.grey700, + ), ) : null, ), minLines: widget.minLines, maxLines: widget.minLines ?? 0 + 1, ), - if (widget.hintTextBelowTextField != null) ...[VSpace.xsmall8(), AppText.xsRegular(text: widget.hintTextBelowTextField)], + if (widget.hintTextBelowTextField != null) ...[ + VSpace.xsmall8(), + AppText.xsRegular(text: widget.hintTextBelowTextField), + ], ], ); } From 20dea338f0f2b8ffc12be90115216f568140f818 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Thu, 26 Jun 2025 19:49:00 +0530 Subject: [PATCH 17/31] Feat: Add AppTimer widget and integrate with OTP verification This commit introduces a new `AppTimer` widget and integrates it into the OTP verification screen. Key changes: - Added `AppTimer` widget to `app_ui` for displaying a countdown timer. - Integrated `AppTimer` into `VerifyOTPScreen` to show the OTP resend countdown. - Refactored `VerifyOTPScreen` to use `AppTimer` and removed manual timer logic. - Updated `VerifyOTPBloc` to use `LengthValidator` for OTP validation. - Modified navigation from `ForgotPasswordScreen` to `VerifyOTPScreen` to use `replaceRoute` instead of `pushRoute`. - Ensured OTP input is cleared and focus is removed when resending OTP. - Displayed error messages using `SnackbarType.failed` for invalid OTP. - Updated Pinput widget to show error text based on `state.otp.error`. - Allowed only digits in OTP input using `FilteringTextInputFormatter.digitsOnly`. --- .../screens/forgot_password_screen.dart | 2 +- .../verify_otp/bloc/verify_otp_bloc.dart | 33 +++------ .../verify_otp/bloc/verify_otp_state.dart | 25 +++---- .../verify_otp/screens/verify_otp_screen.dart | 42 +++--------- .../lib/src/widgets/molecules/app_timer.dart | 67 +++++++++++++++++++ .../lib/src/widgets/molecules/molecules.dart | 1 + 6 files changed, 98 insertions(+), 72 deletions(-) create mode 100644 packages/app_ui/lib/src/widgets/molecules/app_timer.dart diff --git a/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart index d891668..88d91f8 100644 --- a/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart +++ b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart @@ -35,7 +35,7 @@ class ForgotPasswordPage extends StatelessWidget implements AutoRouteWrapper { showAppSnackbar(context, state.errorMessage); } else if (state.status.isSuccess) { showAppSnackbar(context, context.t.reset_password_mail_sent); - await context.pushRoute(VerifyOTPRoute(emailAddress: state.email.value)); + await context.replaceRoute(VerifyOTPRoute(emailAddress: state.email.value)); } }, child: SingleChildScrollView( diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart index 95313f2..a63531c 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart @@ -1,15 +1,15 @@ import 'dart:async'; import 'package:api_client/api_client.dart'; -import 'package:app_core/core/domain/validators/login_validators.dart'; +import 'package:app_core/core/domain/validators/length_validator.dart'; import 'package:app_core/modules/auth/model/auth_request_model.dart'; import 'package:app_core/modules/auth/repository/auth_repository.dart'; +import 'package:app_core/modules/verify_otp/bloc/verify_otp_state.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:fpdart/fpdart.dart'; part 'verify_otp_event.dart'; -part 'verify_otp_state.dart'; class VerifyOTPBloc extends Bloc { VerifyOTPBloc(this.authenticationRepository, this.emailAddress) : super(const VerifyOTPState()) { @@ -24,7 +24,7 @@ class VerifyOTPBloc extends Bloc { Future _onVerifyButtonPressed(VerifyButtonPressed event, Emitter emit) async { emit(state.copyWith(statusForVerifyOTP: ApiStatus.loading, statusForResendOTP: ApiStatus.initial)); final verifyOTPEither = - await authenticationRepository.verifyOTP(AuthRequestModel.verifyOTP(email: emailAddress, token: state.otp)).run(); + await authenticationRepository.verifyOTP(AuthRequestModel.verifyOTP(email: emailAddress, token: state.otp.value)).run(); verifyOTPEither.fold( (failure) { @@ -45,31 +45,18 @@ class VerifyOTPBloc extends Bloc { static const int _otpLength = 6; Future _onVerifyOTPChanged(VerifyOTPChanged event, Emitter emit) async { - if (event.otp.length == _otpLength) { - emit( - state.copyWith( - otpIsValid: true, - otp: event.otp, - statusForVerifyOTP: ApiStatus.initial, - statusForResendOTP: ApiStatus.initial, - ), - ); - } else { - emit( - state.copyWith( - otpIsValid: false, - otp: event.otp, - statusForVerifyOTP: ApiStatus.initial, - statusForResendOTP: ApiStatus.initial, - ), - ); - } + final otp = LengthValidator.dirty(_otpLength, event.otp); + emit(state.copyWith(otp: otp, statusForVerifyOTP: ApiStatus.initial, statusForResendOTP: ApiStatus.initial)); return unit; } Future _onResendEmail(ResendEmailEvent event, Emitter emit) async { emit( - state.copyWith(statusForVerifyOTP: ApiStatus.initial, statusForResendOTP: ApiStatus.loading, otp: '', otpIsValid: false), + state.copyWith( + statusForVerifyOTP: ApiStatus.initial, + statusForResendOTP: ApiStatus.loading, + otp: const LengthValidator.pure(_otpLength), + ), ); final response = await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: emailAddress)).run(); diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart index 29a0b10..316b1e8 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -1,44 +1,41 @@ -part of 'verify_otp_bloc.dart'; +import 'package:api_client/api_client.dart'; +import 'package:app_core/core/domain/validators/email_validator.dart'; +import 'package:app_core/core/domain/validators/length_validator.dart'; +import 'package:equatable/equatable.dart'; final class VerifyOTPState extends Equatable { const VerifyOTPState({ this.statusForResendOTP = ApiStatus.initial, this.statusForVerifyOTP = ApiStatus.initial, this.email = const EmailValidator.pure(), - this.isValid = false, this.errorMessage = '', - this.otp = '', - this.otpIsValid = false, + this.otp = const LengthValidator.pure(6), }); VerifyOTPState copyWith({ EmailValidator? email, - bool? otpIsValid, - bool? isValid, + LengthValidator? otp, ApiStatus? statusForResendOTP, ApiStatus? statusForVerifyOTP, String? errorMessage, - String? otp, }) { return VerifyOTPState( email: email ?? this.email, - otpIsValid: otpIsValid ?? this.otpIsValid, - isValid: isValid ?? this.isValid, + otp: otp ?? this.otp, statusForResendOTP: statusForResendOTP ?? this.statusForResendOTP, statusForVerifyOTP: statusForVerifyOTP ?? this.statusForVerifyOTP, errorMessage: errorMessage ?? this.errorMessage, - otp: otp ?? this.otp, ); } final ApiStatus statusForResendOTP; final ApiStatus statusForVerifyOTP; final EmailValidator email; - final bool otpIsValid; - final bool isValid; + final LengthValidator otp; final String errorMessage; - final String otp; + + bool get isValid => otp.isValid; @override - List get props => [statusForResendOTP, email, otp, otpIsValid, isValid, errorMessage, statusForVerifyOTP]; + List get props => [statusForResendOTP, email, otp, errorMessage, statusForVerifyOTP]; } diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 8ee6b25..6911967 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -5,6 +5,7 @@ import 'package:app_core/app/routes/app_router.dart'; import 'package:app_core/core/presentation/widgets/app_snackbar.dart'; import 'package:app_core/modules/auth/repository/auth_repository.dart'; import 'package:app_core/modules/verify_otp/bloc/verify_otp_bloc.dart'; +import 'package:app_core/modules/verify_otp/bloc/verify_otp_state.dart'; import 'package:app_translations/app_translations.dart'; import 'package:app_ui/app_ui.dart'; import 'package:auto_route/auto_route.dart'; @@ -36,8 +37,6 @@ class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { } class _VerifyOTPScreenState extends State with TickerProviderStateMixin { - late final TextEditingController pinController; - late final FocusNode focusNode; late final GlobalKey formKey; Timer? _timer; @@ -47,16 +46,12 @@ class _VerifyOTPScreenState extends State with TickerProviderSt @override void dispose() { _timer?.cancel(); - pinController.dispose(); - focusNode.dispose(); super.dispose(); } @override void initState() { formKey = GlobalKey(); - pinController = TextEditingController(); - focusNode = FocusNode(); _startTimer(); super.initState(); } @@ -91,14 +86,6 @@ class _VerifyOTPScreenState extends State with TickerProviderSt } void _onResendOTP(BuildContext context) { - final email = widget.emailAddress; - if (email == null || email.isEmpty) { - showAppSnackbar(context, 'Email address is missing. Cannot resend OTP.'); - return; - } - debugPrint('Resending OTP to email: $email'); - - pinController.clear(); FocusScope.of(context).unfocus(); context.read().add(const ResendEmailEvent()); _startTimer(); @@ -108,15 +95,14 @@ class _VerifyOTPScreenState extends State with TickerProviderSt TextInput.finishAutofillContext(); FocusScope.of(context).unfocus(); // Static check for OTP - if (state.otp == '222222') { - pinController.clear(); + if (state.otp.value == '222222') { showAppSnackbar(contextBuild, 'OTP verified successfully!'); contextBuild.maybePop(); if (mounted) { - contextBuild.pushRoute(const ChangePasswordRoute()); + contextBuild.replaceRoute(const ChangePasswordRoute()); } } else { - showAppSnackbar(contextBuild, 'Invalid OTP'); + showAppSnackbar(contextBuild, 'Invalid OTP', type: SnackbarType.failed); } } @@ -135,7 +121,6 @@ class _VerifyOTPScreenState extends State with TickerProviderSt listener: (BuildContext context, VerifyOTPState state) { if (state.statusForResendOTP == ApiStatus.error || state.statusForVerifyOTP == ApiStatus.error) { final errorMessage = state.errorMessage; - pinController.clear(); showAppSnackbar(context, errorMessage); } if (state.statusForResendOTP == ApiStatus.loaded) { @@ -163,27 +148,16 @@ class _VerifyOTPScreenState extends State with TickerProviderSt builder: (context, state) => Pinput( length: 6, - controller: pinController, - focusNode: focusNode, separatorBuilder: (index) => HSpace.xxsmall4(), - validator: (value) { - //For now we added static validation - return value == '222222' ? null : 'Pin is incorrect'; - }, - onCompleted: (pin) { - debugPrint('onCompleted: $pin'); - }, + errorText: state.otp.error != null ? 'Pin is incorrect' : null, onChanged: (value) { context.read().add(VerifyOTPChanged(value)); }, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), ), VSpace.xsmall8(), - if (_isTimerRunning) - AppText( - text: '00:${_secondsRemaining.toString().padLeft(2, '0')}', - style: context.textTheme?.sSemiBold.copyWith(color: context.colorScheme.primary400), - ), + if (_isTimerRunning) AppTimer(seconds: 30, onTick: (remaining) {}, onFinished: () {}), VSpace.small12(), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -205,7 +179,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt BlocBuilder( builder: (contextBuild, state) => Visibility( - visible: state.otpIsValid, + visible: state.isValid, child: Padding( padding: const EdgeInsets.symmetric(horizontal: Insets.large24), child: AppButton( diff --git a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart new file mode 100644 index 0000000..8ee7499 --- /dev/null +++ b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:app_ui/app_ui.dart'; +import 'package:flutter/material.dart'; + +class AppTimer extends StatefulWidget { + const AppTimer({required this.seconds, super.key, this.onTick, this.onFinished, this.textStyle}); + final int seconds; + final void Function(int secondsRemaining)? onTick; + final VoidCallback? onFinished; + final TextStyle? textStyle; + + @override + State createState() => _AppTimerState(); +} + +class _AppTimerState extends State with TickerProviderStateMixin { + late int _secondsRemaining; + Timer? _timer; + + @override + void initState() { + super.initState(); + _secondsRemaining = widget.seconds; + _startTimer(); + } + + @override + void didUpdateWidget(covariant AppTimer oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.seconds != widget.seconds) { + _timer?.cancel(); + _secondsRemaining = widget.seconds; + _startTimer(); + } + } + + void _startTimer() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_secondsRemaining > 0) { + setState(() { + _secondsRemaining--; + }); + if (widget.onTick != null) widget.onTick?.call(_secondsRemaining); + } else { + timer.cancel(); + if (widget.onFinished != null) widget.onFinished?.call(); + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final timerText = '00:${_secondsRemaining.toString().padLeft(2, '0')}'; + return AppText( + text: timerText, + style: widget.textStyle ?? context.textTheme?.sSemiBold.copyWith(color: context.colorScheme.primary400), + ); + } +} diff --git a/packages/app_ui/lib/src/widgets/molecules/molecules.dart b/packages/app_ui/lib/src/widgets/molecules/molecules.dart index 545f7ed..fce2c9c 100644 --- a/packages/app_ui/lib/src/widgets/molecules/molecules.dart +++ b/packages/app_ui/lib/src/widgets/molecules/molecules.dart @@ -7,6 +7,7 @@ export 'app_network_image.dart'; export 'app_profile_image.dart'; export 'app_refresh_indicator.dart'; export 'app_textfield.dart'; +export 'app_timer.dart'; export 'empty_ui.dart'; export 'no_internet_widget.dart'; export 'user_concent_widget.dart'; From afb98c83b0cd96faa18d52417992024602226b00 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 11:19:30 +0530 Subject: [PATCH 18/31] Refactor: Streamline AppTimer and Verify OTP UI - Removed unused `formKey` from `VerifyOTPScreen`. - Simplified `AppTimer` widget by removing `onTick` and `textStyle` parameters. - Updated `VerifyOTPState`'s `isValid` getter to include email validation. --- .../modules/verify_otp/bloc/verify_otp_state.dart | 2 +- .../verify_otp/screens/verify_otp_screen.dart | 5 +---- .../app_ui/lib/src/widgets/molecules/app_timer.dart | 13 ++++--------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart index 316b1e8..7c50a65 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -34,7 +34,7 @@ final class VerifyOTPState extends Equatable { final LengthValidator otp; final String errorMessage; - bool get isValid => otp.isValid; + bool get isValid => otp.isValid && email.isValid; @override List get props => [statusForResendOTP, email, otp, errorMessage, statusForVerifyOTP]; diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 6911967..53c22b2 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -37,8 +37,6 @@ class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { } class _VerifyOTPScreenState extends State with TickerProviderStateMixin { - late final GlobalKey formKey; - Timer? _timer; int _secondsRemaining = 30; bool _isTimerRunning = true; @@ -51,7 +49,6 @@ class _VerifyOTPScreenState extends State with TickerProviderSt @override void initState() { - formKey = GlobalKey(); _startTimer(); super.initState(); } @@ -157,7 +154,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt ), ), VSpace.xsmall8(), - if (_isTimerRunning) AppTimer(seconds: 30, onTick: (remaining) {}, onFinished: () {}), + if (_isTimerRunning) AppTimer(seconds: 30, onFinished: () {}), VSpace.small12(), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart index 8ee7499..88c87eb 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart @@ -4,17 +4,16 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; class AppTimer extends StatefulWidget { - const AppTimer({required this.seconds, super.key, this.onTick, this.onFinished, this.textStyle}); + const AppTimer({required this.seconds, super.key, this.onFinished}); final int seconds; - final void Function(int secondsRemaining)? onTick; + final VoidCallback? onFinished; - final TextStyle? textStyle; @override State createState() => _AppTimerState(); } -class _AppTimerState extends State with TickerProviderStateMixin { +class _AppTimerState extends State { late int _secondsRemaining; Timer? _timer; @@ -42,7 +41,6 @@ class _AppTimerState extends State with TickerProviderStateMixin { setState(() { _secondsRemaining--; }); - if (widget.onTick != null) widget.onTick?.call(_secondsRemaining); } else { timer.cancel(); if (widget.onFinished != null) widget.onFinished?.call(); @@ -59,9 +57,6 @@ class _AppTimerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { final timerText = '00:${_secondsRemaining.toString().padLeft(2, '0')}'; - return AppText( - text: timerText, - style: widget.textStyle ?? context.textTheme?.sSemiBold.copyWith(color: context.colorScheme.primary400), - ); + return AppText(text: timerText, style: context.textTheme?.sSemiBold.copyWith(color: context.colorScheme.primary400)); } } From 800c80b6e48786969bbcba9fe33c32d241865924 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 12:15:05 +0530 Subject: [PATCH 19/31] Fix: Prevent `AppTimer` from starting if initial seconds is 0 The `AppTimer` widget was modified to prevent the timer from starting if the initial `seconds` value is 0. Additionally, the `onFinished` callback is now always called when the timer finishes, even if it's null. --- packages/app_ui/lib/src/widgets/molecules/app_timer.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart index 88c87eb..0ec79fb 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart @@ -35,6 +35,7 @@ class _AppTimerState extends State { } void _startTimer() { + if (widget.seconds == 0) return; _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_secondsRemaining > 0) { @@ -43,7 +44,7 @@ class _AppTimerState extends State { }); } else { timer.cancel(); - if (widget.onFinished != null) widget.onFinished?.call(); + widget.onFinished?.call(); } }); } From f99792374e91fd5e51b3f265317ef219b4e64916 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 13:09:44 +0530 Subject: [PATCH 20/31] Fix: AppTimer displays minutes and seconds The `AppTimer` widget now correctly formats the remaining time to show both minutes and seconds (MM:SS) instead of just seconds. --- packages/app_ui/lib/src/widgets/molecules/app_timer.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart index 0ec79fb..6e8a8e4 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart @@ -57,7 +57,9 @@ class _AppTimerState extends State { @override Widget build(BuildContext context) { - final timerText = '00:${_secondsRemaining.toString().padLeft(2, '0')}'; + final minutes = _secondsRemaining ~/ 60; + final seconds = _secondsRemaining % 60; + final timerText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; return AppText(text: timerText, style: context.textTheme?.sSemiBold.copyWith(color: context.colorScheme.primary400)); } } From 2de49f7913a2bf9ed3c54b9b6a96b79709cb402f Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 14:22:39 +0530 Subject: [PATCH 21/31] Refactor: Improve OTP verification and UI - Added `pin_incorrect` localization. - Made `VerifyOTPEvent` subclasses final. - Added assertion for non-negative seconds in `AppTimer`. - Used localized error message for incorrect PIN in `VerifyOTPScreen`. --- .../lib/modules/verify_otp/bloc/verify_otp_event.dart | 6 +++--- .../lib/modules/verify_otp/screens/verify_otp_screen.dart | 2 +- packages/app_translations/assets/i18n/en.i18n.json | 3 ++- packages/app_ui/lib/src/widgets/molecules/app_timer.dart | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart index 77447ac..f6cde1e 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -15,17 +15,17 @@ final class VerifyOTPChanged extends VerifyOTPEvent { List get props => [otp]; } -class EmailAddressChanged extends VerifyOTPEvent { +final class EmailAddressChanged extends VerifyOTPEvent { const EmailAddressChanged(this.email); final String email; @override List get props => [email]; } -class VerifyButtonPressed extends VerifyOTPEvent { +final class VerifyButtonPressed extends VerifyOTPEvent { const VerifyButtonPressed(); } -class ResendEmailEvent extends VerifyOTPEvent { +final class ResendEmailEvent extends VerifyOTPEvent { const ResendEmailEvent(); } diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 53c22b2..89fa5d1 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -146,7 +146,7 @@ class _VerifyOTPScreenState extends State with TickerProviderSt (context, state) => Pinput( length: 6, separatorBuilder: (index) => HSpace.xxsmall4(), - errorText: state.otp.error != null ? 'Pin is incorrect' : null, + errorText: state.otp.error != null ? context.t.pin_incorrect : null, onChanged: (value) { context.read().add(VerifyOTPChanged(value)); }, diff --git a/packages/app_translations/assets/i18n/en.i18n.json b/packages/app_translations/assets/i18n/en.i18n.json index 4191bcb..1e0c8c4 100644 --- a/packages/app_translations/assets/i18n/en.i18n.json +++ b/packages/app_translations/assets/i18n/en.i18n.json @@ -63,5 +63,6 @@ "verify_otp": "Verify OTP", "resend_otp": "Resend OTP", "otp_send_to_email": "OTP sent to your email", - "did_not_receive_otp": "Didn't receive the verification OTP?" + "did_not_receive_otp": "Didn't receive the verification OTP?", + "pin_incorrect": "Pin is incorrect" } \ No newline at end of file diff --git a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart index 6e8a8e4..af029e9 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_timer.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart @@ -4,7 +4,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; class AppTimer extends StatefulWidget { - const AppTimer({required this.seconds, super.key, this.onFinished}); + const AppTimer({required this.seconds, super.key, this.onFinished}) : assert(seconds >= 0, 'seconds must be non-negative'); final int seconds; final VoidCallback? onFinished; From 566257dcc4604228e27f31193b61d7a4f7241c2e Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 16:11:50 +0530 Subject: [PATCH 22/31] Refactor: Update OTP verification and forgot password functionality - Modified `VerifyOTPBloc` to initialize email via `SetEmailEvent` instead of constructor. - Renamed API status fields in `VerifyOTPState` for clarity (e.g., `statusForResendOTP` to `resendOtpStatus`). - Removed redundant `BlocBuilder` widgets in `VerifyOTPScreen`. - Trimmed email input in `ForgotPasswordBloc` before validation and submission. - Changed `AuthRepository.forgotPassword` return type from `TaskEither` to `TaskEither`. - Replaced `SingleChildScrollView` with `ListView` in `ForgotPasswordScreen` and removed `AutofillGroup`. --- .../auth/repository/auth_repository.dart | 6 +-- .../bloc/forgot_password_bloc.dart | 4 +- .../screens/forgot_password_screen.dart | 34 ++++++------ .../verify_otp/bloc/verify_otp_bloc.dart | 48 ++++++++--------- .../verify_otp/bloc/verify_otp_event.dart | 8 +++ .../verify_otp/bloc/verify_otp_state.dart | 18 +++---- .../verify_otp/screens/verify_otp_screen.dart | 52 ++++++++----------- 7 files changed, 80 insertions(+), 90 deletions(-) diff --git a/apps/app_core/lib/modules/auth/repository/auth_repository.dart b/apps/app_core/lib/modules/auth/repository/auth_repository.dart index 70ed223..26ba9ef 100644 --- a/apps/app_core/lib/modules/auth/repository/auth_repository.dart +++ b/apps/app_core/lib/modules/auth/repository/auth_repository.dart @@ -19,7 +19,7 @@ abstract interface class IAuthRepository { TaskEither logout(); - TaskEither forgotPassword(AuthRequestModel authRequestModel); + TaskEither forgotPassword(AuthRequestModel authRequestModel); TaskEither socialLogin({required AuthRequestModel requestModel}); @@ -121,14 +121,14 @@ class AuthRepository implements IAuthRepository { } @override - TaskEither forgotPassword(AuthRequestModel authRequestModel) => makeForgotPasswordRequest(authRequestModel) + TaskEither forgotPassword(AuthRequestModel authRequestModel) => makeForgotPasswordRequest(authRequestModel) .chainEither(RepositoryUtils.checkStatusCode) .chainEither( (response) => RepositoryUtils.mapToModel(() { return response.data; }), ) - .map((_) {}); + .map((_) => unit); TaskEither makeForgotPasswordRequest(AuthRequestModel authRequestModel) => userApiClient.request( requestType: RequestType.post, diff --git a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart index 5e119e3..58efaba 100644 --- a/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart @@ -22,7 +22,7 @@ class ForgotPasswordBloc extends Bloc final IAuthRepository _authenticationRepository; void _onEmailChanged(ForgotPasswordEmailChanged event, Emitter emit) { - final email = EmailValidator.dirty(event.email); + final email = EmailValidator.dirty(event.email.trim()); emit(state.copyWith(email: email, isValid: Formz.validate([email]))); } @@ -32,7 +32,7 @@ class ForgotPasswordBloc extends Bloc if (state.isValid) { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); final result = - await _authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value.trim())).run(); + await _authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value)).run(); result.fold( (failure) => emit(state.copyWith(status: FormzSubmissionStatus.failure)), (success) => emit(state.copyWith(status: FormzSubmissionStatus.success)), diff --git a/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart index 88d91f8..429cd4d 100644 --- a/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart +++ b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart @@ -38,27 +38,23 @@ class ForgotPasswordPage extends StatelessWidget implements AutoRouteWrapper { await context.replaceRoute(VerifyOTPRoute(emailAddress: state.email.value)); } }, - child: SingleChildScrollView( + child: ListView( padding: const EdgeInsets.symmetric(horizontal: Insets.large24), - child: AutofillGroup( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - VSpace.xxxlarge66(), - SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), - VSpace.large24(), - SlideAndFadeAnimationWrapper( - delay: 200, - child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), - ), - VSpace.large24(), - SlideAndFadeAnimationWrapper(delay: 300, child: _EmailInput()), - VSpace.xxlarge40(), - const SlideAndFadeAnimationWrapper(delay: 500, child: _ForgotPasswordButton()), - VSpace.large24(), - ], + + children: [ + VSpace.xxxlarge66(), + SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), + VSpace.large24(), + SlideAndFadeAnimationWrapper( + delay: 200, + child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), ), - ), + VSpace.large24(), + SlideAndFadeAnimationWrapper(delay: 300, child: _EmailInput()), + VSpace.xxlarge40(), + const SlideAndFadeAnimationWrapper(delay: 500, child: _ForgotPasswordButton()), + VSpace.large24(), + ], ), ), ); diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart index a63531c..70a74e6 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:api_client/api_client.dart'; +import 'package:app_core/core/domain/validators/email_validator.dart'; import 'package:app_core/core/domain/validators/length_validator.dart'; import 'package:app_core/modules/auth/model/auth_request_model.dart'; import 'package:app_core/modules/auth/repository/auth_repository.dart'; @@ -12,66 +13,61 @@ import 'package:fpdart/fpdart.dart'; part 'verify_otp_event.dart'; class VerifyOTPBloc extends Bloc { - VerifyOTPBloc(this.authenticationRepository, this.emailAddress) : super(const VerifyOTPState()) { + VerifyOTPBloc(this.authenticationRepository) : super(const VerifyOTPState()) { + on(_onSetEmail); on(_onVerifyButtonPressed); on(_onVerifyOTPChanged); on(_onResendEmail); } final AuthRepository authenticationRepository; - final String emailAddress; + + void _onSetEmail(SetEmailEvent event, Emitter emit) { + emit(state.copyWith(email: EmailValidator.dirty(event.email))); + } Future _onVerifyButtonPressed(VerifyButtonPressed event, Emitter emit) async { - emit(state.copyWith(statusForVerifyOTP: ApiStatus.loading, statusForResendOTP: ApiStatus.initial)); + emit(state.copyWith(verifyOtpStatus: ApiStatus.loading, resendOtpStatus: ApiStatus.initial)); final verifyOTPEither = - await authenticationRepository.verifyOTP(AuthRequestModel.verifyOTP(email: emailAddress, token: state.otp.value)).run(); + await authenticationRepository + .verifyOTP(AuthRequestModel.verifyOTP(email: state.email.value, token: state.otp.value)) + .run(); verifyOTPEither.fold( (failure) { - emit( - state.copyWith( - statusForVerifyOTP: ApiStatus.error, - statusForResendOTP: ApiStatus.initial, - errorMessage: failure.message, - ), - ); + emit(state.copyWith(verifyOtpStatus: ApiStatus.error, resendOtpStatus: ApiStatus.initial, errorMessage: failure.message)); }, (success) { - emit(state.copyWith(statusForVerifyOTP: ApiStatus.loaded, statusForResendOTP: ApiStatus.initial)); + emit(state.copyWith(verifyOtpStatus: ApiStatus.loaded, resendOtpStatus: ApiStatus.initial)); }, ); return unit; } - static const int _otpLength = 6; + final int _otpLength = 6; Future _onVerifyOTPChanged(VerifyOTPChanged event, Emitter emit) async { final otp = LengthValidator.dirty(_otpLength, event.otp); - emit(state.copyWith(otp: otp, statusForVerifyOTP: ApiStatus.initial, statusForResendOTP: ApiStatus.initial)); + emit(state.copyWith(otp: otp, verifyOtpStatus: ApiStatus.initial, resendOtpStatus: ApiStatus.initial)); return unit; } Future _onResendEmail(ResendEmailEvent event, Emitter emit) async { emit( state.copyWith( - statusForVerifyOTP: ApiStatus.initial, - statusForResendOTP: ApiStatus.loading, - otp: const LengthValidator.pure(_otpLength), + verifyOtpStatus: ApiStatus.initial, + resendOtpStatus: ApiStatus.loading, + otp: LengthValidator.pure(_otpLength), ), ); - final response = await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: emailAddress)).run(); + final response = + await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value)).run(); response.fold( (failure) { - emit( - state.copyWith( - statusForResendOTP: ApiStatus.error, - statusForVerifyOTP: ApiStatus.initial, - errorMessage: failure.message, - ), - ); + emit(state.copyWith(resendOtpStatus: ApiStatus.error, verifyOtpStatus: ApiStatus.initial, errorMessage: failure.message)); }, (success) { - emit(state.copyWith(statusForVerifyOTP: ApiStatus.initial, statusForResendOTP: ApiStatus.loaded)); + emit(state.copyWith(verifyOtpStatus: ApiStatus.initial, resendOtpStatus: ApiStatus.loaded)); }, ); return unit; diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart index f6cde1e..f2e145f 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -29,3 +29,11 @@ final class VerifyButtonPressed extends VerifyOTPEvent { final class ResendEmailEvent extends VerifyOTPEvent { const ResendEmailEvent(); } + +class SetEmailEvent extends VerifyOTPEvent { + final String email; + const SetEmailEvent(this.email); + + @override + List get props => [email]; +} diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart index 7c50a65..7139166 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -5,8 +5,8 @@ import 'package:equatable/equatable.dart'; final class VerifyOTPState extends Equatable { const VerifyOTPState({ - this.statusForResendOTP = ApiStatus.initial, - this.statusForVerifyOTP = ApiStatus.initial, + this.resendOtpStatus = ApiStatus.initial, + this.verifyOtpStatus = ApiStatus.initial, this.email = const EmailValidator.pure(), this.errorMessage = '', this.otp = const LengthValidator.pure(6), @@ -15,21 +15,21 @@ final class VerifyOTPState extends Equatable { VerifyOTPState copyWith({ EmailValidator? email, LengthValidator? otp, - ApiStatus? statusForResendOTP, - ApiStatus? statusForVerifyOTP, + ApiStatus? resendOtpStatus, + ApiStatus? verifyOtpStatus, String? errorMessage, }) { return VerifyOTPState( email: email ?? this.email, otp: otp ?? this.otp, - statusForResendOTP: statusForResendOTP ?? this.statusForResendOTP, - statusForVerifyOTP: statusForVerifyOTP ?? this.statusForVerifyOTP, + resendOtpStatus: resendOtpStatus ?? this.resendOtpStatus, + verifyOtpStatus: verifyOtpStatus ?? this.verifyOtpStatus, errorMessage: errorMessage ?? this.errorMessage, ); } - final ApiStatus statusForResendOTP; - final ApiStatus statusForVerifyOTP; + final ApiStatus resendOtpStatus; + final ApiStatus verifyOtpStatus; final EmailValidator email; final LengthValidator otp; final String errorMessage; @@ -37,5 +37,5 @@ final class VerifyOTPState extends Equatable { bool get isValid => otp.isValid && email.isValid; @override - List get props => [statusForResendOTP, email, otp, errorMessage, statusForVerifyOTP]; + List get props => [resendOtpStatus, email, otp, errorMessage, verifyOtpStatus]; } diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 89fa5d1..b3de6d1 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -28,8 +28,8 @@ class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { return RepositoryProvider( create: (context) => const AuthRepository(), child: BlocProvider( - lazy: false, - create: (context) => VerifyOTPBloc(RepositoryProvider.of(context), emailAddress ?? ''), + create: + (context) => VerifyOTPBloc(RepositoryProvider.of(context))..add(SetEmailEvent(emailAddress ?? '')), child: this, ), ); @@ -116,14 +116,13 @@ class _VerifyOTPScreenState extends State with TickerProviderSt padding: const EdgeInsets.all(Insets.small12), child: BlocConsumer( listener: (BuildContext context, VerifyOTPState state) { - if (state.statusForResendOTP == ApiStatus.error || state.statusForVerifyOTP == ApiStatus.error) { + if (state.resendOtpStatus == ApiStatus.error || state.verifyOtpStatus == ApiStatus.error) { final errorMessage = state.errorMessage; showAppSnackbar(context, errorMessage); } - if (state.statusForResendOTP == ApiStatus.loaded) { + if (state.resendOtpStatus == ApiStatus.loaded) { showAppSnackbar(context, context.t.otp_send_to_email); } - // Remove API success navigation, handled in static check }, builder: (context, state) { return SingleChildScrollView( @@ -141,17 +140,14 @@ class _VerifyOTPScreenState extends State with TickerProviderSt VSpace.medium16(), Padding(padding: const EdgeInsets.all(Insets.small12), child: AppText.sSemiBold(text: context.t.enter_otp)), VSpace.small12(), - BlocBuilder( - builder: - (context, state) => Pinput( - length: 6, - separatorBuilder: (index) => HSpace.xxsmall4(), - errorText: state.otp.error != null ? context.t.pin_incorrect : null, - onChanged: (value) { - context.read().add(VerifyOTPChanged(value)); - }, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - ), + Pinput( + length: 6, + separatorBuilder: (index) => HSpace.xxsmall4(), + errorText: state.otp.error != null ? context.t.pin_incorrect : null, + onChanged: (value) { + context.read().add(VerifyOTPChanged(value)); + }, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), VSpace.xsmall8(), if (_isTimerRunning) AppTimer(seconds: 30, onFinished: () {}), @@ -169,24 +165,18 @@ class _VerifyOTPScreenState extends State with TickerProviderSt textColor: context.colorScheme.primary400, onPressed: _isTimerRunning ? null : () => _onResendOTP(context), ), - const SizedBox(width: 8), + HSpace.xsmall8(), ], ), VSpace.large24(), - BlocBuilder( - builder: - (contextBuild, state) => Visibility( - visible: state.isValid, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: Insets.large24), - child: AppButton( - isExpanded: true, - text: context.t.verify_otp, - isLoading: state.statusForVerifyOTP == ApiStatus.loading, - onPressed: () => _onVerifyOTP(contextBuild, state), - ), - ), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: Insets.large24), + child: AppButton( + isExpanded: true, + text: context.t.verify_otp, + isLoading: state.verifyOtpStatus == ApiStatus.loading, + onPressed: () => _onVerifyOTP(context, state), + ), ), ], ), From 042b7be1be6a649f83f1eb67d3ee161fa1a06bac Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 16:43:25 +0530 Subject: [PATCH 23/31] Refactor: Standardize email property in SetEmailEvent Moved the `email` property declaration after the constructor in `SetEmailEvent` for consistency with other event classes. --- apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart index f2e145f..480cd70 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -31,8 +31,8 @@ final class ResendEmailEvent extends VerifyOTPEvent { } class SetEmailEvent extends VerifyOTPEvent { - final String email; const SetEmailEvent(this.email); + final String email; @override List get props => [email]; From 81917b58d1f7d64a7a81bb3456f97ed1cec43243 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 17:35:11 +0530 Subject: [PATCH 24/31] Refactor: Update VerifyOTPScreen and related BLoC - Made `emailAddress` a required parameter in `VerifyOTPScreen`. - Removed local timer management from `VerifyOTPScreen` and moved timer logic to `VerifyOTPBloc`. - Simplified OTP verification logic in `VerifyOTPScreen` and `VerifyOTPBloc` to use a static OTP for now. - Replaced `SingleChildScrollView` with `ListView` in `VerifyOTPScreen`. - Updated `VerifyOTPState` to include `isTimerRunning` and changed `email` type to `String`. - Added `StartTimerEvent` and `TimerFinishedEvent` to `VerifyOTPEvent`. - Updated `VerifyOTPBloc` to handle new timer events and use the string email. - Renamed `readOnly` to `isReadOnly` in `AppTextField`. --- .../verify_otp/bloc/verify_otp_bloc.dart | 39 ++-- .../verify_otp/bloc/verify_otp_event.dart | 8 + .../verify_otp/bloc/verify_otp_state.dart | 16 +- .../verify_otp/screens/verify_otp_screen.dart | 198 +++++++----------- .../src/widgets/molecules/app_textfield.dart | 8 +- 5 files changed, 118 insertions(+), 151 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart index 70a74e6..cb63515 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:api_client/api_client.dart'; -import 'package:app_core/core/domain/validators/email_validator.dart'; import 'package:app_core/core/domain/validators/length_validator.dart'; import 'package:app_core/modules/auth/model/auth_request_model.dart'; import 'package:app_core/modules/auth/repository/auth_repository.dart'; @@ -18,29 +17,36 @@ class VerifyOTPBloc extends Bloc { on(_onVerifyButtonPressed); on(_onVerifyOTPChanged); on(_onResendEmail); + on((event, emit) { + emit(state.copyWith(isTimerRunning: true)); + }); + on((event, emit) { + emit(state.copyWith(isTimerRunning: false)); + }); } final AuthRepository authenticationRepository; void _onSetEmail(SetEmailEvent event, Emitter emit) { - emit(state.copyWith(email: EmailValidator.dirty(event.email))); + emit(state.copyWith(email: event.email)); } Future _onVerifyButtonPressed(VerifyButtonPressed event, Emitter emit) async { emit(state.copyWith(verifyOtpStatus: ApiStatus.loading, resendOtpStatus: ApiStatus.initial)); - final verifyOTPEither = - await authenticationRepository - .verifyOTP(AuthRequestModel.verifyOTP(email: state.email.value, token: state.otp.value)) - .run(); - - verifyOTPEither.fold( - (failure) { - emit(state.copyWith(verifyOtpStatus: ApiStatus.error, resendOtpStatus: ApiStatus.initial, errorMessage: failure.message)); - }, - (success) { - emit(state.copyWith(verifyOtpStatus: ApiStatus.loaded, resendOtpStatus: ApiStatus.initial)); - }, - ); + // Static OTP check for now + if (state.otp.value == '222222') { + emit(state.copyWith( + verifyOtpStatus: ApiStatus.loaded, + resendOtpStatus: ApiStatus.initial, + errorMessage: 'OTP verified successfully!', + )); + } else { + emit(state.copyWith( + verifyOtpStatus: ApiStatus.error, + resendOtpStatus: ApiStatus.initial, + errorMessage: 'Invalid OTP', + )); + } return unit; } @@ -60,7 +66,7 @@ class VerifyOTPBloc extends Bloc { ), ); final response = - await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email.value)).run(); + await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email)).run(); response.fold( (failure) { @@ -68,6 +74,7 @@ class VerifyOTPBloc extends Bloc { }, (success) { emit(state.copyWith(verifyOtpStatus: ApiStatus.initial, resendOtpStatus: ApiStatus.loaded)); + add(const StartTimerEvent()); }, ); return unit; diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart index 480cd70..645945f 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -37,3 +37,11 @@ class SetEmailEvent extends VerifyOTPEvent { @override List get props => [email]; } + +class StartTimerEvent extends VerifyOTPEvent { + const StartTimerEvent(); +} + +class TimerFinishedEvent extends VerifyOTPEvent { + const TimerFinishedEvent(); +} diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart index 7139166..77725b3 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -7,17 +7,19 @@ final class VerifyOTPState extends Equatable { const VerifyOTPState({ this.resendOtpStatus = ApiStatus.initial, this.verifyOtpStatus = ApiStatus.initial, - this.email = const EmailValidator.pure(), + this.email = '', this.errorMessage = '', this.otp = const LengthValidator.pure(6), + this.isTimerRunning = true, }); VerifyOTPState copyWith({ - EmailValidator? email, + String? email, LengthValidator? otp, ApiStatus? resendOtpStatus, ApiStatus? verifyOtpStatus, String? errorMessage, + bool? isTimerRunning, }) { return VerifyOTPState( email: email ?? this.email, @@ -25,17 +27,19 @@ final class VerifyOTPState extends Equatable { resendOtpStatus: resendOtpStatus ?? this.resendOtpStatus, verifyOtpStatus: verifyOtpStatus ?? this.verifyOtpStatus, errorMessage: errorMessage ?? this.errorMessage, + isTimerRunning: isTimerRunning ?? this.isTimerRunning, ); } final ApiStatus resendOtpStatus; final ApiStatus verifyOtpStatus; - final EmailValidator email; - final LengthValidator otp; + final String email; final String errorMessage; + final LengthValidator otp; + final bool isTimerRunning; - bool get isValid => otp.isValid && email.isValid; + bool get isValid => otp.isValid; @override - List get props => [resendOtpStatus, email, otp, errorMessage, verifyOtpStatus]; + List get props => [resendOtpStatus, email, otp, errorMessage, verifyOtpStatus, isTimerRunning]; } diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index b3de6d1..817007d 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:api_client/api_client.dart'; import 'package:app_core/app/routes/app_router.dart'; import 'package:app_core/core/presentation/widgets/app_snackbar.dart'; @@ -16,9 +14,9 @@ import 'package:pinput/pinput.dart'; @RoutePage() class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { - const VerifyOTPScreen({super.key, this.emailAddress}); + const VerifyOTPScreen({required this.emailAddress, super.key}); - final String? emailAddress; + final String emailAddress; @override State createState() => _VerifyOTPScreenState(); @@ -28,8 +26,7 @@ class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { return RepositoryProvider( create: (context) => const AuthRepository(), child: BlocProvider( - create: - (context) => VerifyOTPBloc(RepositoryProvider.of(context))..add(SetEmailEvent(emailAddress ?? '')), + create: (context) => VerifyOTPBloc(RepositoryProvider.of(context))..add(SetEmailEvent(emailAddress)), child: this, ), ); @@ -37,72 +34,11 @@ class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { } class _VerifyOTPScreenState extends State with TickerProviderStateMixin { - Timer? _timer; - int _secondsRemaining = 30; - bool _isTimerRunning = true; - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - @override void initState() { - _startTimer(); super.initState(); } - void _startTimer() { - if (!mounted) return; - setState(() { - _secondsRemaining = 30; - _isTimerRunning = true; - }); - _timer?.cancel(); - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_secondsRemaining > 0) { - if (!mounted) { - timer.cancel(); - return; - } - setState(() { - _secondsRemaining--; - }); - } else { - if (!mounted) { - timer.cancel(); - return; - } - setState(() { - _isTimerRunning = false; - }); - _timer?.cancel(); - } - }); - } - - void _onResendOTP(BuildContext context) { - FocusScope.of(context).unfocus(); - context.read().add(const ResendEmailEvent()); - _startTimer(); - } - - void _onVerifyOTP(BuildContext contextBuild, VerifyOTPState state) { - TextInput.finishAutofillContext(); - FocusScope.of(context).unfocus(); - // Static check for OTP - if (state.otp.value == '222222') { - showAppSnackbar(contextBuild, 'OTP verified successfully!'); - contextBuild.maybePop(); - if (mounted) { - contextBuild.replaceRoute(const ChangePasswordRoute()); - } - } else { - showAppSnackbar(contextBuild, 'Invalid OTP', type: SnackbarType.failed); - } - } - @override Widget build(BuildContext context) { return AppScaffold( @@ -115,71 +51,83 @@ class _VerifyOTPScreenState extends State with TickerProviderSt child: Padding( padding: const EdgeInsets.all(Insets.small12), child: BlocConsumer( - listener: (BuildContext context, VerifyOTPState state) { - if (state.resendOtpStatus == ApiStatus.error || state.verifyOtpStatus == ApiStatus.error) { - final errorMessage = state.errorMessage; - showAppSnackbar(context, errorMessage); - } - if (state.resendOtpStatus == ApiStatus.loaded) { - showAppSnackbar(context, context.t.otp_send_to_email); + listener: (context, state) { + if (state.verifyOtpStatus == ApiStatus.loaded && state.otp.value == '222222') { + showAppSnackbar(context, 'OTP verified successfully!'); + context.replaceRoute(const ChangePasswordRoute()); + } else if (state.verifyOtpStatus == ApiStatus.error) { + showAppSnackbar(context, 'Invalid OTP', type: SnackbarType.failed); } }, builder: (context, state) { - return SingleChildScrollView( - child: Column( - children: [ - VSpace.large24(), - SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), - VSpace.large24(), - SlideAndFadeAnimationWrapper( - delay: 200, - child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), - ), - VSpace.large24(), - AppTextField(initialValue: widget.emailAddress, label: context.t.email, readOnly: true), - VSpace.medium16(), - Padding(padding: const EdgeInsets.all(Insets.small12), child: AppText.sSemiBold(text: context.t.enter_otp)), - VSpace.small12(), - Pinput( - length: 6, - separatorBuilder: (index) => HSpace.xxsmall4(), - errorText: state.otp.error != null ? context.t.pin_incorrect : null, - onChanged: (value) { - context.read().add(VerifyOTPChanged(value)); - }, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + return ListView( + children: [ + VSpace.large24(), + SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), + VSpace.large24(), + SlideAndFadeAnimationWrapper( + delay: 200, + child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + ), + VSpace.large24(), + AppTextField(initialValue: widget.emailAddress, label: context.t.email, isReadOnly: true), + VSpace.medium16(), + Center( + child: Padding( + padding: const EdgeInsets.all(Insets.small12), + child: AppText.sSemiBold(text: context.t.enter_otp), ), - VSpace.xsmall8(), - if (_isTimerRunning) AppTimer(seconds: 30, onFinished: () {}), - VSpace.small12(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppText( - text: context.t.did_not_receive_otp, - style: context.textTheme?.xsRegular.copyWith(color: context.colorScheme.black), - ), - AppButton( - text: context.t.resend_otp, - buttonType: ButtonType.text, - textColor: context.colorScheme.primary400, - onPressed: _isTimerRunning ? null : () => _onResendOTP(context), - ), - HSpace.xsmall8(), - ], + ), + VSpace.small12(), + Pinput( + length: 6, + separatorBuilder: (index) => HSpace.xxsmall4(), + errorText: state.otp.error != null ? context.t.pin_incorrect : null, + onChanged: (value) { + context.read().add(VerifyOTPChanged(value)); + }, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ), + VSpace.xsmall8(), + if (state.isTimerRunning) + Center( + child: AppTimer( + seconds: 30, + onFinished: () { + context.read().add(const TimerFinishedEvent()); + }, + ), ), - VSpace.large24(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: Insets.large24), - child: AppButton( - isExpanded: true, - text: context.t.verify_otp, - isLoading: state.verifyOtpStatus == ApiStatus.loading, - onPressed: () => _onVerifyOTP(context, state), + VSpace.small12(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppText.xsRegular(color: context.colorScheme.black, text: context.t.did_not_receive_otp), + AppButton( + text: context.t.resend_otp, + buttonType: ButtonType.text, + textColor: context.colorScheme.primary400, + onPressed: state.isTimerRunning + ? null + : () { + FocusScope.of(context).unfocus(); + context.read().add(const ResendEmailEvent()); + }, ), + HSpace.xsmall8(), + ], + ), + VSpace.large24(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: Insets.large24), + child: AppButton( + isExpanded: true, + text: context.t.verify_otp, + isLoading: state.verifyOtpStatus == ApiStatus.loading, + onPressed: () => context.read().add(const VerifyButtonPressed()), ), - ], - ), + ), + ], ); }, ), diff --git a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart index cc0bf20..8750160 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart @@ -9,7 +9,7 @@ class AppTextField extends StatefulWidget { this.textInputAction = TextInputAction.next, this.showLabel = true, this.hintText, - this.readOnly, + this.isReadOnly, this.keyboardType, this.initialValue, this.onChanged, @@ -39,7 +39,7 @@ class AppTextField extends StatefulWidget { this.backgroundColor, this.minLines, this.focusNode, - this.readOnly, + this.isReadOnly, this.autofillHints, this.hintTextBelowTextField, this.contentPadding, @@ -50,7 +50,7 @@ class AppTextField extends StatefulWidget { final String label; final String? initialValue; final String? hintText; - final bool? readOnly; + final bool? isReadOnly; final String? errorText; final String? hintTextBelowTextField; final TextInputAction? textInputAction; @@ -101,7 +101,7 @@ class _AppTextFieldState extends State { validator: widget.validator, obscureText: isObscureText, onChanged: widget.onChanged, - readOnly: widget.readOnly ?? false, + readOnly: widget.isReadOnly ?? false, autofillHints: widget.autofillHints, focusNode: widget.focusNode, maxLength: widget.maxLength, From a6c47a397749c717370ee6b22cbff01e48a1698a Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 17:35:25 +0530 Subject: [PATCH 25/31] Refactor: Removed unused `EmailValidator` import --- apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart index 77725b3..59d9906 100644 --- a/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -1,5 +1,4 @@ import 'package:api_client/api_client.dart'; -import 'package:app_core/core/domain/validators/email_validator.dart'; import 'package:app_core/core/domain/validators/length_validator.dart'; import 'package:equatable/equatable.dart'; From 956581dc2ee436241e2532f5cf21979eb7f30c72 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 17:40:14 +0530 Subject: [PATCH 26/31] Remove unused initState from VerifyOTPScreen --- .../verify_otp/screens/verify_otp_screen.dart | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 817007d..2d9e1cb 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -34,11 +34,6 @@ class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { } class _VerifyOTPScreenState extends State with TickerProviderStateMixin { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return AppScaffold( @@ -107,12 +102,13 @@ class _VerifyOTPScreenState extends State with TickerProviderSt text: context.t.resend_otp, buttonType: ButtonType.text, textColor: context.colorScheme.primary400, - onPressed: state.isTimerRunning - ? null - : () { - FocusScope.of(context).unfocus(); - context.read().add(const ResendEmailEvent()); - }, + onPressed: + state.isTimerRunning + ? null + : () { + FocusScope.of(context).unfocus(); + context.read().add(const ResendEmailEvent()); + }, ), HSpace.xsmall8(), ], From 57757e31d12e73a945e47c385a1b1dcf15220780 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 17:43:50 +0530 Subject: [PATCH 27/31] Fix: Adjusted button padding on Verify OTP screen The padding for the "Verify OTP" button on the `VerifyOTPScreen` has been updated. --- .../verify_otp/screens/verify_otp_screen.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index 2d9e1cb..c7c7d14 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -114,14 +114,12 @@ class _VerifyOTPScreenState extends State with TickerProviderSt ], ), VSpace.large24(), - Padding( + AppButton( + isExpanded: true, padding: const EdgeInsets.symmetric(horizontal: Insets.large24), - child: AppButton( - isExpanded: true, - text: context.t.verify_otp, - isLoading: state.verifyOtpStatus == ApiStatus.loading, - onPressed: () => context.read().add(const VerifyButtonPressed()), - ), + text: context.t.verify_otp, + isLoading: state.verifyOtpStatus == ApiStatus.loading, + onPressed: () => context.read().add(const VerifyButtonPressed()), ), ], ); From d269391b92c19983b481a55c38253830285d65c4 Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 17:45:26 +0530 Subject: [PATCH 28/31] Refactor: Removed unnecessary Padding from VerifyOTPScreen Removed the `Padding` widget that was wrapping the `BlocConsumer` in the `VerifyOTPScreen` and moved the padding to the `ListView` instead. --- .../verify_otp/screens/verify_otp_screen.dart | 152 +++++++++--------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index c7c7d14..cc773c7 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -43,88 +43,86 @@ class _VerifyOTPScreenState extends State with TickerProviderSt title: context.t.verify_otp, ), body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(Insets.small12), - child: BlocConsumer( - listener: (context, state) { - if (state.verifyOtpStatus == ApiStatus.loaded && state.otp.value == '222222') { - showAppSnackbar(context, 'OTP verified successfully!'); - context.replaceRoute(const ChangePasswordRoute()); - } else if (state.verifyOtpStatus == ApiStatus.error) { - showAppSnackbar(context, 'Invalid OTP', type: SnackbarType.failed); - } - }, - builder: (context, state) { - return ListView( - children: [ - VSpace.large24(), - SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), - VSpace.large24(), - SlideAndFadeAnimationWrapper( - delay: 200, - child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + child: BlocConsumer( + listener: (context, state) { + if (state.verifyOtpStatus == ApiStatus.loaded && state.otp.value == '222222') { + showAppSnackbar(context, 'OTP verified successfully!'); + context.replaceRoute(const ChangePasswordRoute()); + } else if (state.verifyOtpStatus == ApiStatus.error) { + showAppSnackbar(context, 'Invalid OTP', type: SnackbarType.failed); + } + }, + builder: (context, state) { + return ListView( + padding: const EdgeInsets.all(Insets.small12), + children: [ + VSpace.large24(), + SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), + VSpace.large24(), + SlideAndFadeAnimationWrapper( + delay: 200, + child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + ), + VSpace.large24(), + AppTextField(initialValue: widget.emailAddress, label: context.t.email, isReadOnly: true), + VSpace.medium16(), + Center( + child: Padding( + padding: const EdgeInsets.all(Insets.small12), + child: AppText.sSemiBold(text: context.t.enter_otp), ), - VSpace.large24(), - AppTextField(initialValue: widget.emailAddress, label: context.t.email, isReadOnly: true), - VSpace.medium16(), + ), + VSpace.small12(), + Pinput( + length: 6, + separatorBuilder: (index) => HSpace.xxsmall4(), + errorText: state.otp.error != null ? context.t.pin_incorrect : null, + onChanged: (value) { + context.read().add(VerifyOTPChanged(value)); + }, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ), + VSpace.xsmall8(), + if (state.isTimerRunning) Center( - child: Padding( - padding: const EdgeInsets.all(Insets.small12), - child: AppText.sSemiBold(text: context.t.enter_otp), + child: AppTimer( + seconds: 30, + onFinished: () { + context.read().add(const TimerFinishedEvent()); + }, ), ), - VSpace.small12(), - Pinput( - length: 6, - separatorBuilder: (index) => HSpace.xxsmall4(), - errorText: state.otp.error != null ? context.t.pin_incorrect : null, - onChanged: (value) { - context.read().add(VerifyOTPChanged(value)); - }, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - ), - VSpace.xsmall8(), - if (state.isTimerRunning) - Center( - child: AppTimer( - seconds: 30, - onFinished: () { - context.read().add(const TimerFinishedEvent()); - }, - ), + VSpace.small12(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppText.xsRegular(color: context.colorScheme.black, text: context.t.did_not_receive_otp), + AppButton( + text: context.t.resend_otp, + buttonType: ButtonType.text, + textColor: context.colorScheme.primary400, + onPressed: + state.isTimerRunning + ? null + : () { + FocusScope.of(context).unfocus(); + context.read().add(const ResendEmailEvent()); + }, ), - VSpace.small12(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppText.xsRegular(color: context.colorScheme.black, text: context.t.did_not_receive_otp), - AppButton( - text: context.t.resend_otp, - buttonType: ButtonType.text, - textColor: context.colorScheme.primary400, - onPressed: - state.isTimerRunning - ? null - : () { - FocusScope.of(context).unfocus(); - context.read().add(const ResendEmailEvent()); - }, - ), - HSpace.xsmall8(), - ], - ), - VSpace.large24(), - AppButton( - isExpanded: true, - padding: const EdgeInsets.symmetric(horizontal: Insets.large24), - text: context.t.verify_otp, - isLoading: state.verifyOtpStatus == ApiStatus.loading, - onPressed: () => context.read().add(const VerifyButtonPressed()), - ), - ], - ); - }, - ), + HSpace.xsmall8(), + ], + ), + VSpace.large24(), + AppButton( + isExpanded: true, + padding: const EdgeInsets.symmetric(horizontal: Insets.large24), + text: context.t.verify_otp, + isLoading: state.verifyOtpStatus == ApiStatus.loading, + onPressed: () => context.read().add(const VerifyButtonPressed()), + ), + ], + ); + }, ), ), ); From b6c57224dfe3db56a6bee80e2463d651d8846d0f Mon Sep 17 00:00:00 2001 From: tulsigohel Date: Fri, 27 Jun 2025 17:59:59 +0530 Subject: [PATCH 29/31] Refactor: Introduce AppOtpInput widget and update dependencies - Added `pinput` dependency to `app_ui/pubspec.yaml`. - Removed `pinput` dependency from `app_core/pubspec.yaml`. - Created `AppOtpInput` widget in `app_ui` to encapsulate OTP input logic. - Replaced direct usage of `Pinput` with `AppOtpInput` in `VerifyOTPScreen`. - Updated various dependency versions in `widgetbook/pubspec.lock`. - Updated Flutter SDK constraint in `widgetbook/pubspec.lock`. --- .../verify_otp/screens/verify_otp_screen.dart | 152 +++++++++--------- apps/app_core/pubspec.yaml | 2 +- .../src/widgets/molecules/app_otp_input.dart | 23 +++ .../lib/src/widgets/molecules/molecules.dart | 1 + packages/app_ui/pubspec.yaml | 1 + packages/widgetbook/pubspec.lock | 16 ++ 6 files changed, 115 insertions(+), 80 deletions(-) create mode 100644 packages/app_ui/lib/src/widgets/molecules/app_otp_input.dart diff --git a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart index cc773c7..15f07f2 100644 --- a/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -8,9 +8,7 @@ import 'package:app_translations/app_translations.dart'; import 'package:app_ui/app_ui.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinput/pinput.dart'; @RoutePage() class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { @@ -42,88 +40,84 @@ class _VerifyOTPScreenState extends State with TickerProviderSt automaticallyImplyLeading: true, title: context.t.verify_otp, ), - body: SafeArea( - child: BlocConsumer( - listener: (context, state) { - if (state.verifyOtpStatus == ApiStatus.loaded && state.otp.value == '222222') { - showAppSnackbar(context, 'OTP verified successfully!'); - context.replaceRoute(const ChangePasswordRoute()); - } else if (state.verifyOtpStatus == ApiStatus.error) { - showAppSnackbar(context, 'Invalid OTP', type: SnackbarType.failed); - } - }, - builder: (context, state) { - return ListView( - padding: const EdgeInsets.all(Insets.small12), - children: [ - VSpace.large24(), - SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), - VSpace.large24(), - SlideAndFadeAnimationWrapper( - delay: 200, - child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + body: BlocConsumer( + listener: (context, state) { + if (state.verifyOtpStatus == ApiStatus.loaded && state.otp.value == '222222') { + showAppSnackbar(context, 'OTP verified successfully!'); + context.replaceRoute(const ChangePasswordRoute()); + } else if (state.verifyOtpStatus == ApiStatus.error) { + showAppSnackbar(context, 'Invalid OTP', type: SnackbarType.failed); + } + }, + builder: (context, state) { + return ListView( + padding: const EdgeInsets.all(Insets.small12), + children: [ + VSpace.large24(), + SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: Assets.images.logo.image(width: 100))), + VSpace.large24(), + SlideAndFadeAnimationWrapper( + delay: 200, + child: Center(child: AppText.xsSemiBold(text: context.t.welcome, fontSize: 16)), + ), + VSpace.large24(), + AppTextField(initialValue: widget.emailAddress, label: context.t.email, isReadOnly: true), + VSpace.medium16(), + Center( + child: Padding( + padding: const EdgeInsets.all(Insets.small12), + child: AppText.sSemiBold(text: context.t.enter_otp), ), - VSpace.large24(), - AppTextField(initialValue: widget.emailAddress, label: context.t.email, isReadOnly: true), - VSpace.medium16(), + ), + VSpace.small12(), + AppOtpInput( + errorText: state.otp.error != null ? context.t.pin_incorrect : null, + onChanged: (value) { + context.read().add(VerifyOTPChanged(value)); + }, + ), + + VSpace.xsmall8(), + if (state.isTimerRunning) Center( - child: Padding( - padding: const EdgeInsets.all(Insets.small12), - child: AppText.sSemiBold(text: context.t.enter_otp), + child: AppTimer( + seconds: 30, + onFinished: () { + context.read().add(const TimerFinishedEvent()); + }, ), ), - VSpace.small12(), - Pinput( - length: 6, - separatorBuilder: (index) => HSpace.xxsmall4(), - errorText: state.otp.error != null ? context.t.pin_incorrect : null, - onChanged: (value) { - context.read().add(VerifyOTPChanged(value)); - }, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - ), - VSpace.xsmall8(), - if (state.isTimerRunning) - Center( - child: AppTimer( - seconds: 30, - onFinished: () { - context.read().add(const TimerFinishedEvent()); - }, - ), + VSpace.small12(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppText.xsRegular(color: context.colorScheme.black, text: context.t.did_not_receive_otp), + AppButton( + text: context.t.resend_otp, + buttonType: ButtonType.text, + textColor: context.colorScheme.primary400, + onPressed: + state.isTimerRunning + ? null + : () { + FocusScope.of(context).unfocus(); + context.read().add(const ResendEmailEvent()); + }, ), - VSpace.small12(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppText.xsRegular(color: context.colorScheme.black, text: context.t.did_not_receive_otp), - AppButton( - text: context.t.resend_otp, - buttonType: ButtonType.text, - textColor: context.colorScheme.primary400, - onPressed: - state.isTimerRunning - ? null - : () { - FocusScope.of(context).unfocus(); - context.read().add(const ResendEmailEvent()); - }, - ), - HSpace.xsmall8(), - ], - ), - VSpace.large24(), - AppButton( - isExpanded: true, - padding: const EdgeInsets.symmetric(horizontal: Insets.large24), - text: context.t.verify_otp, - isLoading: state.verifyOtpStatus == ApiStatus.loading, - onPressed: () => context.read().add(const VerifyButtonPressed()), - ), - ], - ); - }, - ), + HSpace.xsmall8(), + ], + ), + VSpace.large24(), + AppButton( + isExpanded: true, + padding: const EdgeInsets.symmetric(horizontal: Insets.large24), + text: context.t.verify_otp, + isLoading: state.verifyOtpStatus == ApiStatus.loading, + onPressed: () => context.read().add(const VerifyButtonPressed()), + ), + ], + ); + }, ), ); } diff --git a/apps/app_core/pubspec.yaml b/apps/app_core/pubspec.yaml index 469cf1e..b019f23 100644 --- a/apps/app_core/pubspec.yaml +++ b/apps/app_core/pubspec.yaml @@ -89,7 +89,7 @@ dependencies: # Launch URL url_launcher: ^6.3.1 - pinput: ^5.0.1 + dependency_overrides: web: ^1.0.0 diff --git a/packages/app_ui/lib/src/widgets/molecules/app_otp_input.dart b/packages/app_ui/lib/src/widgets/molecules/app_otp_input.dart new file mode 100644 index 0000000..c0ecb4e --- /dev/null +++ b/packages/app_ui/lib/src/widgets/molecules/app_otp_input.dart @@ -0,0 +1,23 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinput/pinput.dart'; + +class AppOtpInput extends StatelessWidget { + const AppOtpInput({required this.onChanged, this.length = 6, this.errorText, super.key}); + + final void Function(String) onChanged; + final int length; + final String? errorText; + + @override + Widget build(BuildContext context) { + return Pinput( + length: length, + separatorBuilder: (index) => HSpace.xxsmall4(), + errorText: errorText, + onChanged: onChanged, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + } +} diff --git a/packages/app_ui/lib/src/widgets/molecules/molecules.dart b/packages/app_ui/lib/src/widgets/molecules/molecules.dart index fce2c9c..a8e4f56 100644 --- a/packages/app_ui/lib/src/widgets/molecules/molecules.dart +++ b/packages/app_ui/lib/src/widgets/molecules/molecules.dart @@ -4,6 +4,7 @@ export 'app_circular_progress_indicator.dart'; export 'app_dialog.dart'; export 'app_dropdown.dart'; export 'app_network_image.dart'; +export 'app_otp_input.dart'; export 'app_profile_image.dart'; export 'app_refresh_indicator.dart'; export 'app_textfield.dart'; diff --git a/packages/app_ui/pubspec.yaml b/packages/app_ui/pubspec.yaml index 085f719..31c4983 100644 --- a/packages/app_ui/pubspec.yaml +++ b/packages/app_ui/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: timeago: ^3.7.0 url_launcher: ^6.3.1 flutter_svg: ^2.0.17 + pinput: ^5.0.1 dev_dependencies: flutter_test: diff --git a/packages/widgetbook/pubspec.lock b/packages/widgetbook/pubspec.lock index bc858ca..6c60a02 100644 --- a/packages/widgetbook/pubspec.lock +++ b/packages/widgetbook/pubspec.lock @@ -583,6 +583,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + pinput: + dependency: transitive + description: + name: pinput + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + url: "https://pub.dev" + source: hosted + version: "5.0.1" platform: dependency: transitive description: @@ -812,6 +820,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" url_launcher: dependency: transitive description: From fd4c8cab8dff5f79afc1a1d6284b40dc5ee94734 Mon Sep 17 00:00:00 2001 From: Cavin Macwan <113342471+cavin-7span@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:11:15 +0530 Subject: [PATCH 30/31] chore: Added documentation in the build-apk so that devs using puro or fvm can run the command --- melos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/melos.yaml b/melos.yaml index e6acb10..e4d1f4f 100644 --- a/melos.yaml +++ b/melos.yaml @@ -39,7 +39,7 @@ scripts: exec: dart run slang build-apk: - description: Builds the APK File + description: Builds the APK File. If you're using puro or fvm, then make sure to add `puro` or `fvm` before running the command packageFilters: flutter: true dirExists: lib From a6411d43c883da32e993e25bf4b15354c23fedcc Mon Sep 17 00:00:00 2001 From: Avni Prajapati Date: Wed, 16 Jul 2025 22:36:07 +0530 Subject: [PATCH 31/31] Updated app_textfield.dart --- .../app_ui/lib/src/widgets/molecules/app_textfield.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart index 8750160..83368c8 100644 --- a/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart +++ b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart @@ -109,9 +109,7 @@ class _AppTextFieldState extends State { filled: true, fillColor: widget.backgroundColor ?? context.colorScheme.grey100, hintText: widget.hintText, - contentPadding: - widget.contentPadding ?? - const EdgeInsets.only(left: Insets.small12, right: Insets.small12), + contentPadding: widget.contentPadding ?? const EdgeInsets.only(left: Insets.small12, right: Insets.small12), errorMaxLines: 2, focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(Insets.xsmall8), @@ -127,10 +125,7 @@ class _AppTextFieldState extends State { ? IconButton( splashColor: context.colorScheme.primary50, onPressed: toggleObscureText, - icon: Icon( - isObscureText ? Icons.visibility_off : Icons.visibility, - color: context.colorScheme.grey700, - ), + icon: Icon(isObscureText ? Icons.visibility_off : Icons.visibility, color: context.colorScheme.grey700), ) : null, ),