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..e3287e6 --- /dev/null +++ b/.cursor/rules/atomic-design-rule.mdc @@ -0,0 +1,547 @@ +--- +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. + +## 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: () {}, +) +``` \ 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..7b02ff1 --- /dev/null +++ b/.cursor/rules/auto-route.mdc @@ -0,0 +1,157 @@ +--- +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. 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) \ 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.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 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/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 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/.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 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 diff --git a/apps/app_core/lib/app/config/api_endpoints.dart b/apps/app_core/lib/app/config/api_endpoints.dart index c6fa26c..65305fb 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/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/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/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 { 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..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,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((_) => unit); + + 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/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/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..58efaba --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_bloc.dart @@ -0,0 +1,43 @@ +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.trim()); + 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)); + final result = + 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)), + ); + } + 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..b9d336c --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/bloc/forgot_password_state.dart @@ -0,0 +1,27 @@ +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, errorMessage]; +} 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..429cd4d --- /dev/null +++ b/apps/app_core/lib/modules/forgot_password/screens/forgot_password_screen.dart @@ -0,0 +1,103 @@ +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.replaceRoute(VerifyOTPRoute(emailAddress: state.email.value)); + } + }, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: Insets.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(), + ], + ), + ), + ); + } +} + +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/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/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..cb63515 --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_bloc.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:api_client/api_client.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'; + +class VerifyOTPBloc extends Bloc { + VerifyOTPBloc(this.authenticationRepository) : super(const VerifyOTPState()) { + on(_onSetEmail); + 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: event.email)); + } + + Future _onVerifyButtonPressed(VerifyButtonPressed event, Emitter emit) async { + emit(state.copyWith(verifyOtpStatus: ApiStatus.loading, 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; + } + + final int _otpLength = 6; + Future _onVerifyOTPChanged(VerifyOTPChanged event, Emitter emit) async { + final otp = LengthValidator.dirty(_otpLength, event.otp); + emit(state.copyWith(otp: otp, verifyOtpStatus: ApiStatus.initial, resendOtpStatus: ApiStatus.initial)); + return unit; + } + + Future _onResendEmail(ResendEmailEvent event, Emitter emit) async { + emit( + state.copyWith( + verifyOtpStatus: ApiStatus.initial, + resendOtpStatus: ApiStatus.loading, + otp: LengthValidator.pure(_otpLength), + ), + ); + final response = + await authenticationRepository.forgotPassword(AuthRequestModel.forgotPassword(email: state.email)).run(); + + response.fold( + (failure) { + emit(state.copyWith(resendOtpStatus: ApiStatus.error, verifyOtpStatus: ApiStatus.initial, errorMessage: failure.message)); + }, + (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 new file mode 100644 index 0000000..645945f --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_event.dart @@ -0,0 +1,47 @@ +part of 'verify_otp_bloc.dart'; + +sealed class VerifyOTPEvent extends Equatable { + const VerifyOTPEvent(); + + @override + List get props => []; +} + +final class VerifyOTPChanged extends VerifyOTPEvent { + const VerifyOTPChanged(this.otp); + final String otp; + + @override + List get props => [otp]; +} + +final class EmailAddressChanged extends VerifyOTPEvent { + const EmailAddressChanged(this.email); + final String email; + @override + List get props => [email]; +} + +final class VerifyButtonPressed extends VerifyOTPEvent { + const VerifyButtonPressed(); +} + +final class ResendEmailEvent extends VerifyOTPEvent { + const ResendEmailEvent(); +} + +class SetEmailEvent extends VerifyOTPEvent { + const SetEmailEvent(this.email); + final String email; + + @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 new file mode 100644 index 0000000..59d9906 --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/bloc/verify_otp_state.dart @@ -0,0 +1,44 @@ +import 'package:api_client/api_client.dart'; +import 'package:app_core/core/domain/validators/length_validator.dart'; +import 'package:equatable/equatable.dart'; + +final class VerifyOTPState extends Equatable { + const VerifyOTPState({ + this.resendOtpStatus = ApiStatus.initial, + this.verifyOtpStatus = ApiStatus.initial, + this.email = '', + this.errorMessage = '', + this.otp = const LengthValidator.pure(6), + this.isTimerRunning = true, + }); + + VerifyOTPState copyWith({ + String? email, + LengthValidator? otp, + ApiStatus? resendOtpStatus, + ApiStatus? verifyOtpStatus, + String? errorMessage, + bool? isTimerRunning, + }) { + return VerifyOTPState( + email: email ?? this.email, + otp: otp ?? this.otp, + resendOtpStatus: resendOtpStatus ?? this.resendOtpStatus, + verifyOtpStatus: verifyOtpStatus ?? this.verifyOtpStatus, + errorMessage: errorMessage ?? this.errorMessage, + isTimerRunning: isTimerRunning ?? this.isTimerRunning, + ); + } + + final ApiStatus resendOtpStatus; + final ApiStatus verifyOtpStatus; + final String email; + final String errorMessage; + final LengthValidator otp; + final bool isTimerRunning; + + bool get isValid => otp.isValid; + + @override + 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 new file mode 100644 index 0000000..15f07f2 --- /dev/null +++ b/apps/app_core/lib/modules/verify_otp/screens/verify_otp_screen.dart @@ -0,0 +1,124 @@ +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_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'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +@RoutePage() +class VerifyOTPScreen extends StatefulWidget implements AutoRouteWrapper { + const VerifyOTPScreen({required this.emailAddress, super.key}); + + final String emailAddress; + + @override + State createState() => _VerifyOTPScreenState(); + + @override + Widget wrappedRoute(BuildContext context) { + return RepositoryProvider( + create: (context) => const AuthRepository(), + child: BlocProvider( + create: (context) => VerifyOTPBloc(RepositoryProvider.of(context))..add(SetEmailEvent(emailAddress)), + child: this, + ), + ); + } +} + +class _VerifyOTPScreenState extends State with TickerProviderStateMixin { + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: CustomAppBar( + backgroundColor: context.colorScheme.white, + automaticallyImplyLeading: true, + title: context.t.verify_otp, + ), + 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.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: 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()); + }, + ), + 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 622508f..b019f23 100644 --- a/apps/app_core/pubspec.yaml +++ b/apps/app_core/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: # Launch URL url_launcher: ^6.3.1 + dependency_overrides: web: ^1.0.0 source_gen: ^2.0.0 @@ -121,6 +122,8 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 + + flutter_gen: output: lib/gen/ line_length: 80 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 diff --git a/packages/app_translations/assets/i18n/en.i18n.json b/packages/app_translations/assets/i18n/en.i18n.json index 83a146e..1e0c8c4 100644 --- a/packages/app_translations/assets/i18n/en.i18n.json +++ b/packages/app_translations/assets/i18n/en.i18n.json @@ -54,5 +54,15 @@ "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", + "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_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/app_textfield.dart b/packages/app_ui/lib/src/widgets/molecules/app_textfield.dart index f6a39ae..83368c8 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.isReadOnly, this.keyboardType, this.initialValue, this.onChanged, @@ -20,8 +21,9 @@ class AppTextField extends StatefulWidget { this.contentPadding, this.autofillHints, this.hintTextBelowTextField, - }) : isPasswordField = false, - isObscureText = false; + this.maxLength, + }) : isPasswordField = false, + isObscureText = false; const AppTextField.password({ required this.label, @@ -37,15 +39,18 @@ class AppTextField extends StatefulWidget { this.backgroundColor, this.minLines, this.focusNode, + this.isReadOnly, this.autofillHints, this.hintTextBelowTextField, this.contentPadding, - }) : isPasswordField = true, - isObscureText = true; + this.maxLength, + }) : isPasswordField = true, + isObscureText = true; final String label; final String? initialValue; final String? hintText; + final bool? isReadOnly; final String? errorText; final String? hintTextBelowTextField; final TextInputAction? textInputAction; @@ -60,6 +65,7 @@ class AppTextField extends StatefulWidget { final FocusNode? focusNode; final int? minLines; final EdgeInsetsGeometry? contentPadding; + final int? maxLength; @override State createState() => _AppTextFieldState(); @@ -85,10 +91,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,13 +101,15 @@ class _AppTextFieldState extends State { validator: widget.validator, obscureText: isObscureText, onChanged: widget.onChanged, + readOnly: widget.isReadOnly ?? 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), @@ -115,16 +120,14 @@ class _AppTextFieldState extends State { 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, 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..af029e9 --- /dev/null +++ b/packages/app_ui/lib/src/widgets/molecules/app_timer.dart @@ -0,0 +1,65 @@ +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.onFinished}) : assert(seconds >= 0, 'seconds must be non-negative'); + final int seconds; + + final VoidCallback? onFinished; + + @override + State createState() => _AppTimerState(); +} + +class _AppTimerState extends State { + 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() { + if (widget.seconds == 0) return; + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_secondsRemaining > 0) { + setState(() { + _secondsRemaining--; + }); + } else { + timer.cancel(); + widget.onFinished?.call(); + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + 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)); + } +} diff --git a/packages/app_ui/lib/src/widgets/molecules/molecules.dart b/packages/app_ui/lib/src/widgets/molecules/molecules.dart index 545f7ed..a8e4f56 100644 --- a/packages/app_ui/lib/src/widgets/molecules/molecules.dart +++ b/packages/app_ui/lib/src/widgets/molecules/molecules.dart @@ -4,9 +4,11 @@ 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'; +export 'app_timer.dart'; export 'empty_ui.dart'; export 'no_internet_widget.dart'; export 'user_concent_widget.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/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 = diff --git a/packages/widgetbook/pubspec.lock b/packages/widgetbook/pubspec.lock index 1ba8abf..6c60a02 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: @@ -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: @@ -659,10 +667,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 @@ -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: @@ -920,10 +936,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 +1022,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