only-bible-app v3.7.2+24
git clone https://git.pyrossh.dev/only-bible-app
The only bible app you will ever need. No ads. No in-app purchases. No distractions.
lib/error_reporting.dart
import "dart:async";
import "dart:convert";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:flutter/scheduler.dart";
import "package:http/http.dart" as http;
/// Sends (or otherwise records) a single error report. Implementations
/// should be best-effort — a failed report must never throw back into the
/// error handler that triggered it.
typedef ErrorReportSender =
Future<void> Function(String message, StackTrace? stack);
/// Global crash/error reporting: wires [FlutterError.onError] (framework
/// errors) and [PlatformDispatcher.onError] (uncaught async/platform
/// errors), shows a user-consent dialog before reporting in release builds,
/// and just logs to the console in debug builds.
class ErrorReporting {
ErrorReporting._();
static GlobalKey<NavigatorState>? _navigatorKey;
static ErrorReportSender? _onReport;
static Set<String> _ignoredSubstrings = const {};
/// Call once at app startup, after `runApp`'s `navigatorKey` exists.
static void install({
required GlobalKey<NavigatorState> navigatorKey,
required ErrorReportSender onReport,
Set<String> ignoredSubstrings = const {},
}) {
_navigatorKey = navigatorKey;
_onReport = onReport;
_ignoredSubstrings = ignoredSubstrings;
FlutterError.onError = (errorDetails) {
FlutterError.presentError(errorDetails);
_handle(errorDetails.exception.toString(), errorDetails.stack);
};
PlatformDispatcher.instance.onError = (error, stack) {
_handle(error.toString(), stack);
return true;
};
}
/// Manually reports [message] through the same sender configured via
/// [install], bypassing the consent dialog — for call sites that already
/// caught and handled a specific error and just want it recorded (e.g.
/// only-bible-app's audio-playback failure handler).
static Future<void> report(String message, StackTrace? stack) async {
if (kDebugMode) {
debugPrint("Error: $message");
debugPrint("$stack");
return;
}
await _onReport?.call(message, stack);
}
static void _handle(String message, StackTrace? stack) {
if (kDebugMode) {
debugPrint("Error: $message");
debugPrint("$stack");
return;
}
if (_ignoredSubstrings.any(message.contains)) {
// Known framework noise: still recorded for visibility, just not
// worth interrupting the user with a dialog they can't act on.
unawaited(_onReport?.call(message, stack));
return;
}
SchedulerBinding.instance.addPostFrameCallback((_) {
final context = _navigatorKey?.currentContext;
if (context == null || !context.mounted) return;
_showReportDialog(context, message, stack);
});
}
static void _showReportDialog(
BuildContext context,
String message,
StackTrace? stack,
) {
showDialog<void>(
context: context,
barrierColor: Colors.black54,
builder: (dialogContext) => AlertDialog(
title: const Text("Alert"),
content: const Text(
"An error has occurred. Do you want to report this error to us?",
),
actionsAlignment: MainAxisAlignment.end,
actionsOverflowButtonSpacing: 8.0,
actions: [
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
unawaited(_onReport?.call(message, stack));
},
child: const Text("Yes"),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text("No"),
),
],
),
);
}
}
/// Builds an [ErrorReportSender] that POSTs to the shared `error-reporter`
/// Cloudflare Worker (see workers/error-reporter/), which emails the report
/// via Cloudflare Email Sending. [sharedSecret], if provided, is sent as
/// `X-Report-Secret` — this is NOT a real security boundary (it's embedded
/// client-side, same as any app secret), it only filters out generic bots
/// blindly probing the endpoint. Real abuse resistance is the Worker's own
/// rate limiter.
ErrorReportSender httpErrorReportSender({
required Uri endpoint,
required String appName,
String? sharedSecret,
String? appVersion,
String? buildNumber,
}) {
return (message, stack) async {
try {
await http.post(
endpoint,
headers: {
"Content-Type": "application/json",
if (sharedSecret != null) "X-Report-Secret": sharedSecret,
},
body: jsonEncode({
"app": appName,
"message": message,
"stack": stack?.toString(),
"platform": defaultTargetPlatform.name,
"appVersion": appVersion,
"buildNumber": buildNumber,
}),
);
} catch (_) {
// Best-effort — a failed report must not cascade into another error.
}
};
}