payfrit-user/lib/services/beacon_channel.dart
John Mizerek 56f1e1cf63 Add native iOS beacon scanner with CoreBluetooth
- BeaconScanner.swift: Native scanner using CBCentralManager
- AppDelegate.swift: Wire up MethodChannel (same API as Android)
- beacon_channel.dart: Support iOS in isSupported check
- beacon_scanner_service.dart: Use native scanner on both platforms

iOS now gets the same fast 2-second scan as Android.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 08:36:40 -08:00

109 lines
2.9 KiB
Dart

import "dart:io";
import "package:flutter/services.dart";
/// Detected beacon from native scanner
class DetectedBeacon {
final String uuid;
final int rssi;
final int samples;
const DetectedBeacon({
required this.uuid,
required this.rssi,
required this.samples,
});
factory DetectedBeacon.fromMap(Map<dynamic, dynamic> map) {
return DetectedBeacon(
uuid: (map["uuid"] as String?) ?? "",
rssi: (map["rssi"] as int?) ?? -100,
samples: (map["samples"] as int?) ?? 0,
);
}
@override
String toString() => "DetectedBeacon(uuid: $uuid, rssi: $rssi, samples: $samples)";
}
/// Native beacon scanner via MethodChannel
/// Works on both Android and iOS using platform-specific implementations.
class BeaconChannel {
static const _channel = MethodChannel("com.payfrit.app/beacon");
/// Check if native scanner is supported on this platform
static bool get isSupported => Platform.isAndroid || Platform.isIOS;
/// Check if Bluetooth permissions are granted
static Future<bool> hasPermissions() async {
if (!isSupported) return false;
try {
final result = await _channel.invokeMethod<bool>("hasPermissions");
return result ?? false;
} on PlatformException catch (e) {
print("[BeaconChannel] hasPermissions error: $e");
return false;
}
}
/// Check if Bluetooth is enabled
static Future<bool> isBluetoothEnabled() async {
if (!isSupported) return false;
try {
final result = await _channel.invokeMethod<bool>("isBluetoothEnabled");
return result ?? false;
} on PlatformException catch (e) {
print("[BeaconChannel] isBluetoothEnabled error: $e");
return false;
}
}
/// Start scanning for beacons
/// Returns list of detected beacons sorted by RSSI (strongest first)
static Future<List<DetectedBeacon>> startScan({List<String>? regions}) async {
if (!isSupported) {
print("[BeaconChannel] Not supported on this platform");
return [];
}
try {
print("[BeaconChannel] Starting native beacon scan...");
final result = await _channel.invokeMethod<List<dynamic>>(
"startScan",
{"regions": regions ?? []},
);
if (result == null) {
print("[BeaconChannel] Scan returned null");
return [];
}
final beacons = result
.map((e) => DetectedBeacon.fromMap(e as Map<dynamic, dynamic>))
.toList();
print("[BeaconChannel] Scan complete: found ${beacons.length} beacons");
for (final b in beacons) {
print("[BeaconChannel] $b");
}
return beacons;
} on PlatformException catch (e) {
print("[BeaconChannel] Scan error: ${e.message}");
return [];
}
}
/// Stop an ongoing scan
static Future<void> stopScan() async {
if (!isSupported) return;
try {
await _channel.invokeMethod("stopScan");
} on PlatformException catch (e) {
print("[BeaconChannel] stopScan error: $e");
}
}
}