only-bible-app v3.7.2+24

#kotlin#android#ios

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.
    }
  };
}