iOS: degrade gracefully where the Android-only native services are missing

Share/pick/sync calls and streams no longer throw MissingPluginException at
startup; upload/download/sync-now show 'X isn't available on this platform
yet'. Offline/sync mirror dir no longer uses the Android-only external dir.
Info.plist: Face ID and photo library usage strings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-06 19:06:19 -04:00
co-authored by Claude Sonnet 5.5
parent 813c613249
commit d008185f98
10 changed files with 204 additions and 27 deletions
+55
View File
@@ -1,3 +1,4 @@
import 'package:flutter/services.dart';
import '../providers/session_controller.dart';
/// The "get this account's auth header, or bail" guard every native-
@@ -21,3 +22,57 @@ Map<String, String> baseChannelArgs(SessionController session) {
'authHeader': requireAuthHeader(session),
};
}
/// Thrown by user-triggered native services (upload, download, sync now...)
/// on a platform whose native side isn't implemented yet - the message is
/// shown to the user as-is, e.g. in a snackbar.
class NativeServiceUnavailable implements Exception {
final String feature;
const NativeServiceUnavailable(this.feature);
@override
String toString() => "$feature isn't available on this platform yet.";
}
/// Calls [method], treating a missing native implementation (no
/// `MethodChannel` handler registered, as on iOS until each service is
/// built there) as "nothing to report": returns null instead of throwing
/// `MissingPluginException`. For cold-start checks, scheduling and other
/// calls nobody is waiting on.
Future<T?> invokeIfAvailable<T>(
MethodChannel channel,
String method, [
Object? arguments,
]) async {
try {
return await channel.invokeMethod<T>(method, arguments);
} on MissingPluginException {
return null;
}
}
/// Like [invokeIfAvailable], but for an action the user just asked for:
/// throws [NativeServiceUnavailable] naming [feature] rather than silently
/// doing nothing.
Future<T?> invokeOrExplain<T>(
MethodChannel channel,
String method,
String feature, [
Object? arguments,
]) async {
try {
return await channel.invokeMethod<T>(method, arguments);
} on MissingPluginException {
throw NativeServiceUnavailable(feature);
}
}
/// [channel]'s broadcast stream mapped through [convert], with the
/// "no native implementation" error dropped - the stream just never emits.
Stream<T> quietEvents<T>(EventChannel channel, T Function(dynamic) convert) {
return channel
.receiveBroadcastStream()
.handleError((Object _) {}, test: (e) => e is MissingPluginException)
.map(convert);
}