diff --git a/lib/entities/demo_mode.dart b/lib/entities/demo_mode.dart new file mode 100644 index 000000000..6cb59abaa --- /dev/null +++ b/lib/entities/demo_mode.dart @@ -0,0 +1,81 @@ +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hash_wallet/core/auth_service.dart'; +import 'package:hash_wallet/core/execution_state.dart'; +import 'package:hash_wallet/core/new_wallet_arguments.dart'; +import 'package:hash_wallet/di.dart'; +import 'package:hash_wallet/entities/load_current_wallet.dart'; +import 'package:hash_wallet/entities/preferences_key.dart'; +import 'package:hash_wallet/entities/seed_type.dart'; +import 'package:hash_wallet/store/authentication_store.dart'; +import 'package:hash_wallet/view_model/wallet_new_vm.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Whether this build was launched in capture/demo mode. +/// +/// Compile-time const AND gated on [kDebugMode], so in a release build both +/// halves fold to false and every demo path below is tree-shaken out. It is +/// impossible to enable DEMO_MODE in a store binary. +/// +/// Enable for a local capture build with: +/// flutter run --dart-define=DEMO_MODE=true +const bool _demoModeDefine = bool.fromEnvironment('DEMO_MODE'); +bool get isDemoMode => kDebugMode && _demoModeDefine; + +/// PIN the demo build "unlocks" with. +const String _demoPin = String.fromEnvironment('DEMO_PIN', defaultValue: '0801'); + +/// Name of the throwaway wallet the demo build creates on first launch. +const String _demoWalletName = 'Demo Wallet'; + +/// Bring the app up straight into a usable, unlocked wallet so the capture +/// harness never has to drive onboarding or the PIN pad (whose synthetic taps +/// are unreliable under the headless emulator). +/// +/// First launch: create a fresh Wownero wallet through the real new-wallet +/// view model — so walletInfo, derivation, paths, persistence and +/// changeCurrentWallet all happen exactly as they do in the UI flow, just +/// without the screens. No seed import, no network sync: instant, deterministic, +/// zero balance. Later launches: reuse the wallet already on disk. +/// +/// This is a THROWAWAY wallet. Never fund it. To shoot balances/history, point +/// a demo build at a funded wallet instead (out of scope here). +Future maybeSetupDemoWallet() async { + if (!isDemoMode) return; + + try { + final authStore = getIt.get(); + final authService = getIt.get(); + final prefs = getIt.get(); + + await authService.setPassword(_demoPin); + + final existing = prefs.getString(PreferencesKey.currentWalletName); + if (existing != null) { + // A demo wallet already exists from a prior run — just load it. + await loadCurrentWallet(); + authStore.allowed(); + printV('[demo] loaded existing wallet "$existing"'); + return; + } + + // Fresh Wownero wallet via the real create flow. WalletNewVM.create builds + // the WalletInfo, derivation, and paths, creates + saves the wallet, calls + // AppStore.changeCurrentWallet (persisting currentWalletName/type) and + // AuthenticationStore.allowedCreate() on success. + final vm = getIt.get( + param1: NewWalletArguments(type: WalletType.wownero)); + vm.name = _demoWalletName; + await vm.create(options: ['English', MoneroSeedType.polyseed]); + + if (vm.state is! ExecutedSuccessfullyState) { + throw Exception('wallet create did not finish (${vm.state.runtimeType})'); + } + printV('[demo] created fresh Wownero wallet "$_demoWalletName"'); + } catch (e, s) { + // Never let a demo-only path crash the app; fall back to normal onboarding + // so the failure is visible rather than fatal. + printV('[demo] setup failed, falling back to onboarding: $e\n$s'); + } +} diff --git a/lib/main.dart b/lib/main.dart index 383317c76..9b9c96e37 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:hash_wallet/core/trade_monitor.dart'; import 'package:hash_wallet/di.dart'; import 'package:hash_wallet/entities/contact.dart'; import 'package:hash_wallet/entities/default_settings_migration.dart'; +import 'package:hash_wallet/entities/demo_mode.dart'; import 'package:hash_wallet/entities/get_encryption_key.dart'; import 'package:hash_wallet/entities/language_service.dart'; import 'package:hash_wallet/entities/template.dart'; @@ -162,6 +163,11 @@ Future runAppWithZone({Key? topLevelKey}) async { ProxyWrapper.logger = MemoryProxyLogger(); } + // Capture/demo builds only (kDebugMode + --dart-define=DEMO_MODE): come up + // straight into an unlocked wallet so screenshots/video never depend on the + // onboarding/PIN flow. No-op in release. + await maybeSetupDemoWallet(); + // Basically when we're running a test if (topLevelKey != null) { runApp( @@ -376,11 +382,13 @@ class AppState extends State with SingleTickerProviderStateMixin { final settingsStore = appStore.settingsStore; final statusBarColor = Colors.transparent; final authenticationStore = getIt.get(); - final initialRoute = authenticationStore.state == AuthenticationState.uninitialized - ? Routes.welcome - : settingsStore.currentBuiltinTor - ? Routes.startTor - : Routes.login; + final initialRoute = isDemoMode + ? Routes.dashboard + : authenticationStore.state == AuthenticationState.uninitialized + ? Routes.welcome + : settingsStore.currentBuiltinTor + ? Routes.startTor + : Routes.login; final currentTheme = appStore.themeStore.currentTheme; final statusBarBrightness = currentTheme.type == currentTheme.isDark ? Brightness.light : Brightness.dark; diff --git a/tools/demo/build-apk.sh b/tools/demo/build-apk.sh index bb8500331..e8fbe281d 100755 --- a/tools/demo/build-apk.sh +++ b/tools/demo/build-apk.sh @@ -45,6 +45,8 @@ docker run --rm \ -e SKIP_DEPS="$SKIP_DEPS" \ -e BUILD_MODE="$BUILD_MODE" \ -e TARGET_PLATFORM="$TARGET_PLATFORM" \ + -e DEMO="${DEMO:-0}" \ + -e DEMO_PIN="${DEMO_PIN:-0801}" \ -e HOST_UID="$HOST_UID" \ -e HOST_GID="$HOST_GID" \ "$IMAGE" bash -c ' @@ -214,7 +216,15 @@ step "compile svg assets" step "build apk ($BUILD_MODE / ${TARGET_PLATFORM:-universal})" EXTRA="" [[ -n "$TARGET_PLATFORM" ]] && EXTRA="--target-platform=$TARGET_PLATFORM" -flutter build apk --dart-define-from-file=env.json --$BUILD_MODE $EXTRA +# DEMO=1 bakes in DEMO_MODE so the app boots straight to an unlocked demo +# wallet (kDebugMode-gated, impossible in release). Lets capture skip the +# onboarding/PIN flow entirely. +DEMO_DEFINES="" +if [[ "${DEMO:-0}" == "1" ]]; then + DEMO_DEFINES="--dart-define=DEMO_MODE=true --dart-define=DEMO_PIN=${DEMO_PIN:-0801}" + echo "DEMO_MODE baked in" +fi +flutter build apk --dart-define-from-file=env.json --$BUILD_MODE $EXTRA $DEMO_DEFINES echo echo "==> artifacts:" diff --git a/tools/demo/capture.sh b/tools/demo/capture.sh index 6d31b79b2..fcc890320 100755 --- a/tools/demo/capture.sh +++ b/tools/demo/capture.sh @@ -272,6 +272,8 @@ cmd_record() { --target=integration_test/demo/scenarios/'"$scenario"'.dart \ -d "$SERIAL" \ --dart-define-from-file=env.json \ + --dart-define=DEMO_MODE=true \ + --dart-define=DEMO_PIN='"${DEMO_PIN:-0801}"' \ --dart-define=DEMO_WALLET_SEED="$DEMO_SEED" \ --dart-define=DEMO_SEND_ADDRESS="$DEMO_SEND_ADDR" ' 2>&1 | tee "$RAW_DIR/$scenario.drive.log"