diff --git a/.env.example b/.env.example index 774efa6..fa69ec1 100644 --- a/.env.example +++ b/.env.example @@ -6,5 +6,5 @@ AI_API_KEY= AI_BASE_URL= HTTP_ADDR=:8080 # Local demo: keep events in processing long enough to see PENDING → PROCESSING in the app. -DEMO_PROCESS_DELAY_MS=8000 +DEMO_PROCESS_DELAY_MS=5000 diff --git a/README.md b/README.md index cb3cf40..04ab9bd 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ fvm flutter run --dart-define=API_BASE=http://127.0.0.1:8080 # Android emulator: API_BASE=http://10.0.2.2:8080 ``` -In the app: sign in as `demo-user-1@walletops.local` / `ops-secret-1` → **Events → Run live demo → confirm**. Watch each webhook move **PENDING → PROCESSING → PROCESSED** (compose holds processing ~8s so the queue is readable). Open an event for pipeline steps → **Explain**. +In the app: sign in as `demo-user-1@walletops.local` / `ops-secret-1` → **Events → Run live demo → confirm**. Watch each webhook move **PENDING → PROCESSING → PROCESSED** (compose holds processing ~5s so the queue is readable). Open an event for pipeline steps → **Explain**. Check **Rules** for the demo alert rule the worker matches. `POST /v1/demo/simulate` (auth) inserts fresh pending events for the journey; partner HMAC ingest remains `POST /v1/webhooks/events`. diff --git a/demos/README.md b/demos/README.md new file mode 100644 index 0000000..33310d1 --- /dev/null +++ b/demos/README.md @@ -0,0 +1,28 @@ +# Demo captures + +## Latest (symlinks) + +- Recording: [`recordings/latest.mp4`](recordings/latest.mp4) +- Screenshots: [`recordings/latest-screenshots/`](recordings/latest-screenshots) +- Working copy of the last run: [`walletops-demo-walkthrough.mp4`](walletops-demo-walkthrough.mp4) + [`screenshots/`](screenshots) + +## Walkthrough steps + +1. Login +2. Events +3. Rules list (alert rules matched by the worker) +4. Rule detail / edit sheet +5. Events → Run live demo (confirm) +6. PENDING → PROCESSING → PROCESSED +7. Event detail + matched rule in pipeline +8. Rules again after the demo + +## Re-record + +```bash +docker compose up --build -d # DEMO_PROCESS_DELAY_MS=5000 +./scripts/record_demo_walkthrough.sh +``` + +Requires a booted iOS Simulator and API on `http://127.0.0.1:8080`. +Each run also archives a timestamped copy under `recordings/`. diff --git a/demos/recordings/latest-screenshots b/demos/recordings/latest-screenshots new file mode 120000 index 0000000..af672f2 --- /dev/null +++ b/demos/recordings/latest-screenshots @@ -0,0 +1 @@ +screenshots-20260720T102022Z \ No newline at end of file diff --git a/demos/recordings/latest.mp4 b/demos/recordings/latest.mp4 new file mode 120000 index 0000000..f0a8607 --- /dev/null +++ b/demos/recordings/latest.mp4 @@ -0,0 +1 @@ +walletops-demo-20260720T102022Z.mp4 \ No newline at end of file diff --git a/demos/recordings/screenshots-20260720T102022Z/01-login.png b/demos/recordings/screenshots-20260720T102022Z/01-login.png new file mode 100644 index 0000000..70dfcb5 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/01-login.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/02-events.png b/demos/recordings/screenshots-20260720T102022Z/02-events.png new file mode 100644 index 0000000..ef85e31 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/02-events.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/03-rules-list.png b/demos/recordings/screenshots-20260720T102022Z/03-rules-list.png new file mode 100644 index 0000000..1aa7571 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/03-rules-list.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/04-rules-detail.png b/demos/recordings/screenshots-20260720T102022Z/04-rules-detail.png new file mode 100644 index 0000000..cc2af27 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/04-rules-detail.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/05-events-ready.png b/demos/recordings/screenshots-20260720T102022Z/05-events-ready.png new file mode 100644 index 0000000..ef85e31 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/05-events-ready.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/06-demo-confirm.png b/demos/recordings/screenshots-20260720T102022Z/06-demo-confirm.png new file mode 100644 index 0000000..0c99864 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/06-demo-confirm.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/07-demo-started.png b/demos/recordings/screenshots-20260720T102022Z/07-demo-started.png new file mode 100644 index 0000000..25845ea Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/07-demo-started.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/08-demo-processing.png b/demos/recordings/screenshots-20260720T102022Z/08-demo-processing.png new file mode 100644 index 0000000..89fdf2d Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/08-demo-processing.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/09-demo-complete.png b/demos/recordings/screenshots-20260720T102022Z/09-demo-complete.png new file mode 100644 index 0000000..1e38f88 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/09-demo-complete.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/10-event-detail.png b/demos/recordings/screenshots-20260720T102022Z/10-event-detail.png new file mode 100644 index 0000000..0bef803 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/10-event-detail.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/11-event-pipeline-matched-rule.png b/demos/recordings/screenshots-20260720T102022Z/11-event-pipeline-matched-rule.png new file mode 100644 index 0000000..a3a0a44 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/11-event-pipeline-matched-rule.png differ diff --git a/demos/recordings/screenshots-20260720T102022Z/13-rules-after-demo.png b/demos/recordings/screenshots-20260720T102022Z/13-rules-after-demo.png new file mode 100644 index 0000000..f91b279 Binary files /dev/null and b/demos/recordings/screenshots-20260720T102022Z/13-rules-after-demo.png differ diff --git a/demos/recordings/walletops-demo-20260720T102022Z.mp4 b/demos/recordings/walletops-demo-20260720T102022Z.mp4 new file mode 100644 index 0000000..cf7f9b5 Binary files /dev/null and b/demos/recordings/walletops-demo-20260720T102022Z.mp4 differ diff --git a/demos/screenshots/01-login.png b/demos/screenshots/01-login.png new file mode 100644 index 0000000..70dfcb5 Binary files /dev/null and b/demos/screenshots/01-login.png differ diff --git a/demos/screenshots/02-events.png b/demos/screenshots/02-events.png new file mode 100644 index 0000000..ef85e31 Binary files /dev/null and b/demos/screenshots/02-events.png differ diff --git a/demos/screenshots/03-rules-list.png b/demos/screenshots/03-rules-list.png new file mode 100644 index 0000000..1aa7571 Binary files /dev/null and b/demos/screenshots/03-rules-list.png differ diff --git a/demos/screenshots/04-rules-detail.png b/demos/screenshots/04-rules-detail.png new file mode 100644 index 0000000..cc2af27 Binary files /dev/null and b/demos/screenshots/04-rules-detail.png differ diff --git a/demos/screenshots/05-events-ready.png b/demos/screenshots/05-events-ready.png new file mode 100644 index 0000000..ef85e31 Binary files /dev/null and b/demos/screenshots/05-events-ready.png differ diff --git a/demos/screenshots/06-demo-confirm.png b/demos/screenshots/06-demo-confirm.png new file mode 100644 index 0000000..0c99864 Binary files /dev/null and b/demos/screenshots/06-demo-confirm.png differ diff --git a/demos/screenshots/07-demo-started.png b/demos/screenshots/07-demo-started.png new file mode 100644 index 0000000..25845ea Binary files /dev/null and b/demos/screenshots/07-demo-started.png differ diff --git a/demos/screenshots/08-demo-processing.png b/demos/screenshots/08-demo-processing.png new file mode 100644 index 0000000..89fdf2d Binary files /dev/null and b/demos/screenshots/08-demo-processing.png differ diff --git a/demos/screenshots/09-demo-complete.png b/demos/screenshots/09-demo-complete.png new file mode 100644 index 0000000..1e38f88 Binary files /dev/null and b/demos/screenshots/09-demo-complete.png differ diff --git a/demos/screenshots/10-event-detail.png b/demos/screenshots/10-event-detail.png new file mode 100644 index 0000000..0bef803 Binary files /dev/null and b/demos/screenshots/10-event-detail.png differ diff --git a/demos/screenshots/11-event-pipeline-matched-rule.png b/demos/screenshots/11-event-pipeline-matched-rule.png new file mode 100644 index 0000000..a3a0a44 Binary files /dev/null and b/demos/screenshots/11-event-pipeline-matched-rule.png differ diff --git a/demos/screenshots/13-rules-after-demo.png b/demos/screenshots/13-rules-after-demo.png new file mode 100644 index 0000000..f91b279 Binary files /dev/null and b/demos/screenshots/13-rules-after-demo.png differ diff --git a/demos/walletops-demo-walkthrough.mp4 b/demos/walletops-demo-walkthrough.mp4 new file mode 100644 index 0000000..cf7f9b5 Binary files /dev/null and b/demos/walletops-demo-walkthrough.mp4 differ diff --git a/docker-compose.yml b/docker-compose.yml index 79bb1a8..59b8803 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: AI_PROVIDER: mock AI_API_KEY: "" HTTP_ADDR: ":8080" - DEMO_PROCESS_DELAY_MS: "8000" + DEMO_PROCESS_DELAY_MS: "5000" ports: - "8080:8080" depends_on: diff --git a/mobile/integration_test/demo_walkthrough_test.dart b/mobile/integration_test/demo_walkthrough_test.dart new file mode 100644 index 0000000..f4bbffc --- /dev/null +++ b/mobile/integration_test/demo_walkthrough_test.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:walletops_mobile/bootstrap.dart'; +import 'package:walletops_mobile/core/storage/token_storage.dart'; + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + Future snap(String name) async { + await binding.takeScreenshot(name); + } + + Future settle(WidgetTester tester, {int pumps = 12}) async { + for (var i = 0; i < pumps; i++) { + await tester.pump(const Duration(milliseconds: 350)); + } + } + + Future tapNav(WidgetTester tester, String label) async { + final dest = find.widgetWithText(NavigationDestination, label); + if (dest.evaluate().isNotEmpty) { + await tester.tap(dest); + } else { + await tester.tap(find.text(label).last); + } + await settle(tester, pumps: 16); + } + + testWidgets( + 'demo walkthrough screenshots', + (tester) async { + await GetIt.instance.reset(); + await bootstrapApp( + tokenStorage: InMemoryTokenStorage(), + apiBase: const String.fromEnvironment( + 'API_BASE', + defaultValue: 'http://127.0.0.1:8080', + ), + ); + await settle(tester, pumps: 18); + await snap('01-login'); + + await tester.tap(find.text('Sign in')); + await settle(tester, pumps: 22); + await snap('02-events'); + + // Show Rules before the live demo so the viewer sees the matcher. + await tapNav(tester, 'Rules'); + await snap('03-rules-list'); + + final demoRule = find.textContaining('Demo balance watch'); + if (demoRule.evaluate().isNotEmpty) { + await tester.tap(demoRule.first); + await settle(tester, pumps: 14); + await snap('04-rules-detail'); + // Dismiss sheet / return to list. + final close = find.byIcon(Icons.close); + if (close.evaluate().isNotEmpty) { + await tester.tap(close.first); + } else { + await tester.tapAt(const Offset(20, 80)); + } + await settle(tester, pumps: 10); + } else { + // Open create sheet to show how a rule is defined. + final add = find.byTooltip('New rule'); + if (add.evaluate().isNotEmpty) { + await tester.tap(add); + await settle(tester, pumps: 12); + await snap('04-rules-create'); + final cancel = find.text('Cancel'); + if (cancel.evaluate().isNotEmpty) { + await tester.tap(cancel); + await settle(tester, pumps: 10); + } + } + } + + await tapNav(tester, 'Events'); + await snap('05-events-ready'); + + await tester.tap(find.text('Run live demo')); + await settle(tester, pumps: 10); + await snap('06-demo-confirm'); + + await tester.tap(find.text('Start demo')); + await settle(tester, pumps: 12); + await snap('07-demo-started'); + + var sawProcessing = false; + var sawComplete = false; + for (var i = 0; i < 100 && !sawComplete; i++) { + await tester.pump(const Duration(seconds: 1)); + if (!sawProcessing && + find.textContaining('PROCESSING').evaluate().isNotEmpty) { + await snap('08-demo-processing'); + sawProcessing = true; + } + if (find.textContaining('Demo complete').evaluate().isNotEmpty) { + sawComplete = true; + } + } + expect(sawComplete, isTrue, reason: 'guided demo did not finish'); + await settle(tester, pumps: 8); + await snap('09-demo-complete'); + + final eventTitle = find.textContaining('balance drop'); + if (eventTitle.evaluate().isNotEmpty) { + await tester.tap(eventTitle.first); + } else { + await tester.tap(find.text('PROCESSED').first); + } + await settle(tester, pumps: 18); + await snap('10-event-detail'); + + // Scroll to show matched rule / explain CTA. + await tester.drag(find.byType(ListView).first, const Offset(0, -280)); + await settle(tester, pumps: 8); + await snap('11-event-pipeline-matched-rule'); + + final explain = find.text('Explain with schema summary'); + if (explain.evaluate().isNotEmpty) { + await tester.ensureVisible(explain); + await tester.tap(explain); + await settle(tester, pumps: 22); + await snap('12-explain'); + final back = find.byTooltip('Back'); + if (back.evaluate().isNotEmpty) { + await tester.tap(back); + await settle(tester, pumps: 10); + } + } + + // Back to events then Rules again after matching happened. + final eventBack = find.byTooltip('Back'); + if (eventBack.evaluate().isNotEmpty) { + await tester.tap(eventBack); + await settle(tester, pumps: 12); + } + await tapNav(tester, 'Rules'); + await snap('13-rules-after-demo'); + }, + timeout: const Timeout(Duration(minutes: 4)), + ); +} diff --git a/mobile/lib/bootstrap.dart b/mobile/lib/bootstrap.dart new file mode 100644 index 0000000..68c11e4 --- /dev/null +++ b/mobile/lib/bootstrap.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'app.dart'; +import 'core/di/injection.dart'; +import 'core/router/app_router.dart'; +import 'core/storage/token_storage.dart'; +import 'features/auth/presentation/cubit/auth_cubit.dart'; +import 'features/auth/presentation/cubit/auth_state.dart'; +import 'features/ops/presentation/cubit/ops_health_cubit.dart'; + +Future bootstrapApp({ + TokenStorage? tokenStorage, + String? apiBase, +}) async { + WidgetsFlutterBinding.ensureInitialized(); + await configureDependencies( + tokenStorage: tokenStorage, + apiBase: apiBase, + ); + final authCubit = getIt(); + final router = createAppRouter(authCubit); + runApp(WalletOpsApp(authCubit: authCubit, router: router)); + await authCubit.bootstrap(); + if (authCubit.state.status == AuthStatus.authenticated) { + getIt().start(); + } +} diff --git a/mobile/lib/features/auth/presentation/login_page.dart b/mobile/lib/features/auth/presentation/login_page.dart index 037b29c..0182192 100644 --- a/mobile/lib/features/auth/presentation/login_page.dart +++ b/mobile/lib/features/auth/presentation/login_page.dart @@ -78,72 +78,86 @@ class _LoginPageState extends State { ), ), const SizedBox(height: AppSpacing.lg), - Container( - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: scheme.surfaceContainerLow, - borderRadius: - BorderRadius.circular(AppSpacing.radiusSm), - border: Border( - left: BorderSide( - color: scheme.primary, - width: 3, - ), - top: BorderSide( - color: scheme.outline.withValues(alpha: 0.7), - ), - right: BorderSide( - color: scheme.outline.withValues(alpha: 0.7), - ), - bottom: BorderSide( - color: scheme.outline.withValues(alpha: 0.7), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'DEMO ACCOUNT', - style: theme.textTheme.labelMedium?.copyWith( - color: scheme.primary, + Stack( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: scheme.surfaceContainerLow, + borderRadius: BorderRadius.circular( + AppSpacing.radiusSm, ), - ), - const SizedBox(height: AppSpacing.xxs), - Text( - 'Prefilled credentials', - style: theme.textTheme.titleSmall, - ), - const SizedBox(height: AppSpacing.xxs), - Text( - '$kDemoEmail\npassword: $kDemoPassword\n' - 'webhook user_ref: $kDemoUserRef', - style: theme.textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - height: 1.45, + border: Border.all( + color: + scheme.outline.withValues(alpha: 0.7), ), ), - const SizedBox(height: AppSpacing.sm), - Text( - 'Run ./scripts/seed_webhooks.sh once after ' - 'docker compose up.', - style: theme.textTheme.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'DEMO ACCOUNT', + style: + theme.textTheme.labelMedium?.copyWith( + color: scheme.primary, + ), + ), + const SizedBox(height: AppSpacing.xxs), + Text( + 'Prefilled credentials', + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: AppSpacing.xxs), + Text( + '$kDemoEmail\npassword: $kDemoPassword\n' + 'webhook user_ref: $kDemoUserRef', + style: theme.textTheme.bodySmall?.copyWith( + height: 1.45, + ), + ), + const SizedBox(height: AppSpacing.sm), + Text( + 'Prefer in-app “Run live demo” on Events ' + 'after sign-in.', + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: AppSpacing.sm), + Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: () { + _email.text = kDemoEmail; + _password.text = kDemoPassword; + }, + child: const Text( + 'Reset to demo credentials', + ), + ), + ), + ], ), - const SizedBox(height: AppSpacing.sm), - Align( - alignment: Alignment.centerLeft, - child: TextButton( - onPressed: () { - _email.text = kDemoEmail; - _password.text = kDemoPassword; - }, - child: const Text('Reset to demo credentials'), + ), + Positioned( + left: 0, + top: 0, + bottom: 0, + child: Container( + width: 3, + decoration: BoxDecoration( + color: scheme.primary, + borderRadius: const BorderRadius.only( + topLeft: + Radius.circular(AppSpacing.radiusSm), + bottomLeft: + Radius.circular(AppSpacing.radiusSm), + ), ), ), - ], - ), + ), + ], ), const SizedBox(height: AppSpacing.lg), TextFormField( diff --git a/mobile/lib/features/events/presentation/cubit/events_cubit.dart b/mobile/lib/features/events/presentation/cubit/events_cubit.dart index ba25f51..263fe8d 100644 --- a/mobile/lib/features/events/presentation/cubit/events_cubit.dart +++ b/mobile/lib/features/events/presentation/cubit/events_cubit.dart @@ -14,8 +14,8 @@ class EventsCubit extends Cubit { final DemoRepository _demo; Timer? _liveTimer; - static const _betweenEventsPause = Duration(seconds: 3); - static const _terminalWait = Duration(seconds: 40); + static const _betweenEventsPause = Duration(milliseconds: 1500); + static const _terminalWait = Duration(seconds: 30); Future load({String? status, bool updateFilter = false}) async { final filter = updateFilter ? status : (status ?? state.filter); diff --git a/mobile/lib/features/events/presentation/events_page.dart b/mobile/lib/features/events/presentation/events_page.dart index 1d2b2f7..3396052 100644 --- a/mobile/lib/features/events/presentation/events_page.dart +++ b/mobile/lib/features/events/presentation/events_page.dart @@ -192,8 +192,8 @@ class EventsPage extends StatelessWidget { ), const _ConfirmStep( index: '2', - title: 'Inject 3 webhooks slowly', - detail: '~8s processing hold + pause between each', + title: 'Inject 3 webhooks', + detail: '~5s processing hold + short pause between each', ), const _ConfirmStep( index: '3', diff --git a/mobile/lib/features/ops/presentation/widgets/ops_status_bar.dart b/mobile/lib/features/ops/presentation/widgets/ops_status_bar.dart index 530f73e..35a7e41 100644 --- a/mobile/lib/features/ops/presentation/widgets/ops_status_bar.dart +++ b/mobile/lib/features/ops/presentation/widgets/ops_status_bar.dart @@ -24,51 +24,62 @@ class OpsStatusBar extends StatelessWidget { : scheme.errorContainer.withValues(alpha: 0.55), child: InkWell( onTap: () => context.read().refresh(), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border( bottom: BorderSide(color: scheme.outlineVariant), - left: BorderSide( - color: ok ? scheme.primary : scheme.error, - width: 3, - ), ), ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), + child: IntrinsicHeight( child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Icon( - ok ? Icons.monitor_heart_outlined : Icons.cloud_off_outlined, - size: 18, + Container( + width: 3, color: ok ? scheme.primary : scheme.error, ), - const SizedBox(width: AppSpacing.sm), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _headline(state), - style: theme.textTheme.labelLarge?.copyWith( - color: ok ? scheme.onSurface : scheme.error, - fontFamily: 'monospace', + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + ok + ? Icons.monitor_heart_outlined + : Icons.cloud_off_outlined, + size: 18, + color: ok ? scheme.primary : scheme.error, ), - ), - const SizedBox(height: 2), - Text( - _detail(state, health), - style: theme.textTheme.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - height: 1.35, - fontFamily: 'monospace', + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _headline(state), + style: theme.textTheme.labelLarge?.copyWith( + color: ok + ? scheme.onSurface + : scheme.error, + ), + ), + const SizedBox(height: 2), + Text( + _detail(state, health), + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + height: 1.35, + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ], diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4621fe4..74b29a2 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,20 +1,3 @@ -import 'package:flutter/material.dart'; +import 'bootstrap.dart'; -import 'app.dart'; -import 'core/di/injection.dart'; -import 'core/router/app_router.dart'; -import 'features/auth/presentation/cubit/auth_cubit.dart'; -import 'features/auth/presentation/cubit/auth_state.dart'; -import 'features/ops/presentation/cubit/ops_health_cubit.dart'; - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await configureDependencies(); - final authCubit = getIt(); - final router = createAppRouter(authCubit); - runApp(WalletOpsApp(authCubit: authCubit, router: router)); - await authCubit.bootstrap(); - if (authCubit.state.status == AuthStatus.authenticated) { - getIt().start(); - } -} +Future main() => bootstrapApp(); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5f93a27..048b64e 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -198,6 +198,11 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_driver: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -272,6 +277,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" get_it: dependency: "direct main" description: @@ -336,6 +346,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" io: dependency: transitive description: @@ -560,6 +575,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" provider: dependency: transitive description: @@ -669,6 +692,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -757,6 +788,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a6873bc..9db3491 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -24,6 +24,10 @@ dev_dependencies: bloc_test: ^10.0.0 mocktail: ^1.0.5 crypto: ^3.0.6 + integration_test: + sdk: flutter + flutter_driver: + sdk: flutter flutter: uses-material-design: true diff --git a/mobile/test_driver/integration_test.dart b/mobile/test_driver/integration_test.dart new file mode 100644 index 0000000..e33c507 --- /dev/null +++ b/mobile/test_driver/integration_test.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:integration_test/integration_test_driver_extended.dart'; + +Future main() async { + final driver = await FlutterDriver.connect(); + await integrationDriver( + driver: driver, + onScreenshot: (String name, List bytes, [Map? args]) async { + final file = File('/Users/omid/code/walletops/demos/screenshots/$name.png'); + await file.parent.create(recursive: true); + await file.writeAsBytes(bytes); + stdout.writeln('Wrote screenshot ${file.path}'); + return true; + }, + ); +} diff --git a/scripts/record_demo_walkthrough.sh b/scripts/record_demo_walkthrough.sh new file mode 100755 index 0000000..c0b35cc --- /dev/null +++ b/scripts/record_demo_walkthrough.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MOBILE="$ROOT/mobile" +OUT="$ROOT/demos" +SHOTS="$OUT/screenshots" +RECORDINGS="$OUT/recordings" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" +VIDEO="$OUT/walletops-demo-walkthrough.mp4" +SAVED_VIDEO="$RECORDINGS/walletops-demo-$STAMP.mp4" +SAVED_SHOTS="$RECORDINGS/screenshots-$STAMP" +API_BASE="${API_BASE:-http://127.0.0.1:8080}" + +mkdir -p "$SHOTS" "$RECORDINGS" +rm -f "$SHOTS"/*.png +rm -f "$VIDEO" + +echo "Checking API at $API_BASE" +curl -fsS "$API_BASE/v1/health" >/dev/null + +echo "Booting iOS Simulator if needed" +open -a Simulator || true +sleep 3 + +DEVICE_ID="$(xcrun simctl list devices booted | awk -F '[()]' '/iPhone/{print $2; exit}')" +if [[ -z "${DEVICE_ID:-}" ]]; then + (cd "$MOBILE" && fvm flutter emulators --launch apple_ios_simulator) + sleep 10 + DEVICE_ID="$(xcrun simctl list devices booted | awk -F '[()]' '/iPhone/{print $2; exit}')" +fi +echo "Using simulator $DEVICE_ID" + +echo "Starting screen recording → $VIDEO" +xcrun simctl io "$DEVICE_ID" recordVideo --codec=h264 "$VIDEO" & +REC_PID=$! +cleanup() { + if kill -0 "$REC_PID" 2>/dev/null; then + kill -INT "$REC_PID" 2>/dev/null || true + wait "$REC_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +cd "$MOBILE" +fvm flutter drive \ + --driver=test_driver/integration_test.dart \ + --target=integration_test/demo_walkthrough_test.dart \ + -d "$DEVICE_ID" \ + --dart-define=API_BASE="$API_BASE" + +cleanup +trap - EXIT + +mkdir -p "$SAVED_SHOTS" +cp -f "$SHOTS"/*.png "$SAVED_SHOTS/" 2>/dev/null || true +cp -f "$VIDEO" "$SAVED_VIDEO" +ln -sfn "$(basename "$SAVED_VIDEO")" "$RECORDINGS/latest.mp4" +ln -sfn "$(basename "$SAVED_SHOTS")" "$RECORDINGS/latest-screenshots" + +echo "Done." +echo "Latest screenshots: $SHOTS" +ls -la "$SHOTS" || true +echo "Latest recording: $VIDEO" +ls -la "$VIDEO" || true +echo "Saved recording: $SAVED_VIDEO" +echo "Saved screenshots: $SAVED_SHOTS" +ls -la "$RECORDINGS" || true