diff --git a/apps/mobile/integration_test/demo_walkthrough_test.dart b/apps/mobile/integration_test/demo_walkthrough_test.dart new file mode 100644 index 0000000..ba74866 --- /dev/null +++ b/apps/mobile/integration_test/demo_walkthrough_test.dart @@ -0,0 +1,132 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:intentguard/bootstrap.dart'; +import 'package:intentguard/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)); + } + } + + testWidgets( + 'intentguard demo walkthrough', + (tester) async { + await GetIt.instance.reset(); + await bootstrapApp( + storage: MemoryTokenStorage(), + apiBase: const String.fromEnvironment( + 'API_BASE', + defaultValue: 'http://127.0.0.1:8080', + ), + ); + await settle(tester, pumps: 24); + await snap('01-login'); + + await tester.tap(find.text('Sign in')); + await settle(tester, pumps: 28); + await snap('02-home'); + + await tester.tap(find.text('Balances')); + await settle(tester, pumps: 18); + await snap('03-balances'); + + final back = find.byTooltip('Back'); + if (back.evaluate().isNotEmpty) { + await tester.tap(back); + await settle(tester, pumps: 12); + } + + await tester.tap(find.text('New intent')); + await settle(tester, pumps: 16); + await snap('04-composer'); + + // Prefer short fixture chip for mock planner. + final chip = find.text('swap 10 USDC'); + if (chip.evaluate().isNotEmpty) { + await tester.ensureVisible(chip.first); + await tester.tap(chip.first); + await settle(tester, pumps: 8); + } + await snap('05-composer-ready'); + + await tester.tap(find.text('Create plan')); + // Wait for navigation to plan review. + var onReview = false; + for (var i = 0; i < 40 && !onReview; i++) { + await tester.pump(const Duration(milliseconds: 500)); + if (find.text('Plan review').evaluate().isNotEmpty) { + onReview = true; + } + } + expect(onReview, isTrue, reason: 'did not reach plan review'); + await settle(tester, pumps: 10); + await snap('06-plan-review'); + + // Approve each step in order. + for (var step = 0; step < 4; step++) { + final approve = find.text('Approve'); + if (approve.evaluate().isEmpty) { + break; + } + await tester.ensureVisible(approve.first); + await tester.tap(approve.first); + await settle(tester, pumps: 20); + await snap('07-approve-step-${step + 1}'); + if (find.textContaining('completed').evaluate().isNotEmpty || + find.textContaining('Completed').evaluate().isNotEmpty) { + break; + } + } + await settle(tester, pumps: 12); + await snap('08-plan-completed'); + + // History + if (back.evaluate().isNotEmpty) { + await tester.tap(find.byTooltip('Back').first); + await settle(tester, pumps: 12); + } + if (find.text('History').evaluate().isNotEmpty) { + await tester.tap(find.text('History').first); + await settle(tester, pumps: 16); + await snap('09-history'); + } + + // Reject path: compose policy reject + if (find.byTooltip('Back').evaluate().isNotEmpty) { + await tester.tap(find.byTooltip('Back').first); + await settle(tester, pumps: 10); + } + if (find.text('New intent').evaluate().isNotEmpty) { + await tester.tap(find.text('New intent')); + await settle(tester, pumps: 12); + final rejectChip = find.text('swap 150 USDC'); + if (rejectChip.evaluate().isNotEmpty) { + await tester.ensureVisible(rejectChip.first); + await tester.tap(rejectChip.first); + await settle(tester, pumps: 6); + } + await tester.tap(find.text('Create plan')); + for (var i = 0; i < 30; i++) { + await tester.pump(const Duration(milliseconds: 400)); + if (find.textContaining('policy').evaluate().isNotEmpty || + find.textContaining('Rejected').evaluate().isNotEmpty || + find.text('Plan review').evaluate().isNotEmpty) { + break; + } + } + await settle(tester, pumps: 10); + await snap('10-policy-reject'); + } + }, + timeout: const Timeout(Duration(minutes: 4)), + ); +} diff --git a/apps/mobile/lib/bootstrap.dart b/apps/mobile/lib/bootstrap.dart new file mode 100644 index 0000000..ffdad75 --- /dev/null +++ b/apps/mobile/lib/bootstrap.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +import 'app.dart'; +import 'core/di/injection.dart'; +import 'core/storage/token_storage.dart'; + +Future bootstrapApp({ + TokenStorage? storage, + String? apiBase, +}) async { + WidgetsFlutterBinding.ensureInitialized(); + await configureDependencies(storage: storage, apiBase: apiBase); + runApp(const IntentGuardApp()); +} diff --git a/apps/mobile/lib/core/di/injection.dart b/apps/mobile/lib/core/di/injection.dart index 0adb84b..274e1d3 100644 --- a/apps/mobile/lib/core/di/injection.dart +++ b/apps/mobile/lib/core/di/injection.dart @@ -10,13 +10,21 @@ import '../storage/token_storage.dart'; final getIt = GetIt.instance; -Future configureDependencies({TokenStorage? storage}) async { +Future configureDependencies({ + TokenStorage? storage, + String? apiBase, +}) async { + if (getIt.isRegistered()) { + return; + } + final tokenStorage = storage ?? SecureTokenStorage(); getIt.registerSingleton(tokenStorage); late final AuthCubit authCubit; final api = ApiClient( storage: tokenStorage, + baseUrl: apiBase, onSessionExpired: () => authCubit.sessionExpired(), ); getIt.registerSingleton(api); diff --git a/apps/mobile/lib/main.dart b/apps/mobile/lib/main.dart index 3f01172..74b29a2 100644 --- a/apps/mobile/lib/main.dart +++ b/apps/mobile/lib/main.dart @@ -1,10 +1,3 @@ -import 'package:flutter/material.dart'; +import 'bootstrap.dart'; -import 'app.dart'; -import 'core/di/injection.dart'; - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await configureDependencies(); - runApp(const IntentGuardApp()); -} +Future main() => bootstrapApp(); diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index ca645b3..213a0d3 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -190,6 +190,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: @@ -264,6 +269,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: @@ -328,6 +338,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: @@ -552,6 +567,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: @@ -661,6 +684,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: @@ -749,6 +780,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/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 10771db..a04b46f 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -23,6 +23,10 @@ dev_dependencies: flutter_lints: ^5.0.0 bloc_test: ^10.0.0 mocktail: ^1.0.4 + integration_test: + sdk: flutter + flutter_driver: + sdk: flutter flutter: uses-material-design: true diff --git a/apps/mobile/test_driver/integration_test.dart b/apps/mobile/test_driver/integration_test.dart new file mode 100644 index 0000000..fee3517 --- /dev/null +++ b/apps/mobile/test_driver/integration_test.dart @@ -0,0 +1,20 @@ +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/intentguard/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/demos/README.md b/demos/README.md new file mode 100644 index 0000000..4103e00 --- /dev/null +++ b/demos/README.md @@ -0,0 +1,16 @@ +# Demo captures + +## Latest + +- Recording: [`recordings/latest.mp4`](recordings/latest.mp4) +- Screenshots: [`recordings/latest-screenshots/`](recordings/latest-screenshots) + +## Re-record + +```bash +cd deploy && docker compose up --build -d +cd .. && ./scripts/deploy-anvil.sh && ./scripts/seed-anvil.sh +./scripts/record_demo_walkthrough.sh +``` + +Requires iOS Simulator + API on `http://127.0.0.1:8080`. diff --git a/demos/intentguard-demo-walkthrough.mp4 b/demos/intentguard-demo-walkthrough.mp4 new file mode 100644 index 0000000..430adc4 Binary files /dev/null and b/demos/intentguard-demo-walkthrough.mp4 differ diff --git a/demos/recordings/intentguard-demo-20260720T103355Z.mp4 b/demos/recordings/intentguard-demo-20260720T103355Z.mp4 new file mode 100644 index 0000000..430adc4 Binary files /dev/null and b/demos/recordings/intentguard-demo-20260720T103355Z.mp4 differ diff --git a/demos/recordings/latest-screenshots b/demos/recordings/latest-screenshots new file mode 120000 index 0000000..5e0a4a4 --- /dev/null +++ b/demos/recordings/latest-screenshots @@ -0,0 +1 @@ +screenshots-20260720T103355Z \ No newline at end of file diff --git a/demos/recordings/latest.mp4 b/demos/recordings/latest.mp4 new file mode 120000 index 0000000..7b0c00a --- /dev/null +++ b/demos/recordings/latest.mp4 @@ -0,0 +1 @@ +intentguard-demo-20260720T103355Z.mp4 \ No newline at end of file diff --git a/demos/recordings/screenshots-20260720T103355Z/01-login.png b/demos/recordings/screenshots-20260720T103355Z/01-login.png new file mode 100644 index 0000000..50cc595 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/01-login.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/02-home.png b/demos/recordings/screenshots-20260720T103355Z/02-home.png new file mode 100644 index 0000000..99689a2 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/02-home.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/03-balances.png b/demos/recordings/screenshots-20260720T103355Z/03-balances.png new file mode 100644 index 0000000..778bcff Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/03-balances.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/04-composer.png b/demos/recordings/screenshots-20260720T103355Z/04-composer.png new file mode 100644 index 0000000..cafd782 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/04-composer.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/05-composer-ready.png b/demos/recordings/screenshots-20260720T103355Z/05-composer-ready.png new file mode 100644 index 0000000..cafd782 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/05-composer-ready.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/06-plan-review.png b/demos/recordings/screenshots-20260720T103355Z/06-plan-review.png new file mode 100644 index 0000000..278c805 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/06-plan-review.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/07-approve-step-1.png b/demos/recordings/screenshots-20260720T103355Z/07-approve-step-1.png new file mode 100644 index 0000000..8dd55a0 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/07-approve-step-1.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/07-approve-step-2.png b/demos/recordings/screenshots-20260720T103355Z/07-approve-step-2.png new file mode 100644 index 0000000..9e88814 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/07-approve-step-2.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/08-plan-completed.png b/demos/recordings/screenshots-20260720T103355Z/08-plan-completed.png new file mode 100644 index 0000000..9e88814 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/08-plan-completed.png differ diff --git a/demos/recordings/screenshots-20260720T103355Z/10-policy-reject.png b/demos/recordings/screenshots-20260720T103355Z/10-policy-reject.png new file mode 100644 index 0000000..2061c62 Binary files /dev/null and b/demos/recordings/screenshots-20260720T103355Z/10-policy-reject.png differ diff --git a/demos/screenshots/01-login.png b/demos/screenshots/01-login.png new file mode 100644 index 0000000..50cc595 Binary files /dev/null and b/demos/screenshots/01-login.png differ diff --git a/demos/screenshots/02-home.png b/demos/screenshots/02-home.png new file mode 100644 index 0000000..99689a2 Binary files /dev/null and b/demos/screenshots/02-home.png differ diff --git a/demos/screenshots/03-balances.png b/demos/screenshots/03-balances.png new file mode 100644 index 0000000..778bcff Binary files /dev/null and b/demos/screenshots/03-balances.png differ diff --git a/demos/screenshots/04-composer.png b/demos/screenshots/04-composer.png new file mode 100644 index 0000000..cafd782 Binary files /dev/null and b/demos/screenshots/04-composer.png differ diff --git a/demos/screenshots/05-composer-ready.png b/demos/screenshots/05-composer-ready.png new file mode 100644 index 0000000..cafd782 Binary files /dev/null and b/demos/screenshots/05-composer-ready.png differ diff --git a/demos/screenshots/06-plan-review.png b/demos/screenshots/06-plan-review.png new file mode 100644 index 0000000..278c805 Binary files /dev/null and b/demos/screenshots/06-plan-review.png differ diff --git a/demos/screenshots/07-approve-step-1.png b/demos/screenshots/07-approve-step-1.png new file mode 100644 index 0000000..8dd55a0 Binary files /dev/null and b/demos/screenshots/07-approve-step-1.png differ diff --git a/demos/screenshots/07-approve-step-2.png b/demos/screenshots/07-approve-step-2.png new file mode 100644 index 0000000..9e88814 Binary files /dev/null and b/demos/screenshots/07-approve-step-2.png differ diff --git a/demos/screenshots/08-plan-completed.png b/demos/screenshots/08-plan-completed.png new file mode 100644 index 0000000..9e88814 Binary files /dev/null and b/demos/screenshots/08-plan-completed.png differ diff --git a/demos/screenshots/10-policy-reject.png b/demos/screenshots/10-policy-reject.png new file mode 100644 index 0000000..2061c62 Binary files /dev/null and b/demos/screenshots/10-policy-reject.png differ diff --git a/scripts/record_demo_walkthrough.sh b/scripts/record_demo_walkthrough.sh new file mode 100755 index 0000000..82ba9f0 --- /dev/null +++ b/scripts/record_demo_walkthrough.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MOBILE="$ROOT/apps/mobile" +OUT="$ROOT/demos" +SHOTS="$OUT/screenshots" +RECORDINGS="$OUT/recordings" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" +VIDEO="$OUT/intentguard-demo-walkthrough.mp4" +SAVED_VIDEO="$RECORDINGS/intentguard-demo-$STAMP.mp4" +SAVED_SHOTS="$RECORDINGS/screenshots-$STAMP" +API_BASE="${API_BASE:-http://127.0.0.1:8080}" +export PATH="$HOME/.foundry/bin:$PATH" + +mkdir -p "$SHOTS" "$RECORDINGS" +rm -f "$SHOTS"/*.png +rm -f "$VIDEO" + +echo "Checking API at $API_BASE/health" +curl -fsS "$API_BASE/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 + +FLUTTER_BIN="${FLUTTER_BIN:-$HOME/fvm/versions/3.38.6/bin/flutter}" +if [[ ! -x "$FLUTTER_BIN" ]]; then + FLUTTER_BIN="$HOME/code/walletops/.fvm/flutter_sdk/bin/flutter" +fi + +cd "$MOBILE" +"$FLUTTER_BIN" 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 "Saved recording: $SAVED_VIDEO" +ls -la "$RECORDINGS" || true