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' =>