diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 730f7f12..f09668bc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -30,6 +30,8 @@ NSLocalNetworkUsageDescription XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network. + NSCameraUsageDescription + XWorkmate uses the camera only when you explicitly scan a gateway pairing QR code. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/lib/app/ui_feature_manifest.dart b/lib/app/ui_feature_manifest.dart index 4b35e50d..6d61cd11 100644 --- a/lib/app/ui_feature_manifest.dart +++ b/lib/app/ui_feature_manifest.dart @@ -270,7 +270,7 @@ mobile: description: Mobile Vault server integration section ui_surface: settings_page gateway_setup_code: - enabled: false + enabled: true release_tier: experimental build_modes: [debug, profile, release] description: Mobile gateway setup code editor diff --git a/lib/features/mobile/mobile_gateway_pairing_guide_page.dart b/lib/features/mobile/mobile_gateway_pairing_guide_page.dart new file mode 100644 index 00000000..5522d49a --- /dev/null +++ b/lib/features/mobile/mobile_gateway_pairing_guide_page.dart @@ -0,0 +1,516 @@ +import 'dart:convert'; +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../../i18n/app_language.dart'; +import '../../runtime/gateway_runtime.dart'; +import '../../theme/app_palette.dart'; +import '../../theme/app_theme.dart'; + +class MobileGatewayPairingGuidePage extends StatelessWidget { + const MobileGatewayPairingGuidePage({ + super.key, + required this.supportsQrScan, + required this.onManualInput, + required this.onScannedSetupCode, + }); + + final bool supportsQrScan; + final VoidCallback onManualInput; + final Future Function(String setupCode) onScannedSetupCode; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + return Scaffold( + backgroundColor: const Color(0xFFF3F1EF), + body: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 0), + child: Row( + children: [ + _HeaderCircleButton( + key: const ValueKey('pairing-guide-close-button'), + icon: Icons.close_rounded, + onPressed: () => Navigator.of(context).pop(), + ), + Expanded( + child: Center( + child: Text( + '配对网关', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(width: 56), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 28), + child: Column( + children: [ + const SizedBox(height: 12), + Container( + width: 118, + height: 118, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 24, + offset: const Offset(0, 12), + ), + ], + border: Border.all( + color: Colors.black.withValues(alpha: 0.08), + ), + ), + alignment: Alignment.center, + child: Icon( + Icons.hub_outlined, + size: 56, + color: palette.textPrimary, + ), + ), + const SizedBox(height: 26), + Text( + '配对你的 OpenClaw 主机', + textAlign: TextAlign.center, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 12), + Text( + '在 Mac、Windows 或云端部署的 OpenClaw 主机上安装 xworkmate,然后生成配对二维码或配置码。', + textAlign: TextAlign.center, + style: theme.textTheme.bodyLarge?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + const SizedBox(height: 24), + _GuideCard( + key: const ValueKey('pairing-guide-install-card'), + title: '自主安装', + subtitle: '按下面两步在主机上安装 XWorkmate CLI,然后生成配对码。', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '1. 安装', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + _CommandBlock( + key: const ValueKey( + 'pairing-guide-install-command', + ), + command: 'npm install -g xworkmate', + ), + const SizedBox(height: 16), + Text( + '2. 配对', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + _CommandBlock( + key: const ValueKey('pairing-guide-pair-command'), + command: 'xworkmate pair', + ), + ], + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton( + key: const ValueKey('pairing-guide-scan-button'), + onPressed: () async { + if (!supportsQrScan) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + appText( + 'Android 扫码即将支持,当前请先使用手动输入代码。', + 'Android QR scanning is coming soon. Use manual code entry for now.', + ), + ), + ), + ); + return; + } + final result = await Navigator.of(context) + .push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => + const MobileGatewayQrScannerPage(), + ), + ); + if (result == null || !context.mounted) { + return; + } + Navigator.of(context).pop(); + await onScannedSetupCode(result); + }, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + backgroundColor: const Color(0xFF151517), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppRadius.button, + ), + ), + ), + child: Text( + '扫描二维码', + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton( + key: const ValueKey('pairing-guide-manual-button'), + onPressed: () { + Navigator.of(context).pop(); + onManualInput(); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + backgroundColor: Colors.white, + foregroundColor: palette.textPrimary, + side: BorderSide( + color: Colors.black.withValues(alpha: 0.08), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppRadius.button, + ), + ), + ), + child: Text( + '手动输入代码', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class MobileGatewayQrScannerPage extends StatefulWidget { + const MobileGatewayQrScannerPage({super.key}); + + @override + State createState() => + _MobileGatewayQrScannerPageState(); +} + +class _MobileGatewayQrScannerPageState + extends State { + bool _hasHandledDetection = false; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill( + child: _QrScannerSurface(onCodeDetected: _handleDetectedCode), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _HeaderCircleButton( + key: const ValueKey('pairing-scanner-close-button'), + icon: Icons.close_rounded, + onPressed: () => Navigator.of(context).pop(), + foregroundColor: Colors.white, + backgroundColor: Colors.black.withValues(alpha: 0.28), + ), + const Spacer(), + Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(AppRadius.dialog), + border: Border.all( + color: Colors.white.withValues(alpha: 0.12), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '扫描配对二维码', + style: theme.textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + Text( + '将二维码放入取景框内。扫描成功后会自动把配置码带入 Gateway 设置页。', + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.white.withValues(alpha: 0.82), + height: 1.35, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + void _handleDetectedCode(String raw) { + if (_hasHandledDetection) { + return; + } + final setupCode = resolveGatewaySetupCodeFromScan(raw); + if (setupCode == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + appText('未识别到有效配置码,请重试。', 'No valid setup code found. Try again.'), + ), + ), + ); + return; + } + _hasHandledDetection = true; + Navigator.of(context).pop(setupCode); + } +} + +String? resolveGatewaySetupCodeFromScan(String raw) { + final trimmed = raw.trim(); + if (trimmed.isEmpty) { + return null; + } + final candidate = _extractSetupCodeFromJsonPayload(trimmed) ?? trimmed; + return decodeGatewaySetupCode(candidate) != null ? candidate : null; +} + +String? _extractSetupCodeFromJsonPayload(String raw) { + final normalized = raw.trim(); + if (!normalized.startsWith('{')) { + return null; + } + try { + final dynamic decoded = jsonDecode(normalized); + if (decoded is! Map) { + return null; + } + final setupCode = decoded['setupCode']; + if (setupCode is! String || setupCode.trim().isEmpty) { + return null; + } + return setupCode.trim(); + } catch (_) { + return null; + } +} + +class _GuideCard extends StatelessWidget { + const _GuideCard({ + super.key, + required this.title, + required this.subtitle, + required this.child, + }); + + final String title; + final String subtitle; + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Colors.black.withValues(alpha: 0.08)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 6), + Text(subtitle, style: theme.textTheme.bodyLarge), + const SizedBox(height: 18), + child, + ], + ), + ); + } +} + +class _CommandBlock extends StatelessWidget { + const _CommandBlock({super.key, required this.command}); + + final String command; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + return Container( + padding: const EdgeInsets.fromLTRB(18, 14, 14, 14), + decoration: BoxDecoration( + color: const Color(0xFFF8F6F4), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.black.withValues(alpha: 0.08)), + ), + child: Row( + children: [ + Expanded( + child: SelectableText( + command, + style: theme.textTheme.titleMedium?.copyWith( + color: palette.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 12), + IconButton( + onPressed: () async { + await Clipboard.setData(ClipboardData(text: command)); + if (!context.mounted) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(appText('已复制命令。', 'Command copied.'))), + ); + }, + icon: const Icon(Icons.content_copy_rounded), + tooltip: appText('复制命令', 'Copy command'), + ), + ], + ), + ); + } +} + +class _HeaderCircleButton extends StatelessWidget { + const _HeaderCircleButton({ + super.key, + required this.icon, + required this.onPressed, + this.foregroundColor, + this.backgroundColor, + }); + + final IconData icon; + final VoidCallback onPressed; + final Color? foregroundColor; + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + return SizedBox( + width: 56, + height: 56, + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor ?? Colors.white.withValues(alpha: 0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: IconButton( + onPressed: onPressed, + icon: Icon(icon), + color: foregroundColor ?? palette.textPrimary, + ), + ), + ); + } +} + +class _QrScannerSurface extends StatelessWidget { + const _QrScannerSurface({required this.onCodeDetected}); + + final ValueChanged onCodeDetected; + + @override + Widget build(BuildContext context) { + return MobileScanner( + key: const ValueKey('pairing-guide-ios-scanner'), + onDetect: (capture) { + final code = capture.barcodes + .map((item) => item.rawValue?.trim() ?? '') + .firstWhere((item) => item.isNotEmpty, orElse: () => ''); + if (code.isEmpty) { + return; + } + onCodeDetected(code); + }, + ); + } +} diff --git a/lib/features/mobile/mobile_shell.dart b/lib/features/mobile/mobile_shell.dart index 5e6774b9..03f1913f 100644 --- a/lib/features/mobile/mobile_shell.dart +++ b/lib/features/mobile/mobile_shell.dart @@ -11,6 +11,7 @@ import '../../runtime/runtime_models.dart'; import '../../theme/app_palette.dart'; import '../../theme/app_theme.dart'; import '../../widgets/detail_drawer.dart'; +import 'mobile_gateway_pairing_guide_page.dart'; enum MobileShellTab { assistant, tasks, workspace, secrets, settings } @@ -152,6 +153,54 @@ class _MobileShellState extends State { rootLabel: appText('移动端', 'Mobile'), destination: WorkspaceDestination.settings, sectionLabel: appText('集成', 'Integrations'), + gatewayProfileIndex: kGatewayRemoteProfileIndex, + prefersGatewaySetupCode: false, + ), + ); + } + + Future _openGatewaySetupCodeEntry({String? prefilledSetupCode}) async { + final setupCode = prefilledSetupCode?.trim() ?? ''; + if (setupCode.isNotEmpty) { + final current = widget + .controller + .settingsDraft + .gatewayProfiles[kGatewayRemoteProfileIndex]; + await widget.controller.saveSettingsDraft( + widget.controller.settingsDraft.copyWithGatewayProfileAt( + kGatewayRemoteProfileIndex, + current.copyWith(useSetupCode: true, setupCode: setupCode), + ), + ); + } + widget.controller.openSettings( + detail: SettingsDetailPage.gatewayConnection, + navigationContext: SettingsNavigationContext( + rootLabel: appText('移动端', 'Mobile'), + destination: WorkspaceDestination.settings, + sectionLabel: appText('集成', 'Integrations'), + gatewayProfileIndex: kGatewayRemoteProfileIndex, + prefersGatewaySetupCode: true, + ), + ); + } + + void _showPairingGuidePage() { + unawaited(_showPairingGuidePageFlow()); + } + + Future _showPairingGuidePageFlow() async { + final supportsQrScan = Theme.of(context).platform == TargetPlatform.iOS; + await Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => MobileGatewayPairingGuidePage( + supportsQrScan: supportsQrScan, + onManualInput: () => unawaited(_openGatewaySetupCodeEntry()), + onScannedSetupCode: (setupCode) async { + await _openGatewaySetupCodeEntry(prefilledSetupCode: setupCode); + }, + ), ), ); } @@ -172,7 +221,7 @@ class _MobileShellState extends State { Navigator.of(sheetContext).pop(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { - _showConnectSheet(); + _showPairingGuidePage(); } }); }, @@ -267,7 +316,7 @@ class _MobileShellState extends State { _MobileSafeStrip( controller: widget.controller, onOpenSafeSheet: _showMobileSafeSheet, - onOpenGatewayConnect: _showConnectSheet, + onOpenGatewayConnect: _showPairingGuidePage, ), const SizedBox(height: 10), Expanded( @@ -491,11 +540,11 @@ class _MobileSafeStrip extends StatelessWidget { else FilledButton( key: const ValueKey('mobile-safe-connect-button'), - onPressed: handlePrimaryConnect, + onPressed: () => unawaited(handlePrimaryConnect()), child: Text( controller.canQuickConnectGateway ? appText('快速连接', 'Quick Connect') - : appText('连接 Gateway', 'Connect Gateway'), + : appText('配对网关', 'Pair Gateway'), ), ), if (hasPendingRun) @@ -678,14 +727,11 @@ class _MobileSafeSheet extends StatelessWidget { key: const ValueKey( 'mobile-safe-sheet-connect-button', ), - onPressed: handleConnect, + onPressed: () => unawaited(handleConnect()), child: Text( controller.canQuickConnectGateway ? appText('快速连接', 'Quick Connect') - : appText( - '打开集成设置', - 'Open Integrations', - ), + : appText('配对网关', 'Pair Gateway'), ), ), if (hasPendingRun) diff --git a/lib/features/settings/settings_page.dart b/lib/features/settings/settings_page.dart index 7494363b..9730dced 100644 --- a/lib/features/settings/settings_page.dart +++ b/lib/features/settings/settings_page.dart @@ -133,6 +133,32 @@ class _SettingsPageState extends State { if (widget.navigationContext != _navigationContext) { _navigationContext = widget.navigationContext; } + _applyGatewayNavigationHints(); + } + + void _applyGatewayNavigationHints() { + final detail = _detail; + final navigationContext = _navigationContext; + if (detail != SettingsDetailPage.gatewayConnection || + navigationContext == null) { + return; + } + final gatewayProfileIndex = navigationContext.gatewayProfileIndex; + if (gatewayProfileIndex == null) { + return; + } + _selectedGatewayProfileIndex = gatewayProfileIndex.clamp( + 0, + kGatewayProfileListLength - 1, + ); + } + + bool _prefersGatewaySetupCodeForCurrentContext(BuildContext context) { + return resolveUiFeaturePlatformFromContext(context) == + UiFeaturePlatform.mobile && + _detail == SettingsDetailPage.gatewayConnection && + _navigationContext?.prefersGatewaySetupCode == true && + _selectedGatewayProfileIndex != kGatewayLocalProfileIndex; } @override @@ -169,6 +195,7 @@ class _SettingsPageState extends State { _tab = uiFeatures.sanitizeSettingsTab(controller.settingsTab); _detail = controller.settingsDetail; _navigationContext = controller.settingsNavigationContext; + _applyGatewayNavigationHints(); final settings = controller.settingsDraft; final showingDetail = _detail != null; final showGlobalApplyBar = @@ -1160,9 +1187,13 @@ class _SettingsPageState extends State { resolveUiFeaturePlatformFromContext(context), ); final setupCodeFeatureEnabled = uiFeatures.supportsGatewaySetupCode; + final forceSetupCodeMode = _prefersGatewaySetupCodeForCurrentContext( + context, + ); final useSetupCode = selectedProfileIndex == kGatewayLocalProfileIndex ? false - : setupCodeFeatureEnabled && gatewayProfile.useSetupCode; + : forceSetupCodeMode || + (setupCodeFeatureEnabled && gatewayProfile.useSetupCode); final gatewayTls = gatewayMode == RuntimeConnectionMode.local ? false : gatewayProfile.tls; @@ -1220,6 +1251,7 @@ class _SettingsPageState extends State { ), const SizedBox(height: 12), if (selectedProfileIndex != kGatewayLocalProfileIndex && + !forceSetupCodeMode && setupCodeFeatureEnabled) ...[ SectionTabs( items: [appText('配置码', 'Setup Code'), appText('手动配置', 'Manual')], @@ -1245,6 +1277,7 @@ class _SettingsPageState extends State { TextField( key: const ValueKey('gateway-setup-code-field'), controller: _gatewaySetupCodeController, + autofocus: forceSetupCodeMode, minLines: 4, maxLines: 6, decoration: InputDecoration( @@ -3162,9 +3195,13 @@ XWorkmate Privacy Policy _selectedGatewayProfileIndex, current, ); + final forceSetupCodeMode = + _navigationContext?.prefersGatewaySetupCode == true && + _detail == SettingsDetailPage.gatewayConnection && + _selectedGatewayProfileIndex != kGatewayLocalProfileIndex; final useSetupCode = mode == RuntimeConnectionMode.local ? false - : current.useSetupCode; + : forceSetupCodeMode || current.useSetupCode; final tls = mode == RuntimeConnectionMode.local ? false : current.tls; final parsedPort = int.tryParse(_gatewayPortController.text.trim()); final decoded = useSetupCode diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index 0a705029..afc2cd14 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -278,6 +278,8 @@ class SettingsNavigationContext { this.secretsTab, this.aiGatewayTab, this.settingsTab, + this.gatewayProfileIndex, + this.prefersGatewaySetupCode, }); final String rootLabel; @@ -287,6 +289,8 @@ class SettingsNavigationContext { final SecretsTab? secretsTab; final AiGatewayTab? aiGatewayTab; final SettingsTab? settingsTab; + final int? gatewayProfileIndex; + final bool? prefersGatewaySetupCode; } enum AccountTab { profile, workspace, sessions } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9aa45ec2..c24cc424 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import device_info_plus import file_selector_macos import irondash_engine_context +import mobile_scanner import package_info_plus import shared_preferences_foundation import super_native_extensions @@ -16,6 +17,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 3871389f..67d14a35 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -380,6 +380,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + url: "https://pub.dev" + source: hosted + version: "6.0.11" native_toolchain_c: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6e2980f9..3356191a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: flutter_markdown: ^0.7.7+1 http: ^1.5.0 markdown: ^7.3.0 + mobile_scanner: ^6.0.7 package_info_plus: ^8.3.1 path_provider: ^2.1.5 shared_preferences: ^2.5.3 diff --git a/test/features/mobile/ios_mobile_shell_suite.dart b/test/features/mobile/ios_mobile_shell_suite.dart index 5dd8f9e3..eeb8f435 100644 --- a/test/features/mobile/ios_mobile_shell_suite.dart +++ b/test/features/mobile/ios_mobile_shell_suite.dart @@ -3,6 +3,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_shell.dart'; import 'package:xworkmate/app/ui_feature_manifest.dart'; @@ -14,6 +15,34 @@ import 'package:xworkmate/theme/app_theme.dart'; import '../../test_support.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const mobileScannerChannel = MethodChannel( + 'dev.steenbakker.mobile_scanner/scanner/method', + ); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(mobileScannerChannel, (call) async { + return switch (call.method) { + 'state' => 1, + 'request' => true, + 'start' => { + 'textureId': 1, + 'size': {'width': 1080.0, 'height': 1920.0}, + 'numberOfCameras': 1, + 'currentTorchMode': 0, + }, + _ => null, + }; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(mobileScannerChannel, null); + }); + Future pumpMobileShell( WidgetTester tester, { required Widget child,