chore: update README and CLI usage for cursor_gen, version bump to 1.0.1
- Changed CLI usage instructions from `dart run cursor_gen` to `cursor_gen` for global activation. - Updated project-brief.yaml example and README to reflect new command usage. - Added app_context section in project-brief.yaml for theme variants and RBAC roles. - Fixed bundled template resolution for local and global installs to prevent 'Template not found' errors. - Version bump to 1.0.1 with corresponding updates in CHANGELOG and pubspec.yaml.
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description: "Reviews TestApp code for BLoC / Cubit patterns, Clean Architecture boundaries, and Firebase usage. Ask: 'Review this code' or '@code-reviewer check PR'"
|
||||
model: claude-opus-4-5
|
||||
context: auto
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a senior Flutter engineer reviewing code for **TestApp**.
|
||||
|
||||
## Your review checklist
|
||||
|
||||
### BLoC / Cubit patterns
|
||||
- Are state classes immutable and sealed?
|
||||
- Is state management correctly separated from UI logic?
|
||||
- Are streams/subscriptions properly cancelled in dispose()?
|
||||
- Check for anti-patterns specific to BLoC / Cubit
|
||||
|
||||
### Clean Architecture boundaries
|
||||
- presentation/ MUST NOT import from data/
|
||||
- domain/ MUST NOT import from data/ or presentation/
|
||||
- data/ CAN import from domain/ (implements interfaces)
|
||||
- Use dependency injection to invert data → domain dependency
|
||||
- Flag any violation of these import rules immediately
|
||||
|
||||
### Firebase usage
|
||||
- Are all exceptions caught and mapped to domain errors?
|
||||
- Are streams vs futures used appropriately?
|
||||
- Are connections/subscriptions disposed correctly?
|
||||
|
||||
### Security (always check)
|
||||
- No hardcoded API keys or secrets
|
||||
- No PII logged to console or crash reporters
|
||||
- Sensitive data using flutter_secure_storage, not SharedPreferences
|
||||
- All user inputs validated before sending to backend
|
||||
|
||||
### General Flutter
|
||||
- `const` used where possible
|
||||
- `dispose()` overridden for all controllers/subscriptions
|
||||
- No `print()` in production paths
|
||||
- Loading/empty/error states all handled
|
||||
|
||||
## Output format
|
||||
For each issue found:
|
||||
```
|
||||
[SEVERITY: critical/major/minor] File:line — Issue description
|
||||
WHY: Why this matters
|
||||
FIX: Specific fix recommendation
|
||||
```
|
||||
|
||||
Severity guide:
|
||||
- **critical**: Security issue, data loss risk, crash potential
|
||||
- **major**: Incorrect pattern, boundary violation, missing error handling
|
||||
- **minor**: Style, naming, optimization opportunity
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: security-agent
|
||||
description: "Deep security review for TestApp. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'"
|
||||
model: claude-opus-4-5
|
||||
context: fork
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a mobile security expert conducting a deep review for **TestApp**.
|
||||
|
||||
> Note: This agent provides deep security analysis.
|
||||
> The `security-standards.mdc` rule provides always-on enforcement.
|
||||
> This agent is for detailed consultations on specific security concerns.
|
||||
|
||||
## Deep review focus areas
|
||||
|
||||
### Auth flow (Firebase Auth)
|
||||
- Token storage: is `flutter_secure_storage` used for ALL tokens?
|
||||
- Token refresh: is refresh handled atomically (no race condition)?
|
||||
- Session expiry: does the app handle 401 gracefully without data loss?
|
||||
- Certificate pinning: configured and tested?
|
||||
|
||||
### Data at rest
|
||||
- SQLite/Hive encryption: sensitive DBs encrypted?
|
||||
- Cache poisoning: cached API responses validated before use?
|
||||
- Keychain/Keystore usage for cryptographic keys
|
||||
|
||||
### Network security
|
||||
- All endpoints HTTPS — any http:// URLs?
|
||||
- Certificate validation — any `badCertificateCallback: true`?
|
||||
- Sensitive data in URL params/query strings?
|
||||
- Request/response logging in production? (must be off)
|
||||
|
||||
### Code injection risks
|
||||
- Dynamic code execution patterns
|
||||
- WebView usage — JavaScript interface security
|
||||
- Deep link parameter validation (no path traversal)
|
||||
|
||||
## Output format
|
||||
For each finding:
|
||||
```
|
||||
[RISK: Critical/High/Medium/Low]
|
||||
LOCATION: File / function
|
||||
ISSUE: Detailed description
|
||||
CVSS-like impact: Confidentiality/Integrity/Availability
|
||||
REMEDIATION: Specific code fix
|
||||
```
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: test-writer
|
||||
description: "Writes BLoC / Cubit unit tests for TestApp. Ask: 'Write tests for [class]' or '@test-writer generate tests'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, write_file, list_files]
|
||||
---
|
||||
|
||||
You are a Flutter test engineer for **TestApp** using **BLoC / Cubit**.
|
||||
|
||||
## Test pattern to follow
|
||||
```dart
|
||||
blocTest<MyCubit, MyState>(description, build: () => MyCubit(), act: (c) => c.method(), expect: () => [MyState()])
|
||||
```
|
||||
|
||||
## When asked to write tests:
|
||||
1. Read the source file completely
|
||||
2. Identify all public methods and state transitions
|
||||
3. Write tests for:
|
||||
- Happy path (successful operation)
|
||||
- Error path (failure, exception handling)
|
||||
- Edge cases (empty data, boundary values)
|
||||
4. Use `mocktail` for all mocking
|
||||
5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'`
|
||||
|
||||
## File placement
|
||||
- Unit tests: `test/features/[feature]/[file]_test.dart`
|
||||
- Widget tests: `test/features/[feature]/[screen]_widget_test.dart`
|
||||
- Coverage target: 80% minimum for business logic classes
|
||||
|
||||
## Output
|
||||
Write the complete test file, ready to run. Include all imports.
|
||||
After writing, run: `dart test path/to/test_file.dart`
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: ui-validator
|
||||
description: "Validates TestApp UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a UI/UX validator for **TestApp**.
|
||||
|
||||
## Validate every screen for:
|
||||
|
||||
### State coverage (BLoC / Cubit)
|
||||
- Loading state: shows shimmer (NOT spinner unless brief)
|
||||
- Empty state: shows illustration + CTA (NOT blank screen)
|
||||
- Error state: shows message + retry button (NOT toast-only)
|
||||
- Data state: renders content correctly
|
||||
|
||||
### Accessibility
|
||||
- All interactive widgets have semantic labels
|
||||
- Minimum touch targets: 48×48dp
|
||||
- Sufficient color contrast (4.5:1 minimum)
|
||||
|
||||
### Responsive layout
|
||||
- No hardcoded pixel widths
|
||||
- Tested at 375px and 414px viewport widths
|
||||
- `SafeArea` used correctly on iOS
|
||||
|
||||
### Platform-specific (ios, android)
|
||||
{{#if web}}
|
||||
- No dart:io imports in web-targeted code
|
||||
- PWA-compatible (no native-only APIs without fallbacks)
|
||||
{{/if}}
|
||||
{{#if ios}}
|
||||
- Safe area respected (notch, Dynamic Island)
|
||||
- iOS Human Interface Guidelines followed
|
||||
{{/if}}
|
||||
|
||||
### Security (UI layer)
|
||||
- No credentials shown in plaintext
|
||||
- Sensitive screens wrapped with screenshot prevention
|
||||
|
||||
## Output
|
||||
List each violation with:
|
||||
- Location (file:widget)
|
||||
- What's wrong
|
||||
- How to fix it
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// arch-guard.ts — Pre-commit hook: enforces Clean Architecture import boundaries
|
||||
// Generated for TestApp (Clean Architecture architecture)
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
interface ArchRule {
|
||||
sourcePattern: RegExp;
|
||||
forbiddenImports: RegExp[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Architecture-specific rules for Clean Architecture
|
||||
const ARCH_RULES: ArchRule[] = [
|
||||
...getArchRules()
|
||||
];
|
||||
|
||||
function getArchRules(): ArchRule[] {
|
||||
const arch = 'clean';
|
||||
|
||||
if (arch === 'clean') {
|
||||
return [
|
||||
{
|
||||
sourcePattern: /features\/\w+\/domain\//,
|
||||
forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//],
|
||||
message: 'domain/ must not import from data/ or presentation/',
|
||||
},
|
||||
{
|
||||
sourcePattern: /features\/\w+\/presentation\//,
|
||||
forbiddenImports: [/features\/\w+\/data\//],
|
||||
message: 'presentation/ must not import from data/ directly (use domain interfaces)',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (arch === 'feature_first') {
|
||||
return [
|
||||
{
|
||||
// A feature file must not import from another feature
|
||||
sourcePattern: /features\/(\w+)\//,
|
||||
forbiddenImports: [], // checked dynamically below
|
||||
message: 'Features must not import from other feature folders',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (arch === 'mvvm') {
|
||||
return [
|
||||
{
|
||||
sourcePattern: /viewmodels\//,
|
||||
forbiddenImports: [/package:flutter\//, /widgets\//],
|
||||
message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getChangedDartFiles(): string[] {
|
||||
try {
|
||||
const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' });
|
||||
return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function checkFile(filePath: string): string[] {
|
||||
const violations: string[] = [];
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const imports = content.match(/^import\s+'[^']+'/gm) ?? [];
|
||||
|
||||
for (const rule of ARCH_RULES) {
|
||||
if (!rule.sourcePattern.test(filePath)) continue;
|
||||
|
||||
// Feature-first cross-feature check
|
||||
if ('clean' === 'feature_first') {
|
||||
const srcFeatureMatch = filePath.match(/features\/(\w+)\//);
|
||||
if (srcFeatureMatch) {
|
||||
const srcFeature = srcFeatureMatch[1];
|
||||
for (const imp of imports) {
|
||||
const impFeatureMatch = imp.match(/features\/(\w+)\//);
|
||||
if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) {
|
||||
violations.push(
|
||||
`${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` +
|
||||
` ${YELLOW}${rule.message}${RESET}\n` +
|
||||
` Import: ${imp}\n` +
|
||||
` Fix: Move shared code to core/ or shared/`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Standard forbidden import check
|
||||
for (const imp of imports) {
|
||||
for (const forbidden of rule.forbiddenImports) {
|
||||
if (forbidden.test(imp)) {
|
||||
violations.push(
|
||||
`${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` +
|
||||
` ${YELLOW}${rule.message}${RESET}\n` +
|
||||
` Import: ${imp}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
const changedFiles = getChangedDartFiles();
|
||||
if (changedFiles.length === 0) {
|
||||
console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for Clean Architecture violations...`);
|
||||
|
||||
const allViolations: string[] = [];
|
||||
for (const file of changedFiles) {
|
||||
allViolations.push(...checkFile(file));
|
||||
}
|
||||
|
||||
if (allViolations.length > 0) {
|
||||
console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`);
|
||||
for (const v of allViolations) console.error(v + '\n');
|
||||
console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`);
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// flutter-analyze.ts — Pre-commit hook: runs dart analyze and dart format check
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
function run(cmd: string): { stdout: string; code: number } {
|
||||
try {
|
||||
const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
return { stdout, code: 0 };
|
||||
} catch (e: any) {
|
||||
return { stdout: e.stdout ?? e.message, code: e.status ?? 1 };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔍 Running flutter analyze...');
|
||||
const analyze = run('flutter analyze --no-congratulate');
|
||||
if (analyze.code !== 0) {
|
||||
console.error(`${RED}✘ flutter analyze failed:\n${analyze.stdout}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🎨 Checking dart format...');
|
||||
const format = run('dart format --output=none --set-exit-if-changed lib/ test/');
|
||||
if (format.code !== 0) {
|
||||
console.error(`${RED}✘ Unformatted files detected. Run: dart format lib/ test/${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✔ flutter analyze + dart format passed${RESET}`);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// grind-tests.ts — Pre-push hook: runs flutter test
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
console.log('🧪 Running flutter test...');
|
||||
try {
|
||||
execSync('flutter test --coverage', { stdio: 'inherit' });
|
||||
console.log(`${GREEN}✔ All tests passed${RESET}`);
|
||||
} catch {
|
||||
console.error(`${RED}✘ Tests failed. Fix failing tests before pushing.${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "pre-commit: flutter analyze",
|
||||
"command": "npx ts-node .cursor/hooks/flutter-analyze.ts",
|
||||
"events": ["pre-commit"]
|
||||
},
|
||||
{
|
||||
"name": "pre-commit: arch guard",
|
||||
"command": "npx ts-node .cursor/hooks/arch-guard.ts",
|
||||
"events": ["pre-commit"]
|
||||
},
|
||||
{
|
||||
"name": "pre-push: run tests",
|
||||
"command": "npx ts-node .cursor/hooks/grind-tests.ts",
|
||||
"events": ["pre-push"]
|
||||
}
|
||||
]
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
description: "Clean Architecture conventions for TestApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Clean Architecture — TestApp
|
||||
|
||||
## Layer structure
|
||||
```
|
||||
lib/features/[feature]/
|
||||
├── domain/
|
||||
│ ├── entities/ ← Pure Dart, no framework imports
|
||||
│ ├── repositories/ ← Abstract interfaces only
|
||||
│ └── usecases/ ← Single-responsibility business operations
|
||||
├── data/
|
||||
│ ├── datasources/ ← Remote (API) + Local (cache) implementations
|
||||
│ ├── models/ ← DTOs with fromJson/toJson (can use Freezed)
|
||||
│ └── repositories/ ← Implements domain/repositories interfaces
|
||||
└── presentation/
|
||||
├── bloc/ or notifiers/
|
||||
├── pages/
|
||||
└── widgets/
|
||||
```
|
||||
|
||||
## Import rules (STRICTLY ENFORCED by arch-guard hook)
|
||||
- presentation/ MUST NOT import from data/
|
||||
- domain/ MUST NOT import from data/ or presentation/
|
||||
- data/ CAN import from domain/ (implements interfaces)
|
||||
- Use dependency injection to invert data → domain dependency
|
||||
|
||||
## UseCase pattern
|
||||
```dart
|
||||
// One UseCase = one operation = one method
|
||||
class GetProductsUseCase {
|
||||
final ProductRepository _repository;
|
||||
const GetProductsUseCase(this._repository);
|
||||
|
||||
Future<Either<AppError, List<Product>>> call(ProductFilter filter) =>
|
||||
_repository.getProducts(filter);
|
||||
}
|
||||
```
|
||||
|
||||
## Entity rules
|
||||
- Entities are pure Dart — zero Flutter or framework imports
|
||||
- Entities are immutable — use `final` fields + factory constructors
|
||||
- Entities NEVER have `fromJson`/`toJson` — that belongs in the data layer model
|
||||
|
||||
## Repository rules
|
||||
- Domain defines the **interface** (abstract class)
|
||||
- Data layer **implements** it
|
||||
- Use `Either<Failure, T>` or `Result<T>` return types — never throw in domain
|
||||
|
||||
## Dependency injection
|
||||
- Use `injectable` + `get_it` if `codegen` includes `injectable`
|
||||
- All UseCases injected into BLoC/Notifier via constructor
|
||||
- `DataSource → Repository → UseCase → Bloc` dependency direction
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
description: "Firebase conventions for TestApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Firebase Standards — TestApp
|
||||
|
||||
## Firestore
|
||||
- Collection names: `camelCase` plural — `users`, `products`, `orderItems`
|
||||
- Document IDs: use Firebase auto-IDs unless a natural key exists
|
||||
- **Streams vs Futures**: use `snapshots()` for live data, `get()` for one-time reads
|
||||
- Always handle `FirebaseException` explicitly — catch by `e.code` not generic `Exception`
|
||||
- Paginate large collections with `startAfterDocument` — never fetch unbounded collections
|
||||
|
||||
```dart
|
||||
// ✅ Stream-based real-time listener
|
||||
Stream<List<Product>> watchProducts() {
|
||||
return _firestore.collection('products')
|
||||
.where('isActive', isEqualTo: true)
|
||||
.snapshots()
|
||||
.map((snap) => snap.docs.map(Product.fromDoc).toList());
|
||||
}
|
||||
|
||||
// ✅ Handle FirebaseException by code
|
||||
try {
|
||||
await _firestore.collection('orders').add(order.toMap());
|
||||
} on FirebaseException catch (e) {
|
||||
switch (e.code) {
|
||||
case 'permission-denied': throw AppError.authError('Insufficient permissions');
|
||||
case 'unavailable': throw AppError.networkError();
|
||||
default: throw AppError.unknown(e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Firebase Auth
|
||||
- Always use `authStateChanges()` stream — never cache auth state locally
|
||||
- Handle all error codes: `user-not-found`, `wrong-password`, `email-already-in-use`, `network-request-failed`
|
||||
- Sign-out: clear all local state AND call `FirebaseAuth.instance.signOut()`
|
||||
|
||||
## Cloud Functions
|
||||
- Call via `FirebaseFunctions.instance.httpsCallable('functionName')`
|
||||
- Handle `FirebaseFunctionsException` with `.code` and `.message`
|
||||
- Never expose internal errors to client — functions return structured error responses
|
||||
|
||||
## Security (complement to security-standards.mdc)
|
||||
- Firestore Security Rules must be tested with the emulator before deploying
|
||||
- No `allow read, write: if true` — even in development
|
||||
- Rule coverage: every collection must have explicit rules
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
description: "Freezed code generation conventions for TestApp — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Freezed Standards — TestApp
|
||||
|
||||
## When to use Freezed
|
||||
- All domain entities and data models — use `@freezed`
|
||||
- Union types / sealed classes — use `@freezed` with multiple constructors
|
||||
- BLoC states and events — use `@freezed`
|
||||
|
||||
## Model pattern
|
||||
```dart
|
||||
@freezed
|
||||
class Product with _$Product {
|
||||
const factory Product({
|
||||
required String id,
|
||||
required String name,
|
||||
required double price,
|
||||
@Default(0) int stockCount,
|
||||
String? imageUrl,
|
||||
}) = _Product;
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
|
||||
}
|
||||
```
|
||||
|
||||
## Union type pattern
|
||||
```dart
|
||||
@freezed
|
||||
sealed class ProductState with _$ProductState {
|
||||
const factory ProductState.initial() = ProductInitial;
|
||||
const factory ProductState.loading() = ProductLoading;
|
||||
const factory ProductState.loaded(List<Product> products) = ProductLoaded;
|
||||
const factory ProductState.error(String message) = ProductError;
|
||||
}
|
||||
|
||||
// Usage — exhaustive switch
|
||||
final widget = state.when(
|
||||
initial: () => const SizedBox.shrink(),
|
||||
loading: () => const ProductShimmer(),
|
||||
loaded: (products) => ProductList(products: products),
|
||||
error: (msg) => ErrorWidget(message: msg),
|
||||
);
|
||||
```
|
||||
|
||||
## Critical rules
|
||||
- **NEVER** edit `.freezed.dart` or `.g.dart` files — they are generated
|
||||
- Run `dart run build_runner build --delete-conflicting-outputs` after changes
|
||||
- Run `dart run build_runner watch` during development
|
||||
- Add `*.freezed.dart` and `*.g.dart` to `.gitignore` (or commit them — team must decide)
|
||||
- Always define `copyWith` via Freezed — never write manual `copyWith`
|
||||
|
||||
## Patterns to avoid
|
||||
```dart
|
||||
// ❌ Manual copyWith — replaced by Freezed
|
||||
Product copyWith({String? name, double? price}) => Product(
|
||||
id: id, name: name ?? this.name, price: price ?? this.price,
|
||||
);
|
||||
|
||||
// ✅ Let Freezed generate it
|
||||
product.copyWith(name: 'New Name', price: 9.99)
|
||||
```
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Global error handling strategy for TestApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Global Error Handling — TestApp
|
||||
|
||||
## Flutter error boundaries
|
||||
Configure in `main.dart` — do this ONCE and never bypass it:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
// TODO: Send to crash reporter (Sentry/Firebase Crashlytics)
|
||||
crashReporter.recordFlutterError(details);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
// TODO: Send to crash reporter
|
||||
crashReporter.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
## Error types hierarchy
|
||||
Define a sealed class for domain errors — never throw raw exceptions in business logic:
|
||||
|
||||
```dart
|
||||
sealed class AppError {
|
||||
const AppError();
|
||||
}
|
||||
class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); }
|
||||
class AuthError extends AppError { final String reason; const AuthError(this.reason); }
|
||||
class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); }
|
||||
class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); }
|
||||
```
|
||||
|
||||
## Repository layer
|
||||
- Wrap ALL external calls in try/catch and return `Either<AppError, T>` or `Result<T>`
|
||||
- NEVER let raw exceptions bubble to the presentation layer
|
||||
- Log at the repository layer, not the UI layer
|
||||
|
||||
## Presentation layer
|
||||
- Every async widget MUST handle error state explicitly — no silent failures
|
||||
- Show user-friendly error messages: map `AppError` subtype → readable string
|
||||
- Provide a "Try again" action for recoverable errors (network, timeout)
|
||||
- For fatal errors (auth expired), redirect to login — never show a dead screen
|
||||
|
||||
## Crash reporting
|
||||
- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta
|
||||
- Set `user` context on crash reporter after login (id only, no PII)
|
||||
- Add `breadcrumbs` for key user actions to aid reproduction
|
||||
|
||||
## Logging strategy
|
||||
```
|
||||
Level | Use case
|
||||
DEBUG | Development only (strip from release)
|
||||
INFO | Key user flows (login, purchase, etc.)
|
||||
WARNING | Recoverable errors, fallbacks used
|
||||
ERROR | Unrecoverable errors, unexpected states
|
||||
```
|
||||
- Use `logger` package — never bare `print()`
|
||||
- Logger instance per class: `final _log = Logger('ClassName');`
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
description: "Android-specific conventions for TestApp — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Android Platform Standards — TestApp
|
||||
|
||||
## Target SDK
|
||||
- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps
|
||||
- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise
|
||||
- Update `android/app/build.gradle` when bumping target SDK
|
||||
|
||||
## Permissions
|
||||
- Declare only needed permissions in `AndroidManifest.xml`
|
||||
- Runtime permissions: use `permission_handler` — never skip rationale step
|
||||
- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`)
|
||||
- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services
|
||||
|
||||
## Adaptive icons
|
||||
- Provide both foreground and background layers in `android/app/src/main/res/`
|
||||
- Test on dark theme, coloured theme, and themed icons (Android 13+)
|
||||
|
||||
## Deep links / App Links
|
||||
- Verify domain ownership: `.well-known/assetlinks.json` on your server
|
||||
- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://TestApp.com/products/123"`
|
||||
|
||||
## ProGuard / R8
|
||||
- Obfuscation rules in `android/app/proguard-rules.pro`
|
||||
- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes
|
||||
- Test release build thoroughly — obfuscation can break reflection-based code
|
||||
|
||||
## Notifications
|
||||
- Create notification channels before showing any notification (Android 8+)
|
||||
- Notification icons must be monochrome on Android 5+
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: "iOS-specific conventions for TestApp — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# iOS Platform Standards — TestApp
|
||||
|
||||
## Platform-specific imports
|
||||
- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code
|
||||
- iOS-only plugins: declare in `pubspec.yaml` with platform filter
|
||||
- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io`
|
||||
|
||||
## iOS-specific requirements
|
||||
- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`)
|
||||
- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024
|
||||
- Required `Info.plist` keys before using:
|
||||
- Camera: `NSCameraUsageDescription`
|
||||
- Photo library: `NSPhotoLibraryUsageDescription`
|
||||
- Location: `NSLocationWhenInUseUsageDescription`
|
||||
- Notifications: handled via `permission_handler`
|
||||
|
||||
## Push notifications (iOS)
|
||||
- Configure APNs certificates in Xcode signing & capabilities
|
||||
- Request permission with `permission_handler` — show rationale screen first
|
||||
- Handle foreground vs background vs terminated app states separately
|
||||
- Test on a physical device — iOS simulator does not support push
|
||||
|
||||
## Safe area
|
||||
- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding`
|
||||
- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators
|
||||
|
||||
## App Store compliance
|
||||
- Screenshot: use `ScreenshotController` to exclude sensitive screens
|
||||
- Sign in with Apple: required if any third-party social login is offered
|
||||
- IPv6 compatibility required — no IPv4-only network code
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
description: "GoRouter conventions for TestApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GoRouter Standards — TestApp
|
||||
|
||||
## Typed routes (mandatory)
|
||||
```dart
|
||||
// Define typed routes — never use string paths directly in navigation calls
|
||||
@TypedGoRoute<HomeRoute>(path: '/')
|
||||
class HomeRoute extends GoRouteData {
|
||||
const HomeRoute();
|
||||
@override Widget build(BuildContext ctx, GoRouterState state) => const HomeScreen();
|
||||
}
|
||||
|
||||
@TypedGoRoute<ProductRoute>(path: '/products/:id')
|
||||
class ProductRoute extends GoRouteData {
|
||||
final String id;
|
||||
const ProductRoute({required this.id});
|
||||
@override Widget build(BuildContext ctx, GoRouterState state) => ProductScreen(id: id);
|
||||
}
|
||||
|
||||
// Navigate with type safety
|
||||
const ProductRoute(id: product.id).go(context); // ✅
|
||||
context.go('/products/${product.id}'); // ❌ — don't do this
|
||||
```
|
||||
|
||||
## Auth guard
|
||||
```dart
|
||||
// Redirect logic in router config
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = ref.read(authProvider).isAuthenticated;
|
||||
final isLoginRoute = state.matchedLocation == '/login';
|
||||
if (!isLoggedIn && !isLoginRoute) return '/login';
|
||||
if (isLoggedIn && isLoginRoute) return '/';
|
||||
return null;
|
||||
},
|
||||
```
|
||||
|
||||
## Shell routes for bottom navigation
|
||||
```dart
|
||||
ShellRoute(
|
||||
builder: (ctx, state, child) => MainScaffold(child: child),
|
||||
routes: [
|
||||
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
|
||||
GoRoute(path: '/search', builder: (_, __) => const SearchScreen()),
|
||||
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Deep links
|
||||
- Register URL schemes in `Info.plist` (iOS) and `AndroidManifest.xml`
|
||||
- Test deep links with: `adb shell am start -a android.intent.action.VIEW -d "app://com.test.testapp/products/123"`
|
||||
- Handle `GoRouter.of(context).routerDelegate.currentConfiguration` for dynamic links
|
||||
|
||||
## Rules
|
||||
- **NEVER** use `Navigator.push/pop` — use `context.go()`, `context.push()`, `context.pop()`
|
||||
- All routes declared in one file: `lib/core/routing/app_router.dart`
|
||||
- `BlocProvider`s for route-level blocs created inside the `builder` of each route
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
---
|
||||
description: "Security standards for TestApp — ALWAYS APPLIED on every file write"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Security Standards — TestApp
|
||||
> **Pillar 5**: Security is an always-on rule, not just a reactive agent.
|
||||
> These rules apply to EVERY file write, regardless of feature or context.
|
||||
|
||||
## Credential & secret management
|
||||
- **NEVER** hardcode API keys, tokens, or secrets in source code
|
||||
- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example`
|
||||
- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets)
|
||||
- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data
|
||||
- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds
|
||||
|
||||
## Authentication & sessions
|
||||
- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB
|
||||
- Implement token refresh with retry logic — never let a 401 show a raw error to the user
|
||||
- Certificate pinning: required for production builds on Firebase Auth — use `dio_pinning_interceptor`
|
||||
- Biometric re-auth: require for any transaction > defined threshold
|
||||
|
||||
## Data & privacy
|
||||
- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters
|
||||
- Sanitize all user inputs before sending to backend
|
||||
- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails
|
||||
- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions
|
||||
|
||||
## Network security
|
||||
- All API calls use HTTPS — no http:// in production
|
||||
- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true`
|
||||
- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)`
|
||||
- Rate-limit sensitive endpoints: auth, OTP, password reset
|
||||
|
||||
## Dependency security
|
||||
- Run `dart pub outdated` monthly — flag packages with known CVEs
|
||||
- Never use a package with <100 pub points without explicit tech lead approval
|
||||
- Pin critical security packages to exact versions in `pubspec.yaml`
|
||||
|
||||
## Secure coding patterns
|
||||
```dart
|
||||
// ✅ Correct: secure token storage
|
||||
final storage = FlutterSecureStorage();
|
||||
await storage.write(key: 'access_token', value: token);
|
||||
|
||||
// ❌ Wrong: SharedPreferences for sensitive data
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString('access_token', token); // NEVER do this
|
||||
|
||||
// ✅ Correct: PII-safe logging
|
||||
logger.info('User authenticated: userId=${user.id}'); // ok — id, not email
|
||||
logger.debug('Payment processed: orderId=$orderId'); // ok — no card data
|
||||
|
||||
// ❌ Wrong: PII in logs
|
||||
logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER
|
||||
|
||||
// ✅ Correct: --dart-define for secrets (build arg, not in code)
|
||||
// flutter build apk --dart-define=API_KEY=$API_KEY
|
||||
const apiKey = String.fromEnvironment('API_KEY'); // acceptable
|
||||
|
||||
// ✅ Correct: certificate pinning with Dio
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
|
||||
final client = HttpClient();
|
||||
client.badCertificateCallback = (cert, host, port) => false; // strict
|
||||
return client;
|
||||
};
|
||||
```
|
||||
|
||||
## Deep linking & intent security
|
||||
- Validate all incoming deep link parameters — never trust raw URL params
|
||||
- Use `app_links` package for verified deep link handling
|
||||
- Restrict URL schemes to known patterns — reject unknown schemes
|
||||
- For OAuth callbacks: validate `state` parameter to prevent CSRF
|
||||
|
||||
## Storage security
|
||||
- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher`
|
||||
- Clear sensitive data from memory after use (set to null, trigger GC)
|
||||
- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints)
|
||||
- Use `SecureRandom` for nonce/token generation — never `Random()`
|
||||
|
||||
## Code obfuscation & binary protection
|
||||
```yaml
|
||||
# android/app/build.gradle — release config
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
```
|
||||
```bash
|
||||
# Flutter release build with obfuscation
|
||||
flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/
|
||||
flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/
|
||||
```
|
||||
|
||||
## Release checklist
|
||||
Before every production release, verify:
|
||||
- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`)
|
||||
- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths)
|
||||
- [ ] Obfuscation enabled in release build config
|
||||
- [ ] Certificate pinning active and tested on both platforms
|
||||
- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`)
|
||||
- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys
|
||||
- [ ] Network calls all HTTPS — scan for `http://` in lib/
|
||||
- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'`
|
||||
- [ ] Deep link parameters validated
|
||||
- [ ] App transport security / network security config reviewed
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
description: "BLoC / Cubit conventions for TestApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# BLoC / Cubit Standards — TestApp
|
||||
|
||||
## When to use BLoC vs Cubit
|
||||
- **Cubit**: simple state with no meaningful event history (toggle, counter, pagination)
|
||||
- **BLoC**: event-driven flows where event history or transitions matter (auth, checkout, form wizard)
|
||||
|
||||
## Event and State classes
|
||||
```dart
|
||||
// Events — sealed class (exhaustive switch)
|
||||
sealed class AuthEvent { const AuthEvent(); }
|
||||
final class AuthLoginRequested extends AuthEvent {
|
||||
final String email, password;
|
||||
const AuthLoginRequested({required this.email, required this.password});
|
||||
}
|
||||
final class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); }
|
||||
|
||||
// States — sealed class, immutable
|
||||
sealed class AuthState { const AuthState(); }
|
||||
final class AuthInitial extends AuthState { const AuthInitial(); }
|
||||
final class AuthLoading extends AuthState { const AuthLoading(); }
|
||||
final class AuthAuthenticated extends AuthState {
|
||||
final User user;
|
||||
const AuthAuthenticated(this.user);
|
||||
}
|
||||
final class AuthUnauthenticated extends AuthState { const AuthUnauthenticated(); }
|
||||
final class AuthFailure extends AuthState {
|
||||
final String message;
|
||||
const AuthFailure(this.message);
|
||||
}
|
||||
```
|
||||
|
||||
## BlocProvider placement
|
||||
- Create `BlocProvider` at the **route level** (in GoRouter route definitions)
|
||||
- `MultiBlocProvider` at route level for screens needing multiple blocs
|
||||
- **NEVER** create `BlocProvider` inside a widget's `build()` method
|
||||
|
||||
## Usage rules
|
||||
- **NEVER** call `bloc.add()` inside `build()` — only in gesture callbacks or `initState()`
|
||||
- Use `BlocConsumer` only when BOTH `listen` + `build` logic are needed
|
||||
- Use `BlocSelector` when only a subset of state triggers a rebuild
|
||||
- Every BLoC must override `close()` and cancel `StreamSubscription`s
|
||||
|
||||
## BlocBuilder patterns
|
||||
```dart
|
||||
BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) => switch (state) {
|
||||
AuthInitial() => const SizedBox.shrink(),
|
||||
AuthLoading() => const LoadingIndicator(),
|
||||
AuthAuthenticated(user: final u) => HomeScreen(user: u),
|
||||
AuthUnauthenticated() => const LoginScreen(),
|
||||
AuthFailure(message: final m) => ErrorScreen(message: m),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## File locations in TestApp
|
||||
- `lib/features/[feature]/presentation/bloc/[feature]_bloc.dart`
|
||||
- `lib/features/[feature]/presentation/bloc/[feature]_event.dart`
|
||||
- `lib/features/[feature]/presentation/bloc/[feature]_state.dart`
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: "BLoC testing conventions for TestApp"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# BLoC Testing Standards — TestApp
|
||||
|
||||
## Test pattern (bloc_test)
|
||||
```dart
|
||||
// blocTest<MyCubit, MyState>(description, build: () => MyCubit(), act: (c) => c.method(), expect: () => [MyState()])
|
||||
|
||||
void main() {
|
||||
late AuthBloc authBloc;
|
||||
late MockAuthRepository mockRepo;
|
||||
|
||||
setUp(() {
|
||||
mockRepo = MockAuthRepository();
|
||||
authBloc = AuthBloc(repository: mockRepo);
|
||||
});
|
||||
|
||||
tearDown(() => authBloc.close());
|
||||
|
||||
group('AuthBloc', () {
|
||||
blocTest<AuthBloc, AuthState>(
|
||||
'emits [Loading, Authenticated] when login succeeds',
|
||||
build: () {
|
||||
when(() => mockRepo.login(any(), any()))
|
||||
.thenAnswer((_) async => const Right(User(id: '1', email: 'test@test.com')));
|
||||
return authBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(const AuthLoginRequested(email: 'test@test.com', password: 'pass')),
|
||||
expect: () => [
|
||||
const AuthLoading(),
|
||||
isA<AuthAuthenticated>(),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<AuthBloc, AuthState>(
|
||||
'emits [Loading, Failure] when login fails',
|
||||
build: () {
|
||||
when(() => mockRepo.login(any(), any()))
|
||||
.thenAnswer((_) async => const Left(AuthError('Invalid credentials')));
|
||||
return authBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(const AuthLoginRequested(email: 'bad', password: 'bad')),
|
||||
expect: () => [
|
||||
const AuthLoading(),
|
||||
const AuthFailure('Invalid credentials'),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Use `mocktail` for mocking — never `mockito`
|
||||
- Every BLoC test file: `test/features/[feature]/[feature]_bloc_test.dart`
|
||||
- Coverage requirement: all state transitions must be tested
|
||||
- Use `Given/When/Then` naming in test descriptions
|
||||
- Test error paths as thoroughly as success paths
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
description: "Core Flutter conventions for TestApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Flutter Core Standards — TestApp
|
||||
|
||||
## Const and performance
|
||||
- Use `const` constructors wherever possible — compile-time guarantee of no rebuild
|
||||
- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)`
|
||||
- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled
|
||||
|
||||
## Null safety
|
||||
- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment
|
||||
- Prefer `??`, `?.`, and `if (x != null)` guards
|
||||
- Use `required` for all non-nullable named parameters
|
||||
- Never use `late` without a guarantee of initialisation before first access
|
||||
|
||||
## Widget lifecycle
|
||||
- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers
|
||||
- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget`
|
||||
- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)`
|
||||
|
||||
## Naming conventions
|
||||
- Files: `snake_case.dart`
|
||||
- Classes: `PascalCase`
|
||||
- Variables/functions: `camelCase`
|
||||
- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants
|
||||
- Private members: `_camelCase`
|
||||
|
||||
## Imports
|
||||
- Order: dart: → package: → relative
|
||||
- Use relative imports within a feature; absolute for cross-feature
|
||||
- Never import a feature's internal files from outside that feature
|
||||
|
||||
## Code quality
|
||||
- Max function length: 40 lines. Extract widgets and helpers aggressively
|
||||
- No `print()` in production code — use a logging package
|
||||
- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this`
|
||||
- Run `dart format` before every commit
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Project context for TestApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Project Context — TestApp
|
||||
|
||||
## Project identity
|
||||
- **Name:** TestApp
|
||||
- **Package:** com.test.testapp
|
||||
- **Description:** Test app for golden tests
|
||||
- **Scale:** medium
|
||||
|
||||
## Technology stack
|
||||
- **State management:** BLoC / Cubit
|
||||
- **Architecture:** Clean Architecture
|
||||
- **Routing:** GoRouter
|
||||
- **Backend:** Firebase
|
||||
- **Auth:** Firebase Auth
|
||||
- **Platforms:** ios, android
|
||||
- **Code generation:** freezed
|
||||
|
||||
## Feature modules
|
||||
auth, home, products
|
||||
|
||||
## Special capabilities
|
||||
|
||||
|
||||
## Environments / flavors
|
||||
- Flavors: dev, prod
|
||||
- CI/CD: GitHub Actions
|
||||
|
||||
## Design & API references
|
||||
- Design source: none
|
||||
- API docs: none at ``
|
||||
|
||||
## Code references
|
||||
### Git repositories
|
||||
_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context.
|
||||
|
||||
### Local paths
|
||||
_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant.
|
||||
|
||||
## Product UX / themes & roles
|
||||
- **Theme variants:** light, dark
|
||||
- **Roles:** Not enabled (`app_context.roles_enabled: false`).
|
||||
|
||||
|
||||
## Reviews — which rule owns what
|
||||
- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only)
|
||||
- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals)
|
||||
- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule
|
||||
|
||||
## Architecture boundaries
|
||||
- presentation/ MUST NOT import from data/
|
||||
- domain/ MUST NOT import from data/ or presentation/
|
||||
- data/ CAN import from domain/ (implements interfaces)
|
||||
- Use dependency injection to invert data → domain dependency
|
||||
|
||||
## When generating code for this project
|
||||
1. Always use BLoC / Cubit patterns — never suggest alternatives
|
||||
2. Always follow Clean Architecture folder structure
|
||||
3. Always use GoRouter for navigation — never `Navigator.push` directly
|
||||
4. Always target platforms: ios, android
|
||||
5. If code generation tools are used (freezed), follow their conventions
|
||||
6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets
|
||||
7. No user-facing string literals in widgets — l10n or shared constants per localization rule
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description: "UI/UX standards for TestApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# UI / UX Standards — TestApp
|
||||
|
||||
## Theme & design tokens (single source of truth)
|
||||
- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only.
|
||||
- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s).
|
||||
- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI.
|
||||
- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius.
|
||||
|
||||
## Loading states
|
||||
- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms
|
||||
- Use `shimmer` package with a shimmer that matches the final layout shape
|
||||
- Never show a blank screen during loading — skeleton must fill the same space as the content
|
||||
|
||||
## Empty states
|
||||
- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA
|
||||
- Empty state is different from error state — never reuse the same widget for both
|
||||
- Empty state copy: positive framing ("No items yet — add your first one")
|
||||
|
||||
## Error states
|
||||
- Every async failure MUST show: error message + retry button
|
||||
- Never swallow errors silently
|
||||
- Error text: user-friendly, never expose stack traces or raw API messages
|
||||
|
||||
## Navigation & transitions
|
||||
- Use `IndexedStack` for bottom nav tabs — preserves scroll position
|
||||
- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))`
|
||||
- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets
|
||||
|
||||
## Responsive layout
|
||||
- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values
|
||||
- Minimum touch target: 48×48 logical pixels (Material guideline)
|
||||
- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum
|
||||
|
||||
## Haptics
|
||||
- Use `HapticFeedback.lightImpact()` on primary CTAs
|
||||
- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions
|
||||
- Never add haptics to destructive actions without confirmation
|
||||
|
||||
## Accessibility
|
||||
- All interactive widgets must have a `Semantics` label or `tooltip`
|
||||
- Minimum contrast ratio: 4.5:1 (WCAG AA)
|
||||
- Test with TalkBack / VoiceOver before each release
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Create Build Flavor — TestApp
|
||||
|
||||
Creates a new build flavor/environment. Current flavors: dev, prod.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a new flavor called [flavor_name]
|
||||
```
|
||||
|
||||
## What gets created
|
||||
1. Android: new `productFlavors` block in `android/app/build.gradle`
|
||||
2. iOS: new scheme + configuration in Xcode (instructions provided)
|
||||
3. `lib/core/config/[flavor_name]_config.dart`
|
||||
4. `.env.[flavor_name]` template
|
||||
5. GitHub Actions pipeline update
|
||||
|
||||
## CI/CD: GitHub Actions
|
||||
Generate the pipeline config snippet for the new flavor in GitHub Actions format.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Deploy — TestApp
|
||||
|
||||
Guides through deployment to GitHub Actions for any of the flavors: dev, prod.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Deploy [flavor] to [store/environment]
|
||||
```
|
||||
|
||||
## GitHub Actions pipeline
|
||||
The AI will generate or update the github_actions configuration file for:
|
||||
- Build signing
|
||||
- Running tests
|
||||
- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod)
|
||||
|
||||
## Pre-deploy checklist
|
||||
- [ ] Version bump in `pubspec.yaml`
|
||||
- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/`
|
||||
- [ ] No debug flags in production code
|
||||
- [ ] Security checklist from `security-standards.mdc` passed
|
||||
- [ ] `cursor_gen --validate` passes
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Generate Tests — TestApp
|
||||
|
||||
Generates comprehensive unit, widget, and integration tests for **BLoC / Cubit** patterns.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Generate tests for [ClassName or file path]
|
||||
```
|
||||
|
||||
## Test generation process
|
||||
1. Read the source file completely
|
||||
2. Identify all testable units (public methods, state transitions, UI states)
|
||||
3. Generate tests following this pattern:
|
||||
```
|
||||
blocTest<MyCubit, MyState>(description, build: () => MyCubit(), act: (c) => c.method(), expect: () => [MyState()])
|
||||
```
|
||||
4. Create mocks with `mocktail` for all dependencies
|
||||
5. Place test file at `test/[mirror of source path]_test.dart`
|
||||
|
||||
## Coverage targets
|
||||
- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum**
|
||||
- Widget tests: all three states (loading/error/data) must be tested
|
||||
- E2E: only critical user flows
|
||||
|
||||
## Test file structure
|
||||
```dart
|
||||
void main() {
|
||||
// Setup
|
||||
group('[ClassName]', () {
|
||||
// Happy path tests
|
||||
group('success cases', () { ... });
|
||||
// Error path tests
|
||||
group('error cases', () { ... });
|
||||
// Edge cases
|
||||
group('edge cases', () { ... });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Naming convention
|
||||
`'given [precondition], when [action], then [expected outcome]'`
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Scaffold Feature — TestApp
|
||||
|
||||
Scaffolds a complete new feature module following **Clean Architecture** architecture with **BLoC / Cubit** state management.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a feature called [feature_name] with [description]
|
||||
```
|
||||
|
||||
## What gets generated
|
||||
|
||||
### For Clean Architecture architecture:
|
||||
The AI will create all necessary files for the `[feature_name]` feature following Clean Architecture patterns.
|
||||
|
||||
### File structure to create:
|
||||
|
||||
**Clean Architecture:**
|
||||
```
|
||||
lib/features/[feature_name]/
|
||||
domain/
|
||||
entities/[feature_name].dart
|
||||
repositories/[feature_name]_repository.dart
|
||||
usecases/
|
||||
get_[feature_name]_usecase.dart
|
||||
create_[feature_name]_usecase.dart
|
||||
data/
|
||||
models/[feature_name]_dto.dart
|
||||
datasources/[feature_name]_remote_datasource.dart
|
||||
repositories/[feature_name]_repository_impl.dart
|
||||
presentation/
|
||||
[state_files]/ ← based on BLoC / Cubit
|
||||
pages/[feature_name]_page.dart
|
||||
widgets/
|
||||
```
|
||||
|
||||
**Feature-First:**
|
||||
```
|
||||
lib/features/[feature_name]/
|
||||
[feature_name]_screen.dart
|
||||
[feature_name]_provider.dart ← or [feature_name]_bloc.dart
|
||||
[feature_name]_repository.dart
|
||||
[feature_name]_model.dart
|
||||
widgets/
|
||||
```
|
||||
|
||||
## State management boilerplate
|
||||
|
||||
### For BLoC / Cubit:
|
||||
Generate the appropriate state management files with:
|
||||
- Initial/loading/success/error states
|
||||
- All necessary events (for BLoC)
|
||||
- Repository connection
|
||||
- Dependency injection registration
|
||||
|
||||
## Steps the AI takes
|
||||
1. Ask: "What is the feature name and brief description?"
|
||||
2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)"
|
||||
3. Generate all files with correct imports and patterns
|
||||
4. Add the feature to the DI container
|
||||
5. Add the route to GoRouter router
|
||||
6. Create a placeholder test file
|
||||
|
||||
## Code generation
|
||||
{{#if codegen_freezed}}
|
||||
- Generate Freezed model: run `dart run build_runner build` after scaffolding
|
||||
{{/if}}
|
||||
{{#if codegen_injectable}}
|
||||
- Register in injectable: add `@lazySingleton` to repository
|
||||
{{/if}}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Scaffold Screen — TestApp
|
||||
|
||||
Creates a complete screen widget with all states handled, following **BLoC / Cubit** patterns.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a screen for [screen_name] that shows [content description]
|
||||
```
|
||||
|
||||
## Generated screen template
|
||||
|
||||
### BLoC / Cubit screen pattern:
|
||||
|
||||
**BLoC:**
|
||||
```dart
|
||||
class [ScreenName]Screen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<[Feature]Bloc, [Feature]State>(
|
||||
builder: (context, state) => switch (state) {
|
||||
[Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(),
|
||||
[Feature]Loaded(:final data) => [ScreenName]Content(data: data),
|
||||
[Feature]Empty() => const [ScreenName]EmptyState(),
|
||||
[Feature]Error(:final message) => [ScreenName]ErrorState(
|
||||
message: message,
|
||||
onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Riverpod:**
|
||||
```dart
|
||||
class [ScreenName]Screen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch([feature]Provider);
|
||||
return state.when(
|
||||
loading: () => const [ScreenName]Shimmer(),
|
||||
data: (data) => [ScreenName]Content(data: data),
|
||||
error: (e, _) => [ScreenName]ErrorState(error: e),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Required sub-widgets to generate
|
||||
1. `[ScreenName]Shimmer` — skeleton loading layout matching final content
|
||||
2. `[ScreenName]EmptyState` — illustration + headline + CTA button
|
||||
3. `[ScreenName]ErrorState` — error message + retry button
|
||||
4. `[ScreenName]Content` — the actual data display
|
||||
|
||||
## Platform considerations (ios, android)
|
||||
{{#if platform_web}}
|
||||
- Web: ensure no dart:io usage; test at 375px and 1280px widths
|
||||
{{/if}}
|
||||
{{#if platform_desktop}}
|
||||
- Desktop: add keyboard shortcut support for primary actions
|
||||
{{/if}}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: api-client-gen
|
||||
description: "Generates type-safe API clients for LegacyApp from openapi spec. Ask: '@api-client-gen generate client for /products endpoint'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, write_file, list_files]
|
||||
---
|
||||
|
||||
You are an API client generator for **LegacyApp**.
|
||||
API docs: `docs/api.yaml` (format: openapi)
|
||||
|
||||
## Generation steps
|
||||
1. Read the API spec at `docs/api.yaml`
|
||||
2. For the requested endpoint(s), generate:
|
||||
- Request DTO (`@JsonSerializable` or Freezed)
|
||||
- Response DTO (`@JsonSerializable` or Freezed)
|
||||
- Repository method with error handling
|
||||
- Dio/Retrofit client method (if Retrofit in codegen)
|
||||
|
||||
## Output structure
|
||||
```dart
|
||||
// data/models/product_dto.dart
|
||||
@freezed
|
||||
class ProductDto with _$ProductDto {
|
||||
factory ProductDto({...}) = _ProductDto;
|
||||
factory ProductDto.fromJson(Map<String, dynamic> json) => _$ProductDtoFromJson(json);
|
||||
}
|
||||
|
||||
// data/datasources/product_remote_datasource.dart
|
||||
class ProductRemoteDataSource {
|
||||
final Dio _dio;
|
||||
Future<List<ProductDto>> getProducts() async { ... }
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description: "Reviews LegacyApp code for GetX patterns, MVC boundaries, and REST API usage. Ask: 'Review this code' or '@code-reviewer check PR'"
|
||||
model: claude-opus-4-5
|
||||
context: auto
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a senior Flutter engineer reviewing code for **LegacyApp**.
|
||||
|
||||
## Your review checklist
|
||||
|
||||
### GetX patterns
|
||||
- Are state classes immutable and sealed?
|
||||
- Is state management correctly separated from UI logic?
|
||||
- Are streams/subscriptions properly cancelled in dispose()?
|
||||
- Check for anti-patterns specific to GetX
|
||||
|
||||
### MVC boundaries
|
||||
- View (Widget) MUST NOT contain business logic
|
||||
- Controller MUST NOT import Flutter widgets directly
|
||||
- Model MUST be plain Dart, no framework dependencies
|
||||
- Flag any violation of these import rules immediately
|
||||
|
||||
### REST API usage
|
||||
- Are all exceptions caught and mapped to domain errors?
|
||||
- Are streams vs futures used appropriately?
|
||||
- Are connections/subscriptions disposed correctly?
|
||||
|
||||
### Security (always check)
|
||||
- No hardcoded API keys or secrets
|
||||
- No PII logged to console or crash reporters
|
||||
- Sensitive data using flutter_secure_storage, not SharedPreferences
|
||||
- All user inputs validated before sending to backend
|
||||
|
||||
### General Flutter
|
||||
- `const` used where possible
|
||||
- `dispose()` overridden for all controllers/subscriptions
|
||||
- No `print()` in production paths
|
||||
- Loading/empty/error states all handled
|
||||
|
||||
## Output format
|
||||
For each issue found:
|
||||
```
|
||||
[SEVERITY: critical/major/minor] File:line — Issue description
|
||||
WHY: Why this matters
|
||||
FIX: Specific fix recommendation
|
||||
```
|
||||
|
||||
Severity guide:
|
||||
- **critical**: Security issue, data loss risk, crash potential
|
||||
- **major**: Incorrect pattern, boundary violation, missing error handling
|
||||
- **minor**: Style, naming, optimization opportunity
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: migration-agent
|
||||
description: "Migrates GetX controllers to Riverpod Notifiers for LegacyApp. Consult before adding any new GetX code — suggest Riverpod equivalent. Ask: '@migration-agent migrate [feature]'"
|
||||
model: claude-opus-4-5
|
||||
context: fork
|
||||
allowed-tools: [read_file, write_file, list_files]
|
||||
---
|
||||
|
||||
You are a Flutter migration specialist for **LegacyApp**.
|
||||
This project currently uses GetX (MVC). Your goal is incremental,
|
||||
safe migration to Riverpod without breaking existing features.
|
||||
|
||||
## Migration mapping
|
||||
| GetX | Riverpod equivalent |
|
||||
|------|---------------------|
|
||||
| `GetxController` with `.obs` | `AsyncNotifier` or `Notifier` |
|
||||
| `Obx()` | `ref.watch()` in `ConsumerWidget` |
|
||||
| `Get.find<Controller>()` | `ref.read(provider.notifier)` |
|
||||
| `Get.toNamed()` | `context.go()` (after GoRouter migration) |
|
||||
| `GetxController.onInit()` | `build()` method in `AsyncNotifier` |
|
||||
| `GetxController.onClose()` | `ref.onDispose()` |
|
||||
|
||||
## Migration process (feature by feature)
|
||||
1. **Read** the existing `GetxController` in full
|
||||
2. **Write tests** for the existing GetX version first (if none exist)
|
||||
3. **Map** `.obs` variables → state class fields
|
||||
4. **Write** the new `AsyncNotifier` with equivalent logic
|
||||
5. **Write tests** for the Riverpod version using `ProviderContainer`
|
||||
6. **Migrate** the View: `GetView<C>` → `ConsumerWidget`, `Obx()` → `ref.watch()`
|
||||
7. **Verify** all tests pass for both old and new
|
||||
8. **Remove** GetX code from that feature
|
||||
|
||||
## When NOT to migrate (mark with TODO: MIGRATE-LATER)
|
||||
- Controller shared across 5+ screens (high blast radius — plan separately)
|
||||
- Feature ships in the next sprint (postpone — don't hold up a release)
|
||||
- No tests exist AND you can't write them first (write GetX tests first)
|
||||
|
||||
## Output per migration
|
||||
1. New Riverpod provider file
|
||||
2. Updated ConsumerWidget screen file
|
||||
3. Test file for the new provider
|
||||
4. Diff showing what GetX code is removed
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: security-agent
|
||||
description: "Deep security review for LegacyApp. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'"
|
||||
model: claude-opus-4-5
|
||||
context: fork
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a mobile security expert conducting a deep review for **LegacyApp**.
|
||||
|
||||
> Note: This agent provides deep security analysis.
|
||||
> The `security-standards.mdc` rule provides always-on enforcement.
|
||||
> This agent is for detailed consultations on specific security concerns.
|
||||
|
||||
## Deep review focus areas
|
||||
|
||||
### Auth flow (JWT / REST Auth)
|
||||
- Token storage: is `flutter_secure_storage` used for ALL tokens?
|
||||
- Token refresh: is refresh handled atomically (no race condition)?
|
||||
- Session expiry: does the app handle 401 gracefully without data loss?
|
||||
- Certificate pinning: configured and tested?
|
||||
|
||||
### Data at rest
|
||||
- SQLite/Hive encryption: sensitive DBs encrypted?
|
||||
- Cache poisoning: cached API responses validated before use?
|
||||
- Keychain/Keystore usage for cryptographic keys
|
||||
|
||||
### Network security
|
||||
- All endpoints HTTPS — any http:// URLs?
|
||||
- Certificate validation — any `badCertificateCallback: true`?
|
||||
- Sensitive data in URL params/query strings?
|
||||
- Request/response logging in production? (must be off)
|
||||
|
||||
### Code injection risks
|
||||
- Dynamic code execution patterns
|
||||
- WebView usage — JavaScript interface security
|
||||
- Deep link parameter validation (no path traversal)
|
||||
|
||||
## Output format
|
||||
For each finding:
|
||||
```
|
||||
[RISK: Critical/High/Medium/Low]
|
||||
LOCATION: File / function
|
||||
ISSUE: Detailed description
|
||||
CVSS-like impact: Confidentiality/Integrity/Availability
|
||||
REMEDIATION: Specific code fix
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: test-writer
|
||||
description: "Writes GetX unit tests for LegacyApp. Ask: 'Write tests for [class]' or '@test-writer generate tests'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, write_file, list_files]
|
||||
---
|
||||
|
||||
You are a Flutter test engineer for **LegacyApp** using **GetX**.
|
||||
|
||||
## Test pattern to follow
|
||||
```dart
|
||||
Get.put(MyController()); final ctrl = Get.find<MyController>(); expect(ctrl.value, expected);
|
||||
```
|
||||
|
||||
## When asked to write tests:
|
||||
1. Read the source file completely
|
||||
2. Identify all public methods and state transitions
|
||||
3. Write tests for:
|
||||
- Happy path (successful operation)
|
||||
- Error path (failure, exception handling)
|
||||
- Edge cases (empty data, boundary values)
|
||||
4. Use `mocktail` for all mocking
|
||||
5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'`
|
||||
|
||||
## File placement
|
||||
- Unit tests: `test/features/[feature]/[file]_test.dart`
|
||||
- Widget tests: `test/features/[feature]/[screen]_widget_test.dart`
|
||||
- Coverage target: 80% minimum for business logic classes
|
||||
|
||||
## Output
|
||||
Write the complete test file, ready to run. Include all imports.
|
||||
After writing, run: `dart test path/to/test_file.dart`
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: ui-validator
|
||||
description: "Validates LegacyApp UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a UI/UX validator for **LegacyApp**.
|
||||
|
||||
## Validate every screen for:
|
||||
|
||||
### State coverage (GetX)
|
||||
- Loading state: shows shimmer (NOT spinner unless brief)
|
||||
- Empty state: shows illustration + CTA (NOT blank screen)
|
||||
- Error state: shows message + retry button (NOT toast-only)
|
||||
- Data state: renders content correctly
|
||||
|
||||
### Accessibility
|
||||
- All interactive widgets have semantic labels
|
||||
- Minimum touch targets: 48×48dp
|
||||
- Sufficient color contrast (4.5:1 minimum)
|
||||
|
||||
### Responsive layout
|
||||
- No hardcoded pixel widths
|
||||
- Tested at 375px and 414px viewport widths
|
||||
- `SafeArea` used correctly on iOS
|
||||
|
||||
### Platform-specific (ios, android)
|
||||
{{#if web}}
|
||||
- No dart:io imports in web-targeted code
|
||||
- PWA-compatible (no native-only APIs without fallbacks)
|
||||
{{/if}}
|
||||
{{#if ios}}
|
||||
- Safe area respected (notch, Dynamic Island)
|
||||
- iOS Human Interface Guidelines followed
|
||||
{{/if}}
|
||||
|
||||
### Security (UI layer)
|
||||
- No credentials shown in plaintext
|
||||
- Sensitive screens wrapped with screenshot prevention
|
||||
|
||||
## Output
|
||||
List each violation with:
|
||||
- Location (file:widget)
|
||||
- What's wrong
|
||||
- How to fix it
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: "MVC architecture conventions for LegacyApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# MVC Architecture — LegacyApp
|
||||
|
||||
## Layer responsibilities
|
||||
- **Model**: Data + business logic. Pure Dart.
|
||||
- **View**: Renders UI, observes Controller state. No business logic.
|
||||
- **Controller** (GetX): Connects Model ↔ View. Manages state transitions.
|
||||
|
||||
## Import rules
|
||||
- View (Widget) MUST NOT contain business logic
|
||||
- Controller MUST NOT import Flutter widgets directly
|
||||
- Model MUST be plain Dart, no framework dependencies
|
||||
|
||||
## Controller rules
|
||||
- Controllers are injected via `Binding`, never created in widgets
|
||||
- One Controller per feature screen (not per widget)
|
||||
- Controllers fetch data in `onInit()`, clean up in `onClose()`
|
||||
- All reactive state marked with `.obs`
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description: "REST API conventions for LegacyApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# REST API Standards — LegacyApp
|
||||
|
||||
## HTTP client setup (Dio)
|
||||
```dart
|
||||
// In core/network/dio_client.dart
|
||||
Dio createDioClient({required AppConfig config}) {
|
||||
final dio = Dio(BaseOptions(
|
||||
baseUrl: config.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
||||
));
|
||||
|
||||
dio.interceptors.addAll([
|
||||
AuthInterceptor(tokenStorage: getIt<TokenStorage>()),
|
||||
LoggingInterceptor(), // debug builds only
|
||||
RetryInterceptor(dio), // 3 retries on network errors
|
||||
]);
|
||||
return dio;
|
||||
}
|
||||
```
|
||||
|
||||
## DTOs (Data Transfer Objects)
|
||||
- DTOs live in the `data/` layer — **never** pass raw JSON maps to the domain layer
|
||||
- Use Freezed for DTOs if `codegen` includes `json_serializable` or `freezed`
|
||||
- `fromJson` factory must handle null fields gracefully — API response fields are not guaranteed
|
||||
|
||||
## Error handling
|
||||
```dart
|
||||
// Map HTTP errors → AppError domain types
|
||||
AppError _mapDioError(DioException e) => switch (e.type) {
|
||||
DioExceptionType.connectionTimeout => const NetworkError(statusCode: null),
|
||||
DioExceptionType.receiveTimeout => const NetworkError(statusCode: null),
|
||||
DioExceptionType.badResponse => _mapStatusCode(e.response?.statusCode),
|
||||
DioExceptionType.connectionError => const NetworkError(statusCode: null),
|
||||
_ => UnknownError(e),
|
||||
};
|
||||
```
|
||||
|
||||
## API versioning
|
||||
- Base URL includes version: `https://api.LegacyApp.com/v1/`
|
||||
- When upgrading API version, keep old version working until all clients migrate
|
||||
|
||||
## Auth token interceptor
|
||||
- Inject `Authorization: Bearer <token>` automatically on every request
|
||||
- On 401: refresh token once, retry original request, then logout if refresh fails
|
||||
- On 403: map to `AppError.authError('Insufficient permissions')`
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Global error handling strategy for LegacyApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Global Error Handling — LegacyApp
|
||||
|
||||
## Flutter error boundaries
|
||||
Configure in `main.dart` — do this ONCE and never bypass it:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
// TODO: Send to crash reporter (Sentry/Firebase Crashlytics)
|
||||
crashReporter.recordFlutterError(details);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
// TODO: Send to crash reporter
|
||||
crashReporter.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
## Error types hierarchy
|
||||
Define a sealed class for domain errors — never throw raw exceptions in business logic:
|
||||
|
||||
```dart
|
||||
sealed class AppError {
|
||||
const AppError();
|
||||
}
|
||||
class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); }
|
||||
class AuthError extends AppError { final String reason; const AuthError(this.reason); }
|
||||
class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); }
|
||||
class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); }
|
||||
```
|
||||
|
||||
## Repository layer
|
||||
- Wrap ALL external calls in try/catch and return `Either<AppError, T>` or `Result<T>`
|
||||
- NEVER let raw exceptions bubble to the presentation layer
|
||||
- Log at the repository layer, not the UI layer
|
||||
|
||||
## Presentation layer
|
||||
- Every async widget MUST handle error state explicitly — no silent failures
|
||||
- Show user-friendly error messages: map `AppError` subtype → readable string
|
||||
- Provide a "Try again" action for recoverable errors (network, timeout)
|
||||
- For fatal errors (auth expired), redirect to login — never show a dead screen
|
||||
|
||||
## Crash reporting
|
||||
- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta
|
||||
- Set `user` context on crash reporter after login (id only, no PII)
|
||||
- Add `breadcrumbs` for key user actions to aid reproduction
|
||||
|
||||
## Logging strategy
|
||||
```
|
||||
Level | Use case
|
||||
DEBUG | Development only (strip from release)
|
||||
INFO | Key user flows (login, purchase, etc.)
|
||||
WARNING | Recoverable errors, fallbacks used
|
||||
ERROR | Unrecoverable errors, unexpected states
|
||||
```
|
||||
- Use `logger` package — never bare `print()`
|
||||
- Logger instance per class: `final _log = Logger('ClassName');`
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
description: "Android-specific conventions for LegacyApp — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Android Platform Standards — LegacyApp
|
||||
|
||||
## Target SDK
|
||||
- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps
|
||||
- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise
|
||||
- Update `android/app/build.gradle` when bumping target SDK
|
||||
|
||||
## Permissions
|
||||
- Declare only needed permissions in `AndroidManifest.xml`
|
||||
- Runtime permissions: use `permission_handler` — never skip rationale step
|
||||
- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`)
|
||||
- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services
|
||||
|
||||
## Adaptive icons
|
||||
- Provide both foreground and background layers in `android/app/src/main/res/`
|
||||
- Test on dark theme, coloured theme, and themed icons (Android 13+)
|
||||
|
||||
## Deep links / App Links
|
||||
- Verify domain ownership: `.well-known/assetlinks.json` on your server
|
||||
- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://LegacyApp.com/products/123"`
|
||||
|
||||
## ProGuard / R8
|
||||
- Obfuscation rules in `android/app/proguard-rules.pro`
|
||||
- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes
|
||||
- Test release build thoroughly — obfuscation can break reflection-based code
|
||||
|
||||
## Notifications
|
||||
- Create notification channels before showing any notification (Android 8+)
|
||||
- Notification icons must be monochrome on Android 5+
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: "iOS-specific conventions for LegacyApp — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# iOS Platform Standards — LegacyApp
|
||||
|
||||
## Platform-specific imports
|
||||
- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code
|
||||
- iOS-only plugins: declare in `pubspec.yaml` with platform filter
|
||||
- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io`
|
||||
|
||||
## iOS-specific requirements
|
||||
- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`)
|
||||
- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024
|
||||
- Required `Info.plist` keys before using:
|
||||
- Camera: `NSCameraUsageDescription`
|
||||
- Photo library: `NSPhotoLibraryUsageDescription`
|
||||
- Location: `NSLocationWhenInUseUsageDescription`
|
||||
- Notifications: handled via `permission_handler`
|
||||
|
||||
## Push notifications (iOS)
|
||||
- Configure APNs certificates in Xcode signing & capabilities
|
||||
- Request permission with `permission_handler` — show rationale screen first
|
||||
- Handle foreground vs background vs terminated app states separately
|
||||
- Test on a physical device — iOS simulator does not support push
|
||||
|
||||
## Safe area
|
||||
- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding`
|
||||
- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators
|
||||
|
||||
## App Store compliance
|
||||
- Screenshot: use `ScreenshotController` to exclude sensitive screens
|
||||
- Sign in with Apple: required if any third-party social login is offered
|
||||
- IPv6 compatibility required — no IPv4-only network code
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
---
|
||||
description: "GetX Navigation conventions for LegacyApp"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GetX Navigation — LegacyApp
|
||||
|
||||
## Named routes
|
||||
```dart
|
||||
// app_pages.dart — central route definitions
|
||||
abstract class AppPages {
|
||||
static const initial = Routes.home;
|
||||
static final routes = [
|
||||
GetPage(name: Routes.home, page: () => const HomeView(), binding: HomeBinding()),
|
||||
GetPage(name: Routes.product, page: () => const ProductView(), binding: ProductBinding()),
|
||||
];
|
||||
}
|
||||
|
||||
// Navigate — always use named routes
|
||||
Get.toNamed(Routes.product, arguments: product); // push
|
||||
Get.offAllNamed(Routes.home); // replace stack
|
||||
Get.back(); // pop
|
||||
```
|
||||
|
||||
## Bindings
|
||||
- Every route has a `Binding` class that creates and injects dependencies
|
||||
- **NEVER** use `Get.put()` in a widget — only in Bindings
|
||||
- Use `Get.lazyPut()` for deferred creation
|
||||
|
||||
## Rules
|
||||
- **NEVER** use `Navigator.push/pop`
|
||||
- All route strings in `lib/core/routing/routes.dart` as constants
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
---
|
||||
description: "Security standards for LegacyApp — ALWAYS APPLIED on every file write"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Security Standards — LegacyApp
|
||||
> **Pillar 5**: Security is an always-on rule, not just a reactive agent.
|
||||
> These rules apply to EVERY file write, regardless of feature or context.
|
||||
|
||||
## Credential & secret management
|
||||
- **NEVER** hardcode API keys, tokens, or secrets in source code
|
||||
- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example`
|
||||
- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets)
|
||||
- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data
|
||||
- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds
|
||||
|
||||
## Authentication & sessions
|
||||
- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB
|
||||
- Implement token refresh with retry logic — never let a 401 show a raw error to the user
|
||||
- Certificate pinning: required for production builds on JWT / REST Auth — use `dio_pinning_interceptor`
|
||||
- Biometric re-auth: require for any transaction > defined threshold
|
||||
|
||||
## Data & privacy
|
||||
- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters
|
||||
- Sanitize all user inputs before sending to backend
|
||||
- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails
|
||||
- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions
|
||||
|
||||
## Network security
|
||||
- All API calls use HTTPS — no http:// in production
|
||||
- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true`
|
||||
- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)`
|
||||
- Rate-limit sensitive endpoints: auth, OTP, password reset
|
||||
|
||||
## Dependency security
|
||||
- Run `dart pub outdated` monthly — flag packages with known CVEs
|
||||
- Never use a package with <100 pub points without explicit tech lead approval
|
||||
- Pin critical security packages to exact versions in `pubspec.yaml`
|
||||
|
||||
## Secure coding patterns
|
||||
```dart
|
||||
// ✅ Correct: secure token storage
|
||||
final storage = FlutterSecureStorage();
|
||||
await storage.write(key: 'access_token', value: token);
|
||||
|
||||
// ❌ Wrong: SharedPreferences for sensitive data
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString('access_token', token); // NEVER do this
|
||||
|
||||
// ✅ Correct: PII-safe logging
|
||||
logger.info('User authenticated: userId=${user.id}'); // ok — id, not email
|
||||
logger.debug('Payment processed: orderId=$orderId'); // ok — no card data
|
||||
|
||||
// ❌ Wrong: PII in logs
|
||||
logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER
|
||||
|
||||
// ✅ Correct: --dart-define for secrets (build arg, not in code)
|
||||
// flutter build apk --dart-define=API_KEY=$API_KEY
|
||||
const apiKey = String.fromEnvironment('API_KEY'); // acceptable
|
||||
|
||||
// ✅ Correct: certificate pinning with Dio
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
|
||||
final client = HttpClient();
|
||||
client.badCertificateCallback = (cert, host, port) => false; // strict
|
||||
return client;
|
||||
};
|
||||
```
|
||||
|
||||
## Deep linking & intent security
|
||||
- Validate all incoming deep link parameters — never trust raw URL params
|
||||
- Use `app_links` package for verified deep link handling
|
||||
- Restrict URL schemes to known patterns — reject unknown schemes
|
||||
- For OAuth callbacks: validate `state` parameter to prevent CSRF
|
||||
|
||||
## Storage security
|
||||
- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher`
|
||||
- Clear sensitive data from memory after use (set to null, trigger GC)
|
||||
- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints)
|
||||
- Use `SecureRandom` for nonce/token generation — never `Random()`
|
||||
|
||||
## Code obfuscation & binary protection
|
||||
```yaml
|
||||
# android/app/build.gradle — release config
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
```
|
||||
```bash
|
||||
# Flutter release build with obfuscation
|
||||
flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/
|
||||
flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/
|
||||
```
|
||||
|
||||
## Release checklist
|
||||
Before every production release, verify:
|
||||
- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`)
|
||||
- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths)
|
||||
- [ ] Obfuscation enabled in release build config
|
||||
- [ ] Certificate pinning active and tested on both platforms
|
||||
- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`)
|
||||
- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys
|
||||
- [ ] Network calls all HTTPS — scan for `http://` in lib/
|
||||
- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'`
|
||||
- [ ] Deep link parameters validated
|
||||
- [ ] App transport security / network security config reviewed
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "GetX conventions for LegacyApp (legacy — migration available)"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GetX Standards — LegacyApp
|
||||
> ⚠️ This project uses GetX. See `migration-agent` for incremental migration to Riverpod.
|
||||
|
||||
## Controller structure
|
||||
```dart
|
||||
class ProductsController extends GetxController {
|
||||
final ProductRepository _repo;
|
||||
ProductsController(this._repo);
|
||||
|
||||
final RxList<Product> products = <Product>[].obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final Rx<String?> error = Rx(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProducts();
|
||||
}
|
||||
|
||||
Future<void> fetchProducts() async {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
products.value = await _repo.getProducts();
|
||||
} catch (e) {
|
||||
error.value = e.toString();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## View pattern
|
||||
```dart
|
||||
// Views extend GetView<Controller> — never GetWidget or raw StatelessWidget
|
||||
class ProductsView extends GetView<ProductsController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) return const ProductShimmer();
|
||||
if (controller.error.value != null) return ErrorWidget(controller.error.value!);
|
||||
return ProductList(controller.products);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- **NEVER** pass `BuildContext` into a controller
|
||||
- Use `Binding` classes for dependency injection — never `Get.put()` in a widget
|
||||
- Use `.obs` for all reactive state — never call `update()` on non-observable state
|
||||
- Use `Get.find<Controller>()` only in `Binding` classes, not in widgets
|
||||
- **No business logic in Views** — controllers handle all logic
|
||||
|
||||
## File locations in LegacyApp
|
||||
- `lib/features/[feature]/views/[feature]_view.dart`
|
||||
- `lib/features/[feature]/controllers/[feature]_controller.dart`
|
||||
- `lib/features/[feature]/bindings/[feature]_binding.dart`
|
||||
- `lib/features/[feature]/models/[feature]_model.dart`
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
description: "GetX testing conventions for LegacyApp"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# GetX Testing Standards — LegacyApp
|
||||
|
||||
## Test pattern
|
||||
```dart
|
||||
void main() {
|
||||
late ProductsController controller;
|
||||
late MockProductRepository mockRepo;
|
||||
|
||||
setUp(() {
|
||||
mockRepo = MockProductRepository();
|
||||
Get.testMode = true;
|
||||
controller = Get.put(ProductsController(mockRepo));
|
||||
});
|
||||
|
||||
tearDown(() => Get.deleteAll());
|
||||
|
||||
test('loads products on init', () async {
|
||||
when(() => mockRepo.getProducts()).thenAnswer((_) async => [fakeProduct]);
|
||||
await controller.fetchProducts();
|
||||
expect(controller.products, [fakeProduct]);
|
||||
expect(controller.isLoading.value, false);
|
||||
});
|
||||
|
||||
testWidgets('ProductsView shows shimmer while loading', (tester) async {
|
||||
controller.isLoading.value = true;
|
||||
await tester.pumpWidget(GetMaterialApp(home: ProductsView()));
|
||||
expect(find.byType(ProductShimmer), findsOneWidget);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Use `Get.testMode = true` in setUp
|
||||
- Always call `Get.deleteAll()` in tearDown
|
||||
- Wrap widget tests in `GetMaterialApp`, not `MaterialApp`
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
description: "Core Flutter conventions for LegacyApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Flutter Core Standards — LegacyApp
|
||||
|
||||
## Const and performance
|
||||
- Use `const` constructors wherever possible — compile-time guarantee of no rebuild
|
||||
- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)`
|
||||
- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled
|
||||
|
||||
## Null safety
|
||||
- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment
|
||||
- Prefer `??`, `?.`, and `if (x != null)` guards
|
||||
- Use `required` for all non-nullable named parameters
|
||||
- Never use `late` without a guarantee of initialisation before first access
|
||||
|
||||
## Widget lifecycle
|
||||
- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers
|
||||
- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget`
|
||||
- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)`
|
||||
|
||||
## Naming conventions
|
||||
- Files: `snake_case.dart`
|
||||
- Classes: `PascalCase`
|
||||
- Variables/functions: `camelCase`
|
||||
- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants
|
||||
- Private members: `_camelCase`
|
||||
|
||||
## Imports
|
||||
- Order: dart: → package: → relative
|
||||
- Use relative imports within a feature; absolute for cross-feature
|
||||
- Never import a feature's internal files from outside that feature
|
||||
|
||||
## Code quality
|
||||
- Max function length: 40 lines. Extract widgets and helpers aggressively
|
||||
- No `print()` in production code — use a logging package
|
||||
- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this`
|
||||
- Run `dart format` before every commit
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
description: "Project context for LegacyApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Project Context — LegacyApp
|
||||
|
||||
## Project identity
|
||||
- **Name:** LegacyApp
|
||||
- **Package:** com.test.legacy
|
||||
- **Description:** Legacy GetX app
|
||||
- **Scale:** medium
|
||||
|
||||
## Technology stack
|
||||
- **State management:** GetX
|
||||
- **Architecture:** MVC
|
||||
- **Routing:** GetX Navigation
|
||||
- **Backend:** REST API
|
||||
- **Auth:** JWT / REST Auth
|
||||
- **Platforms:** ios, android
|
||||
- **Code generation:** none
|
||||
|
||||
## Feature modules
|
||||
auth, dashboard
|
||||
|
||||
## Special capabilities
|
||||
|
||||
|
||||
## Environments / flavors
|
||||
- Flavors: dev, prod
|
||||
- CI/CD: Codemagic
|
||||
|
||||
## Design & API references
|
||||
- Design source: none
|
||||
- API docs: openapi at `docs/api.yaml`
|
||||
|
||||
## Code references
|
||||
### Git repositories
|
||||
_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context.
|
||||
|
||||
### Local paths
|
||||
_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant.
|
||||
|
||||
## Product UX / themes & roles
|
||||
- **Theme variants:** light, dark
|
||||
- **Roles:** Not enabled (`app_context.roles_enabled: false`).
|
||||
|
||||
|
||||
## Reviews — which rule owns what
|
||||
- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only)
|
||||
- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals)
|
||||
- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule
|
||||
|
||||
## Architecture boundaries
|
||||
- View (Widget) MUST NOT contain business logic
|
||||
- Controller MUST NOT import Flutter widgets directly
|
||||
- Model MUST be plain Dart, no framework dependencies
|
||||
|
||||
## When generating code for this project
|
||||
1. Always use GetX patterns — never suggest alternatives
|
||||
2. Always follow MVC folder structure
|
||||
3. Always use GetX Navigation for navigation — never `Navigator.push` directly
|
||||
4. Always target platforms: ios, android
|
||||
5. If code generation tools are used (none), follow their conventions
|
||||
6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets
|
||||
7. No user-facing string literals in widgets — l10n or shared constants per localization rule
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description: "UI/UX standards for LegacyApp — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# UI / UX Standards — LegacyApp
|
||||
|
||||
## Theme & design tokens (single source of truth)
|
||||
- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only.
|
||||
- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s).
|
||||
- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI.
|
||||
- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius.
|
||||
|
||||
## Loading states
|
||||
- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms
|
||||
- Use `shimmer` package with a shimmer that matches the final layout shape
|
||||
- Never show a blank screen during loading — skeleton must fill the same space as the content
|
||||
|
||||
## Empty states
|
||||
- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA
|
||||
- Empty state is different from error state — never reuse the same widget for both
|
||||
- Empty state copy: positive framing ("No items yet — add your first one")
|
||||
|
||||
## Error states
|
||||
- Every async failure MUST show: error message + retry button
|
||||
- Never swallow errors silently
|
||||
- Error text: user-friendly, never expose stack traces or raw API messages
|
||||
|
||||
## Navigation & transitions
|
||||
- Use `IndexedStack` for bottom nav tabs — preserves scroll position
|
||||
- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))`
|
||||
- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets
|
||||
|
||||
## Responsive layout
|
||||
- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values
|
||||
- Minimum touch target: 48×48 logical pixels (Material guideline)
|
||||
- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum
|
||||
|
||||
## Haptics
|
||||
- Use `HapticFeedback.lightImpact()` on primary CTAs
|
||||
- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions
|
||||
- Never add haptics to destructive actions without confirmation
|
||||
|
||||
## Accessibility
|
||||
- All interactive widgets must have a `Semantics` label or `tooltip`
|
||||
- Minimum contrast ratio: 4.5:1 (WCAG AA)
|
||||
- Test with TalkBack / VoiceOver before each release
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Create Build Flavor — LegacyApp
|
||||
|
||||
Creates a new build flavor/environment. Current flavors: dev, prod.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a new flavor called [flavor_name]
|
||||
```
|
||||
|
||||
## What gets created
|
||||
1. Android: new `productFlavors` block in `android/app/build.gradle`
|
||||
2. iOS: new scheme + configuration in Xcode (instructions provided)
|
||||
3. `lib/core/config/[flavor_name]_config.dart`
|
||||
4. `.env.[flavor_name]` template
|
||||
5. Codemagic pipeline update
|
||||
|
||||
## CI/CD: Codemagic
|
||||
Generate the pipeline config snippet for the new flavor in Codemagic format.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Deploy — LegacyApp
|
||||
|
||||
Guides through deployment to Codemagic for any of the flavors: dev, prod.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Deploy [flavor] to [store/environment]
|
||||
```
|
||||
|
||||
## Codemagic pipeline
|
||||
The AI will generate or update the codemagic configuration file for:
|
||||
- Build signing
|
||||
- Running tests
|
||||
- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod)
|
||||
|
||||
## Pre-deploy checklist
|
||||
- [ ] Version bump in `pubspec.yaml`
|
||||
- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/`
|
||||
- [ ] No debug flags in production code
|
||||
- [ ] Security checklist from `security-standards.mdc` passed
|
||||
- [ ] `cursor_gen --validate` passes
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Generate API Client — LegacyApp
|
||||
|
||||
Generates type-safe API clients from openapi spec at `docs/api.yaml`.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Generate API client for [endpoint or resource name]
|
||||
```
|
||||
|
||||
## Generated files
|
||||
1. **DTO** (`data/models/[resource]_dto.dart`) — request/response models with json_serializable
|
||||
2. **DataSource** (`data/datasources/[resource]_remote_datasource.dart`) — Dio calls with error handling
|
||||
3. **Repository impl** (`data/repositories/[resource]_repository_impl.dart`)
|
||||
|
||||
## After generation
|
||||
Run: `dart run build_runner build --delete-conflicting-outputs`
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Generate Tests — LegacyApp
|
||||
|
||||
Generates comprehensive unit, widget, and integration tests for **GetX** patterns.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Generate tests for [ClassName or file path]
|
||||
```
|
||||
|
||||
## Test generation process
|
||||
1. Read the source file completely
|
||||
2. Identify all testable units (public methods, state transitions, UI states)
|
||||
3. Generate tests following this pattern:
|
||||
```
|
||||
Get.put(MyController()); final ctrl = Get.find<MyController>(); expect(ctrl.value, expected);
|
||||
```
|
||||
4. Create mocks with `mocktail` for all dependencies
|
||||
5. Place test file at `test/[mirror of source path]_test.dart`
|
||||
|
||||
## Coverage targets
|
||||
- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum**
|
||||
- Widget tests: all three states (loading/error/data) must be tested
|
||||
- E2E: only critical user flows
|
||||
|
||||
## Test file structure
|
||||
```dart
|
||||
void main() {
|
||||
// Setup
|
||||
group('[ClassName]', () {
|
||||
// Happy path tests
|
||||
group('success cases', () { ... });
|
||||
// Error path tests
|
||||
group('error cases', () { ... });
|
||||
// Edge cases
|
||||
group('edge cases', () { ... });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Naming convention
|
||||
`'given [precondition], when [action], then [expected outcome]'`
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Scaffold Feature — LegacyApp
|
||||
|
||||
Scaffolds a complete new feature module following **MVC** architecture with **GetX** state management.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a feature called [feature_name] with [description]
|
||||
```
|
||||
|
||||
## What gets generated
|
||||
|
||||
### For MVC architecture:
|
||||
The AI will create all necessary files for the `[feature_name]` feature following MVC patterns.
|
||||
|
||||
### File structure to create:
|
||||
|
||||
**Clean Architecture:**
|
||||
```
|
||||
lib/features/[feature_name]/
|
||||
domain/
|
||||
entities/[feature_name].dart
|
||||
repositories/[feature_name]_repository.dart
|
||||
usecases/
|
||||
get_[feature_name]_usecase.dart
|
||||
create_[feature_name]_usecase.dart
|
||||
data/
|
||||
models/[feature_name]_dto.dart
|
||||
datasources/[feature_name]_remote_datasource.dart
|
||||
repositories/[feature_name]_repository_impl.dart
|
||||
presentation/
|
||||
[state_files]/ ← based on GetX
|
||||
pages/[feature_name]_page.dart
|
||||
widgets/
|
||||
```
|
||||
|
||||
**Feature-First:**
|
||||
```
|
||||
lib/features/[feature_name]/
|
||||
[feature_name]_screen.dart
|
||||
[feature_name]_provider.dart ← or [feature_name]_bloc.dart
|
||||
[feature_name]_repository.dart
|
||||
[feature_name]_model.dart
|
||||
widgets/
|
||||
```
|
||||
|
||||
## State management boilerplate
|
||||
|
||||
### For GetX:
|
||||
Generate the appropriate state management files with:
|
||||
- Initial/loading/success/error states
|
||||
- All necessary events (for BLoC)
|
||||
- Repository connection
|
||||
- Dependency injection registration
|
||||
|
||||
## Steps the AI takes
|
||||
1. Ask: "What is the feature name and brief description?"
|
||||
2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)"
|
||||
3. Generate all files with correct imports and patterns
|
||||
4. Add the feature to the DI container
|
||||
5. Add the route to GetX Navigation router
|
||||
6. Create a placeholder test file
|
||||
|
||||
## Code generation
|
||||
{{#if codegen_freezed}}
|
||||
- Generate Freezed model: run `dart run build_runner build` after scaffolding
|
||||
{{/if}}
|
||||
{{#if codegen_injectable}}
|
||||
- Register in injectable: add `@lazySingleton` to repository
|
||||
{{/if}}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Scaffold Screen — LegacyApp
|
||||
|
||||
Creates a complete screen widget with all states handled, following **GetX** patterns.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a screen for [screen_name] that shows [content description]
|
||||
```
|
||||
|
||||
## Generated screen template
|
||||
|
||||
### GetX screen pattern:
|
||||
|
||||
**BLoC:**
|
||||
```dart
|
||||
class [ScreenName]Screen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<[Feature]Bloc, [Feature]State>(
|
||||
builder: (context, state) => switch (state) {
|
||||
[Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(),
|
||||
[Feature]Loaded(:final data) => [ScreenName]Content(data: data),
|
||||
[Feature]Empty() => const [ScreenName]EmptyState(),
|
||||
[Feature]Error(:final message) => [ScreenName]ErrorState(
|
||||
message: message,
|
||||
onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Riverpod:**
|
||||
```dart
|
||||
class [ScreenName]Screen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch([feature]Provider);
|
||||
return state.when(
|
||||
loading: () => const [ScreenName]Shimmer(),
|
||||
data: (data) => [ScreenName]Content(data: data),
|
||||
error: (e, _) => [ScreenName]ErrorState(error: e),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Required sub-widgets to generate
|
||||
1. `[ScreenName]Shimmer` — skeleton loading layout matching final content
|
||||
2. `[ScreenName]EmptyState` — illustration + headline + CTA button
|
||||
3. `[ScreenName]ErrorState` — error message + retry button
|
||||
4. `[ScreenName]Content` — the actual data display
|
||||
|
||||
## Platform considerations (ios, android)
|
||||
{{#if platform_web}}
|
||||
- Web: ensure no dart:io usage; test at 375px and 1280px widths
|
||||
{{/if}}
|
||||
{{#if platform_desktop}}
|
||||
- Desktop: add keyboard shortcut support for primary actions
|
||||
{{/if}}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description: "Reviews TaskFlow code for Riverpod patterns, Feature-First boundaries, and Supabase usage. Ask: 'Review this code' or '@code-reviewer check PR'"
|
||||
model: claude-opus-4-5
|
||||
context: auto
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a senior Flutter engineer reviewing code for **TaskFlow**.
|
||||
|
||||
## Your review checklist
|
||||
|
||||
### Riverpod patterns
|
||||
- Are state classes immutable and sealed?
|
||||
- Is state management correctly separated from UI logic?
|
||||
- Are streams/subscriptions properly cancelled in dispose()?
|
||||
- Check for anti-patterns specific to Riverpod
|
||||
|
||||
### Feature-First boundaries
|
||||
- features/auth/ MUST NOT import from features/home/ etc.
|
||||
- Shared code lives in core/ or shared/ only
|
||||
- Each feature is self-contained: screen + provider + model + repo
|
||||
- Flag any violation of these import rules immediately
|
||||
|
||||
### Supabase usage
|
||||
- Are all exceptions caught and mapped to domain errors?
|
||||
- Are streams vs futures used appropriately?
|
||||
- Are connections/subscriptions disposed correctly?
|
||||
|
||||
### Security (always check)
|
||||
- No hardcoded API keys or secrets
|
||||
- No PII logged to console or crash reporters
|
||||
- Sensitive data using flutter_secure_storage, not SharedPreferences
|
||||
- All user inputs validated before sending to backend
|
||||
|
||||
### General Flutter
|
||||
- `const` used where possible
|
||||
- `dispose()` overridden for all controllers/subscriptions
|
||||
- No `print()` in production paths
|
||||
- Loading/empty/error states all handled
|
||||
|
||||
## Output format
|
||||
For each issue found:
|
||||
```
|
||||
[SEVERITY: critical/major/minor] File:line — Issue description
|
||||
WHY: Why this matters
|
||||
FIX: Specific fix recommendation
|
||||
```
|
||||
|
||||
Severity guide:
|
||||
- **critical**: Security issue, data loss risk, crash potential
|
||||
- **major**: Incorrect pattern, boundary violation, missing error handling
|
||||
- **minor**: Style, naming, optimization opportunity
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: security-agent
|
||||
description: "Deep security review for TaskFlow. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'"
|
||||
model: claude-opus-4-5
|
||||
context: fork
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a mobile security expert conducting a deep review for **TaskFlow**.
|
||||
|
||||
> Note: This agent provides deep security analysis.
|
||||
> The `security-standards.mdc` rule provides always-on enforcement.
|
||||
> This agent is for detailed consultations on specific security concerns.
|
||||
|
||||
## Deep review focus areas
|
||||
|
||||
### Auth flow (Supabase Auth)
|
||||
- Token storage: is `flutter_secure_storage` used for ALL tokens?
|
||||
- Token refresh: is refresh handled atomically (no race condition)?
|
||||
- Session expiry: does the app handle 401 gracefully without data loss?
|
||||
- Certificate pinning: configured and tested?
|
||||
|
||||
### Data at rest
|
||||
- SQLite/Hive encryption: sensitive DBs encrypted?
|
||||
- Cache poisoning: cached API responses validated before use?
|
||||
- Keychain/Keystore usage for cryptographic keys
|
||||
|
||||
### Network security
|
||||
- All endpoints HTTPS — any http:// URLs?
|
||||
- Certificate validation — any `badCertificateCallback: true`?
|
||||
- Sensitive data in URL params/query strings?
|
||||
- Request/response logging in production? (must be off)
|
||||
|
||||
### Code injection risks
|
||||
- Dynamic code execution patterns
|
||||
- WebView usage — JavaScript interface security
|
||||
- Deep link parameter validation (no path traversal)
|
||||
|
||||
## Output format
|
||||
For each finding:
|
||||
```
|
||||
[RISK: Critical/High/Medium/Low]
|
||||
LOCATION: File / function
|
||||
ISSUE: Detailed description
|
||||
CVSS-like impact: Confidentiality/Integrity/Availability
|
||||
REMEDIATION: Specific code fix
|
||||
```
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: test-writer
|
||||
description: "Writes Riverpod unit tests for TaskFlow. Ask: 'Write tests for [class]' or '@test-writer generate tests'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, write_file, list_files]
|
||||
---
|
||||
|
||||
You are a Flutter test engineer for **TaskFlow** using **Riverpod**.
|
||||
|
||||
## Test pattern to follow
|
||||
```dart
|
||||
final container = ProviderContainer(overrides: [myProvider.overrideWithValue(mockValue)]); addTearDown(container.dispose);
|
||||
```
|
||||
|
||||
## When asked to write tests:
|
||||
1. Read the source file completely
|
||||
2. Identify all public methods and state transitions
|
||||
3. Write tests for:
|
||||
- Happy path (successful operation)
|
||||
- Error path (failure, exception handling)
|
||||
- Edge cases (empty data, boundary values)
|
||||
4. Use `mocktail` for all mocking
|
||||
5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'`
|
||||
|
||||
## File placement
|
||||
- Unit tests: `test/features/[feature]/[file]_test.dart`
|
||||
- Widget tests: `test/features/[feature]/[screen]_widget_test.dart`
|
||||
- Coverage target: 80% minimum for business logic classes
|
||||
|
||||
## Output
|
||||
Write the complete test file, ready to run. Include all imports.
|
||||
After writing, run: `dart test path/to/test_file.dart`
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: ui-validator
|
||||
description: "Validates TaskFlow UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'"
|
||||
model: claude-sonnet-4-20250514
|
||||
context: auto
|
||||
allowed-tools: [read_file, list_files]
|
||||
---
|
||||
|
||||
You are a UI/UX validator for **TaskFlow**.
|
||||
|
||||
## Validate every screen for:
|
||||
|
||||
### State coverage (Riverpod)
|
||||
- Loading state: shows shimmer (NOT spinner unless brief)
|
||||
- Empty state: shows illustration + CTA (NOT blank screen)
|
||||
- Error state: shows message + retry button (NOT toast-only)
|
||||
- Data state: renders content correctly
|
||||
|
||||
### Accessibility
|
||||
- All interactive widgets have semantic labels
|
||||
- Minimum touch targets: 48×48dp
|
||||
- Sufficient color contrast (4.5:1 minimum)
|
||||
|
||||
### Responsive layout
|
||||
- No hardcoded pixel widths
|
||||
- Tested at 375px and 414px viewport widths
|
||||
- `SafeArea` used correctly on iOS
|
||||
|
||||
### Platform-specific (ios, android, web)
|
||||
{{#if web}}
|
||||
- No dart:io imports in web-targeted code
|
||||
- PWA-compatible (no native-only APIs without fallbacks)
|
||||
{{/if}}
|
||||
{{#if ios}}
|
||||
- Safe area respected (notch, Dynamic Island)
|
||||
- iOS Human Interface Guidelines followed
|
||||
{{/if}}
|
||||
|
||||
### Security (UI layer)
|
||||
- No credentials shown in plaintext
|
||||
- Sensitive screens wrapped with screenshot prevention
|
||||
|
||||
## Output
|
||||
List each violation with:
|
||||
- Location (file:widget)
|
||||
- What's wrong
|
||||
- How to fix it
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// arch-guard.ts — Pre-commit hook: enforces Feature-First import boundaries
|
||||
// Generated for TaskFlow (Feature-First architecture)
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
interface ArchRule {
|
||||
sourcePattern: RegExp;
|
||||
forbiddenImports: RegExp[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Architecture-specific rules for Feature-First
|
||||
const ARCH_RULES: ArchRule[] = [
|
||||
...getArchRules()
|
||||
];
|
||||
|
||||
function getArchRules(): ArchRule[] {
|
||||
const arch = 'feature_first';
|
||||
|
||||
if (arch === 'clean') {
|
||||
return [
|
||||
{
|
||||
sourcePattern: /features\/\w+\/domain\//,
|
||||
forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//],
|
||||
message: 'domain/ must not import from data/ or presentation/',
|
||||
},
|
||||
{
|
||||
sourcePattern: /features\/\w+\/presentation\//,
|
||||
forbiddenImports: [/features\/\w+\/data\//],
|
||||
message: 'presentation/ must not import from data/ directly (use domain interfaces)',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (arch === 'feature_first') {
|
||||
return [
|
||||
{
|
||||
// A feature file must not import from another feature
|
||||
sourcePattern: /features\/(\w+)\//,
|
||||
forbiddenImports: [], // checked dynamically below
|
||||
message: 'Features must not import from other feature folders',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (arch === 'mvvm') {
|
||||
return [
|
||||
{
|
||||
sourcePattern: /viewmodels\//,
|
||||
forbiddenImports: [/package:flutter\//, /widgets\//],
|
||||
message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getChangedDartFiles(): string[] {
|
||||
try {
|
||||
const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' });
|
||||
return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function checkFile(filePath: string): string[] {
|
||||
const violations: string[] = [];
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const imports = content.match(/^import\s+'[^']+'/gm) ?? [];
|
||||
|
||||
for (const rule of ARCH_RULES) {
|
||||
if (!rule.sourcePattern.test(filePath)) continue;
|
||||
|
||||
// Feature-first cross-feature check
|
||||
if ('feature_first' === 'feature_first') {
|
||||
const srcFeatureMatch = filePath.match(/features\/(\w+)\//);
|
||||
if (srcFeatureMatch) {
|
||||
const srcFeature = srcFeatureMatch[1];
|
||||
for (const imp of imports) {
|
||||
const impFeatureMatch = imp.match(/features\/(\w+)\//);
|
||||
if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) {
|
||||
violations.push(
|
||||
`${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` +
|
||||
` ${YELLOW}${rule.message}${RESET}\n` +
|
||||
` Import: ${imp}\n` +
|
||||
` Fix: Move shared code to core/ or shared/`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Standard forbidden import check
|
||||
for (const imp of imports) {
|
||||
for (const forbidden of rule.forbiddenImports) {
|
||||
if (forbidden.test(imp)) {
|
||||
violations.push(
|
||||
`${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` +
|
||||
` ${YELLOW}${rule.message}${RESET}\n` +
|
||||
` Import: ${imp}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
const changedFiles = getChangedDartFiles();
|
||||
if (changedFiles.length === 0) {
|
||||
console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for Feature-First violations...`);
|
||||
|
||||
const allViolations: string[] = [];
|
||||
for (const file of changedFiles) {
|
||||
allViolations.push(...checkFile(file));
|
||||
}
|
||||
|
||||
if (allViolations.length > 0) {
|
||||
console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`);
|
||||
for (const v of allViolations) console.error(v + '\n');
|
||||
console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`);
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// flutter-analyze.ts — Pre-commit hook: runs dart analyze and dart format check
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
function run(cmd: string): { stdout: string; code: number } {
|
||||
try {
|
||||
const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
return { stdout, code: 0 };
|
||||
} catch (e: any) {
|
||||
return { stdout: e.stdout ?? e.message, code: e.status ?? 1 };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔍 Running flutter analyze...');
|
||||
const analyze = run('flutter analyze --no-congratulate');
|
||||
if (analyze.code !== 0) {
|
||||
console.error(`${RED}✘ flutter analyze failed:\n${analyze.stdout}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🎨 Checking dart format...');
|
||||
const format = run('dart format --output=none --set-exit-if-changed lib/ test/');
|
||||
if (format.code !== 0) {
|
||||
console.error(`${RED}✘ Unformatted files detected. Run: dart format lib/ test/${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${GREEN}✔ flutter analyze + dart format passed${RESET}`);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env ts-node
|
||||
// grind-tests.ts — Pre-push hook: runs flutter test
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
console.log('🧪 Running flutter test...');
|
||||
try {
|
||||
execSync('flutter test --coverage', { stdio: 'inherit' });
|
||||
console.log(`${GREEN}✔ All tests passed${RESET}`);
|
||||
} catch {
|
||||
console.error(`${RED}✘ Tests failed. Fix failing tests before pushing.${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "pre-commit: flutter analyze",
|
||||
"command": "npx ts-node .cursor/hooks/flutter-analyze.ts",
|
||||
"events": ["pre-commit"]
|
||||
},
|
||||
{
|
||||
"name": "pre-commit: arch guard",
|
||||
"command": "npx ts-node .cursor/hooks/arch-guard.ts",
|
||||
"events": ["pre-commit"]
|
||||
},
|
||||
{
|
||||
"name": "pre-push: run tests",
|
||||
"command": "npx ts-node .cursor/hooks/grind-tests.ts",
|
||||
"events": ["pre-push"]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
description: "Feature-First architecture conventions for TaskFlow"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Feature-First Architecture — TaskFlow
|
||||
|
||||
## Folder structure
|
||||
```
|
||||
lib/
|
||||
features/
|
||||
auth/
|
||||
auth_screen.dart
|
||||
auth_provider.dart (or auth_bloc.dart)
|
||||
auth_repository.dart
|
||||
auth_model.dart
|
||||
widgets/
|
||||
login_form.dart
|
||||
social_login_button.dart
|
||||
home/
|
||||
home_screen.dart
|
||||
home_provider.dart
|
||||
...
|
||||
core/
|
||||
di/ ← Dependency injection setup
|
||||
network/ ← HTTP client, interceptors
|
||||
storage/ ← Local storage abstraction
|
||||
widgets/ ← Shared widgets used by 3+ features
|
||||
theme/ ← App theme, colors, text styles
|
||||
routing/ ← Router config
|
||||
shared/
|
||||
models/ ← Models shared across features
|
||||
utils/ ← Pure utility functions
|
||||
```
|
||||
|
||||
## Import rules (STRICTLY ENFORCED by arch-guard hook)
|
||||
- features/auth/ MUST NOT import from features/home/ etc.
|
||||
- Shared code lives in core/ or shared/ only
|
||||
- Each feature is self-contained: screen + provider + model + repo
|
||||
|
||||
## Feature isolation rules
|
||||
- A feature folder is self-contained: its own screen, state, model, repository
|
||||
- Features MUST NOT import from other feature folders
|
||||
- Shared code MUST be extracted to `core/` or `shared/` before sharing
|
||||
- The 3-feature rule: if 3+ features need the same widget → move it to `core/widgets/`
|
||||
|
||||
## File naming within a feature
|
||||
- `[feature]_screen.dart` — the main screen widget
|
||||
- `[feature]_provider.dart` — Riverpod providers (or `[feature]_bloc.dart`)
|
||||
- `[feature]_repository.dart` — data fetching + caching
|
||||
- `[feature]_model.dart` — data model (Freezed or plain Dart)
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description: "Supabase conventions for TaskFlow"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Supabase Standards — TaskFlow
|
||||
|
||||
## Row Level Security (RLS) awareness
|
||||
- **ALWAYS** assume RLS is enabled — never write queries that assume full table access
|
||||
- Test queries in the Supabase dashboard before implementing in Flutter
|
||||
- If a query returns empty unexpectedly, check RLS policies first
|
||||
|
||||
## Queries
|
||||
```dart
|
||||
// ✅ Type-safe query with error handling
|
||||
Future<List<Product>> getProducts() async {
|
||||
final response = await _supabase
|
||||
.from('products')
|
||||
.select('id, name, price, category_id, categories(name)')
|
||||
.eq('is_active', true)
|
||||
.order('created_at', ascending: false)
|
||||
.limit(50);
|
||||
return response.map(Product.fromMap).toList();
|
||||
}
|
||||
```
|
||||
|
||||
## Realtime subscriptions
|
||||
```dart
|
||||
StreamSubscription<List<Map<String, dynamic>>>? _sub;
|
||||
|
||||
void watchOrders(String userId) {
|
||||
_sub = _supabase
|
||||
.from('orders')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('user_id', userId)
|
||||
.listen(
|
||||
(data) => _updateOrders(data),
|
||||
onError: (e) => _handleError(e),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel(); // ALWAYS cancel in dispose()
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
## Auth session
|
||||
- Use `supabase.auth.onAuthStateChange` stream — never poll auth state
|
||||
- Persist session: `supabase-flutter` handles this automatically via secure storage
|
||||
- `session.accessToken` expires — check `session.isExpired` before sensitive operations
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
description: "Freezed code generation conventions for TaskFlow — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Freezed Standards — TaskFlow
|
||||
|
||||
## When to use Freezed
|
||||
- All domain entities and data models — use `@freezed`
|
||||
- Union types / sealed classes — use `@freezed` with multiple constructors
|
||||
- BLoC states and events — use `@freezed`
|
||||
|
||||
## Model pattern
|
||||
```dart
|
||||
@freezed
|
||||
class Product with _$Product {
|
||||
const factory Product({
|
||||
required String id,
|
||||
required String name,
|
||||
required double price,
|
||||
@Default(0) int stockCount,
|
||||
String? imageUrl,
|
||||
}) = _Product;
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
|
||||
}
|
||||
```
|
||||
|
||||
## Union type pattern
|
||||
```dart
|
||||
@freezed
|
||||
sealed class ProductState with _$ProductState {
|
||||
const factory ProductState.initial() = ProductInitial;
|
||||
const factory ProductState.loading() = ProductLoading;
|
||||
const factory ProductState.loaded(List<Product> products) = ProductLoaded;
|
||||
const factory ProductState.error(String message) = ProductError;
|
||||
}
|
||||
|
||||
// Usage — exhaustive switch
|
||||
final widget = state.when(
|
||||
initial: () => const SizedBox.shrink(),
|
||||
loading: () => const ProductShimmer(),
|
||||
loaded: (products) => ProductList(products: products),
|
||||
error: (msg) => ErrorWidget(message: msg),
|
||||
);
|
||||
```
|
||||
|
||||
## Critical rules
|
||||
- **NEVER** edit `.freezed.dart` or `.g.dart` files — they are generated
|
||||
- Run `dart run build_runner build --delete-conflicting-outputs` after changes
|
||||
- Run `dart run build_runner watch` during development
|
||||
- Add `*.freezed.dart` and `*.g.dart` to `.gitignore` (or commit them — team must decide)
|
||||
- Always define `copyWith` via Freezed — never write manual `copyWith`
|
||||
|
||||
## Patterns to avoid
|
||||
```dart
|
||||
// ❌ Manual copyWith — replaced by Freezed
|
||||
Product copyWith({String? name, double? price}) => Product(
|
||||
id: id, name: name ?? this.name, price: price ?? this.price,
|
||||
);
|
||||
|
||||
// ✅ Let Freezed generate it
|
||||
product.copyWith(name: 'New Name', price: 9.99)
|
||||
```
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
description: "json_serializable conventions for TaskFlow — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# json_serializable Standards — TaskFlow
|
||||
|
||||
## Model annotation
|
||||
```dart
|
||||
@JsonSerializable(explicitToJson: true) // explicitToJson for nested objects
|
||||
class ProductDto {
|
||||
final String id;
|
||||
final String name;
|
||||
@JsonKey(name: 'unit_price') // snake_case API → camelCase Dart
|
||||
final double unitPrice;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool isActive;
|
||||
final DateTime createdAt; // auto-converted from ISO 8601 string
|
||||
|
||||
const ProductDto({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.unitPrice,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ProductDto.fromJson(Map<String, dynamic> json) => _$ProductDtoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ProductDtoToJson(this);
|
||||
}
|
||||
```
|
||||
|
||||
## Critical rules
|
||||
- **NEVER** edit `*.g.dart` files
|
||||
- Use `@JsonKey(defaultValue: ...)` for nullable API fields — API contracts change
|
||||
- Use `explicitToJson: true` whenever the model has nested objects
|
||||
- Null safety: API fields not guaranteed to be non-null should be `String?` not `String`
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Global error handling strategy for TaskFlow — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Global Error Handling — TaskFlow
|
||||
|
||||
## Flutter error boundaries
|
||||
Configure in `main.dart` — do this ONCE and never bypass it:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
// TODO: Send to crash reporter (Sentry/Firebase Crashlytics)
|
||||
crashReporter.recordFlutterError(details);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
// TODO: Send to crash reporter
|
||||
crashReporter.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
## Error types hierarchy
|
||||
Define a sealed class for domain errors — never throw raw exceptions in business logic:
|
||||
|
||||
```dart
|
||||
sealed class AppError {
|
||||
const AppError();
|
||||
}
|
||||
class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); }
|
||||
class AuthError extends AppError { final String reason; const AuthError(this.reason); }
|
||||
class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); }
|
||||
class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); }
|
||||
```
|
||||
|
||||
## Repository layer
|
||||
- Wrap ALL external calls in try/catch and return `Either<AppError, T>` or `Result<T>`
|
||||
- NEVER let raw exceptions bubble to the presentation layer
|
||||
- Log at the repository layer, not the UI layer
|
||||
|
||||
## Presentation layer
|
||||
- Every async widget MUST handle error state explicitly — no silent failures
|
||||
- Show user-friendly error messages: map `AppError` subtype → readable string
|
||||
- Provide a "Try again" action for recoverable errors (network, timeout)
|
||||
- For fatal errors (auth expired), redirect to login — never show a dead screen
|
||||
|
||||
## Crash reporting
|
||||
- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta
|
||||
- Set `user` context on crash reporter after login (id only, no PII)
|
||||
- Add `breadcrumbs` for key user actions to aid reproduction
|
||||
|
||||
## Logging strategy
|
||||
```
|
||||
Level | Use case
|
||||
DEBUG | Development only (strip from release)
|
||||
INFO | Key user flows (login, purchase, etc.)
|
||||
WARNING | Recoverable errors, fallbacks used
|
||||
ERROR | Unrecoverable errors, unexpected states
|
||||
```
|
||||
- Use `logger` package — never bare `print()`
|
||||
- Logger instance per class: `final _log = Logger('ClassName');`
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: "Localization / i18n conventions for TaskFlow"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Localization Standards — TaskFlow
|
||||
|
||||
## Supported locales: en, fr
|
||||
|
||||
## Setup
|
||||
- Use Flutter's built-in `AppLocalizations` (generated from `.arb` files)
|
||||
- ARB files: `lib/l10n/app_en.arb`, `lib/l10n/app_fr.arb`, etc.
|
||||
- Never hardcode user-facing strings — always use `context.l10n.stringKey`
|
||||
|
||||
## AppLocalizations access
|
||||
```dart
|
||||
// In widgets:
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
Text(l10n.welcomeMessage) // ✅
|
||||
|
||||
// Extension for convenience:
|
||||
extension LocalizationX on BuildContext {
|
||||
AppLocalizations get l10n => AppLocalizations.of(this)!;
|
||||
}
|
||||
Text(context.l10n.welcomeMessage) // ✅
|
||||
```
|
||||
|
||||
## ARB file format
|
||||
```json
|
||||
{
|
||||
"welcomeMessage": "Welcome back, {name}!",
|
||||
"@welcomeMessage": {
|
||||
"description": "Shown on home screen after login",
|
||||
"placeholders": {
|
||||
"name": { "type": "String", "example": "Alice" }
|
||||
}
|
||||
},
|
||||
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": { "type": "int" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- **NEVER** hardcode user-facing strings as string literals in widget files
|
||||
- **Technical keys** (route paths, storage/analytics keys, JSON fields): one shared module (e.g. `lib/core/constants/app_strings.dart`) — no duplicated literals across features
|
||||
- All new strings added to ALL locale ARB files simultaneously — broken translations break builds
|
||||
- Use ICU message format for plurals and gendered strings
|
||||
- Date/time formatting: use `intl` package `DateFormat` — never `date.toString()`
|
||||
- Currency: use `NumberFormat.currency(locale: locale)` — never manual formatting
|
||||
- RTL support: wrap with `Directionality` where needed; test with Arabic locale
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
description: "Android-specific conventions for TaskFlow — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Android Platform Standards — TaskFlow
|
||||
|
||||
## Target SDK
|
||||
- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps
|
||||
- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise
|
||||
- Update `android/app/build.gradle` when bumping target SDK
|
||||
|
||||
## Permissions
|
||||
- Declare only needed permissions in `AndroidManifest.xml`
|
||||
- Runtime permissions: use `permission_handler` — never skip rationale step
|
||||
- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`)
|
||||
- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services
|
||||
|
||||
## Adaptive icons
|
||||
- Provide both foreground and background layers in `android/app/src/main/res/`
|
||||
- Test on dark theme, coloured theme, and themed icons (Android 13+)
|
||||
|
||||
## Deep links / App Links
|
||||
- Verify domain ownership: `.well-known/assetlinks.json` on your server
|
||||
- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://TaskFlow.com/products/123"`
|
||||
|
||||
## ProGuard / R8
|
||||
- Obfuscation rules in `android/app/proguard-rules.pro`
|
||||
- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes
|
||||
- Test release build thoroughly — obfuscation can break reflection-based code
|
||||
|
||||
## Notifications
|
||||
- Create notification channels before showing any notification (Android 8+)
|
||||
- Notification icons must be monochrome on Android 5+
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: "iOS-specific conventions for TaskFlow — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# iOS Platform Standards — TaskFlow
|
||||
|
||||
## Platform-specific imports
|
||||
- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code
|
||||
- iOS-only plugins: declare in `pubspec.yaml` with platform filter
|
||||
- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io`
|
||||
|
||||
## iOS-specific requirements
|
||||
- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`)
|
||||
- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024
|
||||
- Required `Info.plist` keys before using:
|
||||
- Camera: `NSCameraUsageDescription`
|
||||
- Photo library: `NSPhotoLibraryUsageDescription`
|
||||
- Location: `NSLocationWhenInUseUsageDescription`
|
||||
- Notifications: handled via `permission_handler`
|
||||
|
||||
## Push notifications (iOS)
|
||||
- Configure APNs certificates in Xcode signing & capabilities
|
||||
- Request permission with `permission_handler` — show rationale screen first
|
||||
- Handle foreground vs background vs terminated app states separately
|
||||
- Test on a physical device — iOS simulator does not support push
|
||||
|
||||
## Safe area
|
||||
- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding`
|
||||
- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators
|
||||
|
||||
## App Store compliance
|
||||
- Screenshot: use `ScreenshotController` to exclude sensitive screens
|
||||
- Sign in with Apple: required if any third-party social login is offered
|
||||
- IPv6 compatibility required — no IPv4-only network code
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
---
|
||||
description: "Flutter Web conventions for TaskFlow — Pillar 4"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Flutter Web Standards — TaskFlow
|
||||
|
||||
## Critical: No dart:io on web
|
||||
- **NEVER** import `dart:io` in shared code — it crashes on web
|
||||
- Use `dart:html` or `universal_io` for platform-specific I/O
|
||||
- Use `path_provider` alternatives: `universal_html` for web file access
|
||||
- Check: `kIsWeb` constant before any platform-specific code
|
||||
|
||||
```dart
|
||||
// ✅ Platform-safe
|
||||
import 'package:universal_io/io.dart';
|
||||
|
||||
// ❌ Crashes on web
|
||||
import 'dart:io';
|
||||
```
|
||||
|
||||
## Web rendering
|
||||
- Choose renderer carefully:
|
||||
- `canvaskit` — pixel-perfect, larger initial load, better for graphics
|
||||
- `html` — smaller load, uses DOM elements, inconsistent rendering
|
||||
- Set in `index.html`: `flutterWebRenderer: "canvaskit"`
|
||||
|
||||
## PWA requirements
|
||||
- Update `web/manifest.json`: name, icons (192×192, 512×512), theme_color
|
||||
- Service worker: configure for offline caching of app shell
|
||||
- Test with Chrome DevTools → Lighthouse → PWA audit
|
||||
|
||||
## Web-specific rendering caveats
|
||||
- `BackdropFilter` has limited support on `html` renderer
|
||||
- `Canvas` operations differ between renderers — test both
|
||||
- Text selection differs — use `SelectableText` not `Text` where appropriate
|
||||
- Scrollbars appear automatically on web — style or hide with `ScrollbarTheme`
|
||||
|
||||
## URL strategy
|
||||
- Use `usePathUrlStrategy()` in `main.dart` for clean URLs (no `#`)
|
||||
- Configure server to redirect all paths to `index.html` (SPA routing)
|
||||
|
||||
## Performance
|
||||
- Lazy-load routes — use `GoRouter` deferred loading
|
||||
- Initial load budget: < 3MB (canvaskit) or < 1MB (html renderer)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
description: "GoRouter conventions for TaskFlow"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GoRouter Standards — TaskFlow
|
||||
|
||||
## Typed routes (mandatory)
|
||||
```dart
|
||||
// Define typed routes — never use string paths directly in navigation calls
|
||||
@TypedGoRoute<HomeRoute>(path: '/')
|
||||
class HomeRoute extends GoRouteData {
|
||||
const HomeRoute();
|
||||
@override Widget build(BuildContext ctx, GoRouterState state) => const HomeScreen();
|
||||
}
|
||||
|
||||
@TypedGoRoute<ProductRoute>(path: '/products/:id')
|
||||
class ProductRoute extends GoRouteData {
|
||||
final String id;
|
||||
const ProductRoute({required this.id});
|
||||
@override Widget build(BuildContext ctx, GoRouterState state) => ProductScreen(id: id);
|
||||
}
|
||||
|
||||
// Navigate with type safety
|
||||
const ProductRoute(id: product.id).go(context); // ✅
|
||||
context.go('/products/${product.id}'); // ❌ — don't do this
|
||||
```
|
||||
|
||||
## Auth guard
|
||||
```dart
|
||||
// Redirect logic in router config
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = ref.read(authProvider).isAuthenticated;
|
||||
final isLoginRoute = state.matchedLocation == '/login';
|
||||
if (!isLoggedIn && !isLoginRoute) return '/login';
|
||||
if (isLoggedIn && isLoginRoute) return '/';
|
||||
return null;
|
||||
},
|
||||
```
|
||||
|
||||
## Shell routes for bottom navigation
|
||||
```dart
|
||||
ShellRoute(
|
||||
builder: (ctx, state, child) => MainScaffold(child: child),
|
||||
routes: [
|
||||
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
|
||||
GoRoute(path: '/search', builder: (_, __) => const SearchScreen()),
|
||||
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Deep links
|
||||
- Register URL schemes in `Info.plist` (iOS) and `AndroidManifest.xml`
|
||||
- Test deep links with: `adb shell am start -a android.intent.action.VIEW -d "app://com.test.taskflow/products/123"`
|
||||
- Handle `GoRouter.of(context).routerDelegate.currentConfiguration` for dynamic links
|
||||
|
||||
## Rules
|
||||
- **NEVER** use `Navigator.push/pop` — use `context.go()`, `context.push()`, `context.pop()`
|
||||
- All routes declared in one file: `lib/core/routing/app_router.dart`
|
||||
- `BlocProvider`s for route-level blocs created inside the `builder` of each route
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
---
|
||||
description: "Security standards for TaskFlow — ALWAYS APPLIED on every file write"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Security Standards — TaskFlow
|
||||
> **Pillar 5**: Security is an always-on rule, not just a reactive agent.
|
||||
> These rules apply to EVERY file write, regardless of feature or context.
|
||||
|
||||
## Credential & secret management
|
||||
- **NEVER** hardcode API keys, tokens, or secrets in source code
|
||||
- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example`
|
||||
- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets)
|
||||
- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data
|
||||
- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds
|
||||
|
||||
## Authentication & sessions
|
||||
- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB
|
||||
- Implement token refresh with retry logic — never let a 401 show a raw error to the user
|
||||
- Certificate pinning: required for production builds on Supabase Auth — use `dio_pinning_interceptor`
|
||||
- Biometric re-auth: require for any transaction > defined threshold
|
||||
|
||||
## Data & privacy
|
||||
- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters
|
||||
- Sanitize all user inputs before sending to backend
|
||||
- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails
|
||||
- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions
|
||||
|
||||
## Network security
|
||||
- All API calls use HTTPS — no http:// in production
|
||||
- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true`
|
||||
- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)`
|
||||
- Rate-limit sensitive endpoints: auth, OTP, password reset
|
||||
|
||||
## Dependency security
|
||||
- Run `dart pub outdated` monthly — flag packages with known CVEs
|
||||
- Never use a package with <100 pub points without explicit tech lead approval
|
||||
- Pin critical security packages to exact versions in `pubspec.yaml`
|
||||
|
||||
## Secure coding patterns
|
||||
```dart
|
||||
// ✅ Correct: secure token storage
|
||||
final storage = FlutterSecureStorage();
|
||||
await storage.write(key: 'access_token', value: token);
|
||||
|
||||
// ❌ Wrong: SharedPreferences for sensitive data
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString('access_token', token); // NEVER do this
|
||||
|
||||
// ✅ Correct: PII-safe logging
|
||||
logger.info('User authenticated: userId=${user.id}'); // ok — id, not email
|
||||
logger.debug('Payment processed: orderId=$orderId'); // ok — no card data
|
||||
|
||||
// ❌ Wrong: PII in logs
|
||||
logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER
|
||||
|
||||
// ✅ Correct: --dart-define for secrets (build arg, not in code)
|
||||
// flutter build apk --dart-define=API_KEY=$API_KEY
|
||||
const apiKey = String.fromEnvironment('API_KEY'); // acceptable
|
||||
|
||||
// ✅ Correct: certificate pinning with Dio
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
|
||||
final client = HttpClient();
|
||||
client.badCertificateCallback = (cert, host, port) => false; // strict
|
||||
return client;
|
||||
};
|
||||
```
|
||||
|
||||
## Deep linking & intent security
|
||||
- Validate all incoming deep link parameters — never trust raw URL params
|
||||
- Use `app_links` package for verified deep link handling
|
||||
- Restrict URL schemes to known patterns — reject unknown schemes
|
||||
- For OAuth callbacks: validate `state` parameter to prevent CSRF
|
||||
|
||||
## Storage security
|
||||
- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher`
|
||||
- Clear sensitive data from memory after use (set to null, trigger GC)
|
||||
- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints)
|
||||
- Use `SecureRandom` for nonce/token generation — never `Random()`
|
||||
|
||||
## Code obfuscation & binary protection
|
||||
```yaml
|
||||
# android/app/build.gradle — release config
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
```
|
||||
```bash
|
||||
# Flutter release build with obfuscation
|
||||
flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/
|
||||
flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/
|
||||
```
|
||||
|
||||
## Release checklist
|
||||
Before every production release, verify:
|
||||
- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`)
|
||||
- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths)
|
||||
- [ ] Obfuscation enabled in release build config
|
||||
- [ ] Certificate pinning active and tested on both platforms
|
||||
- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`)
|
||||
- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys
|
||||
- [ ] Network calls all HTTPS — scan for `http://` in lib/
|
||||
- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'`
|
||||
- [ ] Deep link parameters validated
|
||||
- [ ] App transport security / network security config reviewed
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
description: "Riverpod conventions for TaskFlow"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Riverpod Standards — TaskFlow
|
||||
|
||||
## Provider types
|
||||
| Type | Use case |
|
||||
|------|----------|
|
||||
| `AsyncNotifier` | Async state from Supabase |
|
||||
| `Notifier` | Synchronous derived/local UI state |
|
||||
| `StreamNotifier` | Real-time subscriptions |
|
||||
| `@riverpod` function | Simple computed/derived values |
|
||||
|
||||
## Code generation (mandatory)
|
||||
```dart
|
||||
// ✅ Always use @riverpod annotation
|
||||
@riverpod
|
||||
class AuthNotifier extends _$AuthNotifier {
|
||||
@override
|
||||
Future<User?> build() => ref.watch(authRepositoryProvider).currentUser();
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(authRepositoryProvider).login(email, password),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Never write manual provider declarations
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AsyncValue<User?>>(
|
||||
(ref) => AuthNotifier(),
|
||||
); // DON'T DO THIS
|
||||
```
|
||||
|
||||
## Rules
|
||||
- `ref.watch()` inside `build()` ONLY — **never** `ref.read()` inside `build()`
|
||||
- `ref.read(provider.notifier).method()` for mutations in gesture handlers
|
||||
- `ref.invalidate(provider)` to refresh — never manually reset state to `AsyncLoading()`
|
||||
- Family providers for parameterized data: `productDetailsProvider(productId)`
|
||||
- Providers scoped at feature level; core providers in `lib/core/di/`
|
||||
|
||||
## AsyncValue in widgets
|
||||
Every `AsyncValue` MUST handle all three states:
|
||||
```dart
|
||||
ref.watch(productsProvider).when(
|
||||
data: (products) => ProductList(products: products),
|
||||
loading: () => const ProductListShimmer(), // required
|
||||
error: (e, _) => ErrorWidget(error: e), // required
|
||||
)
|
||||
```
|
||||
|
||||
## File locations in TaskFlow
|
||||
- `lib/features/[feature]/[feature]_provider.dart` (generated: `[feature]_provider.g.dart`)
|
||||
- `lib/features/[feature]/[feature]_repository.dart`
|
||||
- Run `dart run build_runner watch` during development
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
description: "Riverpod testing conventions for TaskFlow"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Riverpod Testing Standards — TaskFlow
|
||||
|
||||
## Test pattern
|
||||
```dart
|
||||
// final container = ProviderContainer(overrides: [myProvider.overrideWithValue(mockValue)]); addTearDown(container.dispose);
|
||||
|
||||
void main() {
|
||||
late ProviderContainer container;
|
||||
late MockProductRepository mockRepo;
|
||||
|
||||
setUp(() {
|
||||
mockRepo = MockProductRepository();
|
||||
container = ProviderContainer(overrides: [
|
||||
productRepositoryProvider.overrideWithValue(mockRepo),
|
||||
]);
|
||||
});
|
||||
|
||||
tearDown(() => container.dispose()); // ALWAYS dispose
|
||||
|
||||
test('ProductsNotifier loads products successfully', () async {
|
||||
when(() => mockRepo.getProducts()).thenAnswer((_) async => [fakeProduct]);
|
||||
|
||||
final notifier = container.read(productsProvider.notifier);
|
||||
await notifier.loadProducts();
|
||||
|
||||
final state = container.read(productsProvider);
|
||||
expect(state, isA<AsyncData<List<Product>>>());
|
||||
expect(state.value, [fakeProduct]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Widget tests with Riverpod
|
||||
```dart
|
||||
testWidgets('ProductScreen shows shimmer while loading', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
productsProvider.overrideWith((ref) => const AsyncLoading()),
|
||||
],
|
||||
child: const MaterialApp(home: ProductScreen()),
|
||||
),
|
||||
);
|
||||
expect(find.byType(ProductShimmer), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Rules
|
||||
- **Never** use a real `ProviderScope` in unit tests — always use `ProviderContainer` with overrides
|
||||
- `addTearDown(container.dispose)` in every test that creates a container
|
||||
- Test all three `AsyncValue` states: loading, data, error
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
description: "Core Flutter conventions for TaskFlow — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Flutter Core Standards — TaskFlow
|
||||
|
||||
## Const and performance
|
||||
- Use `const` constructors wherever possible — compile-time guarantee of no rebuild
|
||||
- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)`
|
||||
- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled
|
||||
|
||||
## Null safety
|
||||
- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment
|
||||
- Prefer `??`, `?.`, and `if (x != null)` guards
|
||||
- Use `required` for all non-nullable named parameters
|
||||
- Never use `late` without a guarantee of initialisation before first access
|
||||
|
||||
## Widget lifecycle
|
||||
- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers
|
||||
- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget`
|
||||
- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)`
|
||||
|
||||
## Naming conventions
|
||||
- Files: `snake_case.dart`
|
||||
- Classes: `PascalCase`
|
||||
- Variables/functions: `camelCase`
|
||||
- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants
|
||||
- Private members: `_camelCase`
|
||||
|
||||
## Imports
|
||||
- Order: dart: → package: → relative
|
||||
- Use relative imports within a feature; absolute for cross-feature
|
||||
- Never import a feature's internal files from outside that feature
|
||||
|
||||
## Code quality
|
||||
- Max function length: 40 lines. Extract widgets and helpers aggressively
|
||||
- No `print()` in production code — use a logging package
|
||||
- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this`
|
||||
- Run `dart format` before every commit
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
description: "Project context for TaskFlow — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Project Context — TaskFlow
|
||||
|
||||
## Project identity
|
||||
- **Name:** TaskFlow
|
||||
- **Package:** com.test.taskflow
|
||||
- **Description:** Task management app
|
||||
- **Scale:** small
|
||||
|
||||
## Technology stack
|
||||
- **State management:** Riverpod
|
||||
- **Architecture:** Feature-First
|
||||
- **Routing:** GoRouter
|
||||
- **Backend:** Supabase
|
||||
- **Auth:** Supabase Auth
|
||||
- **Platforms:** ios, android, web
|
||||
- **Code generation:** freezed, json_serializable
|
||||
|
||||
## Feature modules
|
||||
auth, tasks, profile
|
||||
|
||||
## Special capabilities
|
||||
|
||||
|
||||
## Environments / flavors
|
||||
- Flavors: dev, prod
|
||||
- CI/CD: GitHub Actions
|
||||
|
||||
## Design & API references
|
||||
- Design source: none
|
||||
- API docs: none at ``
|
||||
|
||||
## Code references
|
||||
### Git repositories
|
||||
_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context.
|
||||
|
||||
### Local paths
|
||||
_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant.
|
||||
|
||||
## Product UX / themes & roles
|
||||
- **Theme variants:** light, dark
|
||||
- **Roles:** Not enabled (`app_context.roles_enabled: false`).
|
||||
|
||||
|
||||
## Reviews — which rule owns what
|
||||
- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only)
|
||||
- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals)
|
||||
- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule
|
||||
|
||||
## Architecture boundaries
|
||||
- features/auth/ MUST NOT import from features/home/ etc.
|
||||
- Shared code lives in core/ or shared/ only
|
||||
- Each feature is self-contained: screen + provider + model + repo
|
||||
|
||||
## When generating code for this project
|
||||
1. Always use Riverpod patterns — never suggest alternatives
|
||||
2. Always follow Feature-First folder structure
|
||||
3. Always use GoRouter for navigation — never `Navigator.push` directly
|
||||
4. Always target platforms: ios, android, web
|
||||
5. If code generation tools are used (freezed, json_serializable), follow their conventions
|
||||
6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets
|
||||
7. No user-facing string literals in widgets — l10n or shared constants per localization rule
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description: "UI/UX standards for TaskFlow — always applied"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# UI / UX Standards — TaskFlow
|
||||
|
||||
## Theme & design tokens (single source of truth)
|
||||
- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only.
|
||||
- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s).
|
||||
- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI.
|
||||
- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius.
|
||||
|
||||
## Loading states
|
||||
- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms
|
||||
- Use `shimmer` package with a shimmer that matches the final layout shape
|
||||
- Never show a blank screen during loading — skeleton must fill the same space as the content
|
||||
|
||||
## Empty states
|
||||
- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA
|
||||
- Empty state is different from error state — never reuse the same widget for both
|
||||
- Empty state copy: positive framing ("No items yet — add your first one")
|
||||
|
||||
## Error states
|
||||
- Every async failure MUST show: error message + retry button
|
||||
- Never swallow errors silently
|
||||
- Error text: user-friendly, never expose stack traces or raw API messages
|
||||
|
||||
## Navigation & transitions
|
||||
- Use `IndexedStack` for bottom nav tabs — preserves scroll position
|
||||
- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))`
|
||||
- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets
|
||||
|
||||
## Responsive layout
|
||||
- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values
|
||||
- Minimum touch target: 48×48 logical pixels (Material guideline)
|
||||
- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum
|
||||
|
||||
## Haptics
|
||||
- Use `HapticFeedback.lightImpact()` on primary CTAs
|
||||
- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions
|
||||
- Never add haptics to destructive actions without confirmation
|
||||
|
||||
## Accessibility
|
||||
- All interactive widgets must have a `Semantics` label or `tooltip`
|
||||
- Minimum contrast ratio: 4.5:1 (WCAG AA)
|
||||
- Test with TalkBack / VoiceOver before each release
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Create Build Flavor — TaskFlow
|
||||
|
||||
Creates a new build flavor/environment. Current flavors: dev, prod.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a new flavor called [flavor_name]
|
||||
```
|
||||
|
||||
## What gets created
|
||||
1. Android: new `productFlavors` block in `android/app/build.gradle`
|
||||
2. iOS: new scheme + configuration in Xcode (instructions provided)
|
||||
3. `lib/core/config/[flavor_name]_config.dart`
|
||||
4. `.env.[flavor_name]` template
|
||||
5. GitHub Actions pipeline update
|
||||
|
||||
## CI/CD: GitHub Actions
|
||||
Generate the pipeline config snippet for the new flavor in GitHub Actions format.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Deploy — TaskFlow
|
||||
|
||||
Guides through deployment to GitHub Actions for any of the flavors: dev, prod.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Deploy [flavor] to [store/environment]
|
||||
```
|
||||
|
||||
## GitHub Actions pipeline
|
||||
The AI will generate or update the github_actions configuration file for:
|
||||
- Build signing
|
||||
- Running tests
|
||||
- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod)
|
||||
|
||||
## Pre-deploy checklist
|
||||
- [ ] Version bump in `pubspec.yaml`
|
||||
- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/`
|
||||
- [ ] No debug flags in production code
|
||||
- [ ] Security checklist from `security-standards.mdc` passed
|
||||
- [ ] `cursor_gen --validate` passes
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Generate Tests — TaskFlow
|
||||
|
||||
Generates comprehensive unit, widget, and integration tests for **Riverpod** patterns.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Generate tests for [ClassName or file path]
|
||||
```
|
||||
|
||||
## Test generation process
|
||||
1. Read the source file completely
|
||||
2. Identify all testable units (public methods, state transitions, UI states)
|
||||
3. Generate tests following this pattern:
|
||||
```
|
||||
final container = ProviderContainer(overrides: [myProvider.overrideWithValue(mockValue)]); addTearDown(container.dispose);
|
||||
```
|
||||
4. Create mocks with `mocktail` for all dependencies
|
||||
5. Place test file at `test/[mirror of source path]_test.dart`
|
||||
|
||||
## Coverage targets
|
||||
- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum**
|
||||
- Widget tests: all three states (loading/error/data) must be tested
|
||||
- E2E: only critical user flows
|
||||
|
||||
## Test file structure
|
||||
```dart
|
||||
void main() {
|
||||
// Setup
|
||||
group('[ClassName]', () {
|
||||
// Happy path tests
|
||||
group('success cases', () { ... });
|
||||
// Error path tests
|
||||
group('error cases', () { ... });
|
||||
// Edge cases
|
||||
group('edge cases', () { ... });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Naming convention
|
||||
`'given [precondition], when [action], then [expected outcome]'`
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Scaffold Feature — TaskFlow
|
||||
|
||||
Scaffolds a complete new feature module following **Feature-First** architecture with **Riverpod** state management.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a feature called [feature_name] with [description]
|
||||
```
|
||||
|
||||
## What gets generated
|
||||
|
||||
### For Feature-First architecture:
|
||||
The AI will create all necessary files for the `[feature_name]` feature following Feature-First patterns.
|
||||
|
||||
### File structure to create:
|
||||
|
||||
**Clean Architecture:**
|
||||
```
|
||||
lib/features/[feature_name]/
|
||||
domain/
|
||||
entities/[feature_name].dart
|
||||
repositories/[feature_name]_repository.dart
|
||||
usecases/
|
||||
get_[feature_name]_usecase.dart
|
||||
create_[feature_name]_usecase.dart
|
||||
data/
|
||||
models/[feature_name]_dto.dart
|
||||
datasources/[feature_name]_remote_datasource.dart
|
||||
repositories/[feature_name]_repository_impl.dart
|
||||
presentation/
|
||||
[state_files]/ ← based on Riverpod
|
||||
pages/[feature_name]_page.dart
|
||||
widgets/
|
||||
```
|
||||
|
||||
**Feature-First:**
|
||||
```
|
||||
lib/features/[feature_name]/
|
||||
[feature_name]_screen.dart
|
||||
[feature_name]_provider.dart ← or [feature_name]_bloc.dart
|
||||
[feature_name]_repository.dart
|
||||
[feature_name]_model.dart
|
||||
widgets/
|
||||
```
|
||||
|
||||
## State management boilerplate
|
||||
|
||||
### For Riverpod:
|
||||
Generate the appropriate state management files with:
|
||||
- Initial/loading/success/error states
|
||||
- All necessary events (for BLoC)
|
||||
- Repository connection
|
||||
- Dependency injection registration
|
||||
|
||||
## Steps the AI takes
|
||||
1. Ask: "What is the feature name and brief description?"
|
||||
2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)"
|
||||
3. Generate all files with correct imports and patterns
|
||||
4. Add the feature to the DI container
|
||||
5. Add the route to GoRouter router
|
||||
6. Create a placeholder test file
|
||||
|
||||
## Code generation
|
||||
{{#if codegen_freezed}}
|
||||
- Generate Freezed model: run `dart run build_runner build` after scaffolding
|
||||
{{/if}}
|
||||
{{#if codegen_injectable}}
|
||||
- Register in injectable: add `@lazySingleton` to repository
|
||||
{{/if}}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Scaffold Screen — TaskFlow
|
||||
|
||||
Creates a complete screen widget with all states handled, following **Riverpod** patterns.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Create a screen for [screen_name] that shows [content description]
|
||||
```
|
||||
|
||||
## Generated screen template
|
||||
|
||||
### Riverpod screen pattern:
|
||||
|
||||
**BLoC:**
|
||||
```dart
|
||||
class [ScreenName]Screen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<[Feature]Bloc, [Feature]State>(
|
||||
builder: (context, state) => switch (state) {
|
||||
[Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(),
|
||||
[Feature]Loaded(:final data) => [ScreenName]Content(data: data),
|
||||
[Feature]Empty() => const [ScreenName]EmptyState(),
|
||||
[Feature]Error(:final message) => [ScreenName]ErrorState(
|
||||
message: message,
|
||||
onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Riverpod:**
|
||||
```dart
|
||||
class [ScreenName]Screen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch([feature]Provider);
|
||||
return state.when(
|
||||
loading: () => const [ScreenName]Shimmer(),
|
||||
data: (data) => [ScreenName]Content(data: data),
|
||||
error: (e, _) => [ScreenName]ErrorState(error: e),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Required sub-widgets to generate
|
||||
1. `[ScreenName]Shimmer` — skeleton loading layout matching final content
|
||||
2. `[ScreenName]EmptyState` — illustration + headline + CTA button
|
||||
3. `[ScreenName]ErrorState` — error message + retry button
|
||||
4. `[ScreenName]Content` — the actual data display
|
||||
|
||||
## Platform considerations (ios, android, web)
|
||||
{{#if platform_web}}
|
||||
- Web: ensure no dart:io usage; test at 375px and 1280px widths
|
||||
{{/if}}
|
||||
{{#if platform_desktop}}
|
||||
- Desktop: add keyboard shortcut support for primary actions
|
||||
{{/if}}
|
||||
Reference in New Issue
Block a user