diff --git a/musadaq-app/android/app/src/main/res/drawable/widget_background.xml b/musadaq-app/android/app/src/main/res/drawable/widget_background.xml
new file mode 100644
index 0000000..ea4ed18
--- /dev/null
+++ b/musadaq-app/android/app/src/main/res/drawable/widget_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/musadaq-app/android/app/src/main/res/drawable/widget_button_bg.xml b/musadaq-app/android/app/src/main/res/drawable/widget_button_bg.xml
new file mode 100644
index 0000000..ca4f600
--- /dev/null
+++ b/musadaq-app/android/app/src/main/res/drawable/widget_button_bg.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/musadaq-app/android/app/src/main/res/layout/widget_musadaq.xml b/musadaq-app/android/app/src/main/res/layout/widget_musadaq.xml
new file mode 100644
index 0000000..75173bf
--- /dev/null
+++ b/musadaq-app/android/app/src/main/res/layout/widget_musadaq.xml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/musadaq-app/android/app/src/main/res/xml/widget_musadaq_info.xml b/musadaq-app/android/app/src/main/res/xml/widget_musadaq_info.xml
new file mode 100644
index 0000000..597e490
--- /dev/null
+++ b/musadaq-app/android/app/src/main/res/xml/widget_musadaq_info.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/musadaq-app/lib/app/routes/app_pages.dart b/musadaq-app/lib/app/routes/app_pages.dart
index b938045..2f05fb4 100644
--- a/musadaq-app/lib/app/routes/app_pages.dart
+++ b/musadaq-app/lib/app/routes/app_pages.dart
@@ -25,6 +25,8 @@ import '../../features/onboarding/views/onboarding_view.dart';
import '../../features/audit/views/audit_log_view.dart';
import '../../features/referral/views/referral_view.dart';
import '../../features/ai_usage/views/ai_usage_view.dart';
+ import '../../features/ar_scanner/views/ar_scanner_view.dart';
+ import '../../features/ar_scanner/controllers/ar_scanner_controller.dart';
part 'app_routes.dart';
@@ -171,5 +173,12 @@ class AppPages {
name: AppRoutes.AI_USAGE,
page: () => const AiUsageView(),
),
+ GetPage(
+ name: AppRoutes.AR_SCANNER,
+ page: () => const WifiScannerView(),
+ binding: BindingsBuilder(() {
+ Get.put(WifiScannerController());
+ }),
+ ),
];
}
diff --git a/musadaq-app/lib/app/routes/app_routes.dart b/musadaq-app/lib/app/routes/app_routes.dart
index 12eb4be..ec47380 100644
--- a/musadaq-app/lib/app/routes/app_routes.dart
+++ b/musadaq-app/lib/app/routes/app_routes.dart
@@ -25,4 +25,5 @@ abstract class AppRoutes {
static const AUDIT_LOG = '/audit-log';
static const REFERRAL = '/referral';
static const AI_USAGE = '/ai-usage';
+ static const AR_SCANNER = '/ar-scanner';
}
diff --git a/musadaq-app/lib/core/services/home_widget_service.dart b/musadaq-app/lib/core/services/home_widget_service.dart
new file mode 100644
index 0000000..597527e
--- /dev/null
+++ b/musadaq-app/lib/core/services/home_widget_service.dart
@@ -0,0 +1,106 @@
+import 'package:flutter/services.dart';
+import 'package:get/get.dart';
+import '../network/dio_client.dart';
+
+/// Home Screen Widget Service
+/// Manages data for native iOS/Android home screen widgets.
+///
+/// Widgets show:
+/// - Quick stats (invoice count, pending, quota)
+/// - Quick scan shortcut
+/// - Today's activity summary
+///
+/// Uses MethodChannel to communicate with native widget code.
+class HomeWidgetService extends GetxService {
+ static const _channel = MethodChannel('com.musadaq.widget');
+
+ // Widget data
+ final totalInvoices = 0.obs;
+ final pendingInvoices = 0.obs;
+ final quotaUsed = 0.obs;
+ final quotaLimit = 0.obs;
+ final lastUpdate = ''.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ // Listen for widget tap events
+ _channel.setMethodCallHandler(_handleWidgetAction);
+ refreshWidgetData();
+ }
+
+ /// Refresh widget data from API
+ Future refreshWidgetData() async {
+ try {
+ final res = await DioClient().client.get('/v1/dashboard/stats');
+ if (res.data['success'] == true) {
+ final data = res.data['data'];
+ totalInvoices.value = data['invoices']?['total'] ?? 0;
+ pendingInvoices.value = data['invoices']?['pending'] ?? 0;
+ }
+
+ final subRes = await DioClient().client.get('/v1/subscriptions/current');
+ if (subRes.data['success'] == true) {
+ final sub = subRes.data['data'];
+ quotaUsed.value = sub['invoices']?['used'] ?? 0;
+ quotaLimit.value = sub['invoices']?['limit'] ?? 0;
+ }
+
+ lastUpdate.value = DateTime.now().toString().substring(0, 16);
+
+ // Push data to native widget
+ await _updateNativeWidget();
+ } catch (e) {
+ // Widget update failure is non-critical
+ }
+ }
+
+ /// Push data to native home screen widget
+ Future _updateNativeWidget() async {
+ try {
+ await _channel.invokeMethod('updateWidget', {
+ 'total_invoices': totalInvoices.value,
+ 'pending_invoices': pendingInvoices.value,
+ 'quota_used': quotaUsed.value,
+ 'quota_limit': quotaLimit.value,
+ 'quota_percent': quotaLimit.value > 0
+ ? ((quotaUsed.value / quotaLimit.value) * 100).round()
+ : 0,
+ 'last_update': lastUpdate.value,
+ });
+ } on MissingPluginException {
+ // Widget not supported on this platform
+ } catch (e) {
+ // Non-critical
+ }
+ }
+
+ /// Handle taps from the native widget
+ Future _handleWidgetAction(MethodCall call) async {
+ switch (call.method) {
+ case 'openScanner':
+ Get.toNamed('/scanner');
+ break;
+ case 'openInvoices':
+ Get.toNamed('/main');
+ break;
+ case 'refreshData':
+ await refreshWidgetData();
+ break;
+ }
+ }
+
+ /// Get widget display data as a map
+ Map getWidgetData() {
+ return {
+ 'total_invoices': totalInvoices.value,
+ 'pending_invoices': pendingInvoices.value,
+ 'quota_used': quotaUsed.value,
+ 'quota_limit': quotaLimit.value,
+ 'quota_percent': quotaLimit.value > 0
+ ? ((quotaUsed.value / quotaLimit.value) * 100).round()
+ : 0,
+ 'last_update': lastUpdate.value,
+ };
+ }
+}
diff --git a/musadaq-app/lib/features/ar_scanner/controllers/ar_scanner_controller.dart b/musadaq-app/lib/features/ar_scanner/controllers/ar_scanner_controller.dart
new file mode 100644
index 0000000..83d6738
--- /dev/null
+++ b/musadaq-app/lib/features/ar_scanner/controllers/ar_scanner_controller.dart
@@ -0,0 +1,463 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart' hide FormData, MultipartFile;
+import 'package:path_provider/path_provider.dart';
+import '../../../core/network/dio_client.dart';
+
+/// WiFi Scanner Controller
+/// Connects to external physical scanners (eSCL/AirScan protocol)
+/// over the local WiFi network, pulls scanned images, and uploads them.
+///
+/// Supported protocols:
+/// - eSCL (AirScan) — most modern WiFi scanners (HP, Epson, Canon, Brother)
+/// - Manual IP + HTTP endpoint (custom scanners)
+///
+/// Flow:
+/// 1. Discover scanners on local network (or enter IP manually)
+/// 2. Connect & configure scan settings
+/// 3. Trigger scan → receive image
+/// 4. Add to queue → upload to Musadaq
+class WifiScannerController extends GetxController {
+ // Scanner discovery
+ final discoveredScanners =